import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; import 'agent_client.dart'; import 'models.dart'; import 'windows_webrtc_peer.dart'; const _frameHeaderBytes = 40; const _maxDimension = 16384; const _maxPixels = 33177600; const _maxCompressedBytes = 64 * 1024 * 1024; const _maxControlLineBytes = 384 * 1024; class WindowsAgentClient implements RemoteDesktopClient { WindowsAgentClient({ required this.host, required this.settings, WindowsWebRtcPeerFactory? webRtcPeerFactory, }) : _webRtcPeerFactory = webRtcPeerFactory ?? (Platform.isAndroid ? FlutterWindowsWebRtcPeer.new : null); final RemoteHost host; final AppSettings settings; final WindowsWebRtcPeerFactory? _webRtcPeerFactory; final StreamController _states = StreamController.broadcast(sync: true); final StreamController _frames = StreamController.broadcast(sync: true); Socket? _socket; WindowsWebRtcPeer? _webRtcPeer; bool _closed = false; bool _desktopOpen = false; int _sequence = 0; int _frameWidth = 0; int _frameHeight = 0; int _pointerX = 0; int _pointerY = 0; @override Stream get states => _states.stream; @override Stream get frames => _frames.stream; @override Future connect({String? pairingCode, required String totpCode}) async { if (pairingCode != null) { throw const AgentException(AgentError.protocol); } await close(); _closed = false; _emit(AgentPhase.connecting); try { final endpoint = normalizeWindowsAgentUri(host.address); final socket = await Socket.connect( endpoint.host, endpoint.port, timeout: const Duration(seconds: 12), ); socket.setOption(SocketOption.tcpNoDelay, true); _socket = socket; final reader = _SocketReader(socket); final hello = await _readJson(reader, const Duration(seconds: 8)); if (hello['kind'] != 'windows_agent_hello' || hello['protocol_major'] != 1 || (hello['protocol_minor'] is! int || hello['protocol_minor'] < 1) || hello['totp_required'] != true) { throw const AgentException(AgentError.protocol); } _emit(AgentPhase.authenticating); _send({'kind': 'authenticate_totp', 'totp_code': totpCode}); final authenticated = await _readJson(reader, const Duration(seconds: 8)); if (authenticated['kind'] != 'authentication_ok') { throw const AgentException(AgentError.totp); } _emit(AgentPhase.openingDesktop); final opened = await _openDesktop(reader, hello); if (opened['kind'] != 'desktop_opened' || (opened['encoding'] != 'zlib_bgra' && opened['encoding'] != 'h264')) { throw const AgentException(AgentError.desktop); } _desktopOpen = true; _emit(AgentPhase.waitingForFrame); if (opened['transport'] == 'webrtc') { final width = opened['width']; final height = opened['height']; final peer = _webRtcPeer; if (width is! int || height is! int || width <= 0 || height <= 0 || width > _maxDimension || height > _maxDimension || width * height > _maxPixels || peer == null) { throw const AgentException(AgentError.protocol); } await peer.waitUntilReady(const Duration(seconds: 5)); await peer.waitForFirstFrame(const Duration(seconds: 15)); _frameWidth = peer.videoWidth > 0 ? peer.videoWidth : width; _frameHeight = peer.videoHeight > 0 ? peer.videoHeight : height; _sequence = 1; _emit(AgentPhase.connected); _frames.add( RemoteFrame( sequence: _sequence, width: _frameWidth, height: _frameHeight, bgra: Uint8List(0), textureId: peer.textureId, ), ); unawaited(_monitorControl(reader)); } else { unawaited(_readFrames(reader)); } } on AgentException catch (error) { await _fail(error.error); rethrow; } on Object { await _fail(AgentError.network); throw const AgentException(AgentError.network); } } Future> _openDesktop( _SocketReader reader, Map hello, ) async { final peerFactory = _webRtcPeerFactory; final transports = hello['supported_media_transports']; final webRtcAdvertised = transports is List && transports.whereType().contains('webrtc'); if (peerFactory != null && webRtcAdvertised) { _send({ 'kind': 'open_desktop', 'capture_mode': 'compatibility', 'media_transport': 'webrtc', 'frames_per_second': settings.framesPerSecond.clamp(1, 30), }); final ready = await _readJson(reader, const Duration(seconds: 15)); if (ready['kind'] == 'webrtc_media_ready') { final width = ready['width']; final height = ready['height']; if (width is! int || height is! int || width <= 0 || height <= 0 || width > _maxDimension || height > _maxDimension || width * height > _maxPixels || ready['encrypted'] != true) { throw const AgentException(AgentError.protocol); } final peer = peerFactory(); _webRtcPeer = peer; await peer.start( iceServers: _parseIceServers(ready['ice_servers']), onSignal: (kind, payload) => _send({ 'kind': 'webrtc_signal', 'signal_kind': kind, 'payload': payload, }), onFailure: () { if (_desktopOpen) unawaited(_fail(AgentError.network)); }, ); while (true) { final response = await _readJson(reader, const Duration(seconds: 20)); if (response['kind'] == 'webrtc_signal') { final kind = response['signal_kind']; final payload = response['payload']; if (kind is! String || payload is! String) { throw const AgentException(AgentError.protocol); } await peer.applySignal(kind, payload); continue; } if (response['kind'] == 'desktop_opened' && response['transport'] == 'webrtc') { return response; } if (response['kind'] != 'webrtc_media_unavailable') { throw const AgentException(AgentError.desktop); } await peer.close(); _webRtcPeer = null; break; } } else if (ready['kind'] != 'webrtc_media_unavailable') { throw const AgentException(AgentError.desktop); } } _send({ 'kind': 'open_desktop', 'capture_mode': 'compatibility', 'media_transport': 'plain_stream', 'frames_per_second': settings.framesPerSecond.clamp(1, 30), }); return _readJson(reader, const Duration(seconds: 15)); } Future _monitorControl(_SocketReader reader) async { try { while (!_closed) { final response = jsonDecode(await reader.readLine()); if (response is! Map) { throw const AgentException(AgentError.protocol); } if (response['kind'] == 'desktop_error') { throw const AgentException(AgentError.desktop); } } } on AgentException catch (error) { await _fail(error.error); } on Object { if (!_closed) await _fail(AgentError.network); } } Future _readFrames(_SocketReader reader) async { try { while (!_closed) { final magic = await reader .readExactly(4) .timeout(const Duration(seconds: 45)); final marker = ascii.decode(magic, allowInvalid: true); if (marker == 'RDWE') { final lengthBytes = await reader.readExactly(4); final length = ByteData.sublistView( lengthBytes, ).getUint32(0, Endian.little); if (length > 16 * 1024) { throw const AgentException(AgentError.protocol); } await reader.readExactly(length); throw const AgentException(AgentError.desktop); } if (marker != 'RDWF') { throw const AgentException(AgentError.frame); } final tail = await reader.readExactly(_frameHeaderBytes - 4); final header = ByteData.sublistView(tail); final version = header.getUint8(0); final codec = header.getUint8(1); final width = header.getUint32(4, Endian.little); final height = header.getUint32(8, Endian.little); final uncompressedBytes = header.getUint32(12, Endian.little); final compressedBytes = header.getUint32(16, Endian.little); final pixels = width * height; if (version != 2 || codec != 1 || width <= 0 || height <= 0 || width > _maxDimension || height > _maxDimension || pixels > _maxPixels || uncompressedBytes != pixels * 4 || compressedBytes <= 0 || compressedBytes > _maxCompressedBytes) { throw const AgentException(AgentError.frame); } final compressed = await reader.readExactly(compressedBytes); final bgra = await Isolate.run( () => Uint8List.fromList(ZLibCodec().decode(compressed)), ); if (bgra.length != uncompressedBytes) { throw const AgentException(AgentError.frame); } if (_closed) return; _frameWidth = width; _frameHeight = height; _sequence += 1; _emit(AgentPhase.connected); _frames.add( RemoteFrame( sequence: _sequence, width: width, height: height, bgra: bgra, ), ); } } on AgentException catch (error) { await _fail(error.error); } on Object { if (!_closed) await _fail(AgentError.network); } } @override void sendPointer(int x, int y) { if (!_desktopOpen || _frameWidth <= 0 || _frameHeight <= 0) return; _pointerX = (x.clamp(0, _frameWidth - 1) * 65535 ~/ _frameWidth).clamp( 0, 65535, ); _pointerY = (y.clamp(0, _frameHeight - 1) * 65535 ~/ _frameHeight).clamp( 0, 65535, ); _sendMouse(); } @override void sendButton(String button, bool pressed) { if (!_desktopOpen) return; final action = switch (button) { 'left' => pressed ? 'left_down' : 'left_up', 'right' => pressed ? 'right_down' : 'right_up', 'middle' => pressed ? 'middle_down' : 'middle_up', _ => null, }; if (action != null) _sendMouse(action); } void _sendMouse([String? action]) { final command = { 'kind': 'input', 'input_type': 'mouse', 'x': _pointerX, 'y': _pointerY, }; command['mouse_action'] = action; _send(command); } @override void sendKey(int keysym) { if (!_desktopOpen) return; final virtualKey = switch (keysym) { 0xff08 => 0x08, 0xff09 => 0x09, 0xff0d => 0x0d, 0xff1b => 0x1b, >= 0x20 && <= 0x7e => keysym, _ => null, }; if (virtualKey == null) return; for (final down in const [true, false]) { _send({ 'kind': 'input', 'input_type': 'key', 'code': virtualKey, 'down': down, }); } } @override void sendText(String value) { if (!_desktopOpen || value.isEmpty) return; final runes = value.runes.toList(growable: false); for (var start = 0; start < runes.length; start += 256) { final end = (start + 256).clamp(0, runes.length); _send({ 'kind': 'input', 'input_type': 'text', 'text': String.fromCharCodes(runes.sublist(start, end)), }); } } @override void acknowledgeFrame(int sequence) {} @override Future close() async { if (_closed) return; try { _send({'kind': 'close'}); await _socket?.flush(); } on Object { // The remote side may already have closed the stream. } _closed = true; _desktopOpen = false; await _webRtcPeer?.close(); _webRtcPeer = null; _socket?.destroy(); _socket = null; _emit(AgentPhase.disconnected); } @override Future dispose() async { await close(); await _states.close(); await _frames.close(); } void _send(Map value) { final peer = _webRtcPeer; if (_desktopOpen && peer != null) { final pointer = value['kind'] == 'input' && value['input_type'] == 'mouse' && value['mouse_action'] == null; try { peer.sendControl(value, pointer: pointer); } on Object { unawaited(_fail(AgentError.network)); } return; } final socket = _socket; if (socket == null) return; socket.add(utf8.encode('${jsonEncode(value)}\n')); } void _emit(AgentPhase phase, [AgentError? error]) { if (!_states.isClosed) _states.add(AgentState(phase, error)); } Future _fail(AgentError error) async { if (_closed) return; _closed = true; _desktopOpen = false; _emit(AgentPhase.failed, error); await _webRtcPeer?.close(); _webRtcPeer = null; _socket?.destroy(); _socket = null; } } Future> _readJson( _SocketReader reader, Duration timeout, ) async { final line = await reader.readLine().timeout(timeout); final decoded = jsonDecode(line); if (decoded is! Map) { throw const AgentException(AgentError.protocol); } return decoded; } List> _parseIceServers(Object? value) { if (value == null) return const []; if (value is! List || value.length > 8) { throw const AgentException(AgentError.protocol); } final servers = >[]; for (final item in value) { if (item is! Map) { throw const AgentException(AgentError.protocol); } final rawUrls = item['urls']; if (rawUrls is List && rawUrls.any((url) => url is! String)) { throw const AgentException(AgentError.protocol); } final urls = rawUrls is String ? [rawUrls] : rawUrls is List ? rawUrls.whereType().toList(growable: false) : const []; final username = item['username'] as String? ?? ''; final credential = item['credential'] as String? ?? ''; if (urls.isEmpty || urls.length > 8 || username.length > 1024 || credential.length > 1024 || username.isEmpty != credential.isEmpty || urls.any( (url) => url.isEmpty || url.length > 2048 || !RegExp(r'^(stun|stuns|turn|turns):').hasMatch(url), )) { throw const AgentException(AgentError.protocol); } servers.add({ 'urls': urls, if (username.isNotEmpty) 'username': username, if (credential.isNotEmpty) 'credential': credential, }); } return servers; } class _SocketReader { _SocketReader(Stream stream) : _iterator = StreamIterator(stream); final StreamIterator _iterator; Uint8List _chunk = Uint8List(0); int _offset = 0; Future readExactly(int length) async { if (length < 0) throw const AgentException(AgentError.protocol); final output = Uint8List(length); var written = 0; while (written < length) { await _ensureData(); final available = _chunk.length - _offset; final take = (length - written < available) ? length - written : available; output.setRange(written, written + take, _chunk, _offset); written += take; _offset += take; } return output; } Future readLine() async { final output = BytesBuilder(copy: false); while (output.length <= _maxControlLineBytes) { await _ensureData(); final newline = _chunk.indexOf(10, _offset); if (newline >= 0) { output.add(Uint8List.sublistView(_chunk, _offset, newline)); _offset = newline + 1; return utf8.decode(output.takeBytes()); } output.add(Uint8List.sublistView(_chunk, _offset)); _offset = _chunk.length; } throw const AgentException(AgentError.protocol); } Future _ensureData() async { while (_offset >= _chunk.length) { if (!await _iterator.moveNext()) { throw const AgentException(AgentError.network); } _chunk = _iterator.current; _offset = 0; } } }