#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
{
///
/// Represents an HTTP proxy server that can be used to route HTTP requests through.
///
///
/// The HTTPProxy class is an implementation of the 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.
///
public sealed class HTTPProxy : Proxy
{
///
/// Gets or sets whether the proxy can act as a transparent proxy. Default value is true.
///
///
/// A transparent proxy forwards client requests without modifying them. When set to true, the proxy behaves as a transparent
/// proxy, meaning it forwards requests as-is. If set to false, it may modify requests, and this can be useful for certain
/// advanced proxy configurations.
///
public bool IsTransparent { get; set; }
///
/// 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 true.
///
public bool SendWholeUri { get; set; }
///
/// Gets or sets whether the plugin will use the proxy as an explicit proxy for secure protocols (HTTPS://, WSS://).
///
///
/// When set to true, 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.
///
public bool NonTransparentForHTTPS { get; set; }
///
/// Creates a new instance of the HTTPProxy class with the specified proxy address.
///
/// The address of the proxy server.
public HTTPProxy(Uri address)
:this(address, null, true)
{}
///
/// Creates a new instance of the HTTPProxy class with the specified proxy address and credentials.
///
/// The address of the proxy server.
/// The credentials for proxy authentication.
public HTTPProxy(Uri address, Credentials credentials)
:this(address, credentials, true)
{}
///
/// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, and transparency settings.
///
/// The address of the proxy server.
/// The credentials for proxy authentication.
/// Specifies whether the proxy can act as a transparent proxy (true) or not (false).
public HTTPProxy(Uri address, Credentials credentials, bool isTransparent)
:this(address, credentials, isTransparent, true)
{ }
///
/// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, transparency settings, and URI handling.
///
/// The address of the proxy server.
/// The credentials for proxy authentication.
/// Specifies whether the proxy can act as a transparent proxy (true) or not (false).
/// Specifies whether the proxy should send the entire URI (true) or just the path and query (false) for non-transparent proxies.
public HTTPProxy(Uri address, Credentials credentials, bool isTransparent, bool sendWholeUri)
: this(address, credentials, isTransparent, sendWholeUri, true)
{ }
///
/// Creates a new instance of the class with the specified proxy address, credentials, transparency settings, URI handling, and HTTPS behavior.
///
/// The address of the proxy server.
/// The credentials for proxy authentication.
/// Specifies whether the proxy can act as a transparent proxy (true) or not (false).
/// Specifies whether the proxy should send the entire URI (true) or just the path and query (false) for non-transparent proxies.
/// Specifies whether the plugin should use the proxy as an explicit proxy for secure protocols (HTTPS://, WSS://) (true) or not (false).
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 &&
this.Credentials != null &&
digest.IsUriProtected(this.Address) &&
(/*connectParameters.request == null || !connectParameters.request.HasHeader("Proxy-Authorization") ||*/ digest.Stale);
}
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