HttpConnector.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Net;
  5. using System.Net.Sockets;
  6. using System.Text.RegularExpressions;
  7. using Renci.SshNet.Abstractions;
  8. using Renci.SshNet.Common;
  9. namespace Renci.SshNet.Connection
  10. {
  11. /// <summary>
  12. /// Establishes a tunnel via an HTTP proxy server.
  13. /// </summary>
  14. /// <remarks>
  15. /// <list type="table">
  16. /// <listheader>
  17. /// <term>Specification</term>
  18. /// <description>URL</description>
  19. /// </listheader>
  20. /// <item>
  21. /// <term>HTTP CONNECT method</term>
  22. /// <description>https://tools.ietf.org/html/rfc7231#section-4.3.6</description>
  23. /// </item>
  24. /// <item>
  25. /// <term>HTTP Authentication: Basic and Digest Access Authentication</term>
  26. /// <description>https://tools.ietf.org/html/rfc2617</description>
  27. /// </item>
  28. /// </list>
  29. /// </remarks>
  30. internal sealed partial class HttpConnector : ProxyConnector
  31. {
  32. private const string HttpResponsePattern = @"HTTP/(?<version>\d[.]\d) (?<statusCode>\d{3}) (?<reasonPhrase>.+)$";
  33. private const string HttpHeaderPattern = @"(?<fieldName>[^\[\]()<>@,;:\""/?={} \t]+):(?<fieldValue>.+)?";
  34. #if NET7_0_OR_GREATER
  35. private static readonly Regex HttpResponseRegex = GetHttpResponseRegex();
  36. private static readonly Regex HttpHeaderRegex = GetHttpHeaderRegex();
  37. [GeneratedRegex(HttpResponsePattern)]
  38. private static partial Regex GetHttpResponseRegex();
  39. [GeneratedRegex(HttpHeaderPattern)]
  40. private static partial Regex GetHttpHeaderRegex();
  41. #else
  42. private static readonly Regex HttpResponseRegex = new Regex(HttpResponsePattern, RegexOptions.Compiled);
  43. private static readonly Regex HttpHeaderRegex = new Regex(HttpHeaderPattern, RegexOptions.Compiled);
  44. #endif
  45. public HttpConnector(ISocketFactory socketFactory)
  46. : base(socketFactory)
  47. {
  48. }
  49. protected override void HandleProxyConnect(IConnectionInfo connectionInfo, Socket socket)
  50. {
  51. SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(string.Format(CultureInfo.InvariantCulture,
  52. "CONNECT {0}:{1} HTTP/1.0\r\n",
  53. connectionInfo.Host,
  54. connectionInfo.Port)));
  55. // Send proxy authorization if specified
  56. if (!string.IsNullOrEmpty(connectionInfo.ProxyUsername))
  57. {
  58. var authorization = string.Format(CultureInfo.InvariantCulture,
  59. "Proxy-Authorization: Basic {0}\r\n",
  60. Convert.ToBase64String(SshData.Ascii.GetBytes($"{connectionInfo.ProxyUsername}:{connectionInfo.ProxyPassword}")));
  61. SocketAbstraction.Send(socket, SshData.Ascii.GetBytes(authorization));
  62. }
  63. SocketAbstraction.Send(socket, SshData.Ascii.GetBytes("\r\n"));
  64. HttpStatusCode? statusCode = null;
  65. var contentLength = 0;
  66. while (true)
  67. {
  68. var response = SocketReadLine(socket, connectionInfo.Timeout);
  69. if (response is null)
  70. {
  71. // server shut down socket
  72. break;
  73. }
  74. if (statusCode is null)
  75. {
  76. var statusMatch = HttpResponseRegex.Match(response);
  77. if (statusMatch.Success)
  78. {
  79. var httpStatusCode = statusMatch.Result("${statusCode}");
  80. statusCode = (HttpStatusCode)int.Parse(httpStatusCode, CultureInfo.InvariantCulture);
  81. if (statusCode != HttpStatusCode.OK)
  82. {
  83. throw new ProxyException($"HTTP: Status code {httpStatusCode}, \"{statusMatch.Result("${reasonPhrase}")}\"");
  84. }
  85. }
  86. continue;
  87. }
  88. // continue on parsing message headers coming from the server
  89. var headerMatch = HttpHeaderRegex.Match(response);
  90. if (headerMatch.Success)
  91. {
  92. var fieldName = headerMatch.Result("${fieldName}");
  93. if (fieldName.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
  94. {
  95. contentLength = int.Parse(headerMatch.Result("${fieldValue}"), CultureInfo.InvariantCulture);
  96. }
  97. continue;
  98. }
  99. // check if we've reached the CRLF which separates request line and headers from the message body
  100. if (response.Length == 0)
  101. {
  102. // read response body if specified
  103. if (contentLength > 0)
  104. {
  105. var contentBody = new byte[contentLength];
  106. _ = SocketRead(socket, contentBody, 0, contentLength, connectionInfo.Timeout);
  107. }
  108. break;
  109. }
  110. }
  111. if (statusCode is null)
  112. {
  113. throw new ProxyException("HTTP response does not contain status line.");
  114. }
  115. }
  116. /// <summary>
  117. /// Performs a blocking read on the socket until a line is read.
  118. /// </summary>
  119. /// <param name="socket">The <see cref="Socket"/> to read from.</param>
  120. /// <param name="readTimeout">A <see cref="TimeSpan"/> that represents the time to wait until a line is read.</param>
  121. /// <exception cref="SshOperationTimeoutException">The read has timed-out.</exception>
  122. /// <exception cref="SocketException">An error occurred when trying to access the socket.</exception>
  123. /// <returns>
  124. /// The line read from the socket, or <see langword="null"/> when the remote server has shutdown and all data has been received.
  125. /// </returns>
  126. private static string SocketReadLine(Socket socket, TimeSpan readTimeout)
  127. {
  128. var encoding = SshData.Ascii;
  129. var buffer = new List<byte>();
  130. var data = new byte[1];
  131. // read data one byte at a time to find end of line and leave any unhandled information in the buffer
  132. // to be processed by subsequent invocations
  133. do
  134. {
  135. var bytesRead = SocketAbstraction.Read(socket, data, 0, data.Length, readTimeout);
  136. if (bytesRead == 0)
  137. {
  138. // the remote server shut down the socket
  139. break;
  140. }
  141. var b = data[0];
  142. buffer.Add(b);
  143. if (b == Session.LineFeed && buffer.Count > 1 && buffer[buffer.Count - 2] == Session.CarriageReturn)
  144. {
  145. // Return line without CRLF
  146. return encoding.GetString(buffer.ToArray(), 0, buffer.Count - 2);
  147. }
  148. }
  149. while (true);
  150. if (buffer.Count == 0)
  151. {
  152. return null;
  153. }
  154. return encoding.GetString(buffer.ToArray(), 0, buffer.Count);
  155. }
  156. }
  157. }