blob: 455415838da1dc155c6cae6587cdfe21cd3c6809 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart=2.12
import 'dart:io';
import 'package:logging/logging.dart';
import 'sl4f_client.dart';
/// `TcpProxyController` is a utility for host-driven tests that need to access ports on a DUT that
/// only permit localhost connections.
///
/// `TcpProxyController` opens ports that are accessible from the host and tunnels connections from
/// the open ports to the restricted ports. Example use cases include access to Webdriver and
/// Flutter debug ports.
///
/// Note that unlike ssh port forwarding, the accessible ports are located on the DUT, rather than
/// on the host.
class TcpProxyController {
final _log = Logger('tcp_proxy');
final Sl4f _sl4f;
/// The pool of ports to use that are explicitly forwarded by the user. It is
/// a FIFO circular queue that is used by [openProxy] and [dropProxy].
final List<int>? proxyPorts;
/// A map from currently proxied target ports to their proxy ports.
final Map<int, int> _targetToProxyPorts = <int, int>{};
/// Creates a TcpProxyController. If [proxyPorts] isn't provided or is empty,
/// the proxy will pick available ports itself.
TcpProxyController(this._sl4f, {List<int>? proxyPorts})
: proxyPorts =
(proxyPorts != null && proxyPorts.isNotEmpty) ? proxyPorts : null;
/// Open a tunnel to [targetPort] on the DUT.
///
/// Returns the port on the DUT through which the tunnel is accessible. Pick
/// the head of [proxyPorts] if it was provided to the constructor; otherwise,
/// the proxy will pick an available port itself.
///
/// Do not call with the same [targetPort] multiple times.
Future<int> openProxy(int targetPort) async {
int wantProxyPort = 0;
if (proxyPorts != null) {
if (proxyPorts!.isEmpty) {
// Halt the test and notify the user that they ran out of proxy ports.
throw SocketException('Tcp proxy ran out of ports.');
}
wantProxyPort = proxyPorts!.removeAt(0);
}
final int gotProxyPort = await _sl4f
.request('proxy_facade.OpenProxy', [targetPort, wantProxyPort]);
_targetToProxyPorts[targetPort] = gotProxyPort;
_log.fine('Forwarding TCP ports on DUT: $targetPort -> $gotProxyPort');
return gotProxyPort;
}
/// Stop a tunnel to [targetPort] on the DUT.
///
/// If the proxy port was retrieved from [proxyPorts], it will be returned to
/// the back of it for reuse.
///
/// Note: In case that n requests were made to proxy [targetPort], the proxy
/// is closed only after n calls to dropProxy with [targetPort] are made.
Future<void> dropProxy(int targetPort) async {
_log.fine('Cancelling TCP port forwarding on DUT for $targetPort');
await _sl4f.request('proxy_facade.DropProxy', targetPort);
final releasedProxyPort = _targetToProxyPorts.remove(targetPort);
if (releasedProxyPort != null) {
proxyPorts?.add(releasedProxyPort);
}
}
/// Forcibly terminate all proxies.
///
/// Terminates all proxies, ignoring the number of clients that requested
/// proxies. This method is intended for cleanup after a test only.
Future<void> stopAllProxies() async {
await _sl4f.request('proxy_facade.StopAllProxies');
proxyPorts?.addAll(_targetToProxyPorts.values);
_targetToProxyPorts.clear();
}
}