Skip to content

Commit

Permalink
feat: add at_test_proxy tool
Browse files Browse the repository at this point in the history
  • Loading branch information
XavierChanth committed Jan 10, 2025
1 parent 28a8368 commit 02cc134
Show file tree
Hide file tree
Showing 12 changed files with 538 additions and 0 deletions.
3 changes: 3 additions & 0 deletions packages/at_test_proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
3 changes: 3 additions & 0 deletions packages/at_test_proxy/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 0.1.0

- Initial version.
57 changes: 57 additions & 0 deletions packages/at_test_proxy/README.md
Original file line number Diff line number Diff line change
@@ -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 <host-of-service:port-of-service> \
-s localhost:<local-port> \
--pub <tls-certficate-chain> \
--priv <tls-private-key> \
--cert <tls-trusted-certificates>
```

TCP socket (-r for raw tcp socket):

```bash
./attp -r -c <host-of-service:port-of-service> \
-s localhost:<local-port>
```

### 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`


30 changes: 30 additions & 0 deletions packages/at_test_proxy/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions packages/at_test_proxy/bin/at_test_proxy.dart
Original file line number Diff line number Diff line change
@@ -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<String> argv) async {
final Args args;
try {
args = parseArgs(argv);
} catch (_) {
exit(1);
}
if (args.useTLS) {
await startTlsServer(args);
} else {
await startTcpServer(args);
}
}
140 changes: 140 additions & 0 deletions packages/at_test_proxy/lib/src/args.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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,
);
}
110 changes: 110 additions & 0 deletions packages/at_test_proxy/lib/src/command.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<void> 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
}
}
21 changes: 21 additions & 0 deletions packages/at_test_proxy/lib/src/message.dart
Original file line number Diff line number Diff line change
@@ -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,
});
}
Loading

0 comments on commit 02cc134

Please sign in to comment.