Refactor TorHttpClient.
MODIFY
WalletWasabi.Tests/IntegrationTests/TorTests.cs
WalletWasabi.Tests/IntegrationTests/TorTests.cs
@@ -6,11 +6,11 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WalletWasabi.Tor;
using WalletWasabi.Tor.Http;
+using WalletWasabi.Tor.Socks5;
using Xunit;
namespace WalletWasabi.Tests.IntegrationTests
{
- // Tor must be running
public class TorTests : IAsyncLifetime
{
public async Task InitializeAsync()
@@ -184,8 +184,8 @@ namespace WalletWasabi.Tests.IntegrationTests
[Fact]
public async Task TorRunningAsync()
{
- Assert.True(await TorProcessManager.IsTorRunningAsync(new IPEndPoint(IPAddress.Loopback, 9050)));
- Assert.False(await TorProcessManager.IsTorRunningAsync(new IPEndPoint(IPAddress.Loopback, 9054)));
+ Assert.True(await new TorSocks5Client(new IPEndPoint(IPAddress.Loopback, 9050)).IsTorRunningAsync());
+ Assert.False(await new TorSocks5Client(new IPEndPoint(IPAddress.Loopback, 9054)).IsTorRunningAsync());
}
private static async Task<List<string>> QBitTestAsync(TorHttpClient client, int times, bool alterRequests = false)

MODIFY
WalletWasabi/Tor/Http/Interfaces/ITorHttpClient.cs
WalletWasabi/Tor/Http/Interfaces/ITorHttpClient.cs
@@ -14,7 +14,7 @@ namespace WalletWasabi.Tor.Http.Interfaces
bool IsTorUsed { get; }
- Task<HttpResponseMessage> SendAsync(HttpMethod method, string relativeUri, HttpContent content = null, CancellationToken cancel = default);
+ Task<HttpResponseMessage> SendAsync(HttpMethod method, string relativeUri, HttpContent? content = null, CancellationToken cancel = default);
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancel = default);
}

MODIFY
WalletWasabi/Tor/Http/TorHttpClient.cs
WalletWasabi/Tor/Http/TorHttpClient.cs
@@ -6,7 +6,6 @@ using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Net.Sockets;
-using System.Runtime.InteropServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
@@ -27,17 +26,28 @@ namespace WalletWasabi.Tor.Http
{
private static DateTimeOffset? TorDoesntWorkSinceBacking = null;
+ /// <summary>TLS protocols we support for both clearnet and Tor proxy.</summary>
+ private static readonly SslProtocols SupportedSslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
+
+ /// <summary>Predefined HTTP client that handles HTTP requests when Tor is disabled.</summary>
+ private static readonly HttpClient ClearnetHttpClient = new HttpClient(new HttpClientHandler() { SslProtocols = SupportedSslProtocols });
+
private volatile bool _disposedValue = false; // To detect redundant calls
- public TorHttpClient(Uri baseUri, EndPoint torSocks5EndPoint, bool isolateStream = false)
+ public TorHttpClient(Uri baseUri, EndPoint? torSocks5EndPoint, bool isolateStream = false):
+ this(() => baseUri, torSocks5EndPoint, isolateStream)
{
baseUri = Guard.NotNull(nameof(baseUri), baseUri);
- Create(torSocks5EndPoint, isolateStream, () => baseUri);
}
- public TorHttpClient(Func<Uri> baseUriAction, EndPoint torSocks5EndPoint, bool isolateStream = false)
+ public TorHttpClient(Func<Uri> baseUriAction, EndPoint? torSocks5EndPoint, bool isolateStream = false)
{
- Create(torSocks5EndPoint, isolateStream, baseUriAction);
+ DestinationUriAction = Guard.NotNull(nameof(baseUriAction), baseUriAction);
+
+ // Connecting to loopback's URIs cannot be done via Tor.
+ TorSocks5EndPoint = DestinationUri.IsLoopback ? null : torSocks5EndPoint;
+ TorSocks5Client = null;
+ IsolateStream = isolateStream;
}
public static DateTimeOffset? TorDoesntWorkSince
@@ -56,41 +66,41 @@ namespace WalletWasabi.Tor.Http
}
}
- public static Exception LatestTorException { get; private set; } = null;
+ public static Exception? LatestTorException { get; private set; } = null;
public Uri DestinationUri => DestinationUriAction();
- public Func<Uri> DestinationUriAction { get; private set; }
- public EndPoint TorSocks5EndPoint { get; private set; }
+ public Func<Uri> DestinationUriAction { get; }
+ public EndPoint? TorSocks5EndPoint { get; private set; }
public bool IsTorUsed => TorSocks5EndPoint is { };
- public bool IsolateStream { get; private set; }
+ private bool IsolateStream { get; }
- public TorSocks5Client TorSocks5Client { get; private set; }
+ private TorSocks5Client? TorSocks5Client { get; set; }
private static AsyncLock AsyncLock { get; } = new AsyncLock(); // We make everything synchronous, so slow, but at least stable.
- private void Create(EndPoint torSocks5EndPoint, bool isolateStream, Func<Uri> baseUriAction)
- {
- DestinationUriAction = Guard.NotNull(nameof(baseUriAction), baseUriAction);
- TorSocks5EndPoint = DestinationUri.IsLoopback ? null : torSocks5EndPoint;
- TorSocks5Client = null;
- IsolateStream = isolateStream;
- }
-
/// <remarks>
- /// Throws OperationCancelledException if <paramref name="cancel"/> is set.
+ /// Throws <see cref="OperationCanceledException"/> if <paramref name="cancel"/> is set.
/// </remarks>
- public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string relativeUri, HttpContent content = null, CancellationToken cancel = default)
+ public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string relativeUri, HttpContent? content = null, CancellationToken cancel = default)
{
Guard.NotNull(nameof(method), method);
relativeUri = Guard.NotNull(nameof(relativeUri), relativeUri);
var requestUri = new Uri(DestinationUri, relativeUri);
using var request = new HttpRequestMessage(method, requestUri);
+ request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
+
if (content is { })
{
request.Content = content;
}
- request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
+
+ // Use clearnet HTTP client when Tor is disabled.
+ if (TorSocks5EndPoint == null)
+ {
+ var response = await ClearnetHttpClient.SendAsync(request);
+ return response;
+ }
try
{
@@ -165,12 +175,19 @@ namespace WalletWasabi.Tor.Http
}
/// <remarks>
- /// Throws OperationCancelledException if <paramref name="cancel"/> is set.
+ /// Throws <see cref="OperationCanceledException"/> if <paramref name="cancel"/> is set.
/// </remarks>
public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancel = default)
{
Guard.NotNull(nameof(request), request);
+ // Use clearnet HTTP client when Tor is disabled.
+ if (TorSocks5EndPoint == null)
+ {
+ var response = await ClearnetHttpClient.SendAsync(request);
+ return response;
+ }
+
// https://tools.ietf.org/html/rfc7230#section-2.7.1
// A sender MUST NOT generate an "http" URI with an empty host identifier.
string host = Guard.NotNullOrEmptyOrWhitespace($"{nameof(request)}.{nameof(request.RequestUri)}.{nameof(request.RequestUri.DnsSafeHost)}", request.RequestUri.DnsSafeHost, trim: true);
@@ -203,7 +220,7 @@ namespace WalletWasabi.Tor.Http
.AuthenticateAsClientAsync(
host,
new X509CertificateCollection(),
- SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12,
+ SupportedSslProtocols,
checkCertificateRevocation: true).ConfigureAwait(false);
stream = sslStream;
}

MODIFY
WalletWasabi/Tor/Socks5/TorSocks5Client.cs
WalletWasabi/Tor/Socks5/TorSocks5Client.cs
using Nito.AsyncEx;
using System;
using System.IO;
-using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
@@ -24,11 +23,11 @@ namespace WalletWasabi.Tor.Socks5
#region Constructors
- /// <param name="endPoint">Opt out Tor with null.</param>
+ /// <param name="endPoint">Valid Tor end point.</param>
internal TorSocks5Client(EndPoint endPoint)
{
TorSocks5EndPoint = endPoint;
- TcpClient = endPoint is null ? new TcpClient() : new TcpClient(endPoint.AddressFamily);
+ TcpClient = new TcpClient(endPoint.AddressFamily);
AsyncLock = new AsyncLock();
}
@@ -54,11 +53,6 @@ namespace WalletWasabi.Tor.Socks5
internal async Task ConnectAsync()
{
- if (TorSocks5EndPoint is null)
- {
- return;
- }
-
using (await AsyncLock.LockAsync().ConfigureAwait(false))
{
string host = TorSocks5EndPoint.GetHostOrDefault();
@@ -78,6 +72,24 @@ namespace WalletWasabi.Tor.Socks5
}
}
+ /// <summary>
+ /// Checks whether communication can be established with Tor over <see cref="TorSocks5EndPoint"/> endpoint.
+ /// </summary>
+ /// <returns></returns>
+ public async Task<bool> IsTorRunningAsync()
+ {
+ try
+ {
+ await ConnectAsync().ConfigureAwait(false);
+ await HandshakeAsync(isolateStream: true).ConfigureAwait(false);
+ }
+ catch (ConnectionException)
+ {
+ return false;
+ }
+ return true;
+ }
+
/// <summary>
/// IsolateSOCKSAuth must be on (on by default)
/// https://www.torproject.org/docs/tor-manual.html.en
@@ -94,29 +106,25 @@ namespace WalletWasabi.Tor.Socks5
/// https://gitweb.torproject.org/torspec.git/tree/socks-extensions.txt#n35
/// </summary>
/// <param name="identity">Isolates streams by identity. If identity is empty string, it won't isolate stream.</param>
- internal async Task HandshakeAsync(string identity)
- {
- if (TorSocks5EndPoint is null)
+ private async Task HandshakeAsync(string identity)
{
- return;
- }
-
- Guard.NotNull(nameof(identity), identity);
+ Logger.LogDebug($"> {nameof(identity)}={identity}");
MethodsField methods = string.IsNullOrWhiteSpace(identity)
? new MethodsField(MethodField.NoAuthenticationRequired)
: new MethodsField(MethodField.UsernamePassword);
- var sendBuffer = new VersionMethodRequest(methods).ToBytes();
-
- var receiveBuffer = await SendAsync(sendBuffer, 2).ConfigureAwait(false);
+ byte[] sendBuffer = new VersionMethodRequest(methods).ToBytes();
+ byte[] receiveBuffer = await SendAsync(sendBuffer, receiveBufferSize: 2).ConfigureAwait(false);
var methodSelection = new MethodSelectionResponse();
methodSelection.FromBytes(receiveBuffer);
+
if (methodSelection.Ver != VerField.Socks5)
{
throw new NotSupportedException($"SOCKS{methodSelection.Ver.Value} not supported. Only SOCKS5 is supported.");
}
+
if (methodSelection.Method == MethodField.NoAcceptableMethods)
{
// https://www.ietf.org/rfc/rfc1928.txt
@@ -125,6 +133,7 @@ namespace WalletWasabi.Tor.Socks5
DisposeTcpClient();
throw new NotSupportedException("Tor's SOCKS5 proxy does not support any of the client's authentication methods.");
}
+
if (methodSelection.Method == MethodField.UsernamePassword)
{
// https://tools.ietf.org/html/rfc1929#section-2
@@ -132,10 +141,8 @@ namespace WalletWasabi.Tor.Socks5
// Username / Password Authentication protocol, the Username / Password
// sub-negotiation begins. This begins with the client producing a
// Username / Password request:
- var username = identity;
- var password = identity;
- var uName = new UNameField(username);
- var passwd = new PasswdField(password);
+ var uName = new UNameField(uName: identity);
+ var passwd = new PasswdField(passwd: identity);
var usernamePasswordRequest = new UsernamePasswordRequest(uName, passwd);
sendBuffer = usernamePasswordRequest.ToBytes();
@@ -159,6 +166,8 @@ namespace WalletWasabi.Tor.Socks5
throw new InvalidOperationException("Wrong username and/or password.");
}
}
+
+ Logger.LogDebug("<");
}
internal async Task ConnectToDestinationAsync(EndPoint destination, bool isRecursiveCall = false)

MODIFY
WalletWasabi/Tor/TorProcessManager.cs
WalletWasabi/Tor/TorProcessManager.cs
@@ -46,6 +46,7 @@ namespace WalletWasabi.Tor
Stop = new CancellationTokenSource();
TorProcess = null;
Settings = settings;
+ TorSocks5Client = new TorSocks5Client(torSocks5EndPoint);
IoHelpers.EnsureContainingDirectoryExists(Settings.LogFilePath);
}
@@ -58,6 +59,8 @@ namespace WalletWasabi.Tor
private TorSettings Settings { get; }
+ private TorSocks5Client TorSocks5Client { get; }
+
public bool IsRunning => Interlocked.Read(ref _monitorState) == StateRunning;
private CancellationTokenSource Stop { get; set; }
@@ -74,7 +77,7 @@ namespace WalletWasabi.Tor
try
{
// Is Tor already running? Either our Tor process from previous Wasabi Wallet run or possibly user's own Tor.
- bool isAlreadyRunning = await IsTorRunningAsync(TorSocks5EndPoint).ConfigureAwait(false);
+ bool isAlreadyRunning = await TorSocks5Client.IsTorRunningAsync().ConfigureAwait(false);
if (isAlreadyRunning)
{
@@ -129,7 +132,7 @@ namespace WalletWasabi.Tor
{
i++;
- bool isRunning = await IsTorRunningAsync(TorSocks5EndPoint).ConfigureAwait(false);
+ bool isRunning = await TorSocks5Client.IsTorRunningAsync().ConfigureAwait(false);
if (isRunning)
{
@@ -160,22 +163,6 @@ namespace WalletWasabi.Tor
return false;
}
- /// <param name="torSocks5EndPoint">Valid Tor end point.</param>
- public static async Task<bool> IsTorRunningAsync(EndPoint torSocks5EndPoint)
- {
- using var client = new TorSocks5Client(torSocks5EndPoint);
- try
- {
- await client.ConnectAsync().ConfigureAwait(false);
- await client.HandshakeAsync().ConfigureAwait(false);
- }
- catch (ConnectionException)
- {
- return false;
- }
- return true;
- }
-
#region Monitor
public void StartMonitor(TimeSpan torMisbehaviorCheckPeriod, TimeSpan checkIfRunningAfterTorMisbehavedFor, Uri fallBackTestRequestUri)

Refactor TorHttpClient.