mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-18 13:26:47 +01:00
Remove internal socks http proxy and use .net60 socks support (#3240)
This commit is contained in:
parent
0e1b872777
commit
0dcfbe8581
@ -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<Socks5HttpProxyServer>();
|
||||
void AssertConnectionDropped()
|
||||
{
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Thread.MemoryBarrier();
|
||||
Assert.Equal(0, proxy.ConnectionCount);
|
||||
});
|
||||
}
|
||||
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
|
||||
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<HttpRequestException>(() => 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<HttpRequestException>(() => client.GetAsync("http://nzwsosflsoquxirwb2zikz6uxr3u5n5u73l33umtdx4hq5mzm5dycuqd.onion/"));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 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/
|
||||
/// </summary>
|
||||
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<byte> 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<byte> 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<byte> 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<byte>.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<byte> 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<byte> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -310,7 +310,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<HostedServices.WebhookNotificationManager>();
|
||||
services.AddSingleton<IHostedService, WebhookNotificationManager>(o => o.GetRequiredService<WebhookNotificationManager>());
|
||||
services.AddHttpClient(WebhookNotificationManager.OnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
|
||||
|
||||
@ -318,7 +317,6 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IPayoutHandler, LightningLikePayoutHandler>();
|
||||
|
||||
services.AddHttpClient(LightningLikePayoutHandler.LightningLikePayoutHandlerOnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
services.AddSingleton<HostedServices.PullPaymentHostedService>();
|
||||
services.AddSingleton<IHostedService, HostedServices.PullPaymentHostedService>(o => o.GetRequiredService<PullPaymentHostedService>());
|
||||
|
@ -13,14 +13,11 @@ namespace BTCPayServer.Payments.PayJoin
|
||||
{
|
||||
services.AddSingleton<DelayedTransactionBroadcaster>();
|
||||
services.AddSingleton<IHostedService, HostedServices.DelayedTransactionBroadcasterHostedService>();
|
||||
services.AddSingleton<HostedServices.Socks5HttpProxyServer>();
|
||||
services.AddSingleton<IHostedService, HostedServices.Socks5HttpProxyServer>(s => s.GetRequiredService<Socks5HttpProxyServer>());
|
||||
services.AddSingleton<PayJoinRepository>();
|
||||
services.AddSingleton<IPayjoinServerCommunicator, PayjoinServerCommunicator>();
|
||||
services.AddSingleton<PayjoinClient>();
|
||||
services.AddTransient<Socks5HttpClientHandler>();
|
||||
services.AddHttpClient(PayjoinServerCommunicator.PayjoinOnionNamedClient)
|
||||
.ConfigureHttpClient(h => h.DefaultRequestHeaders.ConnectionClose = true)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user