WebConnectionTunnel.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. //
  2. // System.Net.WebConnectionTunnel
  3. //
  4. // Authors:
  5. // Gonzalo Paniagua Javier ([email protected])
  6. // Martin Baulig <[email protected]>
  7. //
  8. // (C) 2003 Ximian, Inc (http://www.ximian.com)
  9. // Copyright (c) 2017 Xamarin Inc. (http://www.xamarin.com)
  10. //
  11. //
  12. // Permission is hereby granted, free of charge, to any person obtaining
  13. // a copy of this software and associated documentation files (the
  14. // "Software"), to deal in the Software without restriction, including
  15. // without limitation the rights to use, copy, modify, merge, publish,
  16. // distribute, sublicense, and/or sell copies of the Software, and to
  17. // permit persons to whom the Software is furnished to do so, subject to
  18. // the following conditions:
  19. //
  20. // The above copyright notice and this permission notice shall be
  21. // included in all copies or substantial portions of the Software.
  22. //
  23. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  24. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  25. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  26. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  27. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  28. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  29. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  30. //
  31. using System.IO;
  32. using System.Collections;
  33. using System.Net.Sockets;
  34. using System.Security.Cryptography.X509Certificates;
  35. using System.Text;
  36. using System.Threading;
  37. using System.Threading.Tasks;
  38. using System.Runtime.ExceptionServices;
  39. using System.Diagnostics;
  40. using Mono.Net.Security;
  41. namespace System.Net
  42. {
  43. class WebConnectionTunnel
  44. {
  45. public HttpWebRequest Request {
  46. get;
  47. }
  48. public Uri ConnectUri {
  49. get;
  50. }
  51. public WebConnectionTunnel (HttpWebRequest request, Uri connectUri)
  52. {
  53. Request = request;
  54. ConnectUri = connectUri;
  55. }
  56. enum NtlmAuthState
  57. {
  58. None,
  59. Challenge,
  60. Response
  61. }
  62. HttpWebRequest connectRequest;
  63. NtlmAuthState ntlmAuthState;
  64. public bool Success {
  65. get;
  66. private set;
  67. }
  68. public bool CloseConnection {
  69. get;
  70. private set;
  71. }
  72. public int StatusCode {
  73. get;
  74. private set;
  75. }
  76. public string StatusDescription {
  77. get;
  78. private set;
  79. }
  80. public string[] Challenge {
  81. get;
  82. private set;
  83. }
  84. public WebHeaderCollection Headers {
  85. get;
  86. private set;
  87. }
  88. public Version ProxyVersion {
  89. get;
  90. private set;
  91. }
  92. public byte[] Data {
  93. get;
  94. private set;
  95. }
  96. internal async Task Initialize (Stream stream, CancellationToken cancellationToken)
  97. {
  98. StringBuilder sb = new StringBuilder ();
  99. sb.Append ("CONNECT ");
  100. sb.Append (Request.Address.Host);
  101. sb.Append (':');
  102. sb.Append (Request.Address.Port);
  103. sb.Append (" HTTP/");
  104. if (Request.ProtocolVersion == HttpVersion.Version11)
  105. sb.Append ("1.1");
  106. else
  107. sb.Append ("1.0");
  108. sb.Append ("\r\nHost: ");
  109. sb.Append (Request.Address.Authority);
  110. bool ntlm = false;
  111. var challenge = Challenge;
  112. Challenge = null;
  113. var auth_header = Request.Headers["Proxy-Authorization"];
  114. bool have_auth = auth_header != null;
  115. if (have_auth) {
  116. sb.Append ("\r\nProxy-Authorization: ");
  117. sb.Append (auth_header);
  118. ntlm = auth_header.ToUpper ().Contains ("NTLM");
  119. } else if (challenge != null && StatusCode == 407) {
  120. ICredentials creds = Request.Proxy.Credentials;
  121. have_auth = true;
  122. if (connectRequest == null) {
  123. // create a CONNECT request to use with Authenticate
  124. connectRequest = (HttpWebRequest)WebRequest.Create (
  125. ConnectUri.Scheme + "://" + ConnectUri.Host + ":" + ConnectUri.Port + "/");
  126. connectRequest.Method = "CONNECT";
  127. connectRequest.Credentials = creds;
  128. }
  129. if (creds != null) {
  130. for (int i = 0; i < challenge.Length; i++) {
  131. var auth = AuthenticationManager.Authenticate (challenge[i], connectRequest, creds);
  132. if (auth == null)
  133. continue;
  134. ntlm = (auth.ModuleAuthenticationType == "NTLM");
  135. sb.Append ("\r\nProxy-Authorization: ");
  136. sb.Append (auth.Message);
  137. break;
  138. }
  139. }
  140. }
  141. if (ntlm) {
  142. sb.Append ("\r\nProxy-Connection: keep-alive");
  143. ntlmAuthState++;
  144. }
  145. sb.Append ("\r\n\r\n");
  146. StatusCode = 0;
  147. byte[] connectBytes = Encoding.Default.GetBytes (sb.ToString ());
  148. await stream.WriteAsync (connectBytes, 0, connectBytes.Length, cancellationToken).ConfigureAwait (false);
  149. (Headers, Data, StatusCode) = await ReadHeaders (stream, cancellationToken).ConfigureAwait (false);
  150. if ((!have_auth || ntlmAuthState == NtlmAuthState.Challenge) && Headers != null && StatusCode == 407) { // Needs proxy auth
  151. var connectionHeader = Headers["Connection"];
  152. if (!string.IsNullOrEmpty (connectionHeader) && connectionHeader.ToLower () == "close") {
  153. // The server is requesting that this connection be closed
  154. CloseConnection = true;
  155. }
  156. Challenge = Headers.GetValues ("Proxy-Authenticate");
  157. Success = false;
  158. } else {
  159. Success = StatusCode == 200 && Headers != null;
  160. }
  161. if (Challenge == null && (StatusCode == 401 || StatusCode == 407)) {
  162. var response = new HttpWebResponse (ConnectUri, "CONNECT", (HttpStatusCode)StatusCode, Headers);
  163. throw new WebException (
  164. StatusCode == 407 ? "(407) Proxy Authentication Required" : "(401) Unauthorized",
  165. null, WebExceptionStatus.ProtocolError, response);
  166. }
  167. }
  168. async Task<(WebHeaderCollection, byte[], int)> ReadHeaders (Stream stream, CancellationToken cancellationToken)
  169. {
  170. byte[] retBuffer = null;
  171. int status = 200;
  172. byte[] buffer = new byte[1024];
  173. MemoryStream ms = new MemoryStream ();
  174. while (true) {
  175. cancellationToken.ThrowIfCancellationRequested ();
  176. int n = await stream.ReadAsync (buffer, 0, 1024, cancellationToken).ConfigureAwait (false);
  177. if (n == 0)
  178. throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null);
  179. ms.Write (buffer, 0, n);
  180. int start = 0;
  181. string str = null;
  182. bool gotStatus = false;
  183. WebHeaderCollection headers = new WebHeaderCollection ();
  184. while (WebConnection.ReadLine (ms.GetBuffer (), ref start, (int)ms.Length, ref str)) {
  185. if (str == null) {
  186. int contentLen;
  187. var clengthHeader = headers["Content-Length"];
  188. if (string.IsNullOrEmpty (clengthHeader) || !int.TryParse (clengthHeader, out contentLen))
  189. contentLen = 0;
  190. if (ms.Length - start - contentLen > 0) {
  191. // we've read more data than the response header and conents,
  192. // give back extra data to the caller
  193. retBuffer = new byte[ms.Length - start - contentLen];
  194. Buffer.BlockCopy (ms.GetBuffer (), start + contentLen, retBuffer, 0, retBuffer.Length);
  195. } else {
  196. // haven't read in some or all of the contents for the response, do so now
  197. FlushContents (stream, contentLen - (int)(ms.Length - start));
  198. }
  199. return (headers, retBuffer, status);
  200. }
  201. if (gotStatus) {
  202. headers.Add (str);
  203. continue;
  204. }
  205. string[] parts = str.Split (' ');
  206. if (parts.Length < 2)
  207. throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null);
  208. if (String.Compare (parts[0], "HTTP/1.1", true) == 0)
  209. ProxyVersion = HttpVersion.Version11;
  210. else if (String.Compare (parts[0], "HTTP/1.0", true) == 0)
  211. ProxyVersion = HttpVersion.Version10;
  212. else
  213. throw WebConnection.GetException (WebExceptionStatus.ServerProtocolViolation, null);
  214. status = (int)UInt32.Parse (parts[1]);
  215. if (parts.Length >= 3)
  216. StatusDescription = String.Join (" ", parts, 2, parts.Length - 2);
  217. gotStatus = true;
  218. }
  219. }
  220. }
  221. void FlushContents (Stream stream, int contentLength)
  222. {
  223. while (contentLength > 0) {
  224. byte[] contentBuffer = new byte[contentLength];
  225. int bytesRead = stream.Read (contentBuffer, 0, contentLength);
  226. if (bytesRead > 0) {
  227. contentLength -= bytesRead;
  228. } else {
  229. break;
  230. }
  231. }
  232. }
  233. }
  234. }