261 lines
8.7 KiB
Dart
261 lines
8.7 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:remotedesk_android/models.dart';
|
|
import 'package:remotedesk_android/windows_agent_client.dart';
|
|
import 'package:remotedesk_android/windows_webrtc_peer.dart';
|
|
|
|
void main() {
|
|
test('plain Windows Agent handshake delivers a bounded zlib frame', () async {
|
|
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
final serverTask = server.first.then((socket) async {
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'windows_agent_hello', 'protocol_major': 1, 'protocol_minor': 1, 'totp_required': true})}\n',
|
|
);
|
|
final lines = _lines(socket);
|
|
expect(await lines.moveNext(), isTrue);
|
|
expect(jsonDecode(lines.current)['totp_code'], '123456');
|
|
socket.write('{"kind":"authentication_ok"}\n');
|
|
expect(await lines.moveNext(), isTrue);
|
|
expect(jsonDecode(lines.current)['media_transport'], 'plain_stream');
|
|
socket.write(
|
|
'{"kind":"desktop_opened","encoding":"zlib_bgra",'
|
|
'"transport":"plain_stream"}\n',
|
|
);
|
|
socket.add(_frame(width: 2, height: 1, pixels: [1, 2, 3, 4, 5, 6, 7, 8]));
|
|
await socket.flush();
|
|
});
|
|
|
|
final client = WindowsAgentClient(
|
|
host: _host('windows-test', server.port),
|
|
settings: const AppSettings(framesPerSecond: 10),
|
|
);
|
|
final frameFuture = client.frames.first.timeout(const Duration(seconds: 5));
|
|
await client.connect(totpCode: '123456');
|
|
final frame = await frameFuture;
|
|
expect(frame.width, 2);
|
|
expect(frame.height, 1);
|
|
expect(frame.bgra, [1, 2, 3, 4, 5, 6, 7, 8]);
|
|
|
|
await client.dispose();
|
|
await serverTask;
|
|
await server.close();
|
|
});
|
|
|
|
test('Windows Agent prefers WebRTC video and DataChannels', () async {
|
|
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
final peer = _FakeWebRtcPeer();
|
|
final serverTask = server.first.then((socket) async {
|
|
socket.write(
|
|
'${jsonEncode({
|
|
'kind': 'windows_agent_hello',
|
|
'protocol_major': 1,
|
|
'protocol_minor': 3,
|
|
'totp_required': true,
|
|
'supported_media_transports': ['webrtc', 'plain_stream'],
|
|
})}\n',
|
|
);
|
|
final lines = _lines(socket);
|
|
expect(await lines.moveNext(), isTrue);
|
|
socket.write('{"kind":"authentication_ok"}\n');
|
|
expect(await lines.moveNext(), isTrue);
|
|
expect(jsonDecode(lines.current)['media_transport'], 'webrtc');
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'webrtc_media_ready', 'width': 2, 'height': 2, 'ice_servers': <Object>[], 'encrypted': true})}\n',
|
|
);
|
|
for (final expected in ['offer', 'ice_candidate', 'ice_end']) {
|
|
expect(await lines.moveNext(), isTrue);
|
|
final signal = jsonDecode(lines.current) as Map<String, dynamic>;
|
|
expect(signal['kind'], 'webrtc_signal');
|
|
expect(signal['signal_kind'], expected);
|
|
}
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'webrtc_signal', 'signal_kind': 'answer', 'payload': '{"type":"answer","sdp":"answer-sdp"}'})}\n',
|
|
);
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'webrtc_signal', 'signal_kind': 'ice_candidate', 'payload': '{"candidate":"candidate:2","sdpMid":"0","sdpMLineIndex":0}'})}\n',
|
|
);
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'webrtc_signal', 'signal_kind': 'ice_end', 'payload': ''})}\n',
|
|
);
|
|
socket.write(
|
|
'{"kind":"desktop_opened","encoding":"h264",'
|
|
'"transport":"webrtc","width":2,"height":2}\n',
|
|
);
|
|
await socket.flush();
|
|
});
|
|
|
|
final client = WindowsAgentClient(
|
|
host: _host('windows-webrtc-test', server.port),
|
|
settings: const AppSettings(framesPerSecond: 30),
|
|
webRtcPeerFactory: () => peer,
|
|
);
|
|
final frameFuture = client.frames.first.timeout(const Duration(seconds: 5));
|
|
await client.connect(totpCode: '123456');
|
|
final frame = await frameFuture;
|
|
expect(frame.textureId, 77);
|
|
expect(frame.width, 2);
|
|
expect(frame.height, 2);
|
|
expect(peer.remoteSignals.map((signal) => signal.$1), [
|
|
'answer',
|
|
'ice_candidate',
|
|
'ice_end',
|
|
]);
|
|
|
|
client.sendPointer(1, 1);
|
|
client.sendButton('left', true);
|
|
expect(peer.commands[0].pointer, isTrue);
|
|
expect(peer.commands[0].value['input_type'], 'mouse');
|
|
expect(peer.commands[1].pointer, isFalse);
|
|
expect(peer.commands[1].value['mouse_action'], 'left_down');
|
|
|
|
await client.dispose();
|
|
expect(peer.closed, isTrue);
|
|
await serverTask;
|
|
await server.close();
|
|
});
|
|
|
|
test('Windows Agent falls back to TCP when WebRTC is unavailable', () async {
|
|
final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
|
final peer = _FakeWebRtcPeer();
|
|
final serverTask = server.first.then((socket) async {
|
|
socket.write(
|
|
'${jsonEncode({
|
|
'kind': 'windows_agent_hello',
|
|
'protocol_major': 1,
|
|
'protocol_minor': 3,
|
|
'totp_required': true,
|
|
'supported_media_transports': ['webrtc', 'plain_stream'],
|
|
})}\n',
|
|
);
|
|
final lines = _lines(socket);
|
|
expect(await lines.moveNext(), isTrue);
|
|
socket.write('{"kind":"authentication_ok"}\n');
|
|
expect(await lines.moveNext(), isTrue);
|
|
expect(jsonDecode(lines.current)['media_transport'], 'webrtc');
|
|
socket.write(
|
|
'${jsonEncode({'kind': 'webrtc_media_ready', 'width': 2, 'height': 1, 'ice_servers': <Object>[], 'encrypted': true})}\n',
|
|
);
|
|
for (var index = 0; index < 3; index++) {
|
|
expect(await lines.moveNext(), isTrue);
|
|
}
|
|
socket.write('{"kind":"webrtc_media_unavailable"}\n');
|
|
await socket.flush();
|
|
expect(await lines.moveNext(), isTrue);
|
|
expect(jsonDecode(lines.current)['media_transport'], 'plain_stream');
|
|
socket.write(
|
|
'{"kind":"desktop_opened","encoding":"zlib_bgra",'
|
|
'"transport":"plain_stream"}\n',
|
|
);
|
|
socket.add(_frame(width: 2, height: 1, pixels: [9, 8, 7, 6, 5, 4, 3, 2]));
|
|
await socket.flush();
|
|
});
|
|
|
|
final client = WindowsAgentClient(
|
|
host: _host('windows-fallback-test', server.port),
|
|
settings: const AppSettings(framesPerSecond: 15),
|
|
webRtcPeerFactory: () => peer,
|
|
);
|
|
final frameFuture = client.frames.first.timeout(const Duration(seconds: 5));
|
|
await client.connect(totpCode: '123456');
|
|
final frame = await frameFuture;
|
|
expect(frame.textureId, isNull);
|
|
expect(frame.bgra, [9, 8, 7, 6, 5, 4, 3, 2]);
|
|
expect(peer.closed, isTrue);
|
|
|
|
await client.dispose();
|
|
await serverTask;
|
|
await server.close();
|
|
});
|
|
}
|
|
|
|
RemoteHost _host(String id, int port) => RemoteHost(
|
|
id: id,
|
|
name: id,
|
|
address: '127.0.0.1:$port',
|
|
user: '',
|
|
certificateSha256: '',
|
|
platform: HostPlatform.windows,
|
|
);
|
|
|
|
StreamIterator<String> _lines(Socket socket) => StreamIterator(
|
|
socket
|
|
.cast<List<int>>()
|
|
.transform(utf8.decoder)
|
|
.transform(const LineSplitter()),
|
|
);
|
|
|
|
Uint8List _frame({
|
|
required int width,
|
|
required int height,
|
|
required List<int> pixels,
|
|
}) {
|
|
final compressed = ZLibCodec().encode(pixels);
|
|
final header = ByteData(40);
|
|
header.setUint8(0, 0x52);
|
|
header.setUint8(1, 0x44);
|
|
header.setUint8(2, 0x57);
|
|
header.setUint8(3, 0x46);
|
|
header.setUint8(4, 2);
|
|
header.setUint8(5, 1);
|
|
header.setUint32(8, width, Endian.little);
|
|
header.setUint32(12, height, Endian.little);
|
|
header.setUint32(16, pixels.length, Endian.little);
|
|
header.setUint32(20, compressed.length, Endian.little);
|
|
return Uint8List.fromList([...header.buffer.asUint8List(), ...compressed]);
|
|
}
|
|
|
|
class _FakeWebRtcPeer implements WindowsWebRtcPeer {
|
|
final remoteSignals = <(String, String)>[];
|
|
final commands = <({Map<String, Object?> value, bool pointer})>[];
|
|
bool closed = false;
|
|
|
|
@override
|
|
int get textureId => 77;
|
|
|
|
@override
|
|
int get videoWidth => 2;
|
|
|
|
@override
|
|
int get videoHeight => 2;
|
|
|
|
@override
|
|
Future<void> start({
|
|
required List<Map<String, dynamic>> iceServers,
|
|
required WindowsWebRtcSignalSender onSignal,
|
|
required void Function() onFailure,
|
|
}) async {
|
|
expect(iceServers, isEmpty);
|
|
onSignal('offer', '{"type":"offer","sdp":"offer-sdp"}');
|
|
onSignal(
|
|
'ice_candidate',
|
|
'{"candidate":"candidate:1","sdpMid":"0","sdpMLineIndex":0}',
|
|
);
|
|
onSignal('ice_end', '');
|
|
}
|
|
|
|
@override
|
|
Future<void> applySignal(String kind, String payload) async {
|
|
remoteSignals.add((kind, payload));
|
|
}
|
|
|
|
@override
|
|
Future<void> waitUntilReady(Duration timeout) async {}
|
|
|
|
@override
|
|
Future<void> waitForFirstFrame(Duration timeout) async {}
|
|
|
|
@override
|
|
void sendControl(Map<String, Object?> command, {required bool pointer}) {
|
|
commands.add((value: Map<String, Object?>.from(command), pointer: pointer));
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
closed = true;
|
|
}
|
|
}
|