|
# 本 Powershell 脚本用于 DockerHub 镜像的测速 |
|
# 传入域名列表,自动测速并按速度输出排名 |
|
# |
|
# 使用默认配置 |
|
# .\docker-mirror-speedtest.ps1 |
|
# |
|
# 自定义域名和测试目标 |
|
# .\docker-mirror-speedtest.ps1 -Domains @("docker.1ms.run", "hub1.nat.tf") |
|
|
|
[CmdletBinding()] |
|
param ( |
|
[Parameter(ValueFromPipeline = $true)] |
|
[string[]]$Domains = @( |
|
# list from status monitor sites |
|
# https://status.anye.xyz/status/docker |
|
# https://status.1panel.top/status/docker |
|
"docker.1panel.live", |
|
"docker.1panel.top", |
|
"docker.m.daocloud.io", |
|
"docker.ketches.cn", |
|
"docker.1ms.run", |
|
"hub1.nat.tf", |
|
"hub2.nat.tf", |
|
"hub.rat.dev" |
|
) |
|
) |
|
|
|
# 设置输出编码为UTF8,以支持特殊字符 |
|
$OutputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 |
|
|
|
$TestTarget = @{ |
|
Name = "Nginx" |
|
Path = "library/nginx" # size: 43,913,652 |
|
Digest = "sha256:8c18fec49d4bdf0813737297228f0afbdc26ed7f8f3fd7e66048114d47d03168" |
|
} |
|
|
|
# 定义颜色输出函数 |
|
function Write-ColorOutput { |
|
param ([string]$Text, [string]$Color = "White") |
|
Write-Host $Text -ForegroundColor $Color |
|
} |
|
|
|
function Parse-WwwAuthenticate { |
|
param ( |
|
[string]$AuthStr, |
|
[hashtable]$Target |
|
) |
|
|
|
$splits = $AuthStr -split '\s+' |
|
$bearer = $splits[0] |
|
if (!$bearer -or $bearer.ToLower() -ne 'bearer') { |
|
throw "Invalid Www-Authenticate: $AuthStr" |
|
} |
|
|
|
$params = $splits[1..($splits.Length - 1)] -join '' -split ',' |
|
$result = @{} |
|
foreach ($param in $params) { |
|
$kvPair = $param -split '=', 2 |
|
if ($kvPair.Length -eq 2) { |
|
$k = $kvPair[0].Trim() |
|
$v = $kvPair[1].Trim() -replace '^"|"$', '' |
|
$result[$k] = $v |
|
} |
|
} |
|
$result["scope"] = "respository:$($Target.Path):pull" |
|
return $result |
|
} |
|
|
|
function Get-DockerToken { |
|
param ( |
|
[string]$Realm, |
|
[string]$Service, |
|
[string]$Scope |
|
) |
|
|
|
$tokenUrl = [System.UriBuilder]$Realm |
|
$query = [System.Web.HttpUtility]::ParseQueryString('') |
|
$query['service'] = $Service |
|
$query['scope'] = $Scope |
|
$tokenUrl.Query = $query.ToString() |
|
|
|
$response = Invoke-RestMethod -Uri $tokenUrl.Uri.ToString() -Method Get |
|
return $response.token |
|
} |
|
|
|
# 添加结果收集类 |
|
class SpeedTestResult { |
|
[string]$Domain |
|
[string]$ImageName |
|
[double]$Speed |
|
[double]$ConnectTime |
|
[bool]$Success |
|
[string]$Error |
|
|
|
[string] GetSpeedString() { |
|
if (-not $this.Success) { return "N/A" } |
|
$nums = $this.Speed |
|
$unit = "B/s" |
|
if ($nums -gt 1024 * 1024) { |
|
$nums = [double]([math]::Round($nums / (1024 * 1024), 2)) |
|
$unit = "MB/s" |
|
} elseif ($nums -gt 1024) { |
|
$nums = [double]([math]::Round($nums / 1024, 2)) |
|
$unit = "KB/s" |
|
} |
|
return "$nums $unit" |
|
} |
|
} |
|
|
|
$global:Results = [System.Collections.ArrayList]::new() |
|
|
|
function Invoke-CurlCommand { |
|
param ( |
|
[Parameter(Mandatory, Position = 0)] |
|
[string[]]$Args, |
|
[Parameter(Mandatory, Position = 1)] |
|
[string]$Url |
|
) |
|
|
|
# 构建完整参数数组 |
|
$fullArgs = $Args + @( |
|
"--user-agent", "curl/8.11.1 (docker-mirror-speedtest.ps1)", |
|
"$Url" |
|
) |
|
|
|
# 调试输出完整命令 |
|
Write-Debug "即将执行: curl $($fullArgs -join ' ')" |
|
return & curl $fullArgs |
|
} |
|
|
|
function Test-RegistrySpeed { |
|
param ( |
|
[string]$Domain, |
|
[hashtable]$Target |
|
) |
|
|
|
$dsize = 41943040 # 40MB |
|
$skill = 102400 # 100KB/s |
|
|
|
$result = [SpeedTestResult]::new() |
|
$result.Domain = $Domain |
|
$result.ImageName = $Target.Name |
|
|
|
try { |
|
Write-Debug "开始测试域名: $Domain" |
|
$probAuthUrl = "https://$Domain/v2/" |
|
$downloadUrl = "https://$Domain/v2/$($Target.Path)/blobs/$($Target.Digest)" |
|
Write-Debug "测试URL: $downloadUrl" |
|
|
|
# 初始请求 |
|
$args = @( "-I", "-L", "--max-time", "8" ) |
|
$response = Invoke-CurlCommand $args $probAuthUrl 2>&1 |
|
Write-Debug "收到响应:`n$($response | Out-String)" |
|
|
|
# 检查状态码 |
|
$statusLine = $response | Where-Object { $_ -match "^HTTP/.+ (\d{3}) " } | Select-Object -First 1 |
|
if ($statusLine -match "200") { |
|
# 直接可访问,无需认证 |
|
Write-ColorOutput "☑️ [$Domain] 无需认证,直接测速..." |
|
$redirectUrl = $downloadUrl |
|
} else { |
|
# 需要认证,解析WWW-Authenticate头 |
|
$authHeader = $response | Where-Object { $_ -match "Www-Authenticate: " } | Select-Object -First 1 |
|
if ($authHeader) { |
|
Write-ColorOutput "🔑 [$Domain] 需要认证,正在获取token..." |
|
$authStr = ($authHeader -split ": ")[1] |
|
Write-Debug "认证字符串: $authStr" |
|
|
|
$auth = Parse-WwwAuthenticate $authStr -Target $Target |
|
Write-Debug "解析后的认证信息: $($auth | ConvertTo-Json)" |
|
|
|
$token = Get-DockerToken -Realm $auth.realm -Service $auth.service -Scope "repository:$($Target.Path)`:pull" |
|
Write-Debug "获取到token: $($token.Substring(0, [Math]::Min(20, $token.Length)))..." |
|
|
|
# 使用token获取重定向URL |
|
Write-ColorOutput "📍 [$Domain] 获取下载地址..." |
|
$args = @( |
|
"-s", "-I", |
|
"-H", "Authorization: Bearer $token", |
|
"-w", "%{redirect_url}", |
|
"-o", "nul") |
|
$redirectUrl = Invoke-CurlCommand $args $downloadUrl |
|
Write-Debug "重定向URL: $redirectUrl" |
|
if ($null -eq $redirectUrl) { |
|
throw "资源重定向失败" |
|
} |
|
} else { |
|
$statusLine = $response | Where-Object { $_ -match "^HTTP/.+ (\d{3}.+)" } | ForEach-Object { $Matches.1 } |
|
if ($statusLine) { |
|
throw "无法获取认证信息 ($statusLine)" |
|
} |
|
$statusLine = $response | Where-Object { $_ -match "(curl:.+)" } | ForEach-Object { $Matches.1 } |
|
if ($statusLine) { |
|
throw "其他异常 ($statusLine)" |
|
} |
|
throw "未知错误: $($response | Out-String)" |
|
} |
|
} |
|
|
|
# 进行速度测试 |
|
Write-ColorOutput "🏃 [$Domain] 正在测速..." "Blue" |
|
|
|
$curl_args = @( |
|
"-L", |
|
"-w", "DNS解析时间: %{time_namelookup}s 连接时间: %{time_connect}s 总时间: %{time_total}s 下载速度: %{speed_download}B/s ", |
|
"-o", "nul", |
|
"--range", "0-$dsize", |
|
"--speed-time", "5", |
|
"--speed-limit", "$skill" |
|
) |
|
if ($token) { |
|
$curl_args += @("-H", "Authorization: Bearer $token") |
|
} |
|
|
|
$resultOutput = Invoke-CurlCommand $curl_args $redirectUrl |
|
Write-ColorOutput "📊 [$Domain] 测速结果: $resultOutput" |
|
|
|
$resultOutput -match '连接时间: (?<ctime>\d+\.?\d*)s.+下载速度: (?<speed>\d+\.?\d*)B/s' | Out-Null |
|
|
|
# 解析测速结果 |
|
$result.Speed = $Matches.speed |
|
$result.ConnectTime = $Matches.ctime |
|
$result.Success = $true |
|
} catch { |
|
$result.Error = $_.Exception.Message |
|
$result.Success = $false |
|
Write-ColorOutput "❌ [$Domain] 测速异常: $($result.Error)" "Red" |
|
} |
|
|
|
[void]$global:Results.Add($result) |
|
return $result |
|
} |
|
|
|
function Show-TestSummary { |
|
$successful = $global:Results | Where-Object { $_.Success } |
|
$failed = $global:Results | Where-Object { -not $_.Success } |
|
|
|
Write-Host "`n`n═══════════════ 测试总结 ═══════════════" -ForegroundColor Magenta |
|
|
|
# 统计信息 |
|
Write-Host "✓ 成功测试: $($successful.Count)" -ForegroundColor Green |
|
Write-Host "✗ 失败测试: $($failed.Count)" -ForegroundColor Red |
|
Write-Host "Σ 总计测试: $($global:Results.Count)" -ForegroundColor Cyan |
|
|
|
# 显示排名 |
|
if ($successful.Count -gt 0) { |
|
Write-Host "`n🏆 速度排名:" -ForegroundColor Yellow |
|
$rank = 1 |
|
$successful | Sort-Object Speed -Descending | ForEach-Object { |
|
$color = switch ($rank) { |
|
1 { "Yellow" } |
|
2 { "White" } |
|
3 { "DarkYellow" } |
|
default { "Gray" } |
|
} |
|
|
|
$medal = switch ($rank) { |
|
1 { "🥇" } |
|
2 { "🥈" } |
|
3 { "🥉" } |
|
default { " " } |
|
} |
|
|
|
Write-Host ("{0} [{1,2}] {2,-30} {3,10}" -f $medal, |
|
$rank, |
|
"$($_.Domain)", |
|
$_.GetSpeedString()) -ForegroundColor $color |
|
$rank++ |
|
} |
|
} |
|
|
|
# 失败清单 |
|
if ($failed.Count -gt 0) { |
|
Write-Host "`n❌ 失败详情:" -ForegroundColor Red |
|
$failed | ForEach-Object { |
|
Write-Host " • $($_.Domain)" -ForegroundColor DarkRed |
|
Write-Host " $($_.Error)" -ForegroundColor DarkGray |
|
} |
|
} |
|
} |
|
|
|
# 主程序入口 |
|
Write-ColorOutput "🚀 开始 Docker 镜像站测速..." "Cyan" |
|
Write-Debug "测试域名列表: $($Domains -join ', ')" |
|
|
|
try { |
|
foreach ($domain in $Domains) { |
|
Test-RegistrySpeed -Domain $domain -Target $TestTarget | Out-Null |
|
} |
|
} finally { |
|
# 完成后显示总结 |
|
Show-TestSummary |
|
} |