// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/daemon.dart'; import 'package:flutter_tools/src/proxied_devices/devices.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; void main() { group('ProxiedPortForwarder', () { late BufferLogger bufferLogger; late DaemonConnection serverDaemonConnection; late DaemonConnection clientDaemonConnection; setUp(() { bufferLogger = BufferLogger.test(); final FakeDaemonStreams serverDaemonStreams = FakeDaemonStreams(); serverDaemonConnection = DaemonConnection( daemonStreams: serverDaemonStreams, logger: bufferLogger, ); final FakeDaemonStreams clientDaemonStreams = FakeDaemonStreams(); clientDaemonConnection = DaemonConnection( daemonStreams: clientDaemonStreams, logger: bufferLogger, ); serverDaemonStreams.inputs.addStream(clientDaemonStreams.outputs.stream); clientDaemonStreams.inputs.addStream(serverDaemonStreams.outputs.stream); }); tearDown(() async { await serverDaemonConnection.dispose(); await clientDaemonConnection.dispose(); }); testWithoutContext('works correctly without device id', () async { final FakeServerSocket fakeServerSocket = FakeServerSocket(200); final ProxiedPortForwarder portForwarder = ProxiedPortForwarder( clientDaemonConnection, logger: bufferLogger, createSocketServer: (Logger logger, int? hostPort) async => fakeServerSocket, ); final int result = await portForwarder.forward(100); expect(result, 200); final FakeSocket fakeSocket = FakeSocket(); fakeServerSocket.controller.add(fakeSocket); final Stream<DaemonMessage> broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); DaemonMessage message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'proxy.connect'); expect(message.data['params'], <String, Object?>{'port': 100}); const String id = 'random_id'; serverDaemonConnection.sendResponse(message.data['id']!, id); // Forwards the data received from socket to daemon. fakeSocket.controller.add(Uint8List.fromList(<int>[1, 2, 3])); message = await broadcastOutput.first; expect(message.data['method'], 'proxy.write'); expect(message.data['params'], <String, Object?>{'id': id}); expect(message.binary, isNotNull); final List<List<int>> binary = await message.binary!.toList(); expect(binary, <List<int>>[<int>[1, 2, 3]]); // Forwards data received as event to socket. expect(fakeSocket.addedData.isEmpty, true); serverDaemonConnection.sendEvent('proxy.data.$id', null, <int>[4, 5, 6]); await pumpEventQueue(); expect(fakeSocket.addedData.isNotEmpty, true); expect(fakeSocket.addedData[0], <int>[4, 5, 6]); // Closes the socket after the remote end disconnects expect(fakeSocket.closeCalled, false); serverDaemonConnection.sendEvent('proxy.disconnected.$id'); await pumpEventQueue(); expect(fakeSocket.closeCalled, true); }); testWithoutContext('forwards the port from the remote end with device id', () async { final FakeServerSocket fakeServerSocket = FakeServerSocket(400); final ProxiedPortForwarder portForwarder = ProxiedPortForwarder( clientDaemonConnection, deviceId: 'device_id', logger: bufferLogger, createSocketServer: (Logger logger, int? hostPort) async => fakeServerSocket, ); final Stream<DaemonMessage> broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); final Future<int> result = portForwarder.forward(300); DaemonMessage message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'device.forward'); expect(message.data['params'], <String, Object?>{'deviceId': 'device_id', 'devicePort': 300}); serverDaemonConnection.sendResponse(message.data['id']!, <String, Object?>{'hostPort': 350}); expect(await result, 400); final FakeSocket fakeSocket = FakeSocket(); fakeServerSocket.controller.add(fakeSocket); message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'proxy.connect'); expect(message.data['params'], <String, Object?>{'port': 350}); const String id = 'random_id'; serverDaemonConnection.sendResponse(message.data['id']!, id); // Unforward will try to disconnect the remote port. portForwarder.forwardedPorts.single.dispose(); expect(fakeServerSocket.closeCalled, true); message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'device.unforward'); expect(message.data['params'], <String, Object?>{ 'deviceId': 'device_id', 'devicePort': 300, 'hostPort': 350, }); }); group('socket done', () { late Stream<DaemonMessage> broadcastOutput; late FakeSocket fakeSocket; const String id = 'random_id'; setUp(() async { final FakeServerSocket fakeServerSocket = FakeServerSocket(400); final ProxiedPortForwarder portForwarder = ProxiedPortForwarder( clientDaemonConnection, deviceId: 'device_id', logger: bufferLogger, createSocketServer: (Logger logger, int? hostPort) async => fakeServerSocket, ); broadcastOutput = serverDaemonConnection.incomingCommands.asBroadcastStream(); unawaited(portForwarder.forward(300)); // Consumes the message. DaemonMessage message = await broadcastOutput.first; serverDaemonConnection.sendResponse(message.data['id']!, <String, Object?>{'hostPort': 350}); fakeSocket = FakeSocket(); fakeServerSocket.controller.add(fakeSocket); // Consumes the message. message = await broadcastOutput.first; serverDaemonConnection.sendResponse(message.data['id']!, id); // Pump the event queue so that the socket future error handler has a // chance to be listened to. await pumpEventQueue(); }); testWithoutContext('without error, should calls proxy.disconnect', () async { // It will try to disconnect the remote port when socket is done. fakeSocket.doneCompleter.complete(true); final DaemonMessage message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'proxy.disconnect'); expect(message.data['params'], <String, Object?>{ 'id': 'random_id', }); }); testWithoutContext('with error, should also calls proxy.disconnect', () async { fakeSocket.doneCompleter.complete(true); final DaemonMessage message = await broadcastOutput.first; expect(message.data['id'], isNotNull); expect(message.data['method'], 'proxy.disconnect'); expect(message.data['params'], <String, Object?>{ 'id': 'random_id', }); // Send an error response and make sure that it won't crash the client. serverDaemonConnection.sendErrorResponse(message.data['id']!, 'some error', StackTrace.current); // Wait the event queue and make sure that it doesn't crash. await pumpEventQueue(); }); }); }); } class FakeDaemonStreams implements DaemonStreams { final StreamController<DaemonMessage> inputs = StreamController<DaemonMessage>(); final StreamController<DaemonMessage> outputs = StreamController<DaemonMessage>(); @override Stream<DaemonMessage> get inputStream { return inputs.stream; } @override void send(Map<String, dynamic> message, [List<int>? binary]) { outputs.add(DaemonMessage(message, binary != null ? Stream<List<int>>.value(binary) : null)); } @override Future<void> dispose() async { await inputs.close(); // In some tests, outputs have no listeners. We don't wait for outputs to close. unawaited(outputs.close()); } } class FakeServerSocket extends Fake implements ServerSocket { FakeServerSocket(this.port); @override final int port; bool closeCalled = false; final StreamController<Socket> controller = StreamController<Socket>(); @override StreamSubscription<Socket> listen( void Function(Socket event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override Future<ServerSocket> close() async { closeCalled = true; return this; } } class FakeSocket extends Fake implements Socket { bool closeCalled = false; final StreamController<Uint8List> controller = StreamController<Uint8List>(); final List<List<int>> addedData = <List<int>>[]; final Completer<bool> doneCompleter = Completer<bool>(); @override StreamSubscription<Uint8List> listen( void Function(Uint8List event)? onData, { Function? onError, void Function()? onDone, bool? cancelOnError, }) { return controller.stream.listen(onData, onError: onError, onDone: onDone, cancelOnError: cancelOnError); } @override void add(List<int> data) { addedData.add(data); } @override Future<void> close() async { closeCalled = true; } @override Future<bool> get done => doneCompleter.future; @override void destroy() {} }