Làm cách nào để tạo proxy đơn giản trong C #?


143

Tôi đã tải xuống Privoxy vài tuần trước và để giải trí, tôi tò mò muốn biết làm thế nào một phiên bản đơn giản của nó có thể được thực hiện.

Tôi hiểu rằng tôi cần định cấu hình trình duyệt (máy khách) để gửi yêu cầu đến proxy. Proxy gửi yêu cầu tới web (giả sử đó là proxy http). Proxy sẽ nhận được câu trả lời ... nhưng làm thế nào proxy có thể gửi lại yêu cầu tới trình duyệt (máy khách)?

Tôi đã tìm kiếm trên web cho C # và http proxy nhưng chưa tìm thấy thứ gì cho phép tôi hiểu cách thức hoạt động chính xác của cảnh phía sau. (Tôi tin rằng tôi không muốn một proxy ngược nhưng tôi không chắc chắn).

Có ai trong số các bạn có một số giải thích hoặc một số thông tin sẽ cho phép tôi tiếp tục dự án nhỏ này không?

Cập nhật

Đây là những gì tôi hiểu (xem hình bên dưới).

Bước 1 Tôi định cấu hình ứng dụng khách (trình duyệt) cho tất cả yêu cầu được gửi tới 127.0.0.1 tại cổng lắng nghe Proxy. Bằng cách này, yêu cầu sẽ không được gửi trực tiếp lên Internet mà sẽ được xử lý bởi proxy.

Bước2 Proxy nhìn thấy một kết nối mới, đọc tiêu đề HTTP và xem yêu cầu anh ta phải thực hiện. Anh ta thực hiện yêu cầu.

Bước 3 Proxy nhận được câu trả lời từ yêu cầu. Bây giờ anh phải gửi câu trả lời từ web cho khách hàng nhưng làm thế nào ???

văn bản thay thế

Liên kết hữu ích

Mentalis Proxy : Tôi đã tìm thấy dự án này là một proxy (nhưng nhiều hơn nữa tôi muốn). Tôi có thể kiểm tra nguồn nhưng tôi thực sự muốn một cái gì đó cơ bản để hiểu thêm về khái niệm này.

Proxy Proxy : Tôi cũng có thể có được một số thông tin ở đây.

Phản xạ yêu cầu : Đây là một ví dụ đơn giản.

Dưới đây là Kho lưu trữ Git Hub với Proxy http đơn giản .


Tôi không có ảnh chụp màn hình năm 2008 trong năm 2015. Xin lỗi.
Patrick Desjardins

Trên thực tế, hóa ra archive.org có nó . Xin lỗi đã làm phiền bạn.
Ilmari Karonen

Câu trả lời:


35

Bạn có thể xây dựng một HttpListenerlớp với lớp để lắng nghe các yêu cầu đến và HttpWebRequestlớp để chuyển tiếp các yêu cầu.


Tôi chuyển tiếp ở đâu? Làm thế nào tôi có thể biết nơi để gửi lại thông tin? Trình duyệt gửi cho phép cho biết 127.0.0.1:9999, khách hàng vào lúc 9999 nhận được yêu cầu và gửi nó lên web. Nhận được câu trả lời ... THAN khách hàng làm gì? Gửi đến địa chỉ nào?
Patrick Desjardins

2
Nếu bạn đang sử dụng HttpListener, bạn chỉ cần viết phản hồi cho HttpListener.GetContext (). Feedback.OutputStream. Không cần phải quan tâm đến địa chỉ.
OregonGhost

Thú vị, tôi sẽ kiểm tra theo cách này.
Patrick Desjardins

8
Tôi sẽ không sử dụng HttpListener cho việc này. Thay vào đó, hãy xây dựng một ứng dụng ASP.NET và lưu trữ nó trong IIS. Khi sử dụng HttpListener, bạn đang từ bỏ mô hình quy trình do IIS cung cấp. Điều này có nghĩa là bạn mất những thứ như quản lý quy trình (khởi động, phát hiện lỗi, tái chế), quản lý nhóm luồng, v.v.
Mauricio Scheffer

2
Đó là, nếu bạn có ý định sử dụng nó cho nhiều máy tính khách ... cho một proxy đồ chơi, httpListener vẫn ổn ...
Mauricio Scheffer

93

Tôi sẽ không sử dụng HttpListener hoặc một cái gì đó tương tự, theo cách đó bạn sẽ gặp rất nhiều vấn đề.

Quan trọng nhất sẽ là một nỗi đau rất lớn để hỗ trợ:

  • Giữ proxy
  • SSL sẽ không hoạt động (một cách chính xác, bạn sẽ nhận được quảng cáo)
  • Các thư viện .NET tuân thủ nghiêm ngặt các RFC khiến một số yêu cầu không thành công (mặc dù IE, FF và bất kỳ trình duyệt nào khác trên thế giới sẽ hoạt động.)

Những gì bạn cần làm là:

  • Nghe một cổng TCP
  • Phân tích yêu cầu trình duyệt
  • Trích xuất máy chủ kết nối với máy chủ đó ở cấp TCP
  • Chuyển tiếp mọi thứ qua lại trừ khi bạn muốn thêm tiêu đề tùy chỉnh, v.v.

Tôi đã viết 2 proxy HTTP khác nhau trong .NET với các yêu cầu khác nhau và tôi có thể nói với bạn rằng đây là cách tốt nhất để làm điều đó.

Mentalis làm điều này, nhưng mã của họ là "spaghetti đại biểu", tệ hơn GoTo :)


1
Bạn đã sử dụng lớp nào cho các kết nối TCP?
Cameron

8
@cameron TCPListener và SslStream.
dr. ác

2
Bạn có thể vui lòng chia sẻ kinh nghiệm của bạn về lý do tại sao HTTPS không hoạt động?
Restuta

10
@Restuta để SSL hoạt động, bạn nên chuyển tiếp kết nối mà không thực sự chạm vào nó ở cấp TCP và HttpListener không thể làm điều đó. Bạn có thể đọc cách SSL hoạt động và bạn sẽ thấy nó yêu cầu xác thực với máy chủ mục tiêu. Vì vậy, khách hàng sẽ cố gắng kết nối với google.com nhưng thực tế sẽ kết nối Httplistener của bạn không phải là google.com và sẽ gặp lỗi không khớp chứng nhận và vì người nghe của bạn sẽ không sử dụng chứng chỉ đã ký, sẽ nhận được chứng chỉ không chính xác, v.v. mặc dù bằng cách cài đặt CA vào máy tính mà máy khách sẽ sử dụng. Đó là một giải pháp khá bẩn.
dr. ác

1
@ dr.evil: +++ 1 cảm ơn vì những lời khuyên tuyệt vời, nhưng tôi tò mò làm thế nào để gửi lại dữ liệu cho khách hàng (trình duyệt), giả sử tôi đã gửi phản hồi cho khách hàng?
hoại

26

Gần đây tôi đã viết một proxy trọng lượng nhẹ trong c # .net bằng cách sử dụng TcpListenerTcpClient .

https://github.com/titanium007/Titanium-Web-Proxy

Nó hỗ trợ HTTP an toàn theo cách chính xác, máy khách cần tin cậy chứng chỉ gốc được sử dụng bởi proxy. Cũng hỗ trợ chuyển tiếp WebSockets. Tất cả các tính năng của HTTP 1.1 được hỗ trợ ngoại trừ đường ống. Pipelining không được sử dụng bởi hầu hết các trình duyệt hiện đại. Cũng hỗ trợ xác thực windows (đơn giản, tiêu hóa).

Bạn có thể kết nối ứng dụng của mình bằng cách tham khảo dự án, sau đó xem và sửa đổi tất cả lưu lượng truy cập. (Yêu cầu và phản hồi).

Về hiệu năng, tôi đã thử nghiệm nó trên máy của mình và hoạt động mà không có sự chậm trễ đáng chú ý nào.


và vẫn được duy trì vào năm 2020, cảm ơn vì đã chia sẻ :)
Mark Adamson

19

Proxy có thể hoạt động theo cách sau.

Bước 1, cấu hình máy khách để sử dụng proxyhost: proxyPort.

Proxy là một máy chủ TCP đang lắng nghe trên proxyhost: proxyPort. Trình duyệt mở kết nối với Proxy và gửi yêu cầu http. Proxy phân tích yêu cầu này và cố gắng phát hiện tiêu đề "Máy chủ". Tiêu đề này sẽ cho Proxy biết nơi mở kết nối.

Bước 2: Proxy mở kết nối đến địa chỉ được chỉ định trong tiêu đề "Máy chủ". Sau đó, nó sẽ gửi yêu cầu HTTP đến máy chủ từ xa đó. Đọc phản hồi.

Bước 3: Sau khi phản hồi được đọc từ máy chủ HTTP từ xa, Proxy sẽ gửi phản hồi thông qua kết nối TCP đã mở trước đó với trình duyệt.

Sơ đồ nó sẽ trông như thế này:

Browser                            Proxy                     HTTP server
  Open TCP connection  
  Send HTTP request  ----------->                       
                                 Read HTTP header
                                 detect Host header
                                 Send request to HTTP ----------->
                                 Server
                                                      <-----------
                                 Read response and send
                   <-----------  it back to the browser
Render content

14

Nếu bạn chỉ tìm cách chặn lưu lượng, bạn có thể sử dụng lõi fiddler để tạo proxy ...

http://fiddler.wikidot.com/fiddlercore

trước tiên hãy chạy fiddler với giao diện người dùng để xem những gì nó làm, nó là một proxy cho phép bạn gỡ lỗi lưu lượng http / https. Nó được viết bằng c # và có một lõi mà bạn có thể xây dựng thành các ứng dụng của riêng mình.

Hãy nhớ rằng FiddlerCore không miễn phí cho các ứng dụng thương mại.



5

Đồng ý với dr evil nếu bạn sử dụng HTTPListener, bạn sẽ gặp nhiều vấn đề, bạn phải phân tích các yêu cầu và sẽ được tham gia vào các tiêu đề và ...

  1. Sử dụng trình nghe tcp để nghe yêu cầu của trình duyệt
  2. chỉ phân tích dòng đầu tiên của yêu cầu và nhận miền máy chủ và cổng để kết nối
  3. gửi yêu cầu thô chính xác đến máy chủ được tìm thấy trên dòng yêu cầu trình duyệt đầu tiên
  4. nhận dữ liệu từ trang đích (tôi có vấn đề trong phần này)
  5. gửi dữ liệu chính xác nhận được từ máy chủ đến trình duyệt

bạn thấy bạn thậm chí không cần biết những gì trong yêu cầu trình duyệt và phân tích nó, chỉ lấy địa chỉ trang đích từ dòng đầu tiên thường thích cái này NHẬN http://google.com HTTP1.1 hoặc CONNECT facebook.com: 443 (đây là cho các yêu cầu ssl)


4

Vớ4 là một giao thức rất đơn giản để thực hiện. Bạn lắng nghe kết nối ban đầu, kết nối với máy chủ / cổng được khách hàng yêu cầu, gửi mã thành công cho khách hàng sau đó chuyển tiếp các luồng đi và đến qua các ổ cắm.

Nếu bạn đi với HTTP, bạn sẽ phải đọc và có thể đặt / xóa một số tiêu đề HTTP để công việc đó hiệu quả hơn một chút.

Nếu tôi nhớ chính xác, SSL sẽ hoạt động trên các proxy HTTP và Vớ. Đối với proxy HTTP, bạn triển khai động từ CONNECT, hoạt động giống như vớ4 như được mô tả ở trên, sau đó máy khách mở kết nối SSL qua luồng tcp được ủy quyền.


2

Trình duyệt được kết nối với proxy để dữ liệu mà proxy nhận được từ máy chủ web chỉ được gửi qua cùng một kết nối mà trình duyệt đã khởi tạo đến proxy.


1

Đối với những gì nó có giá trị, đây là một triển khai async mẫu C # dựa trên HttpListenerHttpClient (Tôi sử dụng nó để có thể kết nối Chrome trong các thiết bị Android với IIS Express, đó là cách duy nhất tôi tìm thấy ...).

Và nếu bạn cần hỗ trợ HTTPS, không cần thêm mã, chỉ cần cấu hình chứng chỉ: Httplistener có hỗ trợ HTTPS

// define http://localhost:5000 and http://127.0.0.1:5000/ to be proxies for http://localhost:53068
using (var server = new ProxyServer("http://localhost:53068", "http://localhost:5000/", "http://127.0.0.1:5000/"))
{
    server.Start();
    Console.WriteLine("Press ESC to stop server.");
    while (true)
    {
        var key = Console.ReadKey(true);
        if (key.Key == ConsoleKey.Escape)
            break;
    }
    server.Stop();
}

....

public class ProxyServer : IDisposable
{
    private readonly HttpListener _listener;
    private readonly int _targetPort;
    private readonly string _targetHost;
    private static readonly HttpClient _client = new HttpClient();

    public ProxyServer(string targetUrl, params string[] prefixes)
        : this(new Uri(targetUrl), prefixes)
    {
    }

    public ProxyServer(Uri targetUrl, params string[] prefixes)
    {
        if (targetUrl == null)
            throw new ArgumentNullException(nameof(targetUrl));

        if (prefixes == null)
            throw new ArgumentNullException(nameof(prefixes));

        if (prefixes.Length == 0)
            throw new ArgumentException(null, nameof(prefixes));

        RewriteTargetInText = true;
        RewriteHost = true;
        RewriteReferer = true;
        TargetUrl = targetUrl;
        _targetHost = targetUrl.Host;
        _targetPort = targetUrl.Port;
        Prefixes = prefixes;

        _listener = new HttpListener();
        foreach (var prefix in prefixes)
        {
            _listener.Prefixes.Add(prefix);
        }
    }

    public Uri TargetUrl { get; }
    public string[] Prefixes { get; }
    public bool RewriteTargetInText { get; set; }
    public bool RewriteHost { get; set; }
    public bool RewriteReferer { get; set; } // this can have performance impact...

    public void Start()
    {
        _listener.Start();
        _listener.BeginGetContext(ProcessRequest, null);
    }

    private async void ProcessRequest(IAsyncResult result)
    {
        if (!_listener.IsListening)
            return;

        var ctx = _listener.EndGetContext(result);
        _listener.BeginGetContext(ProcessRequest, null);
        await ProcessRequest(ctx).ConfigureAwait(false);
    }

    protected virtual async Task ProcessRequest(HttpListenerContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        var url = TargetUrl.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped);
        using (var msg = new HttpRequestMessage(new HttpMethod(context.Request.HttpMethod), url + context.Request.RawUrl))
        {
            msg.Version = context.Request.ProtocolVersion;

            if (context.Request.HasEntityBody)
            {
                msg.Content = new StreamContent(context.Request.InputStream); // disposed with msg
            }

            string host = null;
            foreach (string headerName in context.Request.Headers)
            {
                var headerValue = context.Request.Headers[headerName];
                if (headerName == "Content-Length" && headerValue == "0") // useless plus don't send if we have no entity body
                    continue;

                bool contentHeader = false;
                switch (headerName)
                {
                    // some headers go to content...
                    case "Allow":
                    case "Content-Disposition":
                    case "Content-Encoding":
                    case "Content-Language":
                    case "Content-Length":
                    case "Content-Location":
                    case "Content-MD5":
                    case "Content-Range":
                    case "Content-Type":
                    case "Expires":
                    case "Last-Modified":
                        contentHeader = true;
                        break;

                    case "Referer":
                        if (RewriteReferer && Uri.TryCreate(headerValue, UriKind.Absolute, out var referer)) // if relative, don't handle
                        {
                            var builder = new UriBuilder(referer);
                            builder.Host = TargetUrl.Host;
                            builder.Port = TargetUrl.Port;
                            headerValue = builder.ToString();
                        }
                        break;

                    case "Host":
                        host = headerValue;
                        if (RewriteHost)
                        {
                            headerValue = TargetUrl.Host + ":" + TargetUrl.Port;
                        }
                        break;
                }

                if (contentHeader)
                {
                    msg.Content.Headers.Add(headerName, headerValue);
                }
                else
                {
                    msg.Headers.Add(headerName, headerValue);
                }
            }

            using (var response = await _client.SendAsync(msg).ConfigureAwait(false))
            {
                using (var os = context.Response.OutputStream)
                {
                    context.Response.ProtocolVersion = response.Version;
                    context.Response.StatusCode = (int)response.StatusCode;
                    context.Response.StatusDescription = response.ReasonPhrase;

                    foreach (var header in response.Headers)
                    {
                        context.Response.Headers.Add(header.Key, string.Join(", ", header.Value));
                    }

                    foreach (var header in response.Content.Headers)
                    {
                        if (header.Key == "Content-Length") // this will be set automatically at dispose time
                            continue;

                        context.Response.Headers.Add(header.Key, string.Join(", ", header.Value));
                    }

                    var ct = context.Response.ContentType;
                    if (RewriteTargetInText && host != null && ct != null &&
                        (ct.IndexOf("text/html", StringComparison.OrdinalIgnoreCase) >= 0 ||
                        ct.IndexOf("application/json", StringComparison.OrdinalIgnoreCase) >= 0))
                    {
                        using (var ms = new MemoryStream())
                        {
                            using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                            {
                                await stream.CopyToAsync(ms).ConfigureAwait(false);
                                var enc = context.Response.ContentEncoding ?? Encoding.UTF8;
                                var html = enc.GetString(ms.ToArray());
                                if (TryReplace(html, "//" + _targetHost + ":" + _targetPort + "/", "//" + host + "/", out var replaced))
                                {
                                    var bytes = enc.GetBytes(replaced);
                                    using (var ms2 = new MemoryStream(bytes))
                                    {
                                        ms2.Position = 0;
                                        await ms2.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
                                    }
                                }
                                else
                                {
                                    ms.Position = 0;
                                    await ms.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
                                }
                            }
                        }
                    }
                    else
                    {
                        using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
                        {
                            await stream.CopyToAsync(context.Response.OutputStream).ConfigureAwait(false);
                        }
                    }
                }
            }
        }
    }

    public void Stop() => _listener.Stop();
    public override string ToString() => string.Join(", ", Prefixes) + " => " + TargetUrl;
    public void Dispose() => ((IDisposable)_listener)?.Dispose();

    // out-of-the-box replace doesn't tell if something *was* replaced or not
    private static bool TryReplace(string input, string oldValue, string newValue, out string result)
    {
        if (string.IsNullOrEmpty(input) || string.IsNullOrEmpty(oldValue))
        {
            result = input;
            return false;
        }

        var oldLen = oldValue.Length;
        var sb = new StringBuilder(input.Length);
        bool changed = false;
        var offset = 0;
        for (int i = 0; i < input.Length; i++)
        {
            var c = input[i];

            if (offset > 0)
            {
                if (c == oldValue[offset])
                {
                    offset++;
                    if (oldLen == offset)
                    {
                        changed = true;
                        sb.Append(newValue);
                        offset = 0;
                    }
                    continue;
                }

                for (int j = 0; j < offset; j++)
                {
                    sb.Append(input[i - offset + j]);
                }

                sb.Append(c);
                offset = 0;
            }
            else
            {
                if (c == oldValue[0])
                {
                    if (oldLen == 1)
                    {
                        changed = true;
                        sb.Append(newValue);
                    }
                    else
                    {
                        offset = 1;
                    }
                    continue;
                }

                sb.Append(c);
            }
        }

        if (changed)
        {
            result = sb.ToString();
            return true;
        }

        result = input;
        return false;
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.