You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

PACServer.cs 7.1 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. using Shadowsocks.Net;
  2. using Shadowsocks.Utilities;
  3. using Shadowsocks.Net.Crypto;
  4. using Splat;
  5. using System;
  6. using System.Net;
  7. using System.Net.Sockets;
  8. using System.Text;
  9. using System.Reflection;
  10. namespace Shadowsocks.PAC
  11. {
  12. public class PACServer : StreamService, IEnableLogger
  13. {
  14. public const string RESOURCE_NAME = "pac";
  15. private string PacSecret
  16. {
  17. get
  18. {
  19. if (string.IsNullOrEmpty(_cachedPacSecret))
  20. {
  21. _cachedPacSecret = Base64Url.Encode(RNG.GetBytes(32));
  22. }
  23. return _cachedPacSecret;
  24. }
  25. }
  26. private string _cachedPacSecret = "";
  27. private bool _PACServerEnableSecret;
  28. public string PacUrl { get; private set; } = "";
  29. private PACDaemon _pacDaemon;
  30. public PACServer(PACDaemon pacDaemon, bool PACServerEnableSecret)
  31. {
  32. _pacDaemon = pacDaemon;
  33. _PACServerEnableSecret = PACServerEnableSecret;
  34. }
  35. public void UpdatePACURL(EndPoint localEndPoint)
  36. {
  37. string usedSecret = _PACServerEnableSecret ? $"&secret={PacSecret}" : "";
  38. string contentHash = GetHash(_pacDaemon.GetPACContent());
  39. PacUrl = $"http://{localEndPoint}/{RESOURCE_NAME}?hash={contentHash}{usedSecret}";
  40. this.Log().Debug("Setting PAC URL: {PacUrl}");
  41. }
  42. private static string GetHash(string content)
  43. {
  44. return Base64Url.Encode(CryptoUtils.MD5(Encoding.ASCII.GetBytes(content)));
  45. }
  46. public override bool Handle(CachedNetworkStream stream, object state)
  47. {
  48. byte[] fp = new byte[256];
  49. int len = stream.ReadFirstBlock(fp);
  50. return Handle(fp, len, stream.Socket, state);
  51. }
  52. [Obsolete]
  53. public override bool Handle(byte[] firstPacket, int length, Socket socket, object state)
  54. {
  55. if (socket.ProtocolType != ProtocolType.Tcp)
  56. {
  57. return false;
  58. }
  59. try
  60. {
  61. /*
  62. * RFC 7230
  63. *
  64. GET /hello.txt HTTP/1.1
  65. User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
  66. Host: www.example.com
  67. Accept-Language: en, mi
  68. */
  69. string request = Encoding.UTF8.GetString(firstPacket, 0, length);
  70. string[] lines = request.Split('\r', '\n');
  71. bool hostMatch = false, pathMatch = false, useSocks = false;
  72. bool secretMatch = !_PACServerEnableSecret;
  73. if (lines.Length < 2) // need at lease RequestLine + Host
  74. {
  75. return false;
  76. }
  77. // parse request line
  78. string requestLine = lines[0];
  79. // GET /pac?t=yyyyMMddHHmmssfff&secret=foobar HTTP/1.1
  80. string[] requestItems = requestLine.Split(' ');
  81. if (requestItems.Length == 3 && requestItems[0] == "GET")
  82. {
  83. int index = requestItems[1].IndexOf('?');
  84. if (index < 0)
  85. {
  86. index = requestItems[1].Length;
  87. }
  88. string resourceString = requestItems[1].Substring(0, index).Remove(0, 1);
  89. if (string.Equals(resourceString, RESOURCE_NAME, StringComparison.OrdinalIgnoreCase))
  90. {
  91. pathMatch = true;
  92. if (!secretMatch)
  93. {
  94. string queryString = requestItems[1].Substring(index);
  95. if (queryString.Contains(PacSecret))
  96. {
  97. secretMatch = true;
  98. }
  99. }
  100. }
  101. }
  102. // parse request header
  103. for (int i = 1; i < lines.Length; i++)
  104. {
  105. if (string.IsNullOrEmpty(lines[i]))
  106. continue;
  107. string[] kv = lines[i].Split(new char[] { ':' }, 2);
  108. if (kv.Length == 2)
  109. {
  110. if (kv[0] == "Host")
  111. {
  112. if (kv[1].Trim() == (socket.LocalEndPoint as IPEndPoint)?.ToString())
  113. {
  114. hostMatch = true;
  115. }
  116. }
  117. //else if (kv[0] == "User-Agent")
  118. //{
  119. // // we need to drop connections when changing servers
  120. // if (kv[1].IndexOf("Chrome") >= 0)
  121. // {
  122. // useSocks = true;
  123. // }
  124. //}
  125. }
  126. }
  127. if (hostMatch && pathMatch)
  128. {
  129. if (!secretMatch)
  130. {
  131. socket.Close(); // Close immediately
  132. }
  133. else
  134. {
  135. SendResponse(socket, useSocks);
  136. }
  137. return true;
  138. }
  139. return false;
  140. }
  141. catch (ArgumentException)
  142. {
  143. return false;
  144. }
  145. }
  146. public void SendResponse(Socket socket, bool useSocks)
  147. {
  148. try
  149. {
  150. IPEndPoint localEndPoint = socket.LocalEndPoint as IPEndPoint ?? throw new ArgumentException("Invalid socket local endpoint.", nameof(socket));
  151. string proxy = GetPACAddress(localEndPoint, useSocks);
  152. string pacContent = $"var __PROXY__ = '{proxy}';\n" + _pacDaemon.GetPACContent();
  153. string responseHead =
  154. $@"HTTP/1.1 200 OK
  155. Server: ShadowsocksPAC/{Assembly.GetExecutingAssembly().GetName().Version}
  156. Content-Type: application/x-ns-proxy-autoconfig
  157. Content-Length: { Encoding.UTF8.GetBytes(pacContent).Length}
  158. Connection: Close
  159. ";
  160. byte[] response = Encoding.UTF8.GetBytes(responseHead + pacContent);
  161. socket.BeginSend(response, 0, response.Length, 0, new AsyncCallback(SendCallback), socket);
  162. }
  163. catch (Exception e)
  164. {
  165. this.Log().Error(e, "");
  166. socket.Close();
  167. }
  168. }
  169. private void SendCallback(IAsyncResult ar)
  170. {
  171. Socket? conn = ar.AsyncState as Socket;
  172. try
  173. {
  174. conn?.Shutdown(SocketShutdown.Send);
  175. }
  176. catch
  177. {
  178. }
  179. }
  180. private string GetPACAddress(IPEndPoint localEndPoint, bool useSocks) => $"{(useSocks ? "SOCKS5" : "PROXY")} {localEndPoint};";
  181. }
  182. }