param([string]$OutputDir = "$PSScriptRoot/../artifacts/wss", [ValidateSet(443, 8080)][int]$ServerPort = 443) $ErrorActionPreference = 'Stop' $serverHost = 'banban.api.talkingq.com' New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path $tcp = New-Object System.Net.Sockets.TcpClient $tls = $null try { $pending = $tcp.BeginConnect($serverHost, $serverPort, $null, $null) if (-not $pending.AsyncWaitHandle.WaitOne(15000)) { throw 'TCP connect timeout' } $tcp.EndConnect($pending) $tcp.ReceiveTimeout = 15000 $tcp.SendTimeout = 15000 # Default SslStream validation checks the trusted chain and server hostname. $tls = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false) $tls.ReadTimeout = 15000 $tls.WriteTimeout = 15000 $tls.AuthenticateAsClient($serverHost, $null, [System.Security.Authentication.SslProtocols]::Tls12, $false) $leaf = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate) $chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain $chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck if (-not $chain.Build($leaf)) { throw 'Trusted certificate chain could not be built' } $certificates = @() for ($i = 0; $i -lt $chain.ChainElements.Count; $i++) { $cert = $chain.ChainElements[$i].Certificate $pem = "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($cert.RawData, [Base64FormattingOptions]::InsertLineBreaks).Replace("`r`n", "`n") + "`n-----END CERTIFICATE-----`n" [IO.File]::WriteAllText((Join-Path $OutputDir "chain-$i.pem"), $pem, [Text.Encoding]::ASCII) $certificates += [ordered]@{subject=$cert.Subject; issuer=$cert.Issuer; not_before=$cert.NotBefore.ToUniversalTime().ToString('o'); not_after=$cert.NotAfter.ToUniversalTime().ToString('o'); thumbprint=$cert.Thumbprint} if ($i -eq $chain.ChainElements.Count - 1) { if ($cert.Subject -ne $cert.Issuer) { throw 'Chain did not terminate at a root CA' } [IO.File]::WriteAllText((Join-Path $OutputDir 'root-ca.pem'), $pem, [Text.Encoding]::ASCII) } } $nonce = New-Object byte[] 16 $rng = [Security.Cryptography.RandomNumberGenerator]::Create() $rng.GetBytes($nonce) $rng.Dispose() $key = [Convert]::ToBase64String($nonce) $request = "GET /ws HTTP/1.1`r`nHost: ${serverHost}:${serverPort}`r`nUpgrade: websocket`r`nConnection: Upgrade`r`nSec-WebSocket-Key: $key`r`nSec-WebSocket-Version: 13`r`n`r`n" $bytes = [Text.Encoding]::ASCII.GetBytes($request) $tls.Write($bytes, 0, $bytes.Length) $response = '' while (-not $response.EndsWith("`r`n`r`n") -and $response.Length -lt 16384) { $next = $tls.ReadByte() if ($next -lt 0) { break } $response += [char]$next } $sha1 = [Security.Cryptography.SHA1]::Create() $expected = [Convert]::ToBase64String($sha1.ComputeHash([Text.Encoding]::ASCII.GetBytes($key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))) $sha1.Dispose() $accepted = $response -match '^HTTP/1\.1 101 ' -and $response -match ('(?im)^Sec-WebSocket-Accept:\s*' + [regex]::Escape($expected) + '\s*$') $report = [ordered]@{url="wss://${serverHost}:${serverPort}/ws"; tested_utc=[DateTime]::UtcNow.ToString('o'); tls_protocol=$tls.SslProtocol.ToString(); cipher=$tls.CipherAlgorithm.ToString(); cipher_bits=$tls.CipherStrength; certificate_validation='System trust and hostname validation passed'; certificates=$certificates; websocket_upgrade=$accepted; response_headers=$response; device_auth_sent=$false} $report | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath (Join-Path $OutputDir 'server-probe.json') $report | ConvertTo-Json -Depth 6 if (-not $accepted) { throw 'WebSocket upgrade did not return a valid HTTP 101 response' } } finally { if ($tls) { $tls.Dispose() } $tcp.Close() }