add all
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3369a45fdd3747b44aca8c6fbfd40732
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Best.HTTP.Proxies.Autodetect
|
||||
{
|
||||
public sealed class AndroidProxyDetector : IProxyDetector
|
||||
{
|
||||
private const string ClassPath = "com.Best.HTTP.proxy.ProxyFinder";
|
||||
|
||||
Proxy IProxyDetector.GetProxy(HTTPRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var proxyUrl = FindFor(request.CurrentUri.ToString());
|
||||
|
||||
HTTPManager.Logger.Information(nameof(AndroidProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - FindFor returned with proxyUrl: '{proxyUrl}'", request.Context);
|
||||
|
||||
if (proxyUrl == null)
|
||||
return null;
|
||||
|
||||
if (proxyUrl.StartsWith("socks://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SOCKSProxy(new Uri(proxyUrl), null);
|
||||
}
|
||||
else if (proxyUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new HTTPProxy(new Uri(proxyUrl));
|
||||
}
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(AndroidProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - FindFor returned with unknown format. proxyUrl: '{proxyUrl}'", request.Context);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(AndroidProxyDetector), nameof(IProxyDetector.GetProxy), ex, request.Context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string FindFor(string uriStr) => Call<string>("FindFor", uriStr);
|
||||
|
||||
private static T Call<T>(string methodName, params object[] args)
|
||||
{
|
||||
bool isMainThread = HTTPUpdateDelegator.Instance.IsMainThread();
|
||||
try
|
||||
{
|
||||
if (!isMainThread)
|
||||
AndroidJNI.AttachCurrentThread();
|
||||
|
||||
using (var javaClass = new AndroidJavaClass(ClassPath))
|
||||
return javaClass.CallStatic<T>(methodName, args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!isMainThread)
|
||||
AndroidJNI.DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aa9c1e7ca4f4ea54cba5a91100ffa10b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
// Examples on proxy strings:
|
||||
// https://gist.github.com/yougg/5d2b3353fc5e197a0917aae0b3287d64
|
||||
|
||||
namespace Best.HTTP.Proxies.Autodetect
|
||||
{
|
||||
/// <summary>
|
||||
/// Based on <see href="https://curl.se/docs/manual.html"/>'s "Environment Variables" section.
|
||||
/// </summary>
|
||||
public sealed class EnvironmentProxyDetector : IProxyDetector
|
||||
{
|
||||
private Proxy _cachedProxy;
|
||||
|
||||
Proxy IProxyDetector.GetProxy(HTTPRequest request)
|
||||
{
|
||||
if (this._cachedProxy != null)
|
||||
return this._cachedProxy;
|
||||
|
||||
string proxyUrl = null;
|
||||
|
||||
if (HTTPProtocolFactory.IsSecureProtocol(request.CurrentUri))
|
||||
{
|
||||
proxyUrl = GetEnv("HTTPS_PROXY");
|
||||
HTTPManager.Logger.Information(nameof(EnvironmentProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - HTTPS_PROXY: '{proxyUrl}'", request.Context);
|
||||
}
|
||||
else
|
||||
{
|
||||
proxyUrl = GetEnv("HTTP_PROXY");
|
||||
HTTPManager.Logger.Information(nameof(EnvironmentProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - HTTP_PROXY: '{proxyUrl}'", request.Context);
|
||||
}
|
||||
|
||||
if (proxyUrl == null)
|
||||
{
|
||||
proxyUrl = GetEnv("ALL_PROXY");
|
||||
}
|
||||
else
|
||||
HTTPManager.Logger.Information(nameof(EnvironmentProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - ALL_PROXY: '{proxyUrl}'", request.Context);
|
||||
|
||||
if (string.IsNullOrEmpty(proxyUrl))
|
||||
return null;
|
||||
|
||||
// if the url is just a host[:port], add the http:// part too. Checking for :// should keep and treat the socks:// scheme too.
|
||||
if (proxyUrl.IndexOf("://") == -1 && !proxyUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
proxyUrl = "http://" + proxyUrl;
|
||||
|
||||
string exceptionList = null;
|
||||
try
|
||||
{
|
||||
var proxyUri = new Uri(proxyUrl);
|
||||
|
||||
Proxy proxy = null;
|
||||
if (proxyUri.Scheme.StartsWith("socks", StringComparison.OrdinalIgnoreCase))
|
||||
proxy = new SOCKSProxy(proxyUri, null);
|
||||
else
|
||||
proxy = new HTTPProxy(proxyUri);
|
||||
|
||||
// A comma-separated list of host names that should not go through any proxy is set in (only an asterisk, * matches all hosts)
|
||||
exceptionList = GetEnv("NO_PROXY");
|
||||
if (!string.IsNullOrEmpty(exceptionList))
|
||||
proxy.Exceptions = exceptionList.Split(';').ToList<string>();
|
||||
|
||||
return this._cachedProxy = proxy;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HTTPManager.Logger.Exception(nameof(EnvironmentProxyDetector), $"GetProxy - proxyUrl: '{proxyUrl}', exceptionList: '{exceptionList}'", ex, request.Context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
string GetEnv(string key) => System.Environment.GetEnvironmentVariable(key) ?? System.Environment.GetEnvironmentVariable(key.ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c43a242bd76482c4b9d41d7093047658
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,69 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.Proxies.Autodetect
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a detector using the .net framework's implementation. It might work not just under Windows but MacOS and Linux too.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// More details can be found here:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><see href="https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient.defaultproxy?view=net-6.0">HttpClient.DefaultProxy Property</see></description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class FrameworkProxyDetector : IProxyDetector
|
||||
{
|
||||
Proxy IProxyDetector.GetProxy(HTTPRequest request)
|
||||
{
|
||||
var detectedProxy = System.Net.WebRequest.GetSystemWebProxy() as System.Net.WebProxy;
|
||||
if (detectedProxy != null && detectedProxy.Address != null)
|
||||
{
|
||||
var proxyUri = detectedProxy.GetProxy(request.CurrentUri);
|
||||
if (proxyUri != null && !proxyUri.Equals(request.CurrentUri))
|
||||
{
|
||||
if (proxyUri.Scheme.StartsWith("socks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SetExceptionList(new SOCKSProxy(proxyUri, null), detectedProxy);
|
||||
}
|
||||
else if (proxyUri.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SetExceptionList(new HTTPProxy(proxyUri), detectedProxy);
|
||||
}
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(FrameworkProxyDetector), $"{nameof(IProxyDetector.GetProxy)} - FindFor returned with unknown format. proxyUri: '{proxyUri}'", request.Context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Proxy SetExceptionList(Proxy proxy, System.Net.WebProxy detectedProxy)
|
||||
{
|
||||
if (detectedProxy.BypassProxyOnLocal)
|
||||
{
|
||||
proxy.Exceptions = proxy.Exceptions ?? new System.Collections.Generic.List<string>();
|
||||
|
||||
proxy.Exceptions.Add("localhost");
|
||||
proxy.Exceptions.Add("127.0.0.1");
|
||||
}
|
||||
|
||||
// TODO: use BypassList to put more entries to the Exceptions list.
|
||||
// But because BypassList contains regex strings, we either
|
||||
// 1.) store and use regex strings in the Exception list (not backward compatible)
|
||||
// 2.) store non-regex strings but create a new list for regex
|
||||
// 3.) detect if the stored entry in the Exceptions list is regex or not and use it accordingly
|
||||
// "^.*\\.httpbin\\.org$"
|
||||
// https://github.com/Benedicht/BestHTTP-Issues/issues/141
|
||||
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9dcbe5fe0e354414e8e3c134836f4986
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
namespace Best.HTTP.Proxies.Autodetect
|
||||
{
|
||||
/// <summary>
|
||||
/// This one just returns with HTTPManager.Proxy,
|
||||
/// so when ProgrammaticallyAddedProxyDetector is used in the first place for the ProxyDetector,
|
||||
/// HTTPManager.Proxy gets the highest priority.
|
||||
/// </summary>
|
||||
public sealed class ProgrammaticallyAddedProxyDetector : IProxyDetector
|
||||
{
|
||||
Proxy IProxyDetector.GetProxy(HTTPRequest request) => HTTPManager.Proxy;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd7524aaa58af91418ca03146bd55dbc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,184 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.HTTP.Hosts.Connections;
|
||||
using Best.HTTP.Request.Timings;
|
||||
using Best.HTTP.Shared;
|
||||
|
||||
using System;
|
||||
|
||||
namespace Best.HTTP.Proxies.Autodetect
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for custom proxy-detection logic.
|
||||
/// </summary>
|
||||
public interface IProxyDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives the <see cref="HTTPRequest"/> instance this detector has to try to find a proxy.
|
||||
/// </summary>
|
||||
/// <param name="request"><see cref="HTTPRequest"/>instance to find a proxy for</param>
|
||||
/// <returns>A concrete <see cref="Proxy"/> implementation, or <c>null</c> if no proxy could be found.</returns>
|
||||
Proxy GetProxy(HTTPRequest request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible detection modes the <see cref="ProxyDetector"/> can be in.
|
||||
/// </summary>
|
||||
public enum ProxyDetectionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// In Continuous mode the ProxyDetector will check for a proxy for every request.
|
||||
/// </summary>
|
||||
Continuous,
|
||||
|
||||
/// <summary>
|
||||
/// This mode will cache the first Proxy found and use it for consecutive requests.
|
||||
/// </summary>
|
||||
CacheFirstFound
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to contain, manage and execute logic to detect available proxy on the network. It's a wrapper class to execute the various <see cref="IProxyDetector"/>s.
|
||||
/// </summary>
|
||||
public sealed class ProxyDetector
|
||||
{
|
||||
public static IProxyDetector[] GetDefaultDetectors() => new IProxyDetector[] {
|
||||
// HTTPManager.Proxy has the highest priority
|
||||
new ProgrammaticallyAddedProxyDetector(),
|
||||
|
||||
// then comes the environment set
|
||||
new EnvironmentProxyDetector(),
|
||||
|
||||
// .net framework's detector
|
||||
new FrameworkProxyDetector(),
|
||||
|
||||
#if UNITY_ANDROID && !UNITY_EDITOR
|
||||
new AndroidProxyDetector(),
|
||||
#endif
|
||||
};
|
||||
|
||||
private IProxyDetector[] _proxyDetectors;
|
||||
private ProxyDetectionMode _detectionMode;
|
||||
private bool _attached;
|
||||
|
||||
public ProxyDetector()
|
||||
: this(ProxyDetectionMode.CacheFirstFound, GetDefaultDetectors())
|
||||
{ }
|
||||
|
||||
public ProxyDetector(ProxyDetectionMode detectionMode)
|
||||
: this(detectionMode, GetDefaultDetectors())
|
||||
{ }
|
||||
|
||||
public ProxyDetector(ProxyDetectionMode detectionMode, IProxyDetector[] proxyDetectors)
|
||||
{
|
||||
this._detectionMode = detectionMode;
|
||||
this._proxyDetectors = proxyDetectors;
|
||||
|
||||
if (this._proxyDetectors != null)
|
||||
Reattach();
|
||||
}
|
||||
|
||||
public void Reattach()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(ProxyDetector), $"{nameof(Reattach)}({this._attached})");
|
||||
|
||||
if (!this._attached)
|
||||
{
|
||||
RequestEventHelper.OnEvent += OnRequestEvent;
|
||||
this._attached = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call Detach() to disable ProxyDetector's logic to find and set a proxy.
|
||||
/// </summary>
|
||||
public void Detach()
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(ProxyDetector), $"{nameof(Detach)}({this._attached})");
|
||||
|
||||
if (this._attached)
|
||||
{
|
||||
RequestEventHelper.OnEvent -= OnRequestEvent;
|
||||
this._attached = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRequestEvent(RequestEventInfo @event)
|
||||
{
|
||||
// The Resend event is raised for every request when it's queued up (sent or redirected).
|
||||
if (@event.Event == RequestEvents.QueuedResend &&
|
||||
@event.SourceRequest.ProxySettings != null &&
|
||||
@event.SourceRequest.ProxySettings.Proxy == null)
|
||||
{
|
||||
Uri uri = @event.SourceRequest.CurrentUri;
|
||||
|
||||
if (uri.Scheme.Equals("file"))
|
||||
return;
|
||||
|
||||
@event.SourceRequest.Timing.StartNext(TimingEventNames.ProxyDetection);
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < this._proxyDetectors.Length; i++)
|
||||
{
|
||||
var detector = this._proxyDetectors[i];
|
||||
|
||||
if (detector == null)
|
||||
continue;
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(ProxyDetector), $"Calling {detector.GetType().Name}'s GetProxy", @event.SourceRequest.Context);
|
||||
|
||||
Proxy proxy = null;
|
||||
|
||||
#if ENABLE_PROFILER
|
||||
using (var _ = new Unity.Profiling.ProfilerMarker($"{detector.GetType().Name}.GetProxy").Auto())
|
||||
#endif
|
||||
proxy = detector.GetProxy(@event.SourceRequest);
|
||||
|
||||
#if ENABLE_PROFILER
|
||||
using (var _ = new Unity.Profiling.ProfilerMarker($"{detector.GetType().Name}.UseProxyForAddress").Auto())
|
||||
#endif
|
||||
if (proxy != null && proxy.UseProxyForAddress(uri))
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Verbose(nameof(ProxyDetector), $"[{detector.GetType().Name}] Proxy found: {proxy.Address} ", @event.SourceRequest.Context);
|
||||
|
||||
switch (this._detectionMode)
|
||||
{
|
||||
case ProxyDetectionMode.Continuous:
|
||||
@event.SourceRequest.ProxySettings.Proxy = proxy;
|
||||
break;
|
||||
|
||||
case ProxyDetectionMode.CacheFirstFound:
|
||||
HTTPManager.Proxy = @event.SourceRequest.ProxySettings.Proxy = proxy;
|
||||
|
||||
HTTPManager.Logger.Verbose(nameof(ProxyDetector), $"Proxy cached in HTTPManager.Proxy!", @event.SourceRequest.Context);
|
||||
|
||||
Detach();
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._detectionMode == ProxyDetectionMode.CacheFirstFound)
|
||||
Detach();
|
||||
|
||||
HTTPManager.Logger.Information(nameof(ProxyDetector), $"No Proxy for '{uri}'.", @event.SourceRequest.Context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Exception(nameof(ProxyDetector), $"GetProxyFor({@event.SourceRequest.CurrentUri})", ex, @event.SourceRequest.Context);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@event.SourceRequest.Timing.StartNext(TimingEventNames.Queued);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 168a0e891e4343544b01417cd5fbdad7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
283
Packages/com.tivadar.best.http/Runtime/HTTP/Proxies/HTTPProxy.cs
Normal file
283
Packages/com.tivadar.best.http/Runtime/HTTP/Proxies/HTTPProxy.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
namespace Best.HTTP.Proxies
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an HTTP proxy server that can be used to route HTTP requests through.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The HTTPProxy class is an implementation of the <see cref="Proxy"/> base class, specifically designed for
|
||||
/// HTTP proxy servers. It provides features such as transparent proxy support, sending the entire URI, and handling proxy
|
||||
/// authentication. This class is used to configure and manage HTTP proxy settings for HTTP requests.
|
||||
/// </remarks>
|
||||
public sealed class HTTPProxy : Proxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets whether the proxy can act as a transparent proxy. Default value is <c>true</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A transparent proxy forwards client requests without modifying them. When set to <c>true</c>, the proxy behaves as a transparent
|
||||
/// proxy, meaning it forwards requests as-is. If set to <c>false</c>, it may modify requests, and this can be useful for certain
|
||||
/// advanced proxy configurations.
|
||||
/// </remarks>
|
||||
public bool IsTransparent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the proxy - when it's in non-transparent mode - excepts only the path and query of the request URI. Default value is <c>true</c>.
|
||||
/// </summary>
|
||||
public bool SendWholeUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the plugin will use the proxy as an explicit proxy for secure protocols (HTTPS://, WSS://).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When set to <c>true</c>, the plugin will issue a CONNECT request to the proxy for secure protocols, even if the proxy is
|
||||
/// marked as transparent. This is commonly used for ensuring proper handling of encrypted traffic through the proxy.
|
||||
/// </remarks>
|
||||
public bool NonTransparentForHTTPS { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the HTTPProxy class with the specified proxy address.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
public HTTPProxy(Uri address)
|
||||
:this(address, null, true)
|
||||
{}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the HTTPProxy class with the specified proxy address and credentials.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication.</param>
|
||||
public HTTPProxy(Uri address, Credentials credentials)
|
||||
:this(address, credentials, true)
|
||||
{}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, and transparency settings.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication.</param>
|
||||
/// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
|
||||
public HTTPProxy(Uri address, Credentials credentials, bool isTransparent)
|
||||
:this(address, credentials, isTransparent, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, transparency settings, and URI handling.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication.</param>
|
||||
/// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
|
||||
/// <param name="sendWholeUri">Specifies whether the proxy should send the entire URI (<c>true</c>) or just the path and query (<c>false</c>) for non-transparent proxies.</param>
|
||||
public HTTPProxy(Uri address, Credentials credentials, bool isTransparent, bool sendWholeUri)
|
||||
: this(address, credentials, isTransparent, sendWholeUri, true)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="HTTPProxy"/> class with the specified proxy address, credentials, transparency settings, URI handling, and HTTPS behavior.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication.</param>
|
||||
/// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
|
||||
/// <param name="sendWholeUri">Specifies whether the proxy should send the entire URI (<c>true</c>) or just the path and query (<c>false</c>) for non-transparent proxies.</param>
|
||||
/// <param name="nonTransparentForHTTPS">Specifies whether the plugin should use the proxy as an explicit proxy for secure protocols (HTTPS://, WSS://) (<c>true</c>) or not (<c>false</c>).</param>
|
||||
public HTTPProxy(Uri address, Credentials credentials, bool isTransparent, bool sendWholeUri, bool nonTransparentForHTTPS)
|
||||
:base(address, credentials)
|
||||
{
|
||||
this.IsTransparent = isTransparent;
|
||||
this.SendWholeUri = sendWholeUri;
|
||||
this.NonTransparentForHTTPS = nonTransparentForHTTPS;
|
||||
}
|
||||
|
||||
public override string GetRequestPath(Uri uri)
|
||||
{
|
||||
return this.SendWholeUri ? uri.OriginalString : uri.GetRequestPathAndQueryURL();
|
||||
}
|
||||
|
||||
internal override bool SetupRequest(HTTPRequest request)
|
||||
{
|
||||
if (request == null || request.Response == null || !this.IsTransparent)
|
||||
return false;
|
||||
|
||||
string authHeader = DigestStore.FindBest(request.Response.GetHeaderValues("proxy-authenticate"));
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
var digest = DigestStore.GetOrCreate(this.Address);
|
||||
digest.ParseChallange(authHeader);
|
||||
|
||||
if (this.Credentials != null && digest.IsUriProtected(this.Address) && (!request.HasHeader("Proxy-Authorization") || digest.Stale))
|
||||
{
|
||||
switch (this.Credentials.Type)
|
||||
{
|
||||
case AuthenticationTypes.Basic:
|
||||
// With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
|
||||
var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password));
|
||||
request.SetHeader("Proxy-Authorization", $"Basic {token}");
|
||||
return true;
|
||||
|
||||
case AuthenticationTypes.Unknown:
|
||||
case AuthenticationTypes.Digest:
|
||||
//var digest = DigestStore.Get(request.Proxy.Address);
|
||||
if (digest != null)
|
||||
{
|
||||
string authentication = digest.GenerateResponseHeader(this.Credentials, true, request.MethodType, request.CurrentUri);
|
||||
if (!string.IsNullOrEmpty(authentication))
|
||||
{
|
||||
request.SetHeader("Proxy-Authorization", authentication);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal override void BeginConnect(ProxyConnectParameters parameters)
|
||||
{
|
||||
if (!this.IsTransparent || (parameters.createTunel && this.NonTransparentForHTTPS))
|
||||
{
|
||||
using (var bufferedStream = new WriteOnlyBufferedStream(parameters.stream, 4 * 1024, parameters.context))
|
||||
using (var outStream = new BinaryWriter(bufferedStream, Encoding.UTF8))
|
||||
{
|
||||
// https://www.rfc-editor.org/rfc/rfc9110.html#name-connect
|
||||
|
||||
string connectStr = string.Format("CONNECT {0}:{1} HTTP/1.1", parameters.uri.Host, parameters.uri.Port.ToString());
|
||||
|
||||
HTTPManager.Logger.Information("HTTPProxy", "Sending " + connectStr, parameters.context);
|
||||
|
||||
outStream.SendAsASCII(connectStr);
|
||||
outStream.Write(EOL);
|
||||
|
||||
outStream.SendAsASCII(string.Format("Host: {0}:{1}", parameters.uri.Host, parameters.uri.Port.ToString()));
|
||||
outStream.Write(EOL);
|
||||
|
||||
outStream.SendAsASCII("Proxy-Connection: Keep-Alive");
|
||||
outStream.Write(EOL);
|
||||
|
||||
outStream.SendAsASCII("Connection: Keep-Alive");
|
||||
outStream.Write(EOL);
|
||||
|
||||
// Proxy Authentication
|
||||
if (this.Credentials != null)
|
||||
{
|
||||
switch (this.Credentials.Type)
|
||||
{
|
||||
case AuthenticationTypes.Basic:
|
||||
{
|
||||
// With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
|
||||
var buff = $"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password))}"
|
||||
.GetASCIIBytes();
|
||||
outStream.Write(buff.Data, buff.Offset, buff.Count);
|
||||
BufferPool.Release(buff);
|
||||
|
||||
outStream.Write(EOL);
|
||||
break;
|
||||
}
|
||||
|
||||
case AuthenticationTypes.Unknown:
|
||||
case AuthenticationTypes.Digest:
|
||||
{
|
||||
var digest = DigestStore.Get(this.Address);
|
||||
if (digest != null)
|
||||
{
|
||||
string authentication = digest.GenerateResponseHeader(this.Credentials, true, HTTPMethods.Connect, parameters.uri);
|
||||
if (!string.IsNullOrEmpty(authentication))
|
||||
{
|
||||
string auth = string.Format("Proxy-Authorization: {0}", authentication);
|
||||
if (HTTPManager.Logger.Level <= Loglevels.Information)
|
||||
HTTPManager.Logger.Information("HTTPProxy", "Sending proxy authorization header: " + auth, parameters.context);
|
||||
|
||||
var buff = auth.GetASCIIBytes();
|
||||
outStream.Write(buff.Data, buff.Offset, buff.Count);
|
||||
BufferPool.Release(buff);
|
||||
|
||||
outStream.Write(EOL);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
outStream.Write(EOL);
|
||||
|
||||
// Make sure to send all the wrote data to the wire
|
||||
outStream.Flush();
|
||||
} // using outstream
|
||||
|
||||
new HTTPProxyResponse(parameters)
|
||||
.OnFinished = OnProxyResponse;
|
||||
}
|
||||
else
|
||||
parameters.OnSuccess?.Invoke(parameters);
|
||||
}
|
||||
|
||||
void OnProxyResponse(ProxyConnectParameters connectParameters, HTTPProxyResponse resp, Exception error)
|
||||
{
|
||||
HTTPManager.Logger.Information(nameof(HTTPProxyResponse), $"{nameof(OnProxyResponse)}({connectParameters}, {resp}, {error})", connectParameters.context);
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// Resend request if the proxy response could be read && status code is 407 (authentication required) && we have credentials
|
||||
connectParameters.OnError?.Invoke(connectParameters, error, resp.ReadState == HTTPProxyResponse.PeekableReadState.Finished && resp.StatusCode == 407 && this.Credentials != null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (resp.StatusCode == 200)
|
||||
{
|
||||
connectParameters.OnSuccess?.Invoke(connectParameters);
|
||||
}
|
||||
else if (resp.StatusCode == 407)
|
||||
{
|
||||
// Proxy authentication required
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
|
||||
|
||||
bool retryNeogitiation = false;
|
||||
string authHeader = DigestStore.FindBest(resp.GetHeaderValues("proxy-authenticate"));
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
var digest = DigestStore.GetOrCreate(this.Address);
|
||||
digest.ParseChallange(authHeader);
|
||||
|
||||
retryNeogitiation = (connectParameters.AuthenticationAttempts < ProxyConnectParameters.MaxAuthenticationAttempts || digest.Stale) &&
|
||||
this.Credentials != null &&
|
||||
digest.IsUriProtected(this.Address);
|
||||
}
|
||||
|
||||
if (!retryNeogitiation)
|
||||
connectParameters.OnError?.Invoke(connectParameters, new Exception($"Can't authenticate Proxy! AuthenticationAttempts: {connectParameters.AuthenticationAttempts} {resp}"), false);
|
||||
else
|
||||
{
|
||||
connectParameters.AuthenticationAttempts++;
|
||||
BeginConnect(connectParameters);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
connectParameters.OnError?.Invoke(connectParameters, new Exception($"Proxy returned {resp}"), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5425bc3744cc7b14ca10b851bbcb0916
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,452 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
|
||||
|
||||
namespace Best.HTTP.Proxies
|
||||
{
|
||||
internal sealed class HTTPProxyResponse : IContentConsumer
|
||||
{
|
||||
public PeekableReadState ReadState
|
||||
{
|
||||
get => this._readState;
|
||||
private set
|
||||
{
|
||||
if (this._readState != value)
|
||||
HTTPManager.Logger.Information(nameof(HTTPProxyResponse), $"{this._readState} => {value}", this._parameters.context);
|
||||
this._readState = value;
|
||||
}
|
||||
}
|
||||
|
||||
public int VersionMajor { get; private set; }
|
||||
public int VersionMinor { get; private set; }
|
||||
public int StatusCode { get; private set; }
|
||||
public string Message { get; private set; }
|
||||
|
||||
public Dictionary<string, List<string>> Headers { get; private set; }
|
||||
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
private PeekableReadState _readState;
|
||||
|
||||
enum ContentDeliveryMode
|
||||
{
|
||||
Raw,
|
||||
RawUnknownLength,
|
||||
Chunked,
|
||||
}
|
||||
|
||||
public enum PeekableReadState
|
||||
{
|
||||
StatusLine,
|
||||
Headers,
|
||||
PrepareForContent,
|
||||
ContentSetup,
|
||||
RawContent,
|
||||
Content,
|
||||
Finished
|
||||
}
|
||||
|
||||
|
||||
private ContentDeliveryMode _deliveryMode;
|
||||
private ProxyConnectParameters _parameters;
|
||||
private long _expectedLength;
|
||||
private BufferPoolMemoryStream _output;
|
||||
int _chunkLength = -1;
|
||||
|
||||
enum ReadChunkedStates
|
||||
{
|
||||
ReadChunkLength,
|
||||
ReadChunk,
|
||||
ReadTrailingCRLF,
|
||||
ReadTrailingHeaders
|
||||
}
|
||||
ReadChunkedStates _readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
private long _downloaded;
|
||||
|
||||
public Action<ProxyConnectParameters, HTTPProxyResponse, Exception> OnFinished;
|
||||
|
||||
public string DataAsText { get; private set; }
|
||||
|
||||
public HTTPProxyResponse(ProxyConnectParameters parameters)
|
||||
{
|
||||
this._parameters = parameters;
|
||||
this._parameters.stream.SetTwoWayBinding(this);
|
||||
|
||||
this.Headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
Exception error = null;
|
||||
if (this.ReadState == PeekableReadState.Content && this._deliveryMode == ContentDeliveryMode.RawUnknownLength)
|
||||
{
|
||||
PostProcessContent();
|
||||
error = new Exception($"Proxy returned with {this.StatusCode} - '{this.Message}' : \"{this.DataAsText}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
error = new Exception("Connection to the proxy closed unexpectedly!");
|
||||
}
|
||||
|
||||
CallFinished(error);
|
||||
}
|
||||
|
||||
public void OnError(Exception ex)
|
||||
{
|
||||
//(this._parameters.stream as IPeekableContentProvider).Consumer = null;
|
||||
this.ContentProvider.Unbind();
|
||||
|
||||
CallFinished(ex);
|
||||
}
|
||||
|
||||
void CallFinished(Exception error)
|
||||
{
|
||||
var callback = Interlocked.Exchange(ref this.OnFinished, null);
|
||||
callback?.Invoke(this._parameters, this, error);
|
||||
}
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
switch (ReadState)
|
||||
{
|
||||
case PeekableReadState.StatusLine:
|
||||
if (!IsNewLinePresent(this.ContentProvider))
|
||||
return;
|
||||
|
||||
var statusLine = HTTPResponse.ReadTo(this.ContentProvider, (byte)' ');
|
||||
string[] versions = statusLine.Split(new char[] { '/', '.' });
|
||||
this.VersionMajor = int.Parse(versions[1]);
|
||||
this.VersionMinor = int.Parse(versions[2]);
|
||||
|
||||
int statusCode;
|
||||
string statusCodeStr = HTTPResponse.NoTrimReadTo(this.ContentProvider, (byte)' ', LF);
|
||||
|
||||
if (!int.TryParse(statusCodeStr, out statusCode))
|
||||
throw new Exception($"Couldn't parse '{statusCodeStr}' as a status code!");
|
||||
|
||||
this.StatusCode = statusCode;
|
||||
|
||||
if (statusCodeStr.Length > 0 && (byte)statusCodeStr[statusCodeStr.Length - 1] != LF && (byte)statusCodeStr[statusCodeStr.Length - 1] != CR)
|
||||
this.Message = HTTPResponse.ReadTo(this.ContentProvider, LF);
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Warning(nameof(HTTPProxyResponse), "Skipping Status Message reading!", this._parameters.context);
|
||||
|
||||
this.Message = string.Empty;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
VerboseLogging($"HTTP/'{this.VersionMajor}.{this.VersionMinor}' '{this.StatusCode}' '{this.Message}'");
|
||||
|
||||
this.ReadState = PeekableReadState.Headers;
|
||||
goto case PeekableReadState.Headers;
|
||||
|
||||
case PeekableReadState.Headers:
|
||||
ProcessReadHeaders(this.ContentProvider, PeekableReadState.PrepareForContent);
|
||||
if (this.ReadState == PeekableReadState.PrepareForContent)
|
||||
{
|
||||
if (this.StatusCode == 200)
|
||||
{
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
goto case PeekableReadState.Finished;
|
||||
}
|
||||
|
||||
// if it's an error response from the proxy, read all from the network
|
||||
goto case PeekableReadState.PrepareForContent;
|
||||
}
|
||||
break;
|
||||
|
||||
case PeekableReadState.PrepareForContent:
|
||||
// A content-length header might come with chunked transfer-encoding too.
|
||||
List<string> contentLengthHeaders = GetHeaderValues("content-length");
|
||||
if (contentLengthHeaders != null)
|
||||
this._expectedLength = long.Parse(contentLengthHeaders[0]);
|
||||
|
||||
if (HasHeaderWithValue("transfer-encoding", "chunked"))
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Chunked;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
}
|
||||
else
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.Raw;
|
||||
this.ReadState = PeekableReadState.ContentSetup;
|
||||
var contentRangeHeaders = GetHeaderValues("content-range");
|
||||
|
||||
if (contentLengthHeaders == null && contentRangeHeaders == null)
|
||||
{
|
||||
this._deliveryMode = ContentDeliveryMode.RawUnknownLength;
|
||||
}
|
||||
else if (contentLengthHeaders == null && contentRangeHeaders != null)
|
||||
{
|
||||
throw new NotImplementedException("ranges");
|
||||
}
|
||||
}
|
||||
|
||||
this._output = new BufferPoolMemoryStream(1024);
|
||||
|
||||
this.ReadState = PeekableReadState.Content;
|
||||
goto case PeekableReadState.Content;
|
||||
|
||||
case PeekableReadState.Content:
|
||||
switch (this._deliveryMode)
|
||||
{
|
||||
case ContentDeliveryMode.Raw: ProcessReadRaw(this.ContentProvider); break;
|
||||
case ContentDeliveryMode.RawUnknownLength: ProcessReadRawUnknownLength(this.ContentProvider); break;
|
||||
case ContentDeliveryMode.Chunked: ProcessReadChunked(this.ContentProvider); break;
|
||||
}
|
||||
|
||||
if (this.ReadState == PeekableReadState.Finished)
|
||||
goto case PeekableReadState.Finished;
|
||||
break;
|
||||
|
||||
case PeekableReadState.Finished:
|
||||
//(this._parameters.stream as IPeekableContentProvider).Consumer = null;
|
||||
this.ContentProvider.Unbind();
|
||||
if (this.StatusCode == 200 || this.StatusCode == 407)
|
||||
{
|
||||
CallFinished(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
CallFinished(new Exception($"Proxy returned with {this.StatusCode} - '{this.Message}' : \"{this.DataAsText}\""));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetHeaderValues(string name)
|
||||
{
|
||||
if (Headers == null)
|
||||
return null;
|
||||
|
||||
List<string> values;
|
||||
if (!Headers.TryGetValue(name, out values) || values.Count == 0)
|
||||
return null;
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public string GetFirstHeaderValue(string name)
|
||||
{
|
||||
if (Headers == null)
|
||||
return null;
|
||||
|
||||
List<string> values;
|
||||
if (!Headers.TryGetValue(name, out values) || values.Count == 0)
|
||||
return null;
|
||||
|
||||
return values[0];
|
||||
}
|
||||
|
||||
public bool HasHeaderWithValue(string headerName, string value)
|
||||
{
|
||||
var values = GetHeaderValues(headerName);
|
||||
if (values == null)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < values.Count; ++i)
|
||||
if (string.Compare(values[i], value, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void AddHeader(string name, string value)
|
||||
{
|
||||
if (Headers == null)
|
||||
Headers = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
List<string> values;
|
||||
if (!Headers.TryGetValue(name, out values))
|
||||
Headers.Add(name, values = new List<string>(1));
|
||||
|
||||
values.Add(value);
|
||||
}
|
||||
|
||||
private void VerboseLogging(string v)
|
||||
{
|
||||
HTTPManager.Logger.Verbose(nameof(HTTPProxyResponse), v, this._parameters.context);
|
||||
}
|
||||
|
||||
bool IsNewLinePresent(PeekableStream peekable)
|
||||
{
|
||||
peekable.BeginPeek();
|
||||
|
||||
int nextByte = peekable.PeekByte();
|
||||
while (nextByte >= 0 && nextByte != 0x0A)
|
||||
nextByte = peekable.PeekByte();
|
||||
|
||||
return nextByte == 0x0A;
|
||||
}
|
||||
|
||||
private void ProcessReadHeaders(PeekableStream peekable, PeekableReadState targetState)
|
||||
{
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
string headerName = HTTPResponse.ReadTo(peekable, (byte)':', LF);
|
||||
if (headerName == string.Empty)
|
||||
{
|
||||
this.ReadState = targetState;
|
||||
return;
|
||||
}
|
||||
|
||||
string value = HTTPResponse.ReadTo(peekable, LF);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
VerboseLogging($"Header - '{headerName}': '{value}'");
|
||||
|
||||
AddHeader(headerName, value);
|
||||
} while (IsNewLinePresent(peekable));
|
||||
}
|
||||
|
||||
private void ProcessReadRawUnknownLength(PeekableStream peekable)
|
||||
{
|
||||
while (peekable.Length > 0)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this._parameters.context);
|
||||
using var _ = new AutoReleaseBuffer(buffer);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReadChunkLength(PeekableStream peekable, out int result)
|
||||
{
|
||||
result = -1;
|
||||
if (!IsNewLinePresent(peekable))
|
||||
return false;
|
||||
|
||||
// Read until the end of line, then split the string so we will discard any optional chunk extensions
|
||||
string line = HTTPResponse.ReadTo(peekable, LF);
|
||||
string[] splits = line.Split(';');
|
||||
string num = splits[0];
|
||||
|
||||
return int.TryParse(num, System.Globalization.NumberStyles.AllowHexSpecifier, null, out result);
|
||||
}
|
||||
|
||||
void ProcessReadChunked(PeekableStream peekable)
|
||||
{
|
||||
switch (this._readChunkedState)
|
||||
{
|
||||
case ReadChunkedStates.ReadChunkLength:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunkLength;
|
||||
|
||||
if (TryReadChunkLength(peekable, out this._chunkLength))
|
||||
{
|
||||
if (this._chunkLength == 0)
|
||||
{
|
||||
PostProcessContent();
|
||||
goto case ReadChunkedStates.ReadTrailingHeaders;
|
||||
}
|
||||
|
||||
goto case ReadChunkedStates.ReadChunk;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadChunk:
|
||||
this._readChunkedState = ReadChunkedStates.ReadChunk;
|
||||
|
||||
while (this._chunkLength > 0 && peekable.Length > 0)
|
||||
{
|
||||
int targetReadCount = Math.Min(Math.Min(64 * 1024, this._chunkLength), (int)peekable.Length);
|
||||
|
||||
var buffer = BufferPool.Get(targetReadCount, true, this._parameters.context);
|
||||
using var _ = new AutoReleaseBuffer(buffer);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, targetReadCount);
|
||||
|
||||
if (readCount < 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
this._chunkLength -= readCount;
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
// Every chunk data has a trailing CRLF
|
||||
if (this._chunkLength == 0)
|
||||
goto case ReadChunkedStates.ReadTrailingCRLF;
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingCRLF:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingCRLF;
|
||||
|
||||
if (IsNewLinePresent(peekable))
|
||||
{
|
||||
BufferPool.Release(HTTPResponse.ReadToAsByte(peekable, LF));
|
||||
|
||||
goto case ReadChunkedStates.ReadChunkLength;
|
||||
}
|
||||
break;
|
||||
|
||||
case ReadChunkedStates.ReadTrailingHeaders:
|
||||
this._readChunkedState = ReadChunkedStates.ReadTrailingHeaders;
|
||||
|
||||
ProcessReadHeaders(peekable, PeekableReadState.Finished);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessReadRaw(PeekableStream peekable)
|
||||
{
|
||||
while (peekable.Length > 0)
|
||||
{
|
||||
var buffer = BufferPool.Get(64 * 1024, true, this._parameters.context);
|
||||
using var _ = new AutoReleaseBuffer(buffer);
|
||||
|
||||
var readCount = peekable.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (readCount < 0)
|
||||
throw ExceptionHelper.ServerClosedTCPStream();
|
||||
|
||||
ProcessChunk(buffer.AsBuffer(readCount));
|
||||
}
|
||||
|
||||
if (this._downloaded >= this._expectedLength)
|
||||
{
|
||||
PostProcessContent();
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessChunk(BufferSegment chunk)
|
||||
{
|
||||
this._downloaded += chunk.Count;
|
||||
this._output.Write(chunk.Data, chunk.Offset, chunk.Count);
|
||||
}
|
||||
|
||||
void PostProcessContent()
|
||||
{
|
||||
this.ReadState = PeekableReadState.Finished;
|
||||
|
||||
if (this._output != null)
|
||||
{
|
||||
var buff = this._output.GetBuffer();
|
||||
this.DataAsText = System.Text.Encoding.UTF8.GetString(buff, 0, (int)this._output.Length);
|
||||
|
||||
this._output.Dispose();
|
||||
this._output = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => $"{StatusCode} - {Message}: \"{this.DataAsText}\"";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec169728fcca515438bec166d5a2069e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96240cd9f920506408200787f767bf23
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,471 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Shared;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
using Best.HTTP.Shared.PlatformSupport.Memory;
|
||||
using Best.HTTP.Shared.PlatformSupport.Network.Tcp;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Proxies.Implementations
|
||||
{
|
||||
internal enum SOCKSVersions : byte
|
||||
{
|
||||
Unknown = 0x00,
|
||||
V5 = 0x05
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://tools.ietf.org/html/rfc1928
|
||||
/// The values currently defined for METHOD are:
|
||||
/// o X'00' NO AUTHENTICATION REQUIRED
|
||||
/// o X'01' GSSAPI
|
||||
/// o X'02' USERNAME/PASSWORD
|
||||
/// o X'03' to X'7F' IANA ASSIGNED
|
||||
/// o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
|
||||
/// o X'FF' NO ACCEPTABLE METHODS
|
||||
/// </summary>
|
||||
internal enum SOCKSMethods : byte
|
||||
{
|
||||
NoAuthenticationRequired = 0x00,
|
||||
GSSAPI = 0x01,
|
||||
UsernameAndPassword = 0x02,
|
||||
NoAcceptableMethods = 0xFF
|
||||
}
|
||||
|
||||
internal enum SOCKSReplies : byte
|
||||
{
|
||||
Succeeded = 0x00,
|
||||
GeneralSOCKSServerFailure = 0x01,
|
||||
ConnectionNotAllowedByRuleset = 0x02,
|
||||
NetworkUnreachable = 0x03,
|
||||
HostUnreachable = 0x04,
|
||||
ConnectionRefused = 0x05,
|
||||
TTLExpired = 0x06,
|
||||
CommandNotSupported = 0x07,
|
||||
AddressTypeNotSupported = 0x08
|
||||
}
|
||||
|
||||
internal enum SOCKSAddressTypes
|
||||
{
|
||||
IPV4 = 0x00,
|
||||
DomainName = 0x03,
|
||||
IPv6 = 0x04
|
||||
}
|
||||
|
||||
internal sealed class SOCKSV5Negotiator : IContentConsumer
|
||||
{
|
||||
public PeekableContentProviderStream ContentProvider { get; private set; }
|
||||
|
||||
enum NegotiationStates
|
||||
{
|
||||
MethodSelection,
|
||||
ExpectAuthenticationResponse,
|
||||
ConnectResponse
|
||||
}
|
||||
|
||||
NegotiationStates _state;
|
||||
|
||||
SOCKSProxy _proxy;
|
||||
ProxyConnectParameters _parameters;
|
||||
|
||||
public SOCKSV5Negotiator(SOCKSProxy proxy, ProxyConnectParameters parameters)
|
||||
{
|
||||
this._proxy = proxy;
|
||||
this._parameters = parameters;
|
||||
|
||||
//(this._parameters.stream as IPeekableContentProvider).Consumer = this;
|
||||
(this._parameters.stream as PeekableContentProviderStream).SetTwoWayBinding(this);
|
||||
|
||||
SendHandshake();
|
||||
}
|
||||
|
||||
public void SetBinding(PeekableContentProviderStream contentProvider) => this.ContentProvider = contentProvider;
|
||||
public void UnsetBinding() => this.ContentProvider = null;
|
||||
|
||||
public void OnConnectionClosed()
|
||||
{
|
||||
CallOnError(new Exception($"{nameof(SOCKSV5Negotiator)}: connection closed unexpectedly!"));
|
||||
}
|
||||
|
||||
public void OnError(Exception ex)
|
||||
{
|
||||
CallOnError(ex);
|
||||
}
|
||||
|
||||
void SendHandshake()
|
||||
{
|
||||
var buffer = BufferPool.Get(1024, true);
|
||||
try
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
// The client connects to the server, and sends a version
|
||||
// identifier/method selection message:
|
||||
//
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
//
|
||||
// The VER field is set to X'05' for this version of the protocol. The
|
||||
// NMETHODS field contains the number of method identifier octets that
|
||||
// appear in the METHODS field.
|
||||
//
|
||||
|
||||
buffer[count++] = (byte)SOCKSVersions.V5;
|
||||
if (this._proxy.Credentials != null)
|
||||
{
|
||||
buffer[count++] = 0x02; // method count
|
||||
buffer[count++] = (byte)SOCKSMethods.UsernameAndPassword;
|
||||
buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[count++] = 0x01; // method count
|
||||
buffer[count++] = (byte)SOCKSMethods.NoAuthenticationRequired;
|
||||
}
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Sending method negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
|
||||
|
||||
// enqueue buffer and move its ownership to the tcp streamer
|
||||
this._parameters.stream.Write(buffer.AsBuffer(count));
|
||||
|
||||
// null out the buffer so it won't be released
|
||||
buffer = null;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CallOnError(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
BufferPool.Release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
void SendConnect()
|
||||
{
|
||||
// The SOCKS request is formed as follows:
|
||||
//
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
//
|
||||
// Where:
|
||||
//
|
||||
// o VER protocol version: X'05'
|
||||
// o CMD
|
||||
// o CONNECT X'01'
|
||||
// o BIND X'02'
|
||||
// o UDP ASSOCIATE X'03'
|
||||
// o RSV RESERVED
|
||||
// o ATYP address type of following address
|
||||
// o IP V4 address: X'01'
|
||||
// o DOMAINNAME: X'03'
|
||||
// o IP V6 address: X'04'
|
||||
// o DST.ADDR desired destination address
|
||||
// o DST.PORT desired destination port in network octet
|
||||
// order
|
||||
|
||||
var buffer = BufferPool.Get(512, true);
|
||||
int count = 0;
|
||||
buffer[count++] = (byte)SOCKSVersions.V5; // version: 5
|
||||
buffer[count++] = 0x01; // command: connect
|
||||
buffer[count++] = 0x00; // reserved, bust be 0x00
|
||||
|
||||
if (this._parameters.uri.IsHostIsAnIPAddress())
|
||||
{
|
||||
bool isIPV4 = Extensions.IsIpV4AddressValid(this._parameters.uri.Host);
|
||||
buffer[count++] = isIPV4 ? (byte)SOCKSAddressTypes.IPV4 : (byte)SOCKSAddressTypes.IPv6;
|
||||
|
||||
var ipAddress = System.Net.IPAddress.Parse(this._parameters.uri.Host);
|
||||
var ipBytes = ipAddress.GetAddressBytes();
|
||||
WriteBytes(buffer, ref count, ipBytes); // destination address
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer[count++] = (byte)SOCKSAddressTypes.DomainName;
|
||||
|
||||
// The first octet of the address field contains the number of octets of name that
|
||||
// follow, there is no terminating NUL octet.
|
||||
WriteString(buffer, ref count, this._parameters.uri.Host);
|
||||
}
|
||||
|
||||
// destination port in network octet order
|
||||
buffer[count++] = (byte)((this._parameters.uri.Port >> 8) & 0xFF);
|
||||
buffer[count++] = (byte)(this._parameters.uri.Port & 0xFF);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Sending connect request - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
|
||||
|
||||
this._parameters.stream.Write(buffer.AsBuffer(count));
|
||||
|
||||
this._state = NegotiationStates.ConnectResponse;
|
||||
}
|
||||
|
||||
public void OnContent()
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (this._state)
|
||||
{
|
||||
case NegotiationStates.MethodSelection:
|
||||
{
|
||||
if (this.ContentProvider.Length < 2)
|
||||
return;
|
||||
|
||||
// Read method selection result
|
||||
|
||||
//count = stream.Read(buffer, 0, buffer.Length);
|
||||
var buffer = BufferPool.Get(BufferPool.MIN_BUFFER_SIZE, true);
|
||||
int count = this.ContentProvider.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Negotiation response - count: {count} buffer: {buffer.AsBuffer(count)}", this._parameters.context);
|
||||
|
||||
// The server selects from one of the methods given in METHODS, and
|
||||
// sends a METHOD selection message:
|
||||
//
|
||||
// +----+--------+
|
||||
// |VER | METHOD |
|
||||
// +----+--------+
|
||||
// | 1 | 1 |
|
||||
// +----+--------+
|
||||
//
|
||||
// If the selected METHOD is X'FF', none of the methods listed by the
|
||||
// client are acceptable, and the client MUST close the connection.
|
||||
//
|
||||
// The values currently defined for METHOD are:
|
||||
//
|
||||
// o X'00' NO AUTHENTICATION REQUIRED
|
||||
// o X'01' GSSAPI
|
||||
// o X'02' USERNAME/PASSWORD
|
||||
// o X'03' to X'7F' IANA ASSIGNED
|
||||
// o X'80' to X'FE' RESERVED FOR PRIVATE METHODS
|
||||
// o X'FF' NO ACCEPTABLE METHODS
|
||||
//
|
||||
// The client and server then enter a method-specific sub-negotiation.
|
||||
|
||||
SOCKSVersions version = (SOCKSVersions)buffer[0];
|
||||
SOCKSMethods method = (SOCKSMethods)buffer[1];
|
||||
|
||||
// Expected result:
|
||||
// 1.) Received bytes' count is 2: version + preferred method
|
||||
// 2.) Version must be 5
|
||||
// 3.) Preferred method must NOT be 0xFF
|
||||
if (count != 2)
|
||||
throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
|
||||
else if (version != SOCKSVersions.V5)
|
||||
throw new Exception("SOCKS Proxy - Expected version: 5, received version: " + buffer[0].ToString("X2"));
|
||||
else if (method == SOCKSMethods.NoAcceptableMethods)
|
||||
throw new Exception("SOCKS Proxy - Received 'NO ACCEPTABLE METHODS' (0xFF)");
|
||||
else
|
||||
{
|
||||
HTTPManager.Logger.Information("SOCKSProxy", "Method negotiation over. Method: " + method.ToString(), this._parameters.context);
|
||||
switch (method)
|
||||
{
|
||||
case SOCKSMethods.NoAuthenticationRequired:
|
||||
SendConnect();
|
||||
break;
|
||||
|
||||
case SOCKSMethods.UsernameAndPassword:
|
||||
if (this._proxy.Credentials.UserName.Length > 255)
|
||||
throw new Exception($"SOCKS Proxy - Credentials.UserName too long! {this._proxy.Credentials.UserName.Length} > 255");
|
||||
if (this._proxy.Credentials.Password.Length > 255)
|
||||
throw new Exception($"SOCKS Proxy - Credentials.Password too long! {this._proxy.Credentials.Password.Length} > 255");
|
||||
|
||||
// https://tools.ietf.org/html/rfc1929 : Username/Password Authentication for SOCKS V5
|
||||
// Once the SOCKS V5 server has started, and the client has selected the
|
||||
// Username/Password Authentication protocol, the Username/Password
|
||||
// subnegotiation begins. This begins with the client producing a
|
||||
// Username/Password request:
|
||||
//
|
||||
// +----+------+----------+------+----------+
|
||||
// |VER | ULEN | UNAME | PLEN | PASSWD |
|
||||
// +----+------+----------+------+----------+
|
||||
// | 1 | 1 | 1 to 255 | 1 | 1 to 255 |
|
||||
// +----+------+----------+------+----------+
|
||||
|
||||
HTTPManager.Logger.Information("SOCKSProxy", "starting sub-negotiation", this._parameters.context);
|
||||
count = 0;
|
||||
buffer[count++] = 0x01; // version of sub negotiation
|
||||
|
||||
WriteString(buffer, ref count, this._proxy.Credentials.UserName);
|
||||
WriteString(buffer, ref count, this._proxy.Credentials.Password);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Sending username and password sub-negotiation - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
|
||||
|
||||
// Write negotiation and transfer ownership of buffer
|
||||
this._parameters.stream.Write(buffer.AsBuffer(count));
|
||||
|
||||
this._state = NegotiationStates.ExpectAuthenticationResponse;
|
||||
break;
|
||||
|
||||
case SOCKSMethods.GSSAPI:
|
||||
throw new Exception("SOCKS proxy: GSSAPI not supported!");
|
||||
|
||||
case SOCKSMethods.NoAcceptableMethods:
|
||||
throw new Exception("SOCKS proxy: No acceptable method");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case NegotiationStates.ExpectAuthenticationResponse:
|
||||
{
|
||||
if (this.ContentProvider.Length < 2)
|
||||
return;
|
||||
|
||||
// Read result
|
||||
var buffer = BufferPool.Get(512, true);
|
||||
var count = this._parameters.stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Username and password sub-negotiation response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
|
||||
|
||||
// The server verifies the supplied UNAME and PASSWD, and sends the
|
||||
// following response:
|
||||
//
|
||||
// +----+--------+
|
||||
// |VER | STATUS |
|
||||
// +----+--------+
|
||||
// | 1 | 1 |
|
||||
// +----+--------+
|
||||
|
||||
// A STATUS field of X'00' indicates success. If the server returns a
|
||||
// `failure' (STATUS value other than X'00') status, it MUST close the
|
||||
// connection.
|
||||
bool success = buffer[1] == 0;
|
||||
|
||||
if (count != 2)
|
||||
throw new Exception($"SOCKS Proxy - Expected read count: 2! buffer: {buffer.AsBuffer(count)}");
|
||||
else if (!success)
|
||||
throw new Exception("SOCKS proxy: username+password authentication failed!");
|
||||
|
||||
HTTPManager.Logger.Information("SOCKSProxy", "Authenticated!", this._parameters.context);
|
||||
|
||||
// Send connect
|
||||
SendConnect();
|
||||
break;
|
||||
}
|
||||
|
||||
case NegotiationStates.ConnectResponse:
|
||||
{
|
||||
if (this.ContentProvider.Length < 10)
|
||||
return;
|
||||
|
||||
var buffer = BufferPool.Get(512, true);
|
||||
var count = this._parameters.stream.Read(buffer, 0, buffer.Length);
|
||||
|
||||
if (HTTPManager.Logger.IsDiagnostic)
|
||||
HTTPManager.Logger.Information("SOCKSProxy", $"Connect response - buffer: {buffer.AsBuffer(count)} ", this._parameters.context);
|
||||
|
||||
// The SOCKS request information is sent by the client as soon as it has
|
||||
// established a connection to the SOCKS server, and completed the
|
||||
// authentication negotiations. The server evaluates the request, and
|
||||
// returns a reply formed as follows:
|
||||
//
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
//
|
||||
// Where:
|
||||
// o VER protocol version: X'05'
|
||||
// o REP Reply field:
|
||||
// o X'00' succeeded
|
||||
// o X'01' general SOCKS server failure
|
||||
// o X'02' connection not allowed by ruleset
|
||||
// o X'03' Network unreachable
|
||||
// o X'04' Host unreachable
|
||||
// o X'05' Connection refused
|
||||
// o X'06' TTL expired
|
||||
// o X'07' Command not supported
|
||||
// o X'08' Address type not supported
|
||||
// o X'09' to X'FF' unassigned
|
||||
// o RSV RESERVED
|
||||
// o ATYP address type of following address
|
||||
// o IP V4 address: X'01'
|
||||
// o DOMAINNAME: X'03'
|
||||
// o IP V6 address: X'04'
|
||||
// o BND.ADDR server bound address
|
||||
// o BND.PORT server bound port in network octet order
|
||||
//
|
||||
// Fields marked RESERVED (RSV) must be set to X'00'.
|
||||
|
||||
SOCKSVersions version = (SOCKSVersions)buffer[0];
|
||||
SOCKSReplies reply = (SOCKSReplies)buffer[1];
|
||||
|
||||
// at least 10 bytes expected as a result
|
||||
if (count < 10)
|
||||
throw new Exception($"SOCKS proxy: not enough data returned by the server. Expected count is at least 10 bytes, server returned {count} bytes! content: {buffer.AsBuffer(count)}");
|
||||
else if (reply != SOCKSReplies.Succeeded)
|
||||
throw new Exception("SOCKS proxy error: " + reply.ToString());
|
||||
|
||||
HTTPManager.Logger.Information("SOCKSProxy", "Connected!", this._parameters.context);
|
||||
|
||||
CallOnSuccess();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
CallOnError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
void CallOnError(Exception ex)
|
||||
{
|
||||
var callback = Interlocked.Exchange(ref this._parameters.OnError, null);
|
||||
Interlocked.Exchange(ref this._parameters.OnSuccess, null);
|
||||
|
||||
this.ContentProvider.Unbind();
|
||||
callback?.Invoke(this._parameters, ex, false);
|
||||
}
|
||||
|
||||
void CallOnSuccess()
|
||||
{
|
||||
var callback = Interlocked.Exchange(ref this._parameters.OnSuccess, null);
|
||||
Interlocked.Exchange(ref this._parameters.OnError, null);
|
||||
|
||||
this.ContentProvider.Unbind();
|
||||
|
||||
callback?.Invoke(this._parameters);
|
||||
}
|
||||
|
||||
private void WriteString(byte[] buffer, ref int count, string str)
|
||||
{
|
||||
// Get the bytes
|
||||
int byteCount = Encoding.UTF8.GetByteCount(str);
|
||||
if (byteCount > 255)
|
||||
throw new Exception(string.Format("SOCKS Proxy - String is too large ({0}) to fit in 255 bytes!", byteCount.ToString()));
|
||||
|
||||
// number of bytes
|
||||
buffer[count++] = (byte)byteCount;
|
||||
|
||||
// and the bytes itself
|
||||
Encoding.UTF8.GetBytes(str, 0, str.Length, buffer, count);
|
||||
|
||||
count += byteCount;
|
||||
}
|
||||
|
||||
private void WriteBytes(byte[] buffer, ref int count, byte[] bytes)
|
||||
{
|
||||
Array.Copy(bytes, 0, buffer, count, bytes.Length);
|
||||
count += bytes.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95c59067ac15d2d4aa5f0e97e0a8d5e5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
184
Packages/com.tivadar.best.http/Runtime/HTTP/Proxies/Proxy.cs
Normal file
184
Packages/com.tivadar.best.http/Runtime/HTTP/Proxies/Proxy.cs
Normal file
@@ -0,0 +1,184 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Shared.Logger;
|
||||
using Best.HTTP.Shared.Streams;
|
||||
|
||||
namespace Best.HTTP.Proxies
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents parameters used when connecting through a proxy server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ProxyConnectParameters struct defines the parameters required when initiating a connection
|
||||
/// through a proxy server. It includes information about the proxy, target URI, and callbacks for success and error handling.
|
||||
/// This struct is commonly used during the negotiation steps in the <see cref="Best.HTTP.Shared.PlatformSupport.Network.Tcp.Negotiator"/> class.
|
||||
/// </remarks>
|
||||
public struct ProxyConnectParameters
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum number of authentication attempts allowed during proxy connection.
|
||||
/// </summary>
|
||||
public const int MaxAuthenticationAttempts = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The proxy server through which the connection is established.
|
||||
/// </summary>
|
||||
public Proxy proxy;
|
||||
|
||||
/// <summary>
|
||||
/// The stream used for communication with the proxy server.
|
||||
/// </summary>
|
||||
public PeekableContentProviderStream stream;
|
||||
|
||||
/// <summary>
|
||||
/// The target URI to reach through the proxy server.
|
||||
/// </summary>
|
||||
public Uri uri;
|
||||
|
||||
/// <summary>
|
||||
/// A cancellation token that allows canceling the proxy connection operation.
|
||||
/// </summary>
|
||||
public CancellationToken token;
|
||||
|
||||
/// <summary>
|
||||
/// The number of authentication attempts made during proxy connection.
|
||||
/// </summary>
|
||||
public int AuthenticationAttempts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to create a proxy tunnel.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A proxy tunnel, also known as a TCP tunnel, is established when communication between the client and the target server
|
||||
/// needs to be relayed through the proxy without modification. Setting this field to <c>true</c> indicates the intention
|
||||
/// to create a tunnel, allowing the data to pass through the proxy without interpretation or alteration by the proxy.
|
||||
/// This is typically used for protocols like HTTPS, where end-to-end encryption is desired, and the proxy should act as a
|
||||
/// pass-through conduit.
|
||||
/// </remarks>
|
||||
public bool createTunel;
|
||||
|
||||
/// <summary>
|
||||
/// The logging context for debugging purposes.
|
||||
/// </summary>
|
||||
public LoggingContext context;
|
||||
|
||||
/// <summary>
|
||||
/// A callback to be executed upon successful proxy connection.
|
||||
/// </summary>
|
||||
public Action<ProxyConnectParameters> OnSuccess;
|
||||
|
||||
/// <summary>
|
||||
/// A callback to be executed upon encountering an error during proxy connection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The callback includes parameters for the current connection parameters, the encountered exception,
|
||||
/// and a flag indicating whether the connection should be retried for authentication.
|
||||
/// </remarks>
|
||||
public Action<ProxyConnectParameters, Exception, bool> OnError;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for proxy implementations, providing common proxy configuration and behavior.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The Proxy class serves as the base class for various proxy client implementations,
|
||||
/// such as <see cref="HTTPProxy"/> and <see cref="SOCKSProxy"/>. It provides a foundation for configuring proxy settings and handling
|
||||
/// proxy-related functionality common to all proxy types, like connecting to a proxy, setting up a request to go through the proxy
|
||||
/// and deciding whether an address is usable with the proxy or the plugin must connect directly.
|
||||
/// </remarks>
|
||||
public abstract class Proxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Address of the proxy server. It has to be in the http://proxyaddress:port form.
|
||||
/// </summary>
|
||||
public Uri Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Credentials for authenticating with the proxy server.
|
||||
/// </summary>
|
||||
public Credentials Credentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of exceptions for which the proxy should not be used. Elements of this list are compared to the Host (DNS or IP address) part of the uri.
|
||||
/// </summary>
|
||||
public List<string> Exceptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Proxy class with the specified proxy address and credentials.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication.</param>
|
||||
internal Proxy(Uri address, Credentials credentials)
|
||||
{
|
||||
this.Address = address;
|
||||
this.Credentials = credentials;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initiates a connection through the proxy server. Used during the negotiation steps.
|
||||
/// </summary>
|
||||
/// <param name="parameters">Parameters for the proxy connection.</param>
|
||||
internal abstract void BeginConnect(ProxyConnectParameters parameters);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the request path to be used for proxy communication. In some cases with HTTPProxy, the request must send the whole uri as the request path.
|
||||
/// </summary>
|
||||
/// <param name="uri">The target URI.</param>
|
||||
/// <returns>The request path for proxy communication.</returns>
|
||||
public abstract string GetRequestPath(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Sets up an HTTP request to use the proxy as needed.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request to set up.</param>
|
||||
/// <returns><c>true</c> if the request should use the proxy; otherwise, <c>false</c>.</returns>
|
||||
internal abstract bool SetupRequest(HTTPRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the proxy should be used for a specific address based on the configured exceptions.
|
||||
/// </summary>
|
||||
/// <param name="address">The address to check for proxy usage.</param>
|
||||
/// <returns><c>true</c> if the proxy should be used for the address; otherwise, <c>false</c>.</returns>
|
||||
public bool UseProxyForAddress(Uri address)
|
||||
{
|
||||
if (this.Exceptions == null)
|
||||
return true;
|
||||
|
||||
string host = address.Host;
|
||||
|
||||
// https://github.com/httplib2/httplib2/issues/94
|
||||
// If domain starts with a dot (example: .example.com):
|
||||
// 1. Use endswith to match any subdomain (foo.example.com should match)
|
||||
// 2. Remove the dot and do an exact match (example.com should also match)
|
||||
//
|
||||
// If domain does not start with a dot (example: example.com):
|
||||
// 1. It should be an exact match.
|
||||
for (int i = 0; i < this.Exceptions.Count; ++i)
|
||||
{
|
||||
var exception = this.Exceptions[i];
|
||||
|
||||
if (exception == "*")
|
||||
return false;
|
||||
|
||||
if (exception.StartsWith("."))
|
||||
{
|
||||
// Use EndsWith to match any subdomain
|
||||
if (host.EndsWith(exception))
|
||||
return false;
|
||||
|
||||
// Remove the dot and
|
||||
exception = exception.Substring(1);
|
||||
}
|
||||
|
||||
// do an exact match
|
||||
if (host.Equals(exception))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0dba9c2697d5e17498d9cfd573452f4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,29 @@
|
||||
#if !UNITY_WEBGL || UNITY_EDITOR
|
||||
using System;
|
||||
|
||||
using Best.HTTP.Proxies.Implementations;
|
||||
using Best.HTTP.Request.Authentication;
|
||||
using Best.HTTP.Shared.Extensions;
|
||||
|
||||
namespace Best.HTTP.Proxies
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a SOCKS proxy used for making HTTP requests, supporting SOCKS version 5 (v5).
|
||||
/// </summary>
|
||||
public sealed class SOCKSProxy : Proxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SOCKSProxy class with the specified proxy address and credentials.
|
||||
/// </summary>
|
||||
/// <param name="address">The address of the SOCKS proxy server.</param>
|
||||
/// <param name="credentials">The credentials for proxy authentication (if required).</param>
|
||||
public SOCKSProxy(Uri address, Credentials credentials)
|
||||
: base(address, credentials)
|
||||
{ }
|
||||
|
||||
public override string GetRequestPath(Uri uri) => uri.GetRequestPathAndQueryURL();
|
||||
internal override bool SetupRequest(HTTPRequest request) => false;
|
||||
internal override void BeginConnect(ProxyConnectParameters parameters) => new SOCKSV5Negotiator(this, parameters);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e67b3f4a37c57d74d9efb5312efb13a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user