HTTPProxy.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. #if !UNITY_WEBGL || UNITY_EDITOR
  2. using System;
  3. using System.IO;
  4. using System.Text;
  5. using Best.HTTP.Request.Authentication;
  6. using Best.HTTP.Shared;
  7. using Best.HTTP.Shared.Extensions;
  8. using Best.HTTP.Shared.Logger;
  9. using Best.HTTP.Shared.PlatformSupport.Memory;
  10. using Best.HTTP.Shared.Streams;
  11. using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
  12. namespace Best.HTTP.Proxies
  13. {
  14. /// <summary>
  15. /// Represents an HTTP proxy server that can be used to route HTTP requests through.
  16. /// </summary>
  17. /// <remarks>
  18. /// The HTTPProxy class is an implementation of the <see cref="Proxy"/> base class, specifically designed for
  19. /// HTTP proxy servers. It provides features such as transparent proxy support, sending the entire URI, and handling proxy
  20. /// authentication. This class is used to configure and manage HTTP proxy settings for HTTP requests.
  21. /// </remarks>
  22. public sealed class HTTPProxy : Proxy
  23. {
  24. /// <summary>
  25. /// Gets or sets whether the proxy can act as a transparent proxy. Default value is <c>true</c>.
  26. /// </summary>
  27. /// <remarks>
  28. /// A transparent proxy forwards client requests without modifying them. When set to <c>true</c>, the proxy behaves as a transparent
  29. /// proxy, meaning it forwards requests as-is. If set to <c>false</c>, it may modify requests, and this can be useful for certain
  30. /// advanced proxy configurations.
  31. /// </remarks>
  32. public bool IsTransparent { get; set; }
  33. /// <summary>
  34. /// 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>.
  35. /// </summary>
  36. public bool SendWholeUri { get; set; }
  37. /// <summary>
  38. /// Gets or sets whether the plugin will use the proxy as an explicit proxy for secure protocols (HTTPS://, WSS://).
  39. /// </summary>
  40. /// <remarks>
  41. /// When set to <c>true</c>, the plugin will issue a CONNECT request to the proxy for secure protocols, even if the proxy is
  42. /// marked as transparent. This is commonly used for ensuring proper handling of encrypted traffic through the proxy.
  43. /// </remarks>
  44. public bool NonTransparentForHTTPS { get; set; }
  45. /// <summary>
  46. /// Creates a new instance of the HTTPProxy class with the specified proxy address.
  47. /// </summary>
  48. /// <param name="address">The address of the proxy server.</param>
  49. public HTTPProxy(Uri address)
  50. :this(address, null, true)
  51. {}
  52. /// <summary>
  53. /// Creates a new instance of the HTTPProxy class with the specified proxy address and credentials.
  54. /// </summary>
  55. /// <param name="address">The address of the proxy server.</param>
  56. /// <param name="credentials">The credentials for proxy authentication.</param>
  57. public HTTPProxy(Uri address, Credentials credentials)
  58. :this(address, credentials, true)
  59. {}
  60. /// <summary>
  61. /// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, and transparency settings.
  62. /// </summary>
  63. /// <param name="address">The address of the proxy server.</param>
  64. /// <param name="credentials">The credentials for proxy authentication.</param>
  65. /// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
  66. public HTTPProxy(Uri address, Credentials credentials, bool isTransparent)
  67. :this(address, credentials, isTransparent, true)
  68. { }
  69. /// <summary>
  70. /// Creates a new instance of the HTTPProxy class with the specified proxy address, credentials, transparency settings, and URI handling.
  71. /// </summary>
  72. /// <param name="address">The address of the proxy server.</param>
  73. /// <param name="credentials">The credentials for proxy authentication.</param>
  74. /// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
  75. /// <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>
  76. public HTTPProxy(Uri address, Credentials credentials, bool isTransparent, bool sendWholeUri)
  77. : this(address, credentials, isTransparent, sendWholeUri, true)
  78. { }
  79. /// <summary>
  80. /// Creates a new instance of the <see cref="HTTPProxy"/> class with the specified proxy address, credentials, transparency settings, URI handling, and HTTPS behavior.
  81. /// </summary>
  82. /// <param name="address">The address of the proxy server.</param>
  83. /// <param name="credentials">The credentials for proxy authentication.</param>
  84. /// <param name="isTransparent">Specifies whether the proxy can act as a transparent proxy (<c>true</c>) or not (<c>false</c>).</param>
  85. /// <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>
  86. /// <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>
  87. public HTTPProxy(Uri address, Credentials credentials, bool isTransparent, bool sendWholeUri, bool nonTransparentForHTTPS)
  88. :base(address, credentials)
  89. {
  90. this.IsTransparent = isTransparent;
  91. this.SendWholeUri = sendWholeUri;
  92. this.NonTransparentForHTTPS = nonTransparentForHTTPS;
  93. }
  94. public override string GetRequestPath(Uri uri)
  95. {
  96. return this.SendWholeUri ? uri.OriginalString : uri.GetRequestPathAndQueryURL();
  97. }
  98. internal override bool SetupRequest(HTTPRequest request)
  99. {
  100. if (request == null || request.Response == null || !this.IsTransparent)
  101. return false;
  102. string authHeader = DigestStore.FindBest(request.Response.GetHeaderValues("proxy-authenticate"));
  103. if (!string.IsNullOrEmpty(authHeader))
  104. {
  105. var digest = DigestStore.GetOrCreate(this.Address);
  106. digest.ParseChallange(authHeader);
  107. if (this.Credentials != null && digest.IsUriProtected(this.Address) && (!request.HasHeader("Proxy-Authorization") || digest.Stale))
  108. {
  109. switch (this.Credentials.Type)
  110. {
  111. case AuthenticationTypes.Basic:
  112. // With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
  113. var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password));
  114. request.SetHeader("Proxy-Authorization", $"Basic {token}");
  115. return true;
  116. case AuthenticationTypes.Unknown:
  117. case AuthenticationTypes.Digest:
  118. //var digest = DigestStore.Get(request.Proxy.Address);
  119. if (digest != null)
  120. {
  121. string authentication = digest.GenerateResponseHeader(this.Credentials, true, request.MethodType, request.CurrentUri);
  122. if (!string.IsNullOrEmpty(authentication))
  123. {
  124. request.SetHeader("Proxy-Authorization", authentication);
  125. return true;
  126. }
  127. }
  128. break;
  129. }
  130. }
  131. }
  132. return false;
  133. }
  134. internal override void BeginConnect(ProxyConnectParameters parameters)
  135. {
  136. if (!this.IsTransparent || (parameters.createTunel && this.NonTransparentForHTTPS))
  137. {
  138. using (var bufferedStream = new WriteOnlyBufferedStream(parameters.stream, 4 * 1024, parameters.context))
  139. using (var outStream = new BinaryWriter(bufferedStream, Encoding.UTF8))
  140. {
  141. // https://www.rfc-editor.org/rfc/rfc9110.html#name-connect
  142. string connectStr = string.Format("CONNECT {0}:{1} HTTP/1.1", parameters.uri.Host, parameters.uri.Port.ToString());
  143. HTTPManager.Logger.Information("HTTPProxy", "Sending " + connectStr, parameters.context);
  144. outStream.SendAsASCII(connectStr);
  145. outStream.Write(EOL);
  146. outStream.SendAsASCII(string.Format("Host: {0}:{1}", parameters.uri.Host, parameters.uri.Port.ToString()));
  147. outStream.Write(EOL);
  148. outStream.SendAsASCII("Proxy-Connection: Keep-Alive");
  149. outStream.Write(EOL);
  150. outStream.SendAsASCII("Connection: Keep-Alive");
  151. outStream.Write(EOL);
  152. // Proxy Authentication
  153. if (this.Credentials != null)
  154. {
  155. switch (this.Credentials.Type)
  156. {
  157. case AuthenticationTypes.Basic:
  158. {
  159. // With Basic authentication we don't want to wait for a challenge, we will send the hash with the first request
  160. var buff = $"Proxy-Authorization: Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes(this.Credentials.UserName + ":" + this.Credentials.Password))}"
  161. .GetASCIIBytes();
  162. outStream.Write(buff.Data, buff.Offset, buff.Count);
  163. BufferPool.Release(buff);
  164. outStream.Write(EOL);
  165. break;
  166. }
  167. case AuthenticationTypes.Unknown:
  168. case AuthenticationTypes.Digest:
  169. {
  170. var digest = DigestStore.Get(this.Address);
  171. if (digest != null)
  172. {
  173. string authentication = digest.GenerateResponseHeader(this.Credentials, true, HTTPMethods.Connect, parameters.uri);
  174. if (!string.IsNullOrEmpty(authentication))
  175. {
  176. string auth = string.Format("Proxy-Authorization: {0}", authentication);
  177. if (HTTPManager.Logger.Level <= Loglevels.Information)
  178. HTTPManager.Logger.Information("HTTPProxy", "Sending proxy authorization header: " + auth, parameters.context);
  179. var buff = auth.GetASCIIBytes();
  180. outStream.Write(buff.Data, buff.Offset, buff.Count);
  181. BufferPool.Release(buff);
  182. outStream.Write(EOL);
  183. }
  184. }
  185. break;
  186. }
  187. }
  188. }
  189. outStream.Write(EOL);
  190. // Make sure to send all the wrote data to the wire
  191. outStream.Flush();
  192. } // using outstream
  193. new HTTPProxyResponse(parameters)
  194. .OnFinished = OnProxyResponse;
  195. }
  196. else
  197. parameters.OnSuccess?.Invoke(parameters);
  198. }
  199. void OnProxyResponse(ProxyConnectParameters connectParameters, HTTPProxyResponse resp, Exception error)
  200. {
  201. HTTPManager.Logger.Information(nameof(HTTPProxyResponse), $"{nameof(OnProxyResponse)}({connectParameters}, {resp}, {error})", connectParameters.context);
  202. if (error != null)
  203. {
  204. // Resend request if the proxy response could be read && status code is 407 (authentication required) && we have credentials
  205. connectParameters.OnError?.Invoke(connectParameters, error, resp.ReadState == HTTPProxyResponse.PeekableReadState.Finished && resp.StatusCode == 407 && this.Credentials != null);
  206. }
  207. else
  208. {
  209. if (resp.StatusCode == 200)
  210. {
  211. connectParameters.OnSuccess?.Invoke(connectParameters);
  212. }
  213. else if (resp.StatusCode == 407)
  214. {
  215. // Proxy authentication required
  216. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.8
  217. bool retryNeogitiation = false;
  218. string authHeader = DigestStore.FindBest(resp.GetHeaderValues("proxy-authenticate"));
  219. if (!string.IsNullOrEmpty(authHeader))
  220. {
  221. var digest = DigestStore.GetOrCreate(this.Address);
  222. digest.ParseChallange(authHeader);
  223. retryNeogitiation = connectParameters.AuthenticationAttempts < ProxyConnectParameters.MaxAuthenticationAttempts &&
  224. this.Credentials != null &&
  225. digest.IsUriProtected(this.Address) &&
  226. (/*connectParameters.request == null || !connectParameters.request.HasHeader("Proxy-Authorization") ||*/ digest.Stale);
  227. }
  228. if (!retryNeogitiation)
  229. connectParameters.OnError?.Invoke(connectParameters, new Exception($"Can't authenticate Proxy! AuthenticationAttempts: {connectParameters.AuthenticationAttempts} {resp}"), false);
  230. else
  231. {
  232. connectParameters.AuthenticationAttempts++;
  233. BeginConnect(connectParameters);
  234. }
  235. }
  236. else
  237. {
  238. connectParameters.OnError?.Invoke(connectParameters, new Exception($"Proxy returned {resp}"), false);
  239. }
  240. }
  241. }
  242. }
  243. }
  244. #endif