using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; using NLog; using Shadowsocks.Controller; using Shadowsocks.Util.Sockets; namespace Shadowsocks.Proxy { public class HttpProxy : IProxy { private static Logger logger = LogManager.GetCurrentClassLogger(); private class FakeAsyncResult : IAsyncResult { public readonly HttpState innerState; private readonly IAsyncResult r; public FakeAsyncResult(IAsyncResult orig, HttpState state) { r = orig; innerState = state; } public bool IsCompleted => r.IsCompleted; public WaitHandle AsyncWaitHandle => r.AsyncWaitHandle; public object AsyncState => innerState.AsyncState; public bool CompletedSynchronously => r.CompletedSynchronously; } private class HttpState { public AsyncCallback Callback { get; set; } public object AsyncState { get; set; } public int BytesToRead; public Exception ex { get; set; } } public EndPoint LocalEndPoint => _remote.LocalEndPoint; public EndPoint ProxyEndPoint { get; private set; } public EndPoint DestEndPoint { get; private set; } private readonly WrappedSocket _remote = new WrappedSocket(); public void BeginConnectProxy(EndPoint remoteEP, AsyncCallback callback, object state) { ProxyEndPoint = remoteEP; _remote.BeginConnect(remoteEP, callback, state); } public void EndConnectProxy(IAsyncResult asyncResult) { _remote.EndConnect(asyncResult); _remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); } private const string HTTP_CRLF = "\r\n"; private const string HTTP_CONNECT_TEMPLATE = "CONNECT {0} HTTP/1.1" + HTTP_CRLF + "Host: {0}" + HTTP_CRLF + "Proxy-Connection: keep-alive" + HTTP_CRLF + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36" + HTTP_CRLF + "{1}" + // Proxy-Authorization if any "" + HTTP_CRLF; // End with an empty line private const string PROXY_AUTH_TEMPLATE = "Proxy-Authorization: Basic {0}" + HTTP_CRLF; public void BeginConnectDest(EndPoint destEndPoint, AsyncCallback callback, object state, NetworkCredential auth = null) { DestEndPoint = destEndPoint; String authInfo = ""; if (auth != null) { string authKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(auth.UserName + ":" + auth.Password)); authInfo = string.Format(PROXY_AUTH_TEMPLATE, authKey); } string request = string.Format(HTTP_CONNECT_TEMPLATE, destEndPoint, authInfo); var b = Encoding.UTF8.GetBytes(request); var st = new HttpState(); st.Callback = callback; st.AsyncState = state; _remote.BeginSend(b, 0, b.Length, 0, HttpRequestSendCallback, st); } public void EndConnectDest(IAsyncResult asyncResult) { var state = ((FakeAsyncResult)asyncResult).innerState; if (state.ex != null) { throw state.ex; } } public void BeginSend(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state) { _remote.BeginSend(buffer, offset, size, socketFlags, callback, state); } public int EndSend(IAsyncResult asyncResult) { return _remote.EndSend(asyncResult); } public void BeginReceive(byte[] buffer, int offset, int size, SocketFlags socketFlags, AsyncCallback callback, object state) { _remote.BeginReceive(buffer, offset, size, socketFlags, callback, state); } public int EndReceive(IAsyncResult asyncResult) { return _remote.EndReceive(asyncResult); } public void Shutdown(SocketShutdown how) { _remote.Shutdown(how); } public void Close() { _remote.Dispose(); } private void HttpRequestSendCallback(IAsyncResult ar) { var state = (HttpState) ar.AsyncState; try { _remote.EndSend(ar); // start line read new LineReader(_remote, OnLineRead, OnException, OnFinish, Encoding.UTF8, HTTP_CRLF, 1024, new FakeAsyncResult(ar, state)); } catch (Exception ex) { state.ex = ex; state.Callback?.Invoke(new FakeAsyncResult(ar, state)); } } private void OnFinish(byte[] lastBytes, int index, int length, object state) { var st = (FakeAsyncResult)state; if (st.innerState.ex == null) { if (!_established) { st.innerState.ex = new Exception(I18N.GetString("Proxy request failed")); } // TODO: save last bytes } st.innerState.Callback?.Invoke(st); } private void OnException(Exception ex, object state) { var st = (FakeAsyncResult) state; st.innerState.ex = ex; } private static readonly Regex HttpRespondHeaderRegex = new Regex(@"^(HTTP/1\.\d) (\d{3}) (.+)$", RegexOptions.Compiled); private int _respondLineCount = 0; private bool _established = false; private bool OnLineRead(string line, object state) { logger.Trace(line); if (_respondLineCount == 0) { var m = HttpRespondHeaderRegex.Match(line); if (m.Success) { var resultCode = m.Groups[2].Value; if ("200" != resultCode) { return true; } _established = true; } } else { if (line.IsNullOrEmpty()) { return true; } } _respondLineCount++; return false; } } }