HTTP POST method handling can be moved to HttpRequestMessageExtensions + add some tests.
ADD
WalletWasabi.Tests/UnitTests/Tor/Http/Extensions/HttpRequestMessageExtensionsTests.cs
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using WalletWasabi.Tor.Http.Extensions;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests.Tor.Http.Extensions
+{
+ /// <summary>
+ /// Tests for <see cref="HttpRequestMessageExtensions"/>.
+ /// </summary>
+ public class HttpRequestMessageExtensionsTests
+ {
+ [Fact]
+ public async Task GetTestAsync()
+ {
+ using HttpRequestMessage request = new(HttpMethod.Get, "https://postman-echo.com");
+ string plaintext = await HttpRequestMessageExtensions.ToHttpStringAsync(request, CancellationToken.None);
+
+ Assert.Equal("GET / HTTP/1.1\r\nHost:postman-echo.com\r\n\r\n", plaintext);
+ }
+
+ [Fact]
+ public async Task ConnectTestAsync()
+ {
+ using HttpRequestMessage request = new(new HttpMethod("CONNECT"), "https://postman-echo.com");
+ string plaintext = await HttpRequestMessageExtensions.ToHttpStringAsync(request, CancellationToken.None);
+
+ Assert.Equal("CONNECT / HTTP/1.1\r\n", plaintext);
+ }
+
+ [Fact]
+ public async Task PostTestAsync()
+ {
+ // JSON string with HEX content.
+ using var content = new StringContent(content: @"{""key"": ""value""}", Encoding.UTF8, "application/json");
+ using HttpRequestMessage request = new(HttpMethod.Post, "https://postman-echo.com");
+ request.Content = content;
+
+ string actualPlaintext = await HttpRequestMessageExtensions.ToHttpStringAsync(request, CancellationToken.None);
+ string expected = "POST / HTTP/1.1\r\nHost:postman-echo.com\r\nContent-Type:application/json; charset=utf-8\r\nContent-Length:16\r\n\r\n{\"key\": \"value\"}";
+
+ Assert.Equal(expected, actualPlaintext);
+ }
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi/Tor/Http/Extensions/HttpRequestMessageExtensions.cs
WalletWasabi/Tor/Http/Extensions/HttpRequestMessageExtensions.cs
+using System;
using System.Linq;
using System.Net.Http;
+using System.Threading;
using System.Threading.Tasks;
using WalletWasabi.Tor.Http.Models;
using static WalletWasabi.Tor.Http.Constants;
@@ -8,16 +10,42 @@ namespace WalletWasabi.Tor.Http.Extensions
{
public static class HttpRequestMessageExtensions
{
- public static async Task<string> ToHttpStringAsync(this HttpRequestMessage request)
+ private static readonly HttpMethod ConnectHttpMethod = new("CONNECT");
+
+ /// <summary>
+ /// Converts <see cref="HttpRequestMessage"/> to plaintext HTTP request string.
+ /// </summary>
+ /// <param name="request">HTTP request to convert.</param>
+ /// <returns>String representation of <paramref name="request"/> according to <seealso href="https://tools.ietf.org/html/rfc7230"/>.</returns>
+ public static async Task<string> ToHttpStringAsync(this HttpRequestMessage request, CancellationToken cancellationToken = default)
+ {
+ // https://tools.ietf.org/html/rfc7230#section-3.3.2
+ // A user agent SHOULD send a Content - Length in a request message when no Transfer-Encoding is sent and the request method defines a meaning
+ // for an enclosed payload body. For example, a Content - Length header field is normally sent in a POST request even when the value is 0
+ // (indicating an empty payload body). A user agent SHOULD NOT send a Content - Length header field when the request message does not contain
+ // a payload body and the method semantics do not anticipate such a body.
+ if (request.Method == HttpMethod.Post)
+ {
+ if (request.Headers.TransferEncoding.Count == 0)
+ {
+ if (request.Content is null)
+ {
+ request.Content = new ByteArrayContent(Array.Empty<byte>()); // dummy empty content
+ request.Content.Headers.ContentLength = 0;
+ }
+ else
{
+ request.Content.Headers.ContentLength ??= (await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)).Length;
+ }
+ }
+ }
+
// https://tools.ietf.org/html/rfc7230#section-5.4
- // The "Host" header field in a request provides the host and port
- // information from the target URI, enabling the origin server to
- // distinguish among resources while servicing requests for multiple
- // host names on a single IP address.
+ // The "Host" header field in a request provides the host and port information from the target URI, enabling the origin server to
+ // distinguish among resources while servicing requests for multiple host names on a single IP address.
// Host = uri - host[":" port] ; Section 2.7.1
// A client MUST send a Host header field in all HTTP/1.1 request messages.
- if (request.Method != new HttpMethod("CONNECT"))
+ if (request.Method != ConnectHttpMethod)
{
if (!request.Headers.Contains("Host"))
{
@@ -28,11 +56,11 @@ namespace WalletWasabi.Tor.Http.Extensions
// delimiter(Section 2.7.1).If the authority component is missing or
// undefined for the target URI, then a client MUST send a Host header
// field with an empty field - value.
- request.Headers.TryAddWithoutValidation("Host", request.RequestUri.Authority);
+ request.Headers.TryAddWithoutValidation("Host", request.RequestUri!.Authority);
}
}
- var startLine = new RequestLine(request.Method, request.RequestUri, new HttpProtocol($"HTTP/{request.Version.Major}.{request.Version.Minor}")).ToString();
+ string startLine = new RequestLine(request.Method, request.RequestUri!, new HttpProtocol($"HTTP/{request.Version.Major}.{request.Version.Minor}")).ToString();
string headers = "";
if (request.Headers.NotNullAndNotEmpty())
@@ -50,7 +78,7 @@ namespace WalletWasabi.Tor.Http.Extensions
headers += headerSection.ToString(endWithTwoCRLF: false);
}
- messageBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+ messageBody = await request.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
}
return startLine + headers + CRLF + messageBody;

MODIFY
WalletWasabi/Tor/Http/TorHttpClient.cs
WalletWasabi/Tor/Http/TorHttpClient.cs
@@ -232,32 +232,7 @@ namespace WalletWasabi.Tor.Http
cancel.ThrowIfCancellationRequested();
- // https://tools.ietf.org/html/rfc7230#section-3.3.2
- // A user agent SHOULD send a Content - Length in a request message when
- // no Transfer-Encoding is sent and the request method defines a meaning
- // for an enclosed payload body.For example, a Content - Length header
- // field is normally sent in a POST request even when the value is 0
- // (indicating an empty payload body).A user agent SHOULD NOT send a
- // Content - Length header field when the request message does not contain
- // a payload body and the method semantics do not anticipate such a
- // body.
- if (request.Method == HttpMethod.Post)
- {
- if (request.Headers.TransferEncoding.Count == 0)
- {
- if (request.Content is null)
- {
- request.Content = new ByteArrayContent(Array.Empty<byte>()); // dummy empty content
- request.Content.Headers.ContentLength = 0;
- }
- else
- {
- request.Content.Headers.ContentLength ??= (await request.Content.ReadAsStringAsync(cancel).ConfigureAwait(false)).Length;
- }
- }
- }
-
- string requestString = await request.ToHttpStringAsync().ConfigureAwait(false);
+ string requestString = await request.ToHttpStringAsync(cancel).ConfigureAwait(false);
var bytes = Encoding.UTF8.GetBytes(requestString);

HTTP POST method handling can be moved to HttpRequestMessageExtensions + add some tests.