mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 22:44:29 +01:00
Implement Http Tor proxy as a internal http proxy
This commit is contained in:
@@ -896,14 +896,55 @@ namespace BTCPayServer.Tests
|
|||||||
using (var tester = ServerTester.Create())
|
using (var tester = ServerTester.Create())
|
||||||
{
|
{
|
||||||
await tester.StartAsync();
|
await tester.StartAsync();
|
||||||
var torFactory = tester.PayTester.GetService<Socks5HttpClientFactory>();
|
var proxy = tester.PayTester.GetService<Socks5HttpProxyServer>();
|
||||||
var client = torFactory.CreateClient("test");
|
var httpFactory = tester.PayTester.GetService<IHttpClientFactory>();
|
||||||
|
var client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||||
Assert.NotNull(client);
|
Assert.NotNull(client);
|
||||||
var response = await client.GetAsync("https://check.torproject.org/");
|
var response = await client.GetAsync("https://check.torproject.org/");
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
var result = await response.Content.ReadAsStringAsync();
|
var result = await response.Content.ReadAsStringAsync();
|
||||||
Assert.DoesNotContain("You are not using Tor.", result);
|
Assert.DoesNotContain("You are not using Tor.", result);
|
||||||
Assert.Contains("Congratulations. This browser is configured to use Tor.", result);
|
Assert.Contains("Congratulations. This browser is configured to use Tor.", result);
|
||||||
|
Logs.Tester.LogInformation("Now we should have one connection");
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
Assert.Equal(1, proxy.ConnectionCount);
|
||||||
|
});
|
||||||
|
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
result = await response.Content.ReadAsStringAsync();
|
||||||
|
Assert.Contains("Bitcoin", result);
|
||||||
|
Logs.Tester.LogInformation("Now we should have two connections");
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
Assert.Equal(2, proxy.ConnectionCount);
|
||||||
|
});
|
||||||
|
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
Logs.Tester.LogInformation("Querying the same address should not create additional connection");
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
Assert.Equal(2, proxy.ConnectionCount);
|
||||||
|
});
|
||||||
|
client.Dispose();
|
||||||
|
Logs.Tester.LogInformation("Disposing a HttpClient should not proxy connection");
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
Assert.Equal(2, proxy.ConnectionCount);
|
||||||
|
});
|
||||||
|
client = httpFactory.CreateClient(PayjoinClient.PayjoinOnionNamedClient);
|
||||||
|
response = await client.GetAsync("http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
Logs.Tester.LogInformation("Querying the same address with same client should not create additional connection");
|
||||||
|
TestUtils.Eventually(() =>
|
||||||
|
{
|
||||||
|
Thread.MemoryBarrier();
|
||||||
|
Assert.Equal(2, proxy.ConnectionCount);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
339
BTCPayServer/HostedServices/Socks5HttpProxyServer.cs
Normal file
339
BTCPayServer/HostedServices/Socks5HttpProxyServer.cs
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
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.Lightning.Eclair.Models;
|
||||||
|
using BTCPayServer.Logging;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.Socks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.HostedServices
|
||||||
|
{
|
||||||
|
// Our implementation follow https://devblogs.microsoft.com/dotnet/system-io-pipelines-high-performance-io-in-net/
|
||||||
|
public class Socks5HttpProxyServer : IHostedService
|
||||||
|
{
|
||||||
|
class ProxyConnection
|
||||||
|
{
|
||||||
|
public Socket ClientSocket;
|
||||||
|
public Socket SocksSocket;
|
||||||
|
public CancellationToken CancellationToken;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Socks5HttpProxyServer.Dispose(ClientSocket);
|
||||||
|
Socks5HttpProxyServer.Dispose(SocksSocket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private readonly BTCPayServerOptions _opts;
|
||||||
|
|
||||||
|
public Socks5HttpProxyServer(Configuration.BTCPayServerOptions opts)
|
||||||
|
{
|
||||||
|
_opts = opts;
|
||||||
|
}
|
||||||
|
private Socket _ServerSocket;
|
||||||
|
private CancellationTokenSource _Cts;
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_opts.SocksEndpoint is null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
_Cts = new CancellationTokenSource();
|
||||||
|
_ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
_ServerSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
||||||
|
Port = ((IPEndPoint)(_ServerSocket.LocalEndPoint)).Port;
|
||||||
|
Uri = new Uri($"http://127.0.0.1:{Port}");
|
||||||
|
_ServerSocket.Listen(5);
|
||||||
|
_ServerSocket.BeginAccept(Accept, null);
|
||||||
|
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; }
|
||||||
|
|
||||||
|
void Accept(IAsyncResult ar)
|
||||||
|
{
|
||||||
|
Socket clientSocket = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
clientSocket = _ServerSocket.EndAccept(ar);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException e)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_Cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Dispose(clientSocket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var toSocksProxy = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||||
|
toSocksProxy.BeginConnect(_opts.SocksEndpoint, ConnectToSocks, new ProxyConnection()
|
||||||
|
{
|
||||||
|
ClientSocket = clientSocket,
|
||||||
|
SocksSocket = toSocksProxy,
|
||||||
|
CancellationToken = _Cts.Token
|
||||||
|
});
|
||||||
|
_ServerSocket.BeginAccept(Accept, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConnectToSocks(IAsyncResult ar)
|
||||||
|
{
|
||||||
|
var connection = (ProxyConnection)ar.AsyncState;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
connection.SocksSocket.EndConnect(ar);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
connection.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Interlocked.Increment(ref connectionCount);
|
||||||
|
var pipe = new Pipe(PipeOptions.Default);
|
||||||
|
var reading = FillPipeAsync(connection.ClientSocket, pipe.Writer, connection.CancellationToken);
|
||||||
|
var writing = ReadPipeAsync(connection.SocksSocket, connection.ClientSocket, pipe.Reader, connection.CancellationToken);
|
||||||
|
_ = Task.WhenAll(reading, writing)
|
||||||
|
.ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
connection.Dispose();
|
||||||
|
Interlocked.Decrement(ref connectionCount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int connectionCount = 0;
|
||||||
|
public int ConnectionCount => connectionCount;
|
||||||
|
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.HostUnreachable || e.SocksErrorCode == SocksErrorCode.HostUnreachable)
|
||||||
|
{
|
||||||
|
await SendAsync(clientSocket , $"{httpVersion} 502 Bad Gateway\r\n\r\n", cancellationToken);
|
||||||
|
}
|
||||||
|
catch (SocksException e)
|
||||||
|
{
|
||||||
|
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\nX-Proxy-Error-Type: Socks {e.SocksErrorCode}\r\n\r\n", cancellationToken);
|
||||||
|
}
|
||||||
|
catch (SocketException e)
|
||||||
|
{
|
||||||
|
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\nX-Proxy-Error-Type: Socket {e.SocketErrorCode}\r\n\r\n", cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await SendAsync(clientSocket , $"{httpVersion} 500 Internal Server Error\r\n\r\n", cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
// Allocate at least 512 bytes from the PipeWriter
|
||||||
|
Memory<byte> memory = writer.GetMemory(BufferSize);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None, cancellationToken);
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Tell the PipeWriter how much was read from the Socket
|
||||||
|
writer.Advance(bytesRead);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//LogError(ex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the data available to the PipeReader
|
||||||
|
FlushResult result = await writer.FlushAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsCompleted)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell the PipeReader that there's no more data coming
|
||||||
|
writer.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_ServerSocket is Socket)
|
||||||
|
{
|
||||||
|
_Cts.Cancel();
|
||||||
|
Dispose(_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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,7 +68,6 @@ namespace BTCPayServer.Hosting
|
|||||||
services.TryAddSingleton<SettingsRepository>();
|
services.TryAddSingleton<SettingsRepository>();
|
||||||
services.TryAddSingleton<TorServices>();
|
services.TryAddSingleton<TorServices>();
|
||||||
services.TryAddSingleton<SocketFactory>();
|
services.TryAddSingleton<SocketFactory>();
|
||||||
services.TryAddSingleton<Socks5HttpClientFactory>();
|
|
||||||
services.TryAddSingleton<LightningClientFactoryService>();
|
services.TryAddSingleton<LightningClientFactoryService>();
|
||||||
services.TryAddSingleton<InvoicePaymentNotification>();
|
services.TryAddSingleton<InvoicePaymentNotification>();
|
||||||
services.TryAddSingleton<BTCPayServerOptions>(o =>
|
services.TryAddSingleton<BTCPayServerOptions>(o =>
|
||||||
|
|||||||
@@ -11,8 +11,13 @@ namespace BTCPayServer.Payments.PayJoin
|
|||||||
{
|
{
|
||||||
services.AddSingleton<DelayedTransactionBroadcaster>();
|
services.AddSingleton<DelayedTransactionBroadcaster>();
|
||||||
services.AddSingleton<IHostedService, HostedServices.DelayedTransactionBroadcasterHostedService>();
|
services.AddSingleton<IHostedService, HostedServices.DelayedTransactionBroadcasterHostedService>();
|
||||||
|
services.AddSingleton<HostedServices.Socks5HttpProxyServer>();
|
||||||
|
services.AddSingleton<IHostedService, HostedServices.Socks5HttpProxyServer>(s => s.GetRequiredService<Socks5HttpProxyServer>());
|
||||||
services.AddSingleton<PayJoinRepository>();
|
services.AddSingleton<PayJoinRepository>();
|
||||||
services.AddSingleton<PayjoinClient>();
|
services.AddSingleton<PayjoinClient>();
|
||||||
|
services.AddTransient<Socks5HttpClientHandler>();
|
||||||
|
services.AddHttpClient(PayjoinClient.PayjoinOnionNamedClient)
|
||||||
|
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ using System.Net.Http;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Google.Apis.Http;
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using IHttpClientFactory = System.Net.Http.IHttpClientFactory;
|
||||||
|
|
||||||
namespace BTCPayServer.Services
|
namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,8 @@ namespace BTCPayServer.Services
|
|||||||
|
|
||||||
public class PayjoinClient
|
public class PayjoinClient
|
||||||
{
|
{
|
||||||
|
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
||||||
|
public const string PayjoinClearnetNamedClient = "payjoin.clearnet";
|
||||||
public static readonly ScriptPubKeyType[] SupportedFormats = {
|
public static readonly ScriptPubKeyType[] SupportedFormats = {
|
||||||
ScriptPubKeyType.Segwit,
|
ScriptPubKeyType.Segwit,
|
||||||
ScriptPubKeyType.SegwitP2SH
|
ScriptPubKeyType.SegwitP2SH
|
||||||
@@ -43,16 +47,14 @@ namespace BTCPayServer.Services
|
|||||||
public const string BIP21EndpointKey = "bpu";
|
public const string BIP21EndpointKey = "bpu";
|
||||||
|
|
||||||
private readonly ExplorerClientProvider _explorerClientProvider;
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
||||||
private HttpClient _clearnetHttpClient;
|
private IHttpClientFactory _httpClientFactory;
|
||||||
private HttpClient _torHttpClient;
|
|
||||||
|
|
||||||
public PayjoinClient(ExplorerClientProvider explorerClientProvider, IHttpClientFactory httpClientFactory, Socks5HttpClientFactory socks5HttpClientFactory)
|
public PayjoinClient(ExplorerClientProvider explorerClientProvider, IHttpClientFactory httpClientFactory)
|
||||||
{
|
{
|
||||||
if (httpClientFactory == null) throw new ArgumentNullException(nameof(httpClientFactory));
|
if (httpClientFactory == null) throw new ArgumentNullException(nameof(httpClientFactory));
|
||||||
_explorerClientProvider =
|
_explorerClientProvider =
|
||||||
explorerClientProvider ?? throw new ArgumentNullException(nameof(explorerClientProvider));
|
explorerClientProvider ?? throw new ArgumentNullException(nameof(explorerClientProvider));
|
||||||
_clearnetHttpClient = httpClientFactory.CreateClient("payjoin");
|
_httpClientFactory = httpClientFactory;
|
||||||
_torHttpClient = socks5HttpClientFactory.CreateClient("payjoin");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
|
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
|
||||||
@@ -95,11 +97,7 @@ namespace BTCPayServer.Services
|
|||||||
}
|
}
|
||||||
|
|
||||||
cloned.GlobalXPubs.Clear();
|
cloned.GlobalXPubs.Clear();
|
||||||
HttpClient client = _clearnetHttpClient;
|
using HttpClient client = CreateHttpClient(endpoint);
|
||||||
if (endpoint.IsOnion() && _torHttpClient != null)
|
|
||||||
{
|
|
||||||
client = _torHttpClient;
|
|
||||||
}
|
|
||||||
var bpuresponse = await client.PostAsync(endpoint,
|
var bpuresponse = await client.PostAsync(endpoint,
|
||||||
new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);
|
new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);
|
||||||
if (!bpuresponse.IsSuccessStatusCode)
|
if (!bpuresponse.IsSuccessStatusCode)
|
||||||
@@ -229,6 +227,14 @@ namespace BTCPayServer.Services
|
|||||||
|
|
||||||
return newPSBT;
|
return newPSBT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HttpClient CreateHttpClient(Uri uri)
|
||||||
|
{
|
||||||
|
if (uri.IsOnion())
|
||||||
|
return _httpClientFactory.CreateClient(PayjoinOnionNamedClient);
|
||||||
|
else
|
||||||
|
return _httpClientFactory.CreateClient(PayjoinClearnetNamedClient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PayjoinException : Exception
|
public class PayjoinException : Exception
|
||||||
|
|||||||
@@ -1,415 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BTCPayServer.Services.Proxy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// https://github.com/TheSuunny/Yove.Proxy
|
|
||||||
/// </summary>
|
|
||||||
public class ProxyClient : IDisposable, IWebProxy
|
|
||||||
{
|
|
||||||
public enum ProxyType
|
|
||||||
{
|
|
||||||
Socks4,
|
|
||||||
Socks5
|
|
||||||
}
|
|
||||||
#region IWebProxy
|
|
||||||
|
|
||||||
public ICredentials Credentials { get; set; }
|
|
||||||
|
|
||||||
public int ReadWriteTimeOut { get; set; } = 60000;
|
|
||||||
|
|
||||||
public Uri GetProxy(Uri destination) => HttpProxyURL;
|
|
||||||
public bool IsBypassed(Uri host) => false;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ProxyClient
|
|
||||||
|
|
||||||
private Uri HttpProxyURL { get; set; }
|
|
||||||
private Socket InternalSocketServer { get; set; }
|
|
||||||
private int InternalSocketPort { get; set; }
|
|
||||||
|
|
||||||
private IPAddress Host { get; set; }
|
|
||||||
private int Port { get; set; }
|
|
||||||
private ProxyType Type { get; set; }
|
|
||||||
private int SocksVersion { get; set; }
|
|
||||||
|
|
||||||
public bool IsDisposed { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constants
|
|
||||||
|
|
||||||
private const byte AddressTypeIPV4 = 0x01;
|
|
||||||
private const byte AddressTypeIPV6 = 0x04;
|
|
||||||
private const byte AddressTypeDomainName = 0x03;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public ProxyClient(string Proxy, ProxyType Type)
|
|
||||||
{
|
|
||||||
string Host = Proxy.Split(':')[0]?.Trim();
|
|
||||||
int Port = Convert.ToInt32(Proxy.Split(':')[1]?.Trim());
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(Host))
|
|
||||||
throw new ArgumentNullException("Host null or empty");
|
|
||||||
|
|
||||||
if (Port < 0 || Port > 65535)
|
|
||||||
throw new ArgumentOutOfRangeException("Port goes beyond");
|
|
||||||
|
|
||||||
this.Host = GetHost(Host);
|
|
||||||
this.Port = Port;
|
|
||||||
this.Type = Type;
|
|
||||||
|
|
||||||
SocksVersion = (Type == ProxyType.Socks4) ? 4 : 5;
|
|
||||||
|
|
||||||
CreateInternalServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProxyClient(string Host, int Port, ProxyType Type)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Host))
|
|
||||||
throw new ArgumentNullException("Host null or empty");
|
|
||||||
|
|
||||||
if (Port < 0 || Port > 65535)
|
|
||||||
throw new ArgumentOutOfRangeException("Port goes beyond");
|
|
||||||
|
|
||||||
this.Host = GetHost(Host);
|
|
||||||
this.Port = Port;
|
|
||||||
this.Type = Type;
|
|
||||||
|
|
||||||
SocksVersion = (Type == ProxyType.Socks4) ? 4 : 5;
|
|
||||||
|
|
||||||
CreateInternalServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateInternalServer()
|
|
||||||
{
|
|
||||||
InternalSocketServer = CreateSocketServer();
|
|
||||||
|
|
||||||
InternalSocketServer.Bind(new IPEndPoint(IPAddress.Loopback, 0));
|
|
||||||
InternalSocketPort = ((IPEndPoint)(InternalSocketServer.LocalEndPoint)).Port;
|
|
||||||
|
|
||||||
HttpProxyURL = new Uri($"http://127.0.0.1:{InternalSocketPort}");
|
|
||||||
|
|
||||||
InternalSocketServer.Listen(512);
|
|
||||||
InternalSocketServer.BeginAccept(new AsyncCallback(AcceptCallback), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void AcceptCallback(IAsyncResult e)
|
|
||||||
{
|
|
||||||
if (IsDisposed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Socket Socket = InternalSocketServer.EndAccept(e);
|
|
||||||
InternalSocketServer.BeginAccept(new AsyncCallback(AcceptCallback), null);
|
|
||||||
|
|
||||||
byte[] HeaderBuffer = new byte[8192]; // Default Header size
|
|
||||||
|
|
||||||
Socket.Receive(HeaderBuffer, HeaderBuffer.Length, 0);
|
|
||||||
|
|
||||||
string Header = Encoding.ASCII.GetString(HeaderBuffer);
|
|
||||||
|
|
||||||
string HttpVersion = Header.Split(' ')[2].Split('\r')[0]?.Trim();
|
|
||||||
string TargetURL = Header.Split(' ')[1]?.Trim();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(HttpVersion) || string.IsNullOrEmpty(TargetURL))
|
|
||||||
throw new Exception("Unsupported request.");
|
|
||||||
|
|
||||||
string UriHostname = string.Empty;
|
|
||||||
int UriPort = 0;
|
|
||||||
|
|
||||||
if (TargetURL.Contains(":") && !TargetURL.Contains("http://"))
|
|
||||||
{
|
|
||||||
UriHostname = TargetURL.Split(':')[0];
|
|
||||||
UriPort = int.Parse(TargetURL.Split(':')[1]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Uri URL = new Uri(TargetURL);
|
|
||||||
|
|
||||||
UriHostname = URL.Host;
|
|
||||||
UriPort = URL.Port;
|
|
||||||
}
|
|
||||||
|
|
||||||
Socket TargetSocket = CreateSocketServer();
|
|
||||||
|
|
||||||
SocketError Connection = await TrySocksConnection(UriHostname, UriPort, TargetSocket);
|
|
||||||
|
|
||||||
if (Connection != SocketError.Success)
|
|
||||||
{
|
|
||||||
if (Connection == SocketError.HostUnreachable || Connection == SocketError.ConnectionRefused || Connection == SocketError.ConnectionReset)
|
|
||||||
Send(Socket, $"{HttpVersion} 502 Bad Gateway\r\n\r\n");
|
|
||||||
else if (Connection == SocketError.AccessDenied)
|
|
||||||
Send(Socket, $"{HttpVersion} 401 Unauthorized\r\n\r\n");
|
|
||||||
else
|
|
||||||
Send(Socket, $"{HttpVersion} 500 Internal Server Error\r\nX-Proxy-Error-Type: {Connection}\r\n\r\n");
|
|
||||||
|
|
||||||
Dispose(Socket);
|
|
||||||
Dispose(TargetSocket);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Send(Socket, $"{HttpVersion} 200 Connection established\r\n\r\n");
|
|
||||||
|
|
||||||
Relay(Socket, TargetSocket, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SocketError> TrySocksConnection(string DestinationAddress, int DestinationPort, Socket Socket)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Socket.Connect(new IPEndPoint(Host, Port));
|
|
||||||
|
|
||||||
if (Type == ProxyType.Socks4)
|
|
||||||
return await SendSocks4(Socket, DestinationAddress, DestinationPort).ConfigureAwait(false);
|
|
||||||
else if (Type == ProxyType.Socks5)
|
|
||||||
return await SendSocks5(Socket, DestinationAddress, DestinationPort).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return SocketError.ProtocolNotSupported;
|
|
||||||
}
|
|
||||||
catch (SocketException ex)
|
|
||||||
{
|
|
||||||
return ex.SocketErrorCode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Relay(Socket Source, Socket Target, bool IsTarget)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!IsTarget)
|
|
||||||
Task.Run(() => Relay(Target, Source, true));
|
|
||||||
|
|
||||||
int Read = 0;
|
|
||||||
byte[] Buffer = new byte[8192];
|
|
||||||
|
|
||||||
while ((Read = Source.Receive(Buffer, 0, Buffer.Length, SocketFlags.None)) > 0)
|
|
||||||
Target.Send(Buffer, 0, Read, SocketFlags.None);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignored
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!IsTarget)
|
|
||||||
{
|
|
||||||
Dispose(Source);
|
|
||||||
Dispose(Target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SocketError> SendSocks4(Socket Socket, string DestinationHost, int DestinationPort)
|
|
||||||
{
|
|
||||||
byte AddressType = GetAddressType(DestinationHost);
|
|
||||||
|
|
||||||
if (AddressType == AddressTypeDomainName)
|
|
||||||
DestinationHost = GetHost(DestinationHost).ToString();
|
|
||||||
|
|
||||||
byte[] Address = GetIPAddressBytes(DestinationHost);
|
|
||||||
byte[] Port = GetPortBytes(DestinationPort);
|
|
||||||
byte[] UserId = new byte[0];
|
|
||||||
|
|
||||||
byte[] Request = new byte[9];
|
|
||||||
|
|
||||||
Request[0] = (byte)SocksVersion;
|
|
||||||
Request[1] = 0x01;
|
|
||||||
Address.CopyTo(Request, 4);
|
|
||||||
Port.CopyTo(Request, 2);
|
|
||||||
UserId.CopyTo(Request, 8);
|
|
||||||
Request[8] = 0x00;
|
|
||||||
|
|
||||||
byte[] Response = new byte[8];
|
|
||||||
|
|
||||||
Socket.Send(Request);
|
|
||||||
|
|
||||||
await WaitStream(Socket).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Socket.Receive(Response);
|
|
||||||
|
|
||||||
if (Response[1] != 0x5a)
|
|
||||||
return SocketError.ConnectionRefused;
|
|
||||||
|
|
||||||
return SocketError.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<SocketError> SendSocks5(Socket Socket, string DestinationHost, int DestinationPort)
|
|
||||||
{
|
|
||||||
byte[] Response = new byte[255];
|
|
||||||
|
|
||||||
byte[] Auth = new byte[3];
|
|
||||||
Auth[0] = (byte)SocksVersion;
|
|
||||||
Auth[1] = (byte)1;
|
|
||||||
Auth[2] = (byte)0;
|
|
||||||
|
|
||||||
Socket.Send(Auth);
|
|
||||||
|
|
||||||
await WaitStream(Socket).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Socket.Receive(Response);
|
|
||||||
|
|
||||||
if (Response[1] != 0x00)
|
|
||||||
return SocketError.ConnectionRefused;
|
|
||||||
|
|
||||||
byte AddressType = GetAddressType(DestinationHost);
|
|
||||||
|
|
||||||
if (AddressType == AddressTypeDomainName)
|
|
||||||
DestinationHost = GetHost(DestinationHost).ToString();
|
|
||||||
|
|
||||||
byte[] Address = GetAddressBytes(AddressType, DestinationHost);
|
|
||||||
byte[] Port = GetPortBytes(DestinationPort);
|
|
||||||
|
|
||||||
byte[] Request = new byte[4 + Address.Length + 2];
|
|
||||||
|
|
||||||
Request[0] = (byte)SocksVersion;
|
|
||||||
Request[1] = 0x01;
|
|
||||||
Request[2] = 0x00;
|
|
||||||
Request[3] = AddressType;
|
|
||||||
Address.CopyTo(Request, 4);
|
|
||||||
Port.CopyTo(Request, 4 + Address.Length);
|
|
||||||
|
|
||||||
Socket.Send(Request);
|
|
||||||
|
|
||||||
await WaitStream(Socket).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Socket.Receive(Response);
|
|
||||||
|
|
||||||
if (Response[1] != 0x00)
|
|
||||||
return SocketError.ConnectionRefused;
|
|
||||||
|
|
||||||
return SocketError.Success;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task WaitStream(Socket Socket)
|
|
||||||
{
|
|
||||||
int Sleep = 0;
|
|
||||||
int Delay = (Socket.ReceiveTimeout < 10) ? 10 : Socket.ReceiveTimeout;
|
|
||||||
|
|
||||||
while (Socket.Available == 0)
|
|
||||||
{
|
|
||||||
if (Sleep < Delay)
|
|
||||||
{
|
|
||||||
Sleep += 10;
|
|
||||||
await Task.Delay(10).ConfigureAwait(false);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Exception($"Timeout waiting for data - {Host}:{Port}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Socket CreateSocketServer()
|
|
||||||
{
|
|
||||||
Socket Socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp);
|
|
||||||
|
|
||||||
Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, 0);
|
|
||||||
Socket.ExclusiveAddressUse = true;
|
|
||||||
|
|
||||||
Socket.ReceiveTimeout = Socket.SendTimeout = ReadWriteTimeOut;
|
|
||||||
|
|
||||||
return Socket;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Send(Socket Socket, string Message)
|
|
||||||
{
|
|
||||||
Socket.Send(Encoding.UTF8.GetBytes(Message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private IPAddress GetHost(string Host)
|
|
||||||
{
|
|
||||||
if (IPAddress.TryParse(Host, out IPAddress Ip))
|
|
||||||
return Ip;
|
|
||||||
|
|
||||||
return Dns.GetHostAddresses(Host)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] GetAddressBytes(byte AddressType, string Host)
|
|
||||||
{
|
|
||||||
switch (AddressType)
|
|
||||||
{
|
|
||||||
case AddressTypeIPV4:
|
|
||||||
case AddressTypeIPV6:
|
|
||||||
return IPAddress.Parse(Host).GetAddressBytes();
|
|
||||||
case AddressTypeDomainName:
|
|
||||||
byte[] Bytes = new byte[Host.Length + 1];
|
|
||||||
|
|
||||||
Bytes[0] = (byte)Host.Length;
|
|
||||||
Encoding.ASCII.GetBytes(Host).CopyTo(Bytes, 1);
|
|
||||||
|
|
||||||
return Bytes;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte GetAddressType(string Host)
|
|
||||||
{
|
|
||||||
if (IPAddress.TryParse(Host, out IPAddress Ip))
|
|
||||||
{
|
|
||||||
if (Ip.AddressFamily == AddressFamily.InterNetwork)
|
|
||||||
return AddressTypeIPV4;
|
|
||||||
|
|
||||||
return AddressTypeIPV6;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AddressTypeDomainName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] GetIPAddressBytes(string DestinationHost)
|
|
||||||
{
|
|
||||||
IPAddress Address = null;
|
|
||||||
|
|
||||||
if (!IPAddress.TryParse(DestinationHost, out Address))
|
|
||||||
{
|
|
||||||
IPAddress[] IPs = Dns.GetHostAddresses(DestinationHost);
|
|
||||||
|
|
||||||
if (IPs.Length > 0)
|
|
||||||
Address = IPs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Address.GetAddressBytes();
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] GetPortBytes(int Port)
|
|
||||||
{
|
|
||||||
byte[] ArrayBytes = new byte[2];
|
|
||||||
|
|
||||||
ArrayBytes[0] = (byte)(Port / 256);
|
|
||||||
ArrayBytes[1] = (byte)(Port % 256);
|
|
||||||
|
|
||||||
return ArrayBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Dispose(Socket Socket)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Socket.Close();
|
|
||||||
Socket.Dispose();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (InternalSocketServer != null && !IsDisposed)
|
|
||||||
{
|
|
||||||
IsDisposed = true;
|
|
||||||
|
|
||||||
InternalSocketServer.Disconnect(false);
|
|
||||||
InternalSocketServer.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,39 +5,12 @@ using System.Net.Sockets;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Configuration;
|
using BTCPayServer.Configuration;
|
||||||
using BTCPayServer.Services.Proxy;
|
|
||||||
using NBitcoin;
|
using NBitcoin;
|
||||||
using NBitcoin.Protocol.Connectors;
|
using NBitcoin.Protocol.Connectors;
|
||||||
using NBitcoin.Protocol;
|
using NBitcoin.Protocol;
|
||||||
|
|
||||||
namespace BTCPayServer.Services
|
namespace BTCPayServer.Services
|
||||||
{
|
{
|
||||||
public class Socks5HttpClientFactory : IHttpClientFactory
|
|
||||||
{
|
|
||||||
private readonly BTCPayServerOptions _options;
|
|
||||||
|
|
||||||
public Socks5HttpClientFactory(BTCPayServerOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
private ConcurrentDictionary<string, HttpClient> cachedClients = new ConcurrentDictionary<string, HttpClient>();
|
|
||||||
public HttpClient CreateClient(string name)
|
|
||||||
{
|
|
||||||
return cachedClients.GetOrAdd(name, s =>
|
|
||||||
{
|
|
||||||
if (_options.SocksEndpoint == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var proxy = new ProxyClient(_options.SocksEndpoint.ToEndpointString(), ProxyClient.ProxyType.Socks5);
|
|
||||||
return new HttpClient(
|
|
||||||
new HttpClientHandler {Proxy = proxy, },
|
|
||||||
true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SocketFactory
|
public class SocketFactory
|
||||||
{
|
{
|
||||||
private readonly BTCPayServerOptions _options;
|
private readonly BTCPayServerOptions _options;
|
||||||
|
|||||||
14
BTCPayServer/Services/Socks5HttpClientHandler.cs
Normal file
14
BTCPayServer/Services/Socks5HttpClientHandler.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using BTCPayServer.HostedServices;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Services
|
||||||
|
{
|
||||||
|
public class Socks5HttpClientHandler : HttpClientHandler
|
||||||
|
{
|
||||||
|
public Socks5HttpClientHandler(Socks5HttpProxyServer sock5)
|
||||||
|
{
|
||||||
|
this.Proxy = new WebProxy(sock5.Uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user