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) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-25 09:15:25 -05:00
parent 4277ad73b9
commit 598e9edef2
+48 -2
View File
@@ -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
// -------------------------------------------------------------------------
/// <summary>
/// Builds form-encoded content using minimal encoding that matches curl behavior.
/// .NET's <see cref="FormUrlEncodedContent"/> over-encodes characters like
/// <c>:</c> and <c>!</c> which PVE's internal API consumers (e.g. cluster join)
/// do not properly URL-decode.
/// </summary>
private static StringContent BuildFormContent(Dictionary<string, string> 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");
}
/// <summary>
/// Encodes a form value with minimal percent-encoding — only characters that
/// would break form parsing are encoded. This matches curl's <c>-d</c> behavior
/// and avoids over-encoding that PVE does not handle correctly.
/// </summary>
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
// -------------------------------------------------------------------------