Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions minimal-mock-proxy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules
dist
mt5-router/.venv/
mt5-router/build/
mt5-router/__pycache__/
mt5-router/tests/__pycache__/
mt5-router/native/__pycache__/
225 changes: 225 additions & 0 deletions minimal-mock-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# Minimal Mock Proxy

For MetaTrader 5 process interception on Windows, the optional
[MT5 Router app](mt5-router/README.md) routes TCP ports 80/443 into this proxy.
Its portable build is `dist/mt5-router/MT5Router/MT5Router.exe`.

A small HTTP/HTTPS MITM proxy that applies supported HTTP Toolkit exported rules using Mockttp.

## Quick Start

The included `rules.json` is a portable example: it mocks POST requests to
`https://example.test/gate`, serves the current proxy CA at
`http://proxy.example.test/ca.pem`, and passes through other HTTP/WebSocket traffic.
Replace it with your own HTTP Toolkit export or select one with `--rules`.

### 1. Install Dependencies

```bash
npm install
```

### 2. Build (Production)

```bash
npm run build
npm start -- --port 8888 --rules rules.json
```

### 3. Development (No Build Required)

```bash
npm run dev -- --port 8888 --rules rules.json
```

### Run on Another Computer with a Single JavaScript Bundle

On the build computer, run:

```bash
npm run build:bundle
```

Copy `dist/standalone/server.js`, your rules JSON, and any response files referenced
by the rules to the other computer. Install Node.js 20 or newer there; no npm
install or `node_modules` folder is required on that computer.

For example, with `server.js` beside a `rules` folder:

```bash
node server.js --port 8002 --rules rules/MockProxy.json --log-level debug --telegram-chat-id "YOUR_CHAT_ID" --telegram-bot-token "YOUR_BOT_TOKEN" --add-default-passthrough
```

Rules and response files remain external and editable. Response files are read
for every matched request, so edits take effect immediately. Restart the proxy
after editing the rules JSON. Relative file paths are resolved from the working
directory. Update any absolute paths for the new computer. The proxy generates a
CA certificate on first startup and logs its
location with `--log-level debug`; configure the requesting application to trust
that certificate.

## Command Line Options

### Required Flags

- `--rules <path>` - Path to HTTP Toolkit rules JSON file (e.g., `--rules rules.json`)

### Optional Flags

- `--port <number>` - Port to listen on (default: `8888`; `0` selects a free port and logs the actual port)
- `--host <IP>` - Bind address (default: `127.0.0.1`). Use `::1` for IPv6 loopback. Use `0.0.0.0` or `::` explicitly to accept network connections; the proxy does not provide authentication.
- `--log-level <level>` - Logging level: `error`, `warn`, `info`, `debug` (default: `info`)
- `--body-preview <chars>` - Opt in to text response previews, from `0` to `65536` characters (default: `0`, disabled). Binary and compressed bodies are never decoded for logging. Previews can contain sensitive data.
- `--max-body-size <bytes>` - Maximum body capture and decoded body matching size, from `1` to `67108864` bytes (default: `1048576`). Larger bodies are omitted from logging and body matching; passthrough traffic is streamed without truncation. Set this high enough for bodies that your rules need to match.
- `--add-default-passthrough` - Add unmatched passthrough if rules don't include wildcard passthrough
- `--telegram-bot-token <token>` - Telegram bot token for error notifications
- `--telegram-chat-id <id>` - Telegram chat ID for error notifications
- `--help` or `-h` - Show usage information

Unknown flags, missing values, invalid integers, and unsupported log levels fail
startup. Telegram notifications require both Telegram flags. Request logs redact
URL credentials and query values. Body-preview limits apply before text decoding;
response event capture is disabled at `warn` and `error` log levels.

## Examples

### Basic Usage

```bash
# Development mode
npm run dev -- --port 8003 --rules rules.json

# Production mode
npm run build
npm start -- --port 8003 --rules rules.json
```

### With Logging Configuration

```bash
npm run dev -- --port 8003 --rules rules.json --log-level debug --body-preview 2000
```

### With Telegram Notifications

```bash
npm run dev -- --port 8003 --rules rules.json \
--telegram-bot-token "123456789:ABCdefGHIjklMNOpqrsTUVwxyz" \
--telegram-chat-id "-1001234567890"
```

### Multiple Instances

```bash
# Terminal 1
npm run dev -- --port 8001 --rules ../rules/rules-a.json

# Terminal 2
npm run dev -- --port 8002 --rules ../rules/rules-b.json --log-level warn
```

## HTTPS Certificate

The proxy generates a CA certificate automatically on first run. To intercept HTTPS traffic:

1. Trust the generated CA certificate (stored in your user config directory)
2. Import it into your system's trusted root certification authorities
3. Restart your applications to pick up the new certificate

The built-in `amiusing.httptoolkit.tech/certificate` rule always serves this
proxy's CA, including when importing a desktop export that references another
CA file. The supplied rules use the portable `proxy-certificate` action, which
does not contain a computer-specific path. Existing CA files are checked for a
matching key and certificate; invalid material is reported without replacing a
certificate that clients may already trust.
The certificate endpoint uses the CA loaded by the running proxy. Restart after
intentionally replacing CA files; ordinary response file reloads do not rotate
the active CA.

## Available Scripts

- `npm run dev` - Run in development mode with auto-reload
- `npm run build` - Compile TypeScript to JavaScript
- `npm run build:bundle` - Type-check and bundle runtime dependencies into `dist/standalone/server.js`
- `npm run typecheck` - Check TypeScript types without building
- `npm test` - Build and run rule, networking, HTTPS, HTTP/2, WebSocket, and CLI regression tests
- `npm run test:bundle` - Build and test the standalone bundle in a directory without `node_modules`
- `npm start` - Run the compiled JavaScript (requires build first)

`server.ts`, `runtime.ts`, and `rules.ts` are the runtime source. The root
`server.js` is a compatibility launcher for the compiled output and requires
`npm run build` first. The standalone bundle contains the same runtime.

Mockttp is pinned because enforcing a bind address requires a guarded adapter
for its local transport. Socket tests verify loopback isolation. Re-run the
full test suite when upgrading it. The `basic-ftp` override installs a patched
version in the optional upstream-proxy dependency chain.

Rules format
------------

Use an HTTP Toolkit export with a root `items` array. Rules are validated before
registration, and errors identify the rule ID or its position in the tree.
Unsupported features fail startup instead of being ignored.

Groups retain their displayed order. All matchers in a rule must match.
Explicit numeric priorities and `always`, `once`, `twice`, `thrice`, and `times`
completion checkers are supported. Rules without a completion checker repeat.
An unrestricted, unprioritized, always-active wildcard passthrough becomes a
fallback; scoped passthrough retains its constraints. The flag adds an HTTP
fallback only when one is absent. WebSockets use their own exported rules.

Supported matchers: wildcard, method, protocol, host, hostname, port, simple-path,
regex-path, regex-url, header, query, exact-query-string, form-data,
multipart-form-data, raw-body, raw-body-includes, raw-body-regexp, json-body,
json-body-matching, and cookie.

Supported HTTP actions: simple responses, file responses, proxy-certificate,
passthrough, close-connection, reset-connection, and timeout. Supported WebSocket
actions: ws-passthrough, ws-echo, ws-listen, ws-reject, close-connection,
reset-connection, and timeout. Actions can be preceded by delays. Portable
passthrough forwarding, extra CA certificates, certificate-error settings, and
connection-error settings are supported. Desktop callback channels, serialized
transforms, and other unsupported action options are rejected explicitly.

Missing response files fail startup. A file removed after startup returns 500;
it does not silently return an empty successful response. Static/file response
headers are sanitized for HTTP/1 and HTTP/2, and content lengths are recomputed.

Example:

```
{
"id": "root",
"title": "Rules",
"isRoot": true,
"items": [
{
"id": "rule-1",
"type": "http",
"activated": true,
"matchers": [
{ "type": "method", "method": 1, "uiType": "POST" },
{ "type": "simple-path", "path": "https://example.com/gate" }
],
"steps": [
{
"type": "simple",
"status": 200,
"headers": { "content-type": "text/plain" },
"data": { "type": "Buffer", "data": [72,101,108,108,111] }
}
],
"completionChecker": { "type": "always" }
},
{
"id": "fallback",
"type": "http",
"activated": true,
"matchers": [ { "type": "wildcard", "uiType": "default-wildcard" } ],
"steps": [ { "type": "passthrough" } ],
"completionChecker": { "type": "always" }
}
]
}
```
140 changes: 140 additions & 0 deletions minimal-mock-proxy/mt5-router/Install-Startup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[string]$ProxyScript,
[string]$Rules,
[ValidateRange(1, 65535)][int]$ProxyPort = 8002,
[string]$RouterExe,
[string]$NodeExe,
[string]$WorkingDirectory,
[switch]$Remove,
[switch]$Plan
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest

function Resolve-StartupFile([string]$Value) {
if (-not $Value -or -not (Test-Path -LiteralPath $Value -PathType Leaf)) { throw "Required file not found: $Value" }
return (Resolve-Path -LiteralPath $Value).ProviderPath
}
function Quote-TaskArgument([string]$Value) {
if ($Value -match '["\r\n]') { throw 'Paths cannot contain quotes or line breaks.' }
return '"' + $Value + '"'
}
function Write-StartupConfig([string]$Path, [string]$Content) {
$temporary = Join-Path (Split-Path -Parent $Path) ([guid]::NewGuid().ToString() + '.tmp')
try {
[IO.File]::WriteAllText($temporary, $Content, [Text.UTF8Encoding]::new($false))
# Windows PowerShell 5 converts $null to an empty string for this .NET
# string parameter. NullString supplies a real null backup filename.
if (Test-Path -LiteralPath $Path) { [IO.File]::Replace($temporary, $Path, [NullString]::Value) }
else { [IO.File]::Move($temporary, $Path) }
} finally {
if (Test-Path -LiteralPath $temporary) { Remove-Item -LiteralPath $temporary }
}
}

$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$startupSid = $identity.User.Value
$marker = 'MT5Router login startup v1'
$proxyTaskName = "MT5Router-$startupSid-Proxy"
$routerTaskName = "MT5Router-$startupSid-Router"
$taskNames = @($proxyTaskName, $routerTaskName)
$existing = @{}
foreach ($name in $taskNames) {
$task = Get-ScheduledTask -TaskName $name -TaskPath '\' -ErrorAction SilentlyContinue
if ($null -ne $task) {
if (-not $task.Description.StartsWith($marker)) { throw "Refusing to replace an unrelated task: $name" }
$existing[$name] = Export-ScheduledTask -TaskName $name -TaskPath '\'
}
}
if ($Remove) {
if ($Plan) {
@{ operation = 'Remove'; tasks = $taskNames; stopsRunningApplications = $false } | ConvertTo-Json -Depth 3
return
}
foreach ($name in $taskNames) {
if ($existing.ContainsKey($name) -and $PSCmdlet.ShouldProcess($name, 'Remove login startup task')) {
Unregister-ScheduledTask -TaskName $name -TaskPath '\' -Confirm:$false
}
}
Write-Output 'Login startup removed. Currently running applications were left running.'
return
}
$ProxyScript = Resolve-StartupFile $ProxyScript
$Rules = Resolve-StartupFile $Rules
if (-not $RouterExe) { $RouterExe = Join-Path $PSScriptRoot 'MT5Router.exe' }
$RouterExe = Resolve-StartupFile $RouterExe
$runner = Resolve-StartupFile (Join-Path $PSScriptRoot 'Start-Proxy.ps1')
if (-not $NodeExe) { $NodeExe = (Get-Command node.exe -ErrorAction Stop).Source }
$NodeExe = Resolve-StartupFile $NodeExe
if (-not $WorkingDirectory) { $WorkingDirectory = Split-Path -Parent $Rules }
if (-not (Test-Path -LiteralPath $WorkingDirectory -PathType Container)) { throw 'Proxy working directory was not found.' }
$WorkingDirectory = (Resolve-Path -LiteralPath $WorkingDirectory).ProviderPath
$startupDirectory = Join-Path $env:LOCALAPPDATA 'MT5Router\startup'
$startupConfigPath = Join-Path $startupDirectory 'proxy-startup.json'
$proxyConfig = @{ node = $NodeExe; script = $ProxyScript; rules = $Rules; port = $ProxyPort;
workingDirectory = $WorkingDirectory; log = (Join-Path $startupDirectory 'proxy.log') } | ConvertTo-Json
$powerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$proxyArguments = '-NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File ' +
(Quote-TaskArgument $runner) + ' -StartupConfigPath ' + (Quote-TaskArgument $startupConfigPath)
$routerArguments = '--start --wait-proxy-until-ready --proxy-host 127.0.0.1 --proxy-port ' + $ProxyPort +
' --log-file ' + (Quote-TaskArgument (Join-Path $startupDirectory 'router.log'))
$planData = [ordered]@{ user = $identity.Name; trigger = 'AtLogOn'; proxyPort = $ProxyPort;
configuration = $startupConfigPath; logDirectory = $startupDirectory;
tasks = @(
@{ name = $proxyTaskName; executable = $powerShellExe; arguments = $proxyArguments; workingDirectory = $WorkingDirectory; runLevel = 'Limited' },
@{ name = $routerTaskName; executable = $RouterExe; arguments = $routerArguments; workingDirectory = (Split-Path -Parent $RouterExe); runLevel = 'Highest' }
) }
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -MultipleInstances IgnoreNew `
-ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
-AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$definitions = @{}
foreach ($item in $planData.tasks) {
$action = New-ScheduledTaskAction -Execute $item.executable -Argument $item.arguments -WorkingDirectory $item.workingDirectory
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $startupSid
$trigger.Delay = if ($item.runLevel -eq 'Highest') { 'PT10S' } else { 'PT5S' }
$principal = New-ScheduledTaskPrincipal -UserId $startupSid -LogonType Interactive -RunLevel $item.runLevel
$definitions[$item.name] = New-ScheduledTask -Action $action -Trigger $trigger -Principal $principal -Settings $settings `
-Description "$marker - $($item.name)"
$item.principalSid = $principal.UserId
$item.logonType = $principal.LogonType
$item.delay = $trigger.Delay
}
$planData.settings = @{ executionTimeLimit = $settings.ExecutionTimeLimit;
multipleInstances = $settings.MultipleInstances; restartCount = $settings.RestartCount }
if ($Plan) { $planData | ConvertTo-Json -Depth 5; return }
if (-not $PSCmdlet.ShouldProcess($identity.Name, 'Install proxy and elevated MT5Router login startup tasks')) { return }
$administrator = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $administrator.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this installer once from PowerShell opened as administrator, using the same Windows account as MT5.'
}
$registered = @()
$previousConfig = if (Test-Path -LiteralPath $startupConfigPath) { [IO.File]::ReadAllText($startupConfigPath) } else { $null }
try {
New-Item -ItemType Directory -Path $startupDirectory -Force | Out-Null
Write-StartupConfig $startupConfigPath $proxyConfig
foreach ($item in $planData.tasks) {
Register-ScheduledTask -TaskName $item.name -TaskPath '\' -InputObject $definitions[$item.name] -Force | Out-Null
$registered += $item.name
}
} catch {
$failure = $_
foreach ($name in $registered) {
try {
if ($existing.ContainsKey($name)) { Register-ScheduledTask -TaskName $name -TaskPath '\' -Xml $existing[$name] -Force | Out-Null }
else { Unregister-ScheduledTask -TaskName $name -TaskPath '\' -Confirm:$false }
} catch {
Write-Warning "Rollback failed for task ${name}: $($_.Exception.Message)" -WarningAction Continue
}
}
try {
if ($null -ne $previousConfig) { Write-StartupConfig $startupConfigPath $previousConfig }
elseif (Test-Path -LiteralPath $startupConfigPath) { Remove-Item -LiteralPath $startupConfigPath }
} catch {
Write-Warning "Rollback failed for startup configuration: $($_.Exception.Message)" -WarningAction Continue
}
throw $failure
}
Write-Output "Installed login startup for $($identity.Name), proxy port $ProxyPort."
Write-Output 'The tasks will run at your next login. They have not been started by this installer.'
Write-Output "Logs: $startupDirectory"
Loading