$ErrorActionPreference = "Stop" $script:assertions = 0 $script:connections = 0 $TimeoutMs = 1000 function Assert-True([bool]$Condition, [string]$Message) { $script:assertions++ if (-not $Condition) { throw $Message } } function Assert-Rejected([scriptblock]$Call) { $before = $script:connections $caught = $false try { & $Call | Out-Null } catch { $caught = $true Assert-True ($_.Exception.Message -eq "unsupported_diagnostic_operation_stage_a") "Must reject before transport" } Assert-True $caught "Unsupported diagnostics must throw" Assert-True ($script:connections -eq $before) "Rejected request must create zero sockets and write zero bytes" } function Import-Definitions([string]$Path) { $tokens = $null; $errors = $null $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) Assert-True ($errors.Count -eq 0) "Production script must parse" foreach ($definition in $ast.FindAll({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Parent -is [System.Management.Automation.Language.NamedBlockAst] }, $true)) { . ([scriptblock]::Create("function global:" + $definition.Name + " " + $definition.Body.Extent.Text)) } } # Load only real function definitions: no config, Hub request, or scheduled loop. $root = Split-Path $PSScriptRoot -Parent Import-Definitions (Join-Path $root "EnrichOneMiner.ps1") Import-Definitions (Join-Path $root "MinerOpsApi3EnrichmentPoller.ps1") Import-Definitions (Join-Path $root "MinerOpsPoolPoller.ps1") function New-Object { param([Parameter(Position=0)][string]$TypeName, [Parameter(Position=1)][object[]]$ArgumentList) if ($TypeName -eq "System.Net.Sockets.TcpClient") { $script:connections++ } if ($null -ne $ArgumentList) { return Microsoft.PowerShell.Utility\New-Object -TypeName $TypeName -ArgumentList $ArgumentList } return Microsoft.PowerShell.Utility\New-Object -TypeName $TypeName } # Auditor reproductions plus write families, alias/parameter overrides, injection, # malformed structures and operations that merely contain a read-looking substring. foreach ($command in @("reboot", "set.miner.service", "power_off", "switchpool", "ascset", "summary|reboot", "summary`nreboot", 'summary","cmd":"reboot', "read_then_reboot", "SUMMARY", "", "get.device.info", "get_miner_info|all")) { Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $command } } Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command "summary" -Port 4433 } $badRequests = @( @{ cmd = "set.miner.service"; param = "stop" }, @{ cmd = "reboot" }, @{ cmd = "set.miner.power_mode"; param = "high" }, @{ cmd = "set.device.password"; param = "changed" }, @{ cmd = "get.read_then_write" }, @{ cmd = "get.device.info"; command = "reboot" }, @{ cmd = "get.device.info"; param = "stop" }, @{ cmd = "get.device.info"; account = "operator"; token = "anything" }, @{ cmd = "get.miner.status" }, @{ cmd = "get.miner.status"; param = "all" }, @{ cmd = "get.miner.status"; param = @("summary+pools+edevs") }, @{ cmd = "get.miner.status"; param = "summary+pools+edevs"; command = "reboot" }, @{ Cmd = "get.device.info" }, @{ cmd = "GET.DEVICE.INFO" }, @{ cmd = "get.device.info`nreboot" }, @{ cmd = @("get.device.info", "reboot") }, @{ cmd = $null }, @{} ) foreach ($request in $badRequests) { Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request } Assert-Rejected { Invoke-Api3OkCommand -Ip "127.0.0.1" -Request $request } } # Exact re-audit suffixes: U+0000, U+00AD and U+200B; also prefix/embedded # invisible characters, controls and whitespace. Never trim or normalize inputs. $originalCulture = [Threading.Thread]::CurrentThread.CurrentCulture try { foreach ($cultureName in @("en-US", "tr-TR", "")) { [Threading.Thread]::CurrentThread.CurrentCulture = [Globalization.CultureInfo]::GetCultureInfo($cultureName) $variants = @() foreach ($code in @(0, 0x00ad, 0x200b, 0x200c, 0x200d, 0x2060, 0xfeff, 0x001f, 13, 10, 9, 32, 0x00a0)) { $character = [string][char]$code foreach ($position in @("prefix", "embedded", "suffix")) { $variants += @{ Character = $character; Position = $position } } } foreach ($variant in $variants) { $legacy = "summary"; $api = "get.device.info"; $key = "cmd"; $param = "summary+pools+edevs" if ($variant.Position -eq "prefix") { $legacy = $variant.Character + $legacy; $api = $variant.Character + $api $key = $variant.Character + $key; $param = $variant.Character + $param } elseif ($variant.Position -eq "embedded") { $legacy = "sum" + $variant.Character + "mary"; $api = "get.device." + $variant.Character + "info" $key = "c" + $variant.Character + "md"; $param = "summary+" + $variant.Character + "pools+edevs" } else { $legacy += $variant.Character; $api += $variant.Character $key += $variant.Character; $param += $variant.Character } Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $legacy } Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{ cmd = $api } } Assert-Rejected { Invoke-Api3OkCommand -Ip "127.0.0.1" -Request @{ cmd = $api } } $request = @{}; $request[$key] = "get.device.info" Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request } Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{ cmd = "get.miner.status"; param = $param } } $request = @{ cmd = "get.miner.status" }; $request[$key.Replace("cmd", "param")] = "summary+pools+edevs" Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request $request } } foreach ($legacy in @(("summar" + [char]0x0443), ([char]0xff53 + "ummary"), ("summ" + [char]0x0061 + [char]0x0301 + "ry"), ("summ" + [char]0x00e1 + "ry"), "Summary", "summary/", "summary+")) { Assert-Rejected { Invoke-MinerCommandRaw -IpAddress "127.0.0.1" -Command $legacy } } foreach ($api in @(("get.dev" + [char]0x0456 + "ce.info"), ("get" + [char]0xff0e + "device.info"), ([char]0xff47 + "et.device.info"), "Get.Device.Info", "get.device.info/", "get.device.info+")) { Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd=$api} } } Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd="get.miner.status"; Param="summary+pools+edevs"} } Assert-Rejected { Invoke-WhatsMinerApi3Read -Ip "127.0.0.1" -Request @{cmd="get.miner.status"; param="Summary+pools+edevs"} } } } finally { [Threading.Thread]::CurrentThread.CurrentCulture = $originalCulture } Assert-True ($script:connections -eq 0) "All negative cases produced zero transport connections/writes" # Real TCP compatibility tests: bind loopback only, one request per simulator. # This suite must be run with physical networks disconnected (Docker --network none). Add-Type -TypeDefinition @' using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; public class MinerOpsDiagnosticSimulator : IDisposable { TcpListener listener; Thread thread; public string Payload; public string Error; public MinerOpsDiagnosticSimulator(int port, bool framed) { listener = new TcpListener(IPAddress.Loopback, port); listener.Start(); thread = new Thread(() => { try { using (var client = listener.AcceptTcpClient()) { client.ReceiveTimeout = 2000; var stream = client.GetStream(); var buffer = new byte[65536]; int count; if (framed) { var length = new byte[4]; ReadExact(stream, length, 4); count = BitConverter.ToInt32(length, 0); if(count < 1 || count > buffer.Length) throw new Exception("Bad request length"); ReadExact(stream, buffer, count); } else { count = stream.Read(buffer, 0, buffer.Length); } Payload = Encoding.ASCII.GetString(buffer, 0, count); var response = Encoding.ASCII.GetBytes("{\"code\":0,\"POOLS\":[],\"msg\":{}}"); if(framed) { var length = BitConverter.GetBytes(response.Length); stream.Write(length,0,4); } stream.Write(response, 0, response.Length); } } catch(Exception e) { Error = e.Message; } finally { listener.Stop(); } }); thread.IsBackground = true; thread.Start(); } static void ReadExact(NetworkStream stream, byte[] bytes, int count) { int offset=0; while(offset