diff --git a/source/Halibut/Transport/Proxy/ProxyClientFactory.cs b/source/Halibut/Transport/Proxy/ProxyClientFactory.cs index c7d0f0cbc..3286c4d5e 100644 --- a/source/Halibut/Transport/Proxy/ProxyClientFactory.cs +++ b/source/Halibut/Transport/Proxy/ProxyClientFactory.cs @@ -102,6 +102,12 @@ public IProxyClient CreateProxyClient(ILog logger, ProxyType type, string proxyH { case ProxyType.HTTP: return new HttpProxyClient(logger, proxyHost, proxyPort, proxyUsername, proxyPassword, streamFactory); + #if NET8_0_OR_GREATER + case ProxyType.SOCKS4: + case ProxyType.SOCKS4A: + case ProxyType.SOCKS5: + return new SocksProxyClient(logger, proxyHost, proxyPort, proxyUsername, proxyPassword, streamFactory); + #endif default: throw new ProxyException(string.Format("Unknown proxy type {0}.", type.ToString()), false); } diff --git a/source/Halibut/Transport/Proxy/SocksProxyClient.cs b/source/Halibut/Transport/Proxy/SocksProxyClient.cs new file mode 100644 index 000000000..68eeaa062 --- /dev/null +++ b/source/Halibut/Transport/Proxy/SocksProxyClient.cs @@ -0,0 +1,126 @@ +// Copyright 2012-2013 Octopus Deploy Pty. Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if NET8_0_OR_GREATER + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Halibut.Diagnostics; +using Halibut.Transport.Proxy.Exceptions; +using Halibut.Transport.Streams; + +namespace Halibut.Transport.Proxy +{ + public class SocksProxyClient : IProxyClient + { + readonly ILog log; + readonly string? proxyUsername; + readonly string? proxyPassword; + readonly IStreamFactory streamFactory; + public string ProxyHost { get; set; } + public int ProxyPort { get; set; } + + public TcpClient? TcpClient { get; set; } + public string ProxyName => "SOCKS"; + + Func? tcpClientFactory; + readonly Uri proxyUri; + + static readonly Lazy> EstablishSocksTunnel = new(() => + { + var socksHelperType = typeof(System.Net.Http.HttpClient).Assembly.GetType("System.Net.Http.SocksHelper"); + if (socksHelperType == null) throw new InvalidOperationException("Could not find System.Net.Http.SocksHelper type."); + + var method = socksHelperType.GetMethod("EstablishSocksTunnelAsync", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + if (method == null) throw new InvalidOperationException("Could not find EstablishSocksTunnelAsync method on SocksHelper."); + + return (Func) Delegate.CreateDelegate(typeof(Func), method); }); + + public SocksProxyClient(ILog log, string proxyHost, int proxyPort, string? proxyUsername, string? proxyPassword, IStreamFactory streamFactory) + { + this.log = log; + this.proxyUsername = proxyUsername; + this.proxyPassword = proxyPassword; + this.streamFactory = streamFactory; + ProxyHost = proxyHost; + ProxyPort = proxyPort; + + // ToDo: Use the correct protocol scheme for difference version of SOCKS + proxyUri = new Uri($"socks5://{proxyHost}:{proxyPort}"); + } + + public IProxyClient WithTcpClientFactory(Func tcpClientfactory) + { + tcpClientFactory = tcpClientfactory; + return this; + } + + public async Task CreateConnectionAsync(string destinationHost, int destinationPort, TimeSpan timeout, CancellationToken cancellationToken) + { + try + { + // if we have no connection, create one + if (TcpClient == null) + { + if (string.IsNullOrEmpty(ProxyHost)) + throw new ProxyException("ProxyHost property must contain a value", false); + + if (ProxyPort <= 0 || ProxyPort > 65535) + throw new ProxyException("ProxyPort value must be greater than zero and less than 65535", false); + + if(ProxyHost.Contains("://")) + throw new ProxyException("The proxy's hostname cannot contain a protocol prefix (eg http://)", false); + + TcpClient = tcpClientFactory!(); + + // attempt to open the connection + log.Write(EventType.Diagnostic, "Connecting to proxy at {0}:{1}", ProxyHost, ProxyPort); + await TcpClient.ConnectWithTimeoutAsync(ProxyHost, ProxyPort, timeout, cancellationToken); + log.Write(EventType.Diagnostic, "Connected to proxy at {0}:{1}", ProxyHost, ProxyPort); + } + + var stream = streamFactory.CreateStream(TcpClient!); + await EstablishSocksTunnel.Value(stream, destinationHost, destinationPort, proxyUri, GetCredentials(), true, cancellationToken); + + // return the open proxied tcp client object to the caller for normal use + return TcpClient; + } + catch (AggregateException ae) + { + var se = ae.InnerExceptions.OfType().FirstOrDefault(); + if (se != null) + throw new ProxyException($"Connection to proxy host {ProxyHost} on port {ProxyPort} failed: {se.Message}", se, true); + + throw; + } + catch (SocketException ex) + { + throw new ProxyException($"Connection to proxy host {ProxyHost} on port {ProxyPort} failed: {ex.Message}", ex, true); + } + } + + ICredentials? GetCredentials() + { + if (proxyUsername != null && proxyPassword != null) return new NetworkCredential(proxyUsername, proxyPassword); + return null; + } + } +} + +#endif \ No newline at end of file