實現一個簡單的Http代理服務器


昨天介紹了下微軟的反向代理庫YARP,今天准備實現一個簡單的Http正向代理服務器玩下。首先還是介紹下背景知識:

普通代理(Http)

在Http的時代,大部分是走的RFC 7230中描述的普通代理。這種代理扮演的是「中間人」角色,對於連接到它的客戶端來說,它是服務端;對於要連接的服務端來說,它是客戶端。它就負責在兩端之間來回傳送 HTTP 報文。它的流程是:

  1. 客戶端瀏覽器將請求原封不動的發送給代理服務器
  2. 代理服務器從HttpHeader中獲取目標的主機地址,將請求發送給目標主機
  3. 目標主機將響應回傳給代理服務器
  4. 代理服務器將響應回傳給客戶端瀏覽器。

  • 對於客戶端瀏覽器來說,代理服務器就是目標web服務器。
  • 對於web服務器來說來說,它會把代理當做客戶端,完全察覺不到真正客戶端的存在(代理服務器可以通過X-Forwarded-IP這樣的自定義頭部告訴服務端真正的客戶端 IP)。

這種代理服務器實現是比較簡單的,基本上是原封不動的透傳,主要是第2步,需要從header中識別目標主機地址。

隧道代理(Https)

到了Https時代,這種方式就有問題了,代理服務器是一個web服務器,它是影響了客戶端和服務器的TLS加密連接的。此時主要使用RFC中定義的通過 Web 代理服務器用隧道方式傳輸基於 TCP 的協議的隧道代理方式,它的主要流程為:

  1. 瀏覽器首先發送Http Connect請求給代理服務器,發送目標主機信息。
  2. 代理服務器建立和目標主機的tcp鏈接,並向瀏覽器回應 Connection Established應答。
  3. 瀏覽器將請求發送給代理服務器,代理服務器透傳給目標主機。
  4. 目標主機將響應回給代理服務器,代理服務器將響應回給瀏覽器。

這種模式下,和Sock5等代理協議非常類似了,代理服務器完全就是一個透傳的管道了。只不過是通過http協議協商建立起管道而已。建立連接后,代理服務器只起轉發的作用,理論上也適用於轉發其它TCP協議。

功能實現

兩種代理服務器實際上流程是大同小異的,主要是識別目標主機的指令不同,以及交互的方式有所差異,建立連接和完成第一次交互后,后面基本上都是透傳。從0開始實現也基本上就幾十行代碼,實現帶調試幾個小時差不多可以搞定,如下是我的一個簡單的實現,基於.net core 3.1。 

        static void Main(string[] args)
    {
        ProxyServer.Run();
        Thread.Sleep(-1);
    }

    class ProxyServer
    {
        public static void Run()
        {
            TcpServer.Run(3000, async tcp =>
            {
                using var handlder = new ProxyHandler(tcp);
                await handlder.Process();
            });
        }
    }


    class ProxyHandler : IDisposable
    {
        TcpClient _tcp;
        TcpClient _remoteTcp;

        public ProxyHandler(TcpClient tcp)
        {
            _tcp    = tcp;
            _buffer = MemoryPool<byte>.Shared.Rent(1024 * 3);
        }

        IMemoryOwner<byte> _buffer;
        Memory<byte>       _header;

        public async Task Process()
        {
            var count = await _tcp.GetStream().ReadAsync(_buffer.Memory);
            _header = _buffer.Memory[..count];

            parseHeader(out var method, out var endPoint);

            //Console.WriteLine(endPoint);

            if (method.Equals("CONNECT", StringComparison.OrdinalIgnoreCase))
            {
                await createTunnel(endPoint);
            }
            else
            {
                await createProxy(endPoint);
            }

            await pipeStream(_tcp.GetStream(), _remoteTcp.GetStream());
        }


        static byte[] _tunnelReply = Encoding.UTF8.GetBytes("HTTP/1.0 200 Connection Established\r\n\r\n");

        async ValueTask createTunnel(string endPoint)
        {
            var host = endPoint.Split(":");

            _remoteTcp = new TcpClient();
            await _remoteTcp.ConnectAsync(host[0], int.Parse(host[1]));
            await _tcp.GetStream().WriteAsync(_tunnelReply);
        }

        async ValueTask createProxy(string endPoint)
        {
            var host = new Uri(endPoint);

            _remoteTcp = new TcpClient();
            await _remoteTcp.ConnectAsync(host.Host, host.Port);
            await _remoteTcp.GetStream().WriteAsync(_header);
        }


        void parseHeader(out string method, out string endPoint)
        {
            var reader = new SequenceReader<byte>(new ReadOnlySequence<byte>(_header));

            method   = readToSpace(ref reader);
            endPoint = readToSpace(ref reader);

            static string readToSpace(ref SequenceReader<byte> r)
            {
                //讀到下一個空格
                r.TryReadTo(out ReadOnlySpan<byte> buf, (byte)' ');
                return Encoding.UTF8.GetString(buf);
            }
        }


        async Task pipeStream(Stream s1, Stream s2)
        {
            await Task.WhenAll(pipe(s1, s2), pipe(s2, s1));

            static async Task pipe(Stream source, Stream target)
            {
                try
                {
                    await source.CopyToAsync(target);
                }
                finally
                {
                    target.Close();
                    source.Close();
                }
            }
        }

        public void Dispose()
        {
            _buffer?.Dispose();
            _tcp?.Dispose();
            _remoteTcp?.Dispose();
        }
    }
    
    class TcpServer
    {
        public static async void Run(int port, Func<TcpClient, Task> handler)
        {
            var listener = new TcpListener(IPAddress.Loopback, port);

            listener.Start();
            while (true)
            {
                var tcp = await listener.AcceptTcpClientAsync();
                process(tcp, handler);
            }
        }

        static async void process(TcpClient tcp, Func<TcpClient, Task> handler)
        {
            try
            {
                await Task.Run(() => handler(tcp));
            }
            catch
            {
                // ignored
            }
        }
    }
View Code

簡單的運行測試了一下,基本功能應該是完善的,穩定運行貌似沒有什么大問題。雖然沒有什么額外的功能,但還是考慮了一下性能的,用了內存池,解析http頭的時候也盡量較少了內存的分配。考慮帶代碼的可讀性,也沒有太必要做到性能的極致。后面有空的話准備用它來寫一個SS客戶端,使之有更好的擴展性。

這種模式下,和Sock5等代理協議非常類似了,代理服務器完全就是一個透傳的管道了。只不過是通過http協議協商建立起管道而已。建立連接后,代理服務器只起轉發的作用,理論上也適用於轉發其它TCP協議。

參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM