diff --git a/pubspec.lock b/pubspec.lock index 750c80fcd..464706030 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2135,10 +2135,10 @@ packages: dependency: "direct main" description: name: socks5_proxy - sha256: e0cba6917cd374de6f6cb0ce081e50e6efc24c61644b8e9f20c8bf8b91bb0b75 + sha256: "80fa31a9ebfc0dc8de7b0e568c8d8927b65558ef2c7591cbee5afac814fb8f74" url: "https://pub.dev" source: hosted - version: "1.0.3+dev.3" + version: "2.1.1" socks_socket: dependency: transitive description: diff --git a/scripts/app_config/templates/pubspec.template.yaml b/scripts/app_config/templates/pubspec.template.yaml index 12a7efaa3..067b50ede 100644 --- a/scripts/app_config/templates/pubspec.template.yaml +++ b/scripts/app_config/templates/pubspec.template.yaml @@ -216,7 +216,7 @@ dependencies: git: url: https://github.com/cypherstack/tezart.git ref: 84c563104f1a19c26e49bafccb7da404b210b666 - socks5_proxy: 1.0.3+dev.3 + socks5_proxy: ^2.1.1 convert: ^3.1.1 flutter_hooks: ^0.20.3 meta: ^1.9.1 diff --git a/test/networking/http_socks_proxy_test.dart b/test/networking/http_socks_proxy_test.dart new file mode 100644 index 000000000..62d259643 --- /dev/null +++ b/test/networking/http_socks_proxy_test.dart @@ -0,0 +1,82 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/networking/http.dart'; + +// Minimal SOCKS5 server that records the CONNECT target and answers any +// request with a canned HTTP 200. +class _FakeSocksServer { + late final ServerSocket _server; + int? addressType; + String? target; + int? port; + + int get listeningPort => _server.port; + + Future start() async { + _server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + _server.listen((socket) { + var stage = 0; + socket.listen((Uint8List bytes) { + switch (stage) { + case 0: + socket.add([0x05, 0x00]); + stage = 1; + case 1: + addressType = bytes[3]; + if (addressType == 0x03) { + target = String.fromCharCodes(bytes.sublist(5, 5 + bytes[4])); + } else { + target = bytes.sublist(4, bytes.length - 2).join('.'); + } + port = (bytes[bytes.length - 2] << 8) | bytes[bytes.length - 1]; + socket.add([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]); + stage = 2; + default: + socket.write( + 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok', + ); + socket.close(); + } + }); + }); + } + + Future stop() => _server.close(); +} + +void main() { + late _FakeSocksServer socks; + + setUp(() async { + socks = _FakeSocksServer(); + await socks.start(); + }); + + tearDown(() => socks.stop()); + + Future get(String host) => const HTTP().get( + url: Uri.http(host, '/'), + proxyInfo: (host: InternetAddress.loopbackIPv4, port: socks.listeningPort), + ); + + test('proxied request sends the hostname to the SOCKS5 proxy', () async { + final response = await get('example.invalid'); + + expect(response.code, 200); + expect(socks.addressType, 0x03); + expect(socks.target, 'example.invalid'); + expect(socks.port, 80); + }); + + test('proxied request can target an onion address', () async { + const onion = + 'trocadorfyhlu27aefre5u7zri66gudtzdyelymftvr4yjwcxhfaqsid.onion'; + final response = await get(onion); + + expect(response.code, 200); + expect(socks.addressType, 0x03); + expect(socks.target, onion); + }); +}