Bagaimana cara membuat proxy sederhana di C #?

143

Saya telah mengunduh Privoxy beberapa minggu lalu dan untuk bersenang-senang saya ingin tahu bagaimana versi sederhana dapat dilakukan.

Saya mengerti bahwa saya perlu mengkonfigurasi browser (klien) untuk mengirim permintaan ke proxy. Proxy mengirim permintaan ke web (katakanlah itu proxy http). Proxy akan menerima jawabannya ... tetapi bagaimana proxy mengirim kembali permintaan ke browser (klien)?

Saya telah mencari proxy C # dan http di web tetapi belum menemukan sesuatu yang membuat saya mengerti cara kerjanya di belakang layar dengan benar. (Saya percaya saya tidak ingin proksi terbalik tetapi saya tidak yakin).

Apakah ada di antara Anda yang memiliki beberapa penjelasan atau informasi yang akan membuat saya melanjutkan proyek kecil ini?

Memperbarui

Inilah yang saya mengerti (lihat grafik di bawah).

Langkah 1 Saya mengonfigurasikan klien (browser) untuk semua permintaan agar dikirim ke 127.0.0.1 di port yang Proxy dengarkan. Dengan cara ini, permintaan tidak akan dikirim ke Internet secara langsung tetapi akan diproses oleh proxy.

Step2 Proxy melihat koneksi baru, membaca header HTTP dan melihat permintaan yang harus dijalankan. Dia mengeksekusi permintaan itu.

Step3 Proxy menerima jawaban dari permintaan. Sekarang dia harus mengirim jawaban dari web ke klien tapi bagaimana caranya ???

teks alternatif

Tautan yang bermanfaat

Mentalis Proxy : Saya telah menemukan proyek ini yang merupakan proxy (tetapi lebih banyak yang saya inginkan) Saya mungkin memeriksa sumbernya tetapi saya benar-benar menginginkan sesuatu yang mendasar untuk lebih memahami konsepnya.

ASP Proxy : Saya mungkin bisa mendapatkan beberapa informasi di sini juga.

Permintaan reflektor : Ini adalah contoh sederhana.

Berikut adalah Repositori Git Hub dengan Proxy Http Sederhana .

Patrick Desjardins
sumber
Saya tidak memiliki tangkapan layar 2008 di tahun 2015. Maaf.
Patrick Desjardins
Sebenarnya, archive.org memang memilikinya . Maaf mengganggu Anda.
Ilmari Karonen

Jawaban:

35

Anda dapat membangun satu dengan HttpListenerkelas untuk mendengarkan permintaan yang masuk dan HttpWebRequestkelas untuk menyampaikan permintaan.

Mark Cidade
sumber
Di mana saya menyampaikan? Bagaimana saya tahu di mana saya harus mengirim kembali informasi itu? Browser kirim ke memungkinkan mengatakan 127.0.0.1:9999 klien di 9999 mendapatkan permintaan dan mengirimkannya ke web. Dapatkan jawaban ... DARI apa yang klien lakukan? Kirim ke alamat apa?
Patrick Desjardins
2
Jika Anda menggunakan HttpListener, Anda cukup menulis respons ke HttpListener.GetContext (). Response.OutputStream. Tidak perlu peduli dengan alamatnya.
OregonGhost
Menarik, saya akan periksa dengan cara ini.
Patrick Desjardins
8
Saya tidak akan menggunakan HttpListener untuk ini. Alih-alih, buat aplikasi ASP.NET dan host di dalam IIS. Saat menggunakan HttpListener, Anda menyerah model proses yang disediakan oleh IIS. Ini berarti Anda kehilangan hal-hal seperti manajemen proses (startup, deteksi kegagalan, daur ulang), manajemen kumpulan benang, dll.
Mauricio Scheffer
2
Artinya, jika Anda berniat untuk menggunakannya untuk banyak komputer klien ... untuk mainan Proxy HttpListener ok ...
Mauricio Scheffer
94

Saya tidak akan menggunakan HttpListener atau sesuatu seperti itu, dengan cara itu Anda akan menemukan begitu banyak masalah.

Yang terpenting adalah dukungan yang sangat besar:

  • Proxy Keep-Alives
  • SSL tidak akan berfungsi (dengan cara yang benar, Anda akan mendapatkan popup)
  • Pustaka .NET secara ketat mengikuti RFC yang menyebabkan beberapa permintaan gagal (meskipun IE, FF dan peramban lain di dunia akan berfungsi.)

Yang perlu Anda lakukan adalah:

  • Dengarkan port TCP
  • Uraikan permintaan browser
  • Ekstrak Host terhubung ke host itu di level TCP
  • Teruskan semuanya bolak-balik kecuali jika Anda ingin menambahkan header khusus dll.

Saya menulis 2 proksi HTTP berbeda di .NET dengan persyaratan berbeda dan saya dapat memberi tahu Anda bahwa ini adalah cara terbaik untuk melakukannya.

Mentalis melakukan ini, tetapi kode mereka adalah "delegate spaghetti", lebih buruk daripada GoTo :)

dr. jahat
sumber
1
Kelas apa yang Anda gunakan untuk koneksi TCP?
Cameron
8
@cameron TCPListener dan SslStream.
dr. evil
2
Bisakah Anda berbagi pengalaman Anda tentang mengapa HTTPS tidak berfungsi?
Restuta
10
@Restuta agar SSL berfungsi, Anda harus meneruskan koneksi tanpa benar-benar menyentuhnya di tingkat TCP dan HttpListener tidak dapat melakukannya. Anda dapat membaca cara kerja SSL dan Anda akan melihatnya harus mengautentikasi ke server target. Jadi klien akan mencoba untuk terhubung ke google.com tetapi sebenarnya akan menghubungkan Httplistener Anda yang bukan google.com dan akan mendapatkan kesalahan ketidakcocokan cert dan karena pendengar Anda tidak akan menggunakan cert yang ditandatangani, akan mendapatkan sertifikat salah dll. Anda dapat memperbaiki dengan menginstal CA ke komputer yang akan digunakan klien. Ini solusi yang sangat kotor.
dr. evil
1
@ dr.evil: +++ 1 terima kasih atas tips yang luar biasa, tapi saya ingin tahu bagaimana mengirim kembali data ke klien (browser), katakanlah saya sudah TcpClient bagaimana saya harus mengirim respons kembali ke klien?
saber
26

Saya baru-baru ini menulis proksi ringan di c # .net menggunakan TcpListener dan TcpClient .

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

Ini mendukung HTTP aman dengan cara yang benar, mesin klien perlu memercayai sertifikat root yang digunakan oleh proxy. Juga mendukung relai WebSockets. Semua fitur HTTP 1.1 didukung kecuali pipelining. Pipelining tidak digunakan oleh sebagian besar browser modern. Juga mendukung otentikasi windows (polos, intisari).

Anda dapat menghubungkan aplikasi Anda dengan merujuk proyek dan kemudian melihat dan memodifikasi semua lalu lintas. (Permintaan dan tanggapan).

Sejauh kinerja, saya telah mengujinya di komputer saya dan bekerja tanpa penundaan yang nyata.

justcoding121
sumber
dan masih dipertahankan pada tahun 2020, terima kasih telah berbagi :)
Mark Adamson
20

Proxy dapat bekerja dengan cara berikut.

Langkah 1, konfigurasikan klien untuk menggunakan proxyHost: proxyPort.

Proxy adalah server TCP yang mendengarkan pada proxyHost: proxyPort. Browser membuka koneksi dengan Proxy dan mengirimkan permintaan Http. Proxy mem-parsing permintaan ini dan mencoba mendeteksi tajuk "Host". Header ini akan memberi tahu Proxy tempat membuka koneksi.

Langkah 2: Proxy membuka koneksi ke alamat yang ditentukan dalam header "Host". Kemudian mengirimkan permintaan HTTP ke server jauh itu. Membaca respons.

Langkah 3: Setelah respons dibaca dari server HTTP jauh, Proxy mengirimkan respons melalui koneksi TCP yang dibuka sebelumnya dengan browser.

Secara skematis akan terlihat seperti ini:

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
Vadym Stetsiak
sumber
14

Jika Anda hanya ingin mencegat lalu lintas, Anda bisa menggunakan inti fiddler untuk membuat proksi ...

http://fiddler.wikidot.com/fiddlercore

jalankan fiddler terlebih dahulu dengan UI untuk melihat fungsinya, itu adalah proxy yang memungkinkan Anda untuk men-debug lalu lintas http / https. Itu ditulis dalam c # dan memiliki inti yang dapat Anda bangun ke dalam aplikasi Anda sendiri.

Perlu diingat FiddlerCore tidak gratis untuk aplikasi komersial.

Dean North
sumber
6

Setuju untuk melakukan kejahatan jika Anda menggunakan HTTPListener, Anda akan memiliki banyak masalah, Anda harus mengurai permintaan dan akan bertunangan dengan header dan ...

  1. Gunakan pendengar tcp untuk mendengarkan permintaan browser
  2. parsing hanya baris pertama dari permintaan dan dapatkan domain host dan port untuk terhubung
  3. kirim permintaan mentah yang tepat ke host yang ditemukan di baris pertama permintaan browser
  4. menerima data dari situs target (Saya punya masalah di bagian ini)
  5. mengirim data persis yang diterima dari host ke browser

Anda tahu Anda bahkan tidak perlu tahu apa yang ada dalam permintaan browser dan menguraikannya, hanya dapatkan alamat situs target dari baris pertama, baris pertama biasanya menyukai ini. Dapatkan http://google.com HTTP1.1 atau CONNECT facebook.com: 443 (ini untuk permintaan ssl)

Alireza Rinan
sumber
5

Socks4 adalah protokol yang sangat sederhana untuk diimplementasikan. Anda mendengarkan koneksi awal, menyambungkan ke host / port yang diminta oleh klien, mengirim kode sukses ke klien kemudian meneruskan aliran keluar dan masuk melintasi soket.

Jika Anda menggunakan HTTP, Anda harus membaca dan mungkin mengatur / menghapus beberapa tajuk HTTP sehingga sedikit lebih banyak pekerjaan.

Jika saya ingat dengan benar, SSL akan bekerja di proxy HTTP dan Socks. Untuk proksi HTTP, Anda menerapkan kata kerja CONNECT, yang berfungsi seperti socks4 seperti yang dijelaskan di atas, kemudian klien membuka koneksi SSL melintasi aliran tcp yang diproksi.

CM
sumber
2

Browser terhubung ke proxy sehingga data yang didapat proxy dari server web hanya dikirim melalui koneksi yang sama seperti yang diinisiasi browser ke proxy.

Stephen Caldwell
sumber
2

Untuk apa nilainya, berikut ini adalah implementasi async sampel C # berdasarkan HttpListener dan HttpClient (Saya menggunakannya untuk dapat menghubungkan Chrome di perangkat Android ke IIS Express, itulah satu-satunya cara saya menemukan ...).

Dan Jika Anda memerlukan dukungan HTTPS, seharusnya tidak memerlukan lebih banyak kode, hanya konfigurasi sertifikat: Httplistener dengan dukungan 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;
    }
}
Simon Mourier
sumber