Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' function Initialize-XtrhImageVerification { param([Parameter(Mandatory)][string]$CredentialFile) # 私有文件只读入内存。日志、stdout、命令参数和联调记录都不携带凭证正文。 $privateText = [IO.File]::ReadAllText([IO.Path]::GetFullPath($CredentialFile)) $match = [regex]::Match($privateText, '(?m)^fusion\.token\s*=\s*([^\r\n]+)') if (-not $match.Success -or $match.Groups[1].Value.Trim().Length -lt 16) { throw '集群私有凭证文件无效。' } $script:imageVerificationCredential = $match.Groups[1].Value.Trim() $handler = [Net.Http.HttpClientHandler]::new() $handler.AllowAutoRedirect = $false $handler.UseCookies = $false $script:imageVerificationClient = [Net.Http.HttpClient]::new($handler, $true) $script:imageVerificationClient.Timeout = [TimeSpan]::FromSeconds(5) $script:imageVerificationClient.MaxResponseContentBufferSize = 8 * 1024 * 1024 } function Get-XtrhImageDigest { param([Parameter(Mandatory)][AllowEmptyCollection()][byte[]]$Bytes) $sha = [Security.Cryptography.SHA256]::Create() try { return [BitConverter]::ToString($sha.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } } function Get-XtrhImageSignature { param([Parameter(Mandatory)][string]$Canonical) $mac = [Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($script:imageVerificationCredential)) try { return [BitConverter]::ToString($mac.ComputeHash([Text.Encoding]::UTF8.GetBytes($Canonical))).Replace('-', '').ToLowerInvariant() } finally { $mac.Dispose() } } function Invoke-XtrhImageSigned { param([Parameter(Mandatory)][string]$BaseUrl, [Parameter(Mandatory)][string]$Path, [ValidateSet('GET','POST','PATCH')][string]$Method = 'GET', $Payload = $null) $base = [Uri]$BaseUrl if ($base.Scheme -notin @('http','https') -or $base.UserInfo -or $base.Query -or $base.Fragment -or $base.AbsolutePath -ne '/') { throw '服务根地址格式无效。' } if ($Path -notmatch '^/' -or $Path -match '[\r\n]') { throw '签名接口路径无效。' } # 空数组不能通过 PowerShell 表达式管道返回,否则会被展开为 null。 [byte[]]$body = [byte[]]::new(0) if ($null -ne $Payload) { $body = [Text.Encoding]::UTF8.GetBytes(($Payload | ConvertTo-Json -Depth 12 -Compress)) } $nonce = [Guid]::NewGuid().ToString() $time = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds().ToString() $digest = Get-XtrhImageDigest -Bytes $body $signature = Get-XtrhImageSignature -Canonical "request`n$Method`n$Path`n$time`n$nonce`n$digest" $request = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::new($Method), $BaseUrl.TrimEnd('/') + $Path) try { [void]$request.Headers.TryAddWithoutValidation('X-Xtrh-Time', $time) [void]$request.Headers.TryAddWithoutValidation('X-Xtrh-Nonce', $nonce) [void]$request.Headers.TryAddWithoutValidation('X-Xtrh-Signature', $signature) $request.Content = [Net.Http.ByteArrayContent]::new($body) $request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::new('application/json') try { $response = $script:imageVerificationClient.SendAsync($request).GetAwaiter().GetResult() } catch { throw '服务请求失败,未输出私有请求头或响应内容。' } try { [byte[]]$bytes = $response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult() $status = [int]$response.StatusCode $expected = Get-XtrhImageSignature -Canonical ("response`n$nonce`n$status`n" + (Get-XtrhImageDigest -Bytes $bytes)) $headers = [Collections.Generic.IEnumerable[string]]$null if (-not $response.Headers.TryGetValues('X-Xtrh-Response-Signature', [ref]$headers)) { throw '响应缺少服务身份签名。' } $actual = @($headers)[0] # 固定长度与逐字节累计比较;JPEG 必须签其原始字节,不能按文本转换。 $different = $actual.Length -bxor $expected.Length if ($actual.Length -eq $expected.Length) { for ($i = 0; $i -lt $expected.Length; $i++) { $different = $different -bor ([int]$actual[$i] -bxor [int]$expected[$i]) } } if ($different -ne 0) { throw '响应服务身份认证失败。' } return [pscustomobject]@{ Status = $status; Bytes = $bytes; ContentType = [string]$response.Content.Headers.ContentType } } finally { $response.Dispose() } } finally { $request.Dispose() } } function ConvertFrom-XtrhImageJson { param([Parameter(Mandatory)]$Response) if ($Response.Status -ne 200) { throw ('签名服务接口返回 HTTP ' + $Response.Status) } return [Text.Encoding]::UTF8.GetString($Response.Bytes) | ConvertFrom-Json } function Get-XtrhImageSnapshot { param([Parameter(Mandatory)][string]$CoordinatorUrl) return ConvertFrom-XtrhImageJson (Invoke-XtrhImageSigned -BaseUrl $CoordinatorUrl -Path '/control/snapshot') } function Close-XtrhImageVerification { if ($null -ne $script:imageVerificationClient) { $script:imageVerificationClient.Dispose() } $script:imageVerificationCredential = '' }