From 598e9edef241ece0d91f931d5cdcf48683cffb89 Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Wed, 25 Mar 2026 09:15:25 -0500 Subject: [PATCH] fix: replace FormUrlEncodedContent with minimal form encoding .NET's FormUrlEncodedContent over-encodes characters like : and ! (%3A, %21) in form values. PVE's internal API consumers (specifically the cluster join process) do not URL-decode these values before using them, causing fingerprint comparison failures and password mismatches. Replace with BuildFormContent/EncodeFormValue that only encodes characters that break form parsing (&, =, +, space, %). This matches curl's -d behavior and fixes cluster join "Cluster join aborted!" errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/PSProxmoxVE.Core/Client/PveHttpClient.cs | 50 +++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs index 4982231..84d1209 100644 --- a/src/PSProxmoxVE.Core/Client/PveHttpClient.cs +++ b/src/PSProxmoxVE.Core/Client/PveHttpClient.cs @@ -99,7 +99,7 @@ namespace PSProxmoxVE.Core.Client { var request = BuildRequest(HttpMethod.Post, resource, mutating: true); if (data != null) - request.Content = new FormUrlEncodedContent(data); + request.Content = BuildFormContent(data); return await SendAsync(request, resource, "POST").ConfigureAwait(false); } @@ -111,7 +111,7 @@ namespace PSProxmoxVE.Core.Client { var request = BuildRequest(HttpMethod.Put, resource, mutating: true); if (data != null) - request.Content = new FormUrlEncodedContent(data); + request.Content = BuildFormContent(data); return await SendAsync(request, resource, "PUT").ConfigureAwait(false); } @@ -124,6 +124,52 @@ namespace PSProxmoxVE.Core.Client return await SendAsync(request, resource, "DELETE").ConfigureAwait(false); } + // ------------------------------------------------------------------------- + // Form body encoding + // ------------------------------------------------------------------------- + + /// + /// Builds form-encoded content using minimal encoding that matches curl behavior. + /// .NET's over-encodes characters like + /// : and ! which PVE's internal API consumers (e.g. cluster join) + /// do not properly URL-decode. + /// + private static StringContent BuildFormContent(Dictionary data) + { + var sb = new StringBuilder(); + foreach (var kvp in data) + { + if (sb.Length > 0) sb.Append('&'); + sb.Append(EncodeFormValue(kvp.Key)); + sb.Append('='); + sb.Append(EncodeFormValue(kvp.Value)); + } + return new StringContent(sb.ToString(), Encoding.UTF8, "application/x-www-form-urlencoded"); + } + + /// + /// Encodes a form value with minimal percent-encoding — only characters that + /// would break form parsing are encoded. This matches curl's -d behavior + /// and avoids over-encoding that PVE does not handle correctly. + /// + private static string EncodeFormValue(string value) + { + var sb = new StringBuilder(value.Length); + foreach (char c in value) + { + switch (c) + { + case '&': sb.Append("%26"); break; + case '=': sb.Append("%3D"); break; + case '+': sb.Append("%2B"); break; + case ' ': sb.Append('+'); break; + case '%': sb.Append("%25"); break; + default: sb.Append(c); break; + } + } + return sb.ToString(); + } + // ------------------------------------------------------------------------- // Synchronous wrappers // -------------------------------------------------------------------------