diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs index cd3de749b..b835c5603 100644 --- a/BTCPayServer.Tests/UnitTest1.cs +++ b/BTCPayServer.Tests/UnitTest1.cs @@ -60,6 +60,7 @@ using Microsoft.Extensions.Options; using NBitcoin; using NBitcoin.DataEncoders; using NBitcoin.Payment; +using NBitcoin.Socks; using NBitpayClient; using NBXplorer; using NBXplorer.DerivationStrategy; @@ -655,15 +656,6 @@ namespace BTCPayServer.Tests using (var tester = CreateServerTester()) { await tester.StartAsync(); - var proxy = tester.PayTester.GetService(); - void AssertConnectionDropped() - { - TestUtils.Eventually(() => - { - Thread.MemoryBarrier(); - Assert.Equal(0, proxy.ConnectionCount); - }); - } var httpFactory = tester.PayTester.GetService(); var client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); Assert.NotNull(client); @@ -672,42 +664,23 @@ namespace BTCPayServer.Tests var result = await response.Content.ReadAsStringAsync(); Assert.DoesNotContain("You are not using Tor.", result); Assert.Contains("Congratulations. This browser is configured to use Tor.", result); - AssertConnectionDropped(); response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); response.EnsureSuccessStatusCode(); result = await response.Content.ReadAsStringAsync(); Assert.Contains("Bitcoin", result); - AssertConnectionDropped(); response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); response.EnsureSuccessStatusCode(); - AssertConnectionDropped(); client.Dispose(); - AssertConnectionDropped(); client = httpFactory.CreateClient(PayjoinServerCommunicator.PayjoinOnionNamedClient); response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/"); response.EnsureSuccessStatusCode(); - AssertConnectionDropped(); TestLogs.LogInformation("Querying an onion address which can't be found should send http 500"); - response = await client.GetAsync("http://dwoduwoi.onion/"); - Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); - AssertConnectionDropped(); + await Assert.ThrowsAsync(() => client.GetAsync("http://dwoduwoi.onion/")); TestLogs.LogInformation("Querying valid onion but unreachable should send error 502"); - using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20))) - { - try - { - response = await client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/", cts.Token); - Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); - AssertConnectionDropped(); - } - catch when (cts.Token.IsCancellationRequested) - { - TestLogs.LogInformation("Skipping this test, it timed out"); - } - } + await Assert.ThrowsAsync(() => client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/")); } } diff --git a/BTCPayServer/HostedServices/Socks5HttpProxyServer.cs b/BTCPayServer/HostedServices/Socks5HttpProxyServer.cs deleted file mode 100644 index 3d6462e8d..000000000 --- a/BTCPayServer/HostedServices/Socks5HttpProxyServer.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System; -using System.Buffers; -using System.IO.Pipelines; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using BTCPayServer.Configuration; -using BTCPayServer.Logging; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using NBitcoin; -using NBitcoin.Socks; - -namespace BTCPayServer.HostedServices -{ - - /// - /// This is a very simple Socks HTTP proxy, that can be used through HttpClient.WebProxy - /// However, it only supports a single request/response, so the client must specify Connection: close to not - /// reuse the TCP connection to the proxy for another requests. - /// Inspired from https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/ - /// - public class Socks5HttpProxyServer : IHostedService - { - class ProxyConnection - { - public ServerContext ServerContext; - public Socket ClientSocket; - public Socket SocksSocket; - public CancellationToken CancellationToken; - public CancellationTokenSource CancellationTokenSource; - - public void Dispose() - { - Socks5HttpProxyServer.Dispose(ClientSocket); - Socks5HttpProxyServer.Dispose(SocksSocket); - CancellationTokenSource.Dispose(); - } - } - - class ServerContext - { - public EndPoint SocksEndpoint; - public Socket ServerSocket; - public CancellationToken CancellationToken; - public int ConnectionCount; - } - - public Logs Logs { get; } - - private readonly BTCPayServerOptions _opts; - - public Socks5HttpProxyServer(Configuration.BTCPayServerOptions opts, Logs logs) - { - this.Logs = logs; - _opts = opts; - } - private ServerContext _ServerContext; - private CancellationTokenSource _Cts; - public Task StartAsync(CancellationToken cancellationToken) - { - if (_opts.SocksEndpoint is null || _ServerContext != null) - return Task.CompletedTask; - _Cts = new CancellationTokenSource(); - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); - Port = ((IPEndPoint)(socket.LocalEndPoint)).Port; - Uri = new Uri($"http://127.0.0.1:{Port}"); - socket.Listen(5); - _ServerContext = new ServerContext() - { - SocksEndpoint = _opts.SocksEndpoint, - ServerSocket = socket, - CancellationToken = _Cts.Token, - ConnectionCount = 0 - }; - socket.BeginAccept(Accept, _ServerContext); - Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy listening at {Uri}"); - return Task.CompletedTask; - } - - public int Port { get; private set; } - public Uri Uri { get; private set; } - - static void Accept(IAsyncResult ar) - { - var ctx = (ServerContext)ar.AsyncState; - Socket clientSocket = null; - try - { - clientSocket = ctx.ServerSocket.EndAccept(ar); - } - catch (Exception) - { - return; - } - if (ctx.CancellationToken.IsCancellationRequested) - { - Dispose(clientSocket); - return; - } - var toSocksProxy = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - var connectionCts = CancellationTokenSource.CreateLinkedTokenSource(ctx.CancellationToken); - toSocksProxy.BeginConnect(ctx.SocksEndpoint, ConnectToSocks, new ProxyConnection() - { - ServerContext = ctx, - ClientSocket = clientSocket, - SocksSocket = toSocksProxy, - CancellationToken = connectionCts.Token, - CancellationTokenSource = connectionCts - }); - try - { - ctx.ServerSocket.BeginAccept(Accept, ctx); - } - catch (Exception) - { - return; - } - } - - static void ConnectToSocks(IAsyncResult ar) - { - var connection = (ProxyConnection)ar.AsyncState; - try - { - connection.SocksSocket.EndConnect(ar); - } - catch (Exception) - { - connection.Dispose(); - return; - } - Interlocked.Increment(ref connection.ServerContext.ConnectionCount); - var pipe = new Pipe(PipeOptions.Default); - var reading = FillPipeAsync(connection.ClientSocket, pipe.Writer, connection.CancellationToken) - .ContinueWith(_ => connection.CancellationTokenSource.Cancel(), TaskScheduler.Default); - var writing = ReadPipeAsync(connection.SocksSocket, connection.ClientSocket, pipe.Reader, connection.CancellationToken) - .ContinueWith(_ => connection.CancellationTokenSource.Cancel(), TaskScheduler.Default); - _ = Task.WhenAll(reading, writing) - .ContinueWith(_ => - { - connection.Dispose(); - Interlocked.Decrement(ref connection.ServerContext.ConnectionCount); - }, TaskScheduler.Default); - } - - public int ConnectionCount => _ServerContext is ServerContext s ? s.ConnectionCount : 0; - private static async Task ReadPipeAsync(Socket socksSocket, Socket clientSocket, PipeReader reader, CancellationToken cancellationToken) - { - bool handshaked = false; - bool isConnect = false; - string firstHeader = null; - string httpVersion = null; - while (true) - { - ReadResult result = await reader.ReadAsync(cancellationToken); - ReadOnlySequence buffer = result.Buffer; - SequencePosition? position = null; - - if (!handshaked) - { -nextchunk: -// Look for a EOL in the buffer - position = buffer.PositionOf((byte)'\n'); - if (position == null) - goto readnext; - // Process the line - var line = GetHeaderLine(buffer.Slice(0, position.Value)); - // Skip the line + the \n character (basically position) - buffer = buffer.Slice(buffer.GetPosition(1, position.Value)); - if (firstHeader is null) - { - firstHeader = line; - isConnect = line.StartsWith("CONNECT ", StringComparison.OrdinalIgnoreCase); - if (isConnect) - goto nextchunk; - else - goto handshake; - } - else if (line.Length == 1 && line[0] == '\r') - goto handshake; - else - goto nextchunk; - -handshake: - var split = firstHeader.Split(' '); - if (split.Length != 3) - break; - var targetConnection = split[1].Trim(); - EndPoint destinationEnpoint = null; - if (isConnect) - { - if (!Utils.TryParseEndpoint(targetConnection, - 80, - out destinationEnpoint)) - break; - } - else - { - if (!System.Uri.TryCreate(targetConnection, UriKind.Absolute, out var uri) || - (uri.Scheme != "http" && uri.Scheme != "https")) - break; - if (!Utils.TryParseEndpoint($"{uri.DnsSafeHost}:{uri.Port}", - uri.Scheme == "http" ? 80 : 443, - out destinationEnpoint)) - break; - firstHeader = $"{split[0]} {uri.PathAndQuery} {split[2].TrimEnd()}"; - } - - httpVersion = split[2].Trim(); - try - { - await NBitcoin.Socks.SocksHelper.Handshake(socksSocket, destinationEnpoint, cancellationToken); - handshaked = true; - if (isConnect) - { - await SendAsync(clientSocket, - $"{httpVersion} 200 Connection established\r\nConnection: close\r\n\r\n", - cancellationToken); - } - else - { - await SendAsync(socksSocket, $"{firstHeader}\r\n", cancellationToken); - foreach (ReadOnlyMemory segment in buffer) - { - await socksSocket.SendAsync(segment, SocketFlags.None, cancellationToken); - } - buffer = buffer.Slice(buffer.End); - } - _ = Relay(socksSocket, clientSocket, cancellationToken); - } - catch (SocksException e) when (e.SocksErrorCode == SocksErrorCode.NetworkUnreachable || e.SocksErrorCode == SocksErrorCode.HostUnreachable) - { - await SendAsync(clientSocket, $"{httpVersion} 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n", cancellationToken); - goto done; - } - catch (SocksException e) - { - await SendAsync(clientSocket, $"{httpVersion} 500 Internal Server Error\r\nContent-Length: 0\r\nX-Proxy-Error-Type: Socks {e.SocksErrorCode}\r\n\r\n", cancellationToken); - goto done; - } - catch (SocketException e) - { - await SendAsync(clientSocket, $"{httpVersion} 500 Internal Server Error\r\nContent-Length: 0\r\nX-Proxy-Error-Type: Socket {e.SocketErrorCode}\r\n\r\n", cancellationToken); - goto done; - } - catch - { - await SendAsync(clientSocket, $"{httpVersion} 500 Internal Server Error\r\n\r\n", cancellationToken); - goto done; - } - } - else - { - foreach (ReadOnlyMemory segment in buffer) - { - await socksSocket.SendAsync(segment, SocketFlags.None, cancellationToken); - } - buffer = buffer.Slice(buffer.End); - } - -readnext: -// Tell the PipeReader how much of the buffer we have consumed - reader.AdvanceTo(buffer.Start, buffer.End); - // Stop reading if there's no more data coming - if (result.IsCompleted) - { - break; - } - } - -done: -// Mark the PipeReader as complete - reader.Complete(); - } - - private const int BufferSize = 1024 * 5; - private static async Task Relay(Socket from, Socket to, CancellationToken cancellationToken) - { - using var buffer = MemoryPool.Shared.Rent(BufferSize); - while (true) - { - int bytesRead = await from.ReceiveAsync(buffer.Memory, SocketFlags.None, cancellationToken); - if (bytesRead == 0) - break; - await to.SendAsync(buffer.Memory.Slice(0, bytesRead), SocketFlags.None, cancellationToken); - } - } - - private static async Task SendAsync(Socket clientSocket, string data, CancellationToken cancellationToken) - { - var bytes = new byte[Encoding.ASCII.GetByteCount(data)]; - Encoding.ASCII.GetBytes(data, bytes); - await clientSocket.SendAsync(bytes, SocketFlags.None, cancellationToken); - } - - private static string GetHeaderLine(ReadOnlySequence buffer) - { - if (buffer.IsSingleSegment) - { - return Encoding.ASCII.GetString(buffer.First.Span); - } - - return string.Create((int)buffer.Length, buffer, (span, sequence) => - { - foreach (var segment in sequence) - { - Encoding.ASCII.GetChars(segment.Span, span); - - span = span.Slice(segment.Length); - } - }); - } - - private static async Task FillPipeAsync(Socket socket, PipeWriter writer, CancellationToken cancellationToken) - { - while (true) - { - Memory memory = writer.GetMemory(BufferSize); - int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None, cancellationToken); - if (bytesRead == 0) - { - break; - } - writer.Advance(bytesRead); - FlushResult result = await writer.FlushAsync(cancellationToken); - if (result.IsCompleted) - { - break; - } - } - writer.Complete(); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - if (_ServerContext is ServerContext ctx) - { - _Cts.Cancel(); - Dispose(ctx.ServerSocket); - Logs.PayServer.LogInformation($"Internal Socks HTTP Proxy closed"); - } - return Task.CompletedTask; - } - - static void Dispose(Socket s) - { - try - { - s.Shutdown(SocketShutdown.Both); - s.Close(); - } - catch (Exception) - { - - } - finally - { - s.Dispose(); - } - } - } -} diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs index 6d4a6b8ba..dcfc7b26c 100644 --- a/BTCPayServer/Hosting/BTCPayServerServices.cs +++ b/BTCPayServer/Hosting/BTCPayServerServices.cs @@ -310,7 +310,6 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); services.AddHttpClient(WebhookNotificationManager.OnionNamedClient) - .ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true) .ConfigurePrimaryHttpMessageHandler(); @@ -318,7 +317,6 @@ namespace BTCPayServer.Hosting services.AddSingleton(); services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient) - .ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true) .ConfigurePrimaryHttpMessageHandler(); services.AddSingleton(); services.AddSingleton(o => o.GetRequiredService()); diff --git a/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs b/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs index f9676fdfb..5ff575e52 100644 --- a/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs +++ b/BTCPayServer/Payments/PayJoin/PayJoinExtensions.cs @@ -13,14 +13,11 @@ namespace BTCPayServer.Payments.PayJoin { services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(s => s.GetRequiredService()); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddTransient(); services.AddHttpClient(PayjoinServerCommunicator.PayjoinOnionNamedClient) - .ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true) .ConfigurePrimaryHttpMessageHandler(); } } diff --git a/BTCPayServer/Services/Socks5HttpClientHandler.cs b/BTCPayServer/Services/Socks5HttpClientHandler.cs index 5df345d38..841294c1b 100644 --- a/BTCPayServer/Services/Socks5HttpClientHandler.cs +++ b/BTCPayServer/Services/Socks5HttpClientHandler.cs @@ -1,14 +1,22 @@ using System.Net; using System.Net.Http; +using BTCPayServer.Configuration; using BTCPayServer.HostedServices; namespace BTCPayServer.Services { public class Socks5HttpClientHandler : HttpClientHandler { - public Socks5HttpClientHandler(Socks5HttpProxyServer sock5) + public Socks5HttpClientHandler(BTCPayServerOptions opts) { - this.Proxy = new WebProxy(sock5.Uri); + if (opts.SocksEndpoint is IPEndPoint endpoint) + { + this.Proxy = new WebProxy($"socks5://{endpoint.Address}:{endpoint.Port}"); + } + else if (opts.SocksEndpoint is DnsEndPoint endpoint2) + { + this.Proxy = new WebProxy($"socks5://{endpoint2.Host}:{endpoint2.Port}"); + } } } }