From 02cc134e38b5573ca3fd8517982ec18cdb9dbf0c Mon Sep 17 00:00:00 2001 From: XavierChanth Date: Fri, 10 Jan 2025 15:25:56 -0500 Subject: [PATCH] feat: add at_test_proxy tool --- packages/at_test_proxy/.gitignore | 3 + packages/at_test_proxy/CHANGELOG.md | 3 + packages/at_test_proxy/README.md | 57 +++++++ packages/at_test_proxy/analysis_options.yaml | 30 ++++ packages/at_test_proxy/bin/at_test_proxy.dart | 18 +++ packages/at_test_proxy/lib/src/args.dart | 140 ++++++++++++++++++ packages/at_test_proxy/lib/src/command.dart | 110 ++++++++++++++ packages/at_test_proxy/lib/src/message.dart | 21 +++ packages/at_test_proxy/lib/src/runner.dart | 58 ++++++++ packages/at_test_proxy/lib/src/serial.dart | 21 +++ packages/at_test_proxy/lib/src/session.dart | 59 ++++++++ packages/at_test_proxy/pubspec.yaml | 18 +++ 12 files changed, 538 insertions(+) create mode 100644 packages/at_test_proxy/.gitignore create mode 100644 packages/at_test_proxy/CHANGELOG.md create mode 100644 packages/at_test_proxy/README.md create mode 100644 packages/at_test_proxy/analysis_options.yaml create mode 100644 packages/at_test_proxy/bin/at_test_proxy.dart create mode 100644 packages/at_test_proxy/lib/src/args.dart create mode 100644 packages/at_test_proxy/lib/src/command.dart create mode 100644 packages/at_test_proxy/lib/src/message.dart create mode 100644 packages/at_test_proxy/lib/src/runner.dart create mode 100644 packages/at_test_proxy/lib/src/serial.dart create mode 100644 packages/at_test_proxy/lib/src/session.dart create mode 100644 packages/at_test_proxy/pubspec.yaml diff --git a/packages/at_test_proxy/.gitignore b/packages/at_test_proxy/.gitignore new file mode 100644 index 00000000..3a857904 --- /dev/null +++ b/packages/at_test_proxy/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/packages/at_test_proxy/CHANGELOG.md b/packages/at_test_proxy/CHANGELOG.md new file mode 100644 index 00000000..a0712a79 --- /dev/null +++ b/packages/at_test_proxy/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- Initial version. diff --git a/packages/at_test_proxy/README.md b/packages/at_test_proxy/README.md new file mode 100644 index 00000000..e96735e6 --- /dev/null +++ b/packages/at_test_proxy/README.md @@ -0,0 +1,57 @@ +# at_test_proxy + +A CLI proxy for debugging and testing TLS/TCP communication. + +## Installation + +## Usage + +### Starting the proxy + +TLS socket: + +```bash +./attp -c \ + -s localhost: \ + --pub \ + --priv \ + --cert +``` + +TCP socket (-r for raw tcp socket): + +```bash +./attp -r -c \ + -s localhost: +``` + +### Using the proxy + +Available commands `command (abbreviation)`: + +#### Forward (f) + +Forward the request / response to the other side. + +Example: `forward` + +#### Modify (m) + +Intercept, modify and forward the message. + +Example: `modify my_new_message` + +#### Respond (m) + +Only allowed when the message is a request from the application. +Respond with a message without sending anything to the server. + +Example: `respond my_message` + +#### Skip (s) + +No-op, ignore the message. + +Example: `skip` + + diff --git a/packages/at_test_proxy/analysis_options.yaml b/packages/at_test_proxy/analysis_options.yaml new file mode 100644 index 00000000..dee8927a --- /dev/null +++ b/packages/at_test_proxy/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/at_test_proxy/bin/at_test_proxy.dart b/packages/at_test_proxy/bin/at_test_proxy.dart new file mode 100644 index 00000000..924dd5a7 --- /dev/null +++ b/packages/at_test_proxy/bin/at_test_proxy.dart @@ -0,0 +1,18 @@ +import 'dart:io'; + +import 'package:at_test_proxy/src/args.dart'; +import 'package:at_test_proxy/src/runner.dart'; + +void main(List argv) async { + final Args args; + try { + args = parseArgs(argv); + } catch (_) { + exit(1); + } + if (args.useTLS) { + await startTlsServer(args); + } else { + await startTcpServer(args); + } +} diff --git a/packages/at_test_proxy/lib/src/args.dart b/packages/at_test_proxy/lib/src/args.dart new file mode 100644 index 00000000..2f2ece97 --- /dev/null +++ b/packages/at_test_proxy/lib/src/args.dart @@ -0,0 +1,140 @@ +import 'dart:io'; + +import 'package:args/args.dart'; +import 'package:at_test_proxy/src/serial.dart'; + +typedef HostInfo = (String host, int port); + +class Args { + final HostInfo serverInfo; + final HostInfo? clientInfo; + final bool useTLS; + + final String? tlsPublicKeyPath; + final String? tlsPrivateKeyPath; + final String? tlsCertificatePath; + + Args({ + required this.serverInfo, + this.clientInfo, + this.useTLS = true, + this.tlsPublicKeyPath, + this.tlsPrivateKeyPath, + this.tlsCertificatePath, + }); +} + +ArgParser getParser({bool allowTrailingOptions = true, int? usageLineLength}) { + ArgParser parser = ArgParser( + allowTrailingOptions: allowTrailingOptions, + usageLineLength: usageLineLength); + + parser.addOption( + "server", + abbr: "s", + mandatory: false, + defaultsTo: "localhost:6464", + help: + "The server side connection information of the proxy (address of this proxy)", + valueHelp: "host:port", + ); + + parser.addOption( + "client", + abbr: "c", + help: + "The client side connection information of the proxy (address of the thing we are proxying)", + valueHelp: "host:port", + ); + + parser.addFlag( + "raw-tcp", + abbr: "r", + negatable: false, + help: "Disables TLS and uses raw TCP to accept connections", + ); + + parser.addOption( + "pub", + mandatory: false, + help: "TLS server public key path", + ); + + parser.addOption( + "priv", + mandatory: false, + help: "TLS server private key path", + ); + + parser.addOption( + "cert", + mandatory: false, + help: "TLS trusted certificate path", + ); + + parser.addFlag("help", abbr: "h", negatable: false, callback: (wasParsed) { + if (wasParsed) { + print(parser.usage); + exit(0); + } + }); + + return parser; +} + +Args parseArgs(List argv) { + var parser = getParser(); + final ArgResults res; + try { + res = parser.parse(argv); + } catch (e) { + Serial.log("Failed to parse args: $e"); + rethrow; + } + + final HostInfo serverInfo; + final HostInfo? clientInfo; + final bool useTLS; + String? tlsPublicKeyPath; + String? tlsPrivateKeyPath; + String? tlsCertificatePath; + + try { + var serverParts = (res['server']! as String).split(":"); + var serverHost = serverParts[0]; + var serverPort = int.parse(serverParts[1]); + + serverInfo = (serverHost, serverPort); + var clientParts = res['client']?.split(":"); + if (clientParts != null) { + var clientHost = clientParts[0]; + var clientPort = int.parse(clientParts[1]); + clientInfo = (clientHost, clientPort); + } else { + clientInfo = null; + } + + var rawTcp = res['raw-tcp'] ?? false; + useTLS = !rawTcp; + + if (useTLS) { + tlsPublicKeyPath = res['pub'] ?? (throw "--pub is mandatory in TLS mode"); + tlsPrivateKeyPath = + res['priv'] ?? (throw "--priv is mandatory in TLS mode"); + tlsCertificatePath = + res['cert'] ?? (throw "--cert is mandatory in TLS mode"); + } + } catch (e) { + Serial.log("$e"); + rethrow; + } + + return Args( + serverInfo: serverInfo, + clientInfo: clientInfo, + useTLS: useTLS, + tlsPublicKeyPath: tlsPublicKeyPath, + tlsPrivateKeyPath: tlsPrivateKeyPath, + tlsCertificatePath: tlsCertificatePath, + ); +} diff --git a/packages/at_test_proxy/lib/src/command.dart b/packages/at_test_proxy/lib/src/command.dart new file mode 100644 index 00000000..a7191f85 --- /dev/null +++ b/packages/at_test_proxy/lib/src/command.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:at_test_proxy/src/message.dart'; +import 'package:at_test_proxy/src/serial.dart'; + +const commands = [ + ForwardCommand(), + ModifyCommand(), + RespondCommand(), + SkipCommand(), +]; + +FutureOr parseAndExecuteCommand(Message message, String line) { + var parts = line.split(" "); + if (parts.isEmpty) { + return null; + } + + for (var cmd in commands) { + if (parts[0] == cmd.command || parts[0] == cmd.abbr) { + return cmd.run(message, parts.sublist(1).join(" ")); + } + } +} + +abstract class Command { + final String help; + final String command; + final String? abbr; + + const Command({required this.help, required this.command, this.abbr}); + + FutureOr run(Message message, String? commandArgs); +} + +/// Used by forward and modify +void _forwardMessage(Message message, {String? messageContents}) { + if (message.clientSocket == null) { + throw Exception("No client socket is set, cannot forward"); + } + switch (message.status) { + case MessageStatus.response: + message.serverSocket.write(messageContents ?? message.value); + case MessageStatus.request: + message.clientSocket!.write(messageContents ?? message.value); + case MessageStatus.none: + throw Exception("Nothing to forward"); + } +} + +class ForwardCommand extends Command { + const ForwardCommand() + : super( + help: "Forward this command, as if the proxy weren't even here", + command: "forward", + abbr: "f", + ); + + @override + void run(Message message, String? commandArgs) { + _forwardMessage(message); + } +} + +class ModifyCommand extends Command { + const ModifyCommand() + : super( + help: "modify the message before forwarding it", + command: "modify", + abbr: "m", + ); + + @override + void run(Message message, String? commandArgs) { + _forwardMessage(message, messageContents: commandArgs); + } +} + +class RespondCommand extends Command { + const RespondCommand() + : super( + help: "Respond to the request without forwarding", + command: "respond", + abbr: "r", + ); + + @override + void run(Message message, String? commandArgs) { + if (message.status != MessageStatus.request) { + throw Exception( + "Not handling a request, respond is invalid for this operation"); + } + if (commandArgs == null) {} + message.serverSocket.write(commandArgs); + } +} + +class SkipCommand extends Command { + const SkipCommand() + : super( + help: "Skip/ignore this message", + command: "skip", + abbr: "s", + ); + + @override + void run(Message message, String? commandArgs) { + // noop + } +} diff --git a/packages/at_test_proxy/lib/src/message.dart b/packages/at_test_proxy/lib/src/message.dart new file mode 100644 index 00000000..556923d0 --- /dev/null +++ b/packages/at_test_proxy/lib/src/message.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +enum MessageStatus { + response, // from client to server + request, // from server to client + none, +} + +class Message { + final MessageStatus status; + final String value; // Message from the socket + final Socket serverSocket; + final Socket? clientSocket; + + const Message({ + required this.status, + required this.value, + required this.serverSocket, + required this.clientSocket, + }); +} diff --git a/packages/at_test_proxy/lib/src/runner.dart b/packages/at_test_proxy/lib/src/runner.dart new file mode 100644 index 00000000..c5ec0d41 --- /dev/null +++ b/packages/at_test_proxy/lib/src/runner.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'dart:async'; +import 'package:at_test_proxy/src/session.dart'; +import 'package:at_test_proxy/src/serial.dart'; +import 'package:at_test_proxy/src/args.dart'; + +Future startTlsServer(Args args) async { + var context = SecurityContext(); + context.useCertificateChain(args.tlsPublicKeyPath!); + context.usePrivateKey(args.tlsPrivateKeyPath!); + context.setTrustedCertificates(args.tlsCertificatePath!); + + var serverSocket = await SecureServerSocket.bind( + args.serverInfo.$1, + args.serverInfo.$2, + context, + ); + + Serial.log( + "Started TLS server at ${args.serverInfo.$1}:${args.serverInfo.$2}"); + + int sessionCounter = 0; + await serverSocket.forEach((socket) async { + sessionCounter += 1; + var clientSocket = + await SecureSocket.connect(args.serverInfo.$1, args.serverInfo.$2); + + Serial.log( + "Creating TLS session $sessionCounter to ${args.serverInfo.$1}:${args.serverInfo.$2}"); + + unawaited(handleSession(sessionCounter, clientSocket, socket)); + }); +} + +Future startTcpServer(Args args) async { + var serverSocket = await ServerSocket.bind( + args.serverInfo.$1, + args.serverInfo.$2, + ); + + Serial.log( + "Started TCP server at ${args.serverInfo.$1}:${args.serverInfo.$2}"); + + int sessionCounter = 0; + await serverSocket.forEach((socket) async { + sessionCounter += 1; + var clientSocket = await Socket.connect( + args.serverInfo.$1, + args.serverInfo.$2, + ); + + Serial.log( + "Creating TCP session $sessionCounter to ${args.serverInfo.$1}:${args.serverInfo.$2}"); + + unawaited(handleSession(sessionCounter, clientSocket, socket)); + }); +} diff --git a/packages/at_test_proxy/lib/src/serial.dart b/packages/at_test_proxy/lib/src/serial.dart new file mode 100644 index 00000000..28a35ff6 --- /dev/null +++ b/packages/at_test_proxy/lib/src/serial.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +import 'package:sync/mutex.dart'; + +class Serial { + static IOSink loggingSink = stderr; + static IOSink promptSink = stdout; + + static void log(String line) { + loggingSink.writeln(line); + } + + static final Mutex _stdinMutex = Mutex(); + static Future blockForInput(String prompt) async { + await _stdinMutex.acquire(); + promptSink.writeln(prompt); + var res = stdin.readLineSync(); + _stdinMutex.release(); + return res; + } +} diff --git a/packages/at_test_proxy/lib/src/session.dart b/packages/at_test_proxy/lib/src/session.dart new file mode 100644 index 00000000..9703927c --- /dev/null +++ b/packages/at_test_proxy/lib/src/session.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:at_test_proxy/src/command.dart'; +import 'package:at_test_proxy/src/message.dart'; +import 'package:at_test_proxy/src/serial.dart'; + +enum StreamType { + client, + server, +} + +Future handleSession( + int sessionId, Socket clientSocket, Socket serverSocket) async { + StreamController<(StreamType, Uint8List)> unifiedStream = + StreamController<(StreamType, Uint8List)>(); + + int closed = 0; + clientSocket.listen( + (element) => unifiedStream.add((StreamType.client, element)), + onDone: () { + closed += 1; + if (closed == 2) { + unifiedStream.close(); + } + }, + cancelOnError: true, + ); + serverSocket.listen( + (element) => unifiedStream.add((StreamType.server, element)), + onDone: () { + closed += 1; + if (closed == 2) { + unifiedStream.close(); + } + }, + cancelOnError: true, + ); + + await unifiedStream.stream.forEach((element) async { + var (type, bytes) = element; + var status = switch (type) { + StreamType.client => MessageStatus.response, + StreamType.server => MessageStatus.request, + }; + var value = String.fromCharCodes(bytes); + var message = Message( + status: status, + value: value, + serverSocket: serverSocket, + clientSocket: clientSocket, + ); + var res = await Serial.blockForInput("$sessionId:$status:$value"); + if (res != null) { + await parseAndExecuteCommand(message, res); + } + }); +} diff --git a/packages/at_test_proxy/pubspec.yaml b/packages/at_test_proxy/pubspec.yaml new file mode 100644 index 00000000..7b2d5631 --- /dev/null +++ b/packages/at_test_proxy/pubspec.yaml @@ -0,0 +1,18 @@ +name: at_test_proxy +description: A CLI proxy for debugging and testing TLS/TCP communication. +version: 0.1.0-rc.1 +repository: https://github.com/atsign-foundation/at_mono.git + +executables: + attp: at_test_proxy + +environment: + sdk: ^3.5.4 + +dependencies: + args: ^2.6.0 + sync: ^0.3.0 + +dev_dependencies: + lints: ^4.0.0 + test: ^1.24.0