mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-07-26 07:58:14 +00:00
887429a729
Replaced hardcoded PVE host IP and API token in Capture-UploadDiff.ps1 with placeholder values that must be filled in before running. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
193 lines
7.6 KiB
PowerShell
193 lines
7.6 KiB
PowerShell
#Requires -Version 7.0
|
|
# Capture-UploadDiff.ps1
|
|
# Starts mitmproxy, captures working PS approach vs C# module upload,
|
|
# then prints a side-by-side diff of the multipart framing.
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$DebugDir = $PSScriptRoot
|
|
$ProxyPort = 8080
|
|
$TestFile = '/tmp/pvetest.iso'
|
|
# Replace with your PVE host and API token before running
|
|
$UploadUri = 'https://<PVE_HOST>:8006/api2/json/nodes/pve/storage/local/upload'
|
|
$AuthHeader = 'PVEAPIToken=<USER>@<REALM>!<TOKENID>=<TOKEN_UUID>'
|
|
$ModuleDll = '/Users/goodolclint/Source/PSProxmoxVE/src/PSProxmoxVE/bin/Debug/net9.0/PSProxmoxVE.dll'
|
|
|
|
if (-not (Test-Path $TestFile)) {
|
|
Write-Error "Test file not found: $TestFile"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
function Start-Mitmdump([string]$FlowFile) {
|
|
Get-Process mitmdump -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
|
Start-Sleep -Seconds 1
|
|
Remove-Item $FlowFile -ErrorAction SilentlyContinue
|
|
|
|
$p = Start-Process mitmdump -ArgumentList @(
|
|
'--mode', 'regular',
|
|
'--ssl-insecure',
|
|
'--listen-port', $ProxyPort,
|
|
'-w', $FlowFile
|
|
) -PassThru -RedirectStandardOutput '/tmp/mitmdump-out.log' -RedirectStandardError '/tmp/mitmdump-err.log'
|
|
|
|
Start-Sleep -Seconds 2
|
|
if ($p.HasExited) {
|
|
Write-Error "mitmdump failed to start: $(Get-Content /tmp/mitmdump-err.log -Raw)"
|
|
}
|
|
Write-Host " mitmdump PID $($p.Id)" -ForegroundColor DarkGray
|
|
return $p
|
|
}
|
|
|
|
function Stop-Mitmdump($p) {
|
|
Start-Sleep -Seconds 1
|
|
if (-not $p.HasExited) { $p.Kill(); $p.WaitForExit(3000) | Out-Null }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Write-Host '=== Step 1: Capture working PowerShell HttpClient upload ===' -ForegroundColor Cyan
|
|
$psFlow = "$DebugDir/ps-module-capture.flow"
|
|
$mitm = Start-Mitmdump $psFlow
|
|
|
|
try {
|
|
$handler = [System.Net.Http.HttpClientHandler]::new()
|
|
$handler.ServerCertificateCustomValidationCallback = [System.Net.Http.HttpClientHandler]::DangerousAcceptAnyServerCertificateValidator
|
|
$handler.Proxy = [System.Net.WebProxy]::new("http://localhost:$ProxyPort")
|
|
$handler.UseProxy = $true
|
|
$client = [System.Net.Http.HttpClient]::new($handler)
|
|
$client.Timeout = [TimeSpan]::FromMinutes(5)
|
|
$client.DefaultRequestHeaders.TryAddWithoutValidation('Authorization', $AuthHeader) | Out-Null
|
|
|
|
$boundary = 'PSDirectBoundary12345678'
|
|
$content = [System.Net.Http.MultipartFormDataContent]::new($boundary)
|
|
$content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("multipart/form-data; boundary=$boundary")
|
|
|
|
function New-TextPart([string]$name, [string]$value) {
|
|
$part = [System.Net.Http.StringContent]::new($value, [System.Text.Encoding]::UTF8)
|
|
$part.Headers.ContentType = $null
|
|
$part.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
|
|
$part.Headers.ContentDisposition.Name = "`"$name`""
|
|
return $part
|
|
}
|
|
|
|
$content.Add((New-TextPart 'content' 'iso'))
|
|
|
|
$fileStream = [System.IO.File]::OpenRead($TestFile)
|
|
$fileName = [System.IO.Path]::GetFileName($TestFile)
|
|
$fileContent = [System.Net.Http.StreamContent]::new($fileStream)
|
|
$fileContent.Headers.ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new('form-data')
|
|
$fileContent.Headers.ContentDisposition.Name = '"filename"'
|
|
$fileContent.Headers.ContentDisposition.FileName = "`"$fileName`""
|
|
$fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream')
|
|
$content.Add($fileContent)
|
|
|
|
Write-Host " Posting..."
|
|
$response = $client.PostAsync($UploadUri, $content).GetAwaiter().GetResult()
|
|
$body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
Write-Host " Status: $($response.StatusCode)" -ForegroundColor Green
|
|
Write-Host " Body: $body"
|
|
} catch {
|
|
Write-Host " ERROR: $_" -ForegroundColor Red
|
|
} finally {
|
|
if ($fileStream) { $fileStream.Dispose() }
|
|
if ($client) { $client.Dispose() }
|
|
}
|
|
|
|
Stop-Mitmdump $mitm
|
|
Write-Host " Saved: $psFlow" -ForegroundColor Green
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Write-Host ''
|
|
Write-Host '=== Step 2: Capture C# module upload ===' -ForegroundColor Cyan
|
|
$moduleFlow = "$DebugDir/cs-module-capture.flow"
|
|
$mitm = Start-Mitmdump $moduleFlow
|
|
|
|
# Route HttpClient through proxy via env vars (.NET honors HTTPS_PROXY)
|
|
$env:HTTPS_PROXY = "http://localhost:$ProxyPort"
|
|
$env:HTTP_PROXY = "http://localhost:$ProxyPort"
|
|
|
|
try {
|
|
Import-Module $ModuleDll -Force -ErrorAction Stop
|
|
|
|
# Replace with your PVE host and API token before running
|
|
Connect-PveServer -Server '<PVE_HOST>' -Port 8006 `
|
|
-ApiToken '<USER>@<REALM>!<TOKENID>=<TOKEN_UUID>' `
|
|
-SkipCertificateCheck
|
|
|
|
Write-Host " Calling Send-PveIso..."
|
|
Send-PveIso -Node pve -Storage local -Path $TestFile -Confirm:$false -ErrorAction Stop
|
|
Write-Host " Upload succeeded!" -ForegroundColor Green
|
|
} catch {
|
|
Write-Host " ERROR: $_" -ForegroundColor Red
|
|
Write-Host " Type: $($_.Exception.GetType().FullName)"
|
|
if ($_.Exception.InnerException) {
|
|
Write-Host " Inner: $($_.Exception.InnerException.Message)"
|
|
}
|
|
} finally {
|
|
$env:HTTPS_PROXY = ''
|
|
$env:HTTP_PROXY = ''
|
|
}
|
|
|
|
Stop-Mitmdump $mitm
|
|
Write-Host " Saved: $moduleFlow" -ForegroundColor Green
|
|
|
|
# ---------------------------------------------------------------------------
|
|
Write-Host ''
|
|
Write-Host '=== Step 3: Diff multipart framing ===' -ForegroundColor Cyan
|
|
|
|
$analyzeScript = @'
|
|
import sys
|
|
from mitmproxy.io import FlowReader
|
|
|
|
def dump_flow(path, label):
|
|
print(f'\n{"=" * 70}')
|
|
print(f' {label}')
|
|
print(f'{"=" * 70}')
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
reader = FlowReader(f)
|
|
flows = list(reader.stream())
|
|
if not flows:
|
|
print(' NO FLOWS CAPTURED')
|
|
return
|
|
for i, flow in enumerate(flows):
|
|
req = flow.request
|
|
print(f'\nFlow {i}: {req.method} {req.pretty_url}')
|
|
print(f'HTTP version: {req.http_version}')
|
|
print(f'\nRequest headers:')
|
|
for k, v in req.headers.items(multi=True):
|
|
display_v = v[:40] + '...[REDACTED]' if k.lower() == 'authorization' else v
|
|
print(f' {k}: {display_v}')
|
|
body = req.content
|
|
print(f'\nBody size: {len(body)} bytes')
|
|
# Show first 1500 bytes (multipart preamble before file data)
|
|
preview = body[:1500]
|
|
print(f'\nBody start (first 1500 bytes):')
|
|
for line in preview.split(b'\r\n'):
|
|
try:
|
|
print(f' {line.decode("ascii")}')
|
|
except Exception:
|
|
if len(line) > 40:
|
|
print(f' [binary: {len(line)} bytes]')
|
|
else:
|
|
print(f' {line!r}')
|
|
if flow.response:
|
|
print(f'\nResponse: {flow.response.status_code} {flow.response.reason}')
|
|
body_text = flow.response.content[:300].decode('utf-8', errors='replace')
|
|
print(f' Body: {body_text}')
|
|
else:
|
|
print('\nResponse: NONE (connection reset/failed)')
|
|
except Exception as e:
|
|
print(f'Error reading {path}: {e}')
|
|
|
|
dump_flow(sys.argv[1], 'WORKING: PS Direct HttpClient')
|
|
dump_flow(sys.argv[2], 'FAILING: C# Module (Send-PveIso)')
|
|
'@
|
|
|
|
$py = "$DebugDir/_analyze.py"
|
|
Set-Content -Path $py -Value $analyzeScript
|
|
python3 $py $psFlow $moduleFlow
|
|
|
|
Write-Host ''
|
|
Write-Host '=== Done ===' -ForegroundColor Cyan
|