Wie erstelle ich einen einfachen Proxy in C #?

143

Ich habe Privoxy vor einigen Wochen heruntergeladen und war zum Spaß neugierig, wie eine einfache Version davon gemacht werden kann.

Ich verstehe, dass ich den Browser (Client) konfigurieren muss, um eine Anfrage an den Proxy zu senden. Der Proxy sendet die Anfrage an das Web (beispielsweise ein http-Proxy). Der Proxy erhält die Antwort ... aber wie kann der Proxy die Anfrage an den Browser (Client) zurücksenden?

Ich habe im Web nach C # und http-Proxy gesucht, aber nichts gefunden, was mich verstehen lässt, wie es hinter den Kulissen richtig funktioniert. (Ich glaube, ich möchte keinen Reverse-Proxy, bin mir aber nicht sicher).

Hat jemand von euch Erklärungen oder Informationen, mit denen ich dieses kleine Projekt fortsetzen kann?

Aktualisieren

Das verstehe ich (siehe Grafik unten).

Schritt 1 Ich konfiguriere den Client (Browser) so, dass alle Anforderungen an 127.0.0.1 an dem Port gesendet werden, den der Proxy abhört. Auf diese Weise wird die Anfrage nicht direkt an das Internet gesendet, sondern vom Proxy verarbeitet.

Schritt 2 Der Proxy sieht eine neue Verbindung, liest den HTTP-Header und sieht die Anforderung, die er ausführen muss. Er führt die Anfrage aus.

Schritt 3 Der Proxy erhält eine Antwort von der Anfrage. Jetzt muss er die Antwort aus dem Web an den Kunden senden, aber wie ???

Alt-Text

Nützlicher Link

Mentalis Proxy : Ich habe dieses Projekt gefunden, das ein Proxy ist (aber mehr, als ich möchte). Ich könnte die Quelle überprüfen, aber ich wollte wirklich etwas Grundlegendes, um das Konzept besser zu verstehen.

ASP-Proxy : Möglicherweise kann ich auch hier einige Informationen abrufen.

Reflektor anfordern : Dies ist ein einfaches Beispiel.

Hier ist ein Git Hub Repository mit einem einfachen HTTP-Proxy .

Patrick Desjardins
quelle
Ich habe keinen Screenshot von 2008 im Jahr 2015. Entschuldigung.
Patrick Desjardins
Tatsächlich stellt sich heraus, dass archive.org es hat . Entschuldige, dass ich dich belästige.
Ilmari Karonen

Antworten:

35

Sie können eine mit der HttpListenerKlasse erstellen , um auf eingehende Anforderungen zu warten, und die HttpWebRequestKlasse, um die Anforderungen weiterzuleiten.

Mark Cidade
quelle
Wo leite ich weiter? Wie kann ich wissen, wohin ich die Informationen zurücksenden soll? Der Browser sendet an lässt 127.0.0.1:9999 der Client bei 9999 die Anfrage erhalten und an das Web senden. Erhalten Sie eine Antwort ... ALS was der Kunde tut? An welche Adresse senden?
Patrick Desjardins
2
Wenn Sie HttpListener verwenden, schreiben Sie einfach die Antwort an HttpListener.GetContext (). Response.OutputStream. Die Adresse muss nicht gepflegt werden.
OregonGhost
Interessant, ich werde auf diese Weise überprüfen.
Patrick Desjardins
8
Ich würde HttpListener dafür nicht verwenden. Erstellen Sie stattdessen eine ASP.NET-App und hosten Sie sie in IIS. Wenn Sie HttpListener verwenden, geben Sie das von IIS bereitgestellte Prozessmodell auf. Dies bedeutet, dass Sie Dinge wie Prozessmanagement (Start, Fehlererkennung, Recycling), Thread-Pool-Management usw. Verlieren.
Mauricio Scheffer
2
Das heißt, wenn Sie beabsichtigen, es für viele Client-Computer zu verwenden ... für einen Spielzeug-Proxy ist HttpListener in Ordnung ...
Mauricio Scheffer
93

Ich würde HttpListener oder ähnliches nicht verwenden, auf diese Weise werden Sie auf so viele Probleme stoßen.

Vor allem wird es ein großer Schmerz sein, dies zu unterstützen:

  • Proxy Keep-Alives
  • SSL funktioniert nicht (korrekt erhalten Sie Popups)
  • .NET-Bibliotheken folgen strikt RFCs, was dazu führt, dass einige Anforderungen fehlschlagen (obwohl IE, FF und jeder andere Browser auf der Welt funktionieren).

Was Sie tun müssen, ist:

  • Hören Sie sich einen TCP-Port an
  • Analysieren Sie die Browseranforderung
  • Extrahieren Sie die Host-Verbindung zu diesem Host auf TCP-Ebene
  • Leiten Sie alles hin und her weiter, es sei denn, Sie möchten benutzerdefinierte Header usw. hinzufügen.

Ich habe 2 verschiedene HTTP-Proxys in .NET mit unterschiedlichen Anforderungen geschrieben und kann Ihnen sagen, dass dies der beste Weg ist, dies zu tun.

Mentalis macht das, aber ihr Code ist "Delegate Spaghetti", schlimmer als GoTo :)

DR. böse
quelle
1
Welche Klasse (n) haben Sie für die TCP-Verbindungen verwendet?
Cameron
8
@ Cameron TCPListener und SslStream.
dr. böse
2
Könnten Sie uns bitte mitteilen, warum HTTPS nicht funktioniert?
Restuta
10
@Restuta damit SSL funktioniert, sollten Sie die Verbindung weiterleiten, ohne sie auf TCP-Ebene zu berühren, und HttpListener kann das nicht. Sie können lesen, wie SSL funktioniert, und sehen, dass eine Authentifizierung beim Zielserver erforderlich ist. Der Client versucht also, eine Verbindung zu google.com herzustellen , verbindet jedoch tatsächlich Ihren Httplistener, der nicht google.com ist, und es wird ein Fehler bei der Nichtübereinstimmung von Zertifikaten angezeigt. Da Ihr Listener kein signiertes Zertifikat verwendet, erhält er ein falsches Zertifikat usw. Sie können das Problem beheben Dies geschieht durch die Installation einer Zertifizierungsstelle auf einem Computer, den der Client verwendet. Es ist eine ziemlich schmutzige Lösung.
dr. böse
1
@ dr.evil: +++ 1 Danke für die tollen Tipps, aber ich bin gespannt, wie ich Daten an den Client (Browser) zurücksenden kann. Nehmen wir an, ich habe TcpClient. Wie soll ich eine Antwort an den Client zurücksenden?
Säbel
26

Ich habe kürzlich einen leichten Proxy in c # .net mit TcpListener und TcpClient geschrieben .

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

Es unterstützt sicheres HTTP auf die richtige Weise. Der Client-Computer muss dem vom Proxy verwendeten Stammzertifikat vertrauen. Unterstützt auch WebSockets Relay. Alle Funktionen von HTTP 1.1 werden mit Ausnahme von Pipelining unterstützt. Pipelining wird von den meisten modernen Browsern sowieso nicht verwendet. Unterstützt auch die Windows-Authentifizierung (Plain, Digest).

Sie können Ihre Anwendung verbinden, indem Sie auf das Projekt verweisen und dann den gesamten Datenverkehr anzeigen und ändern. (Anfrage und Antwort).

Was die Leistung angeht, habe ich es auf meinem Computer getestet und arbeite ohne merkliche Verzögerung.

justcoding121
quelle
und noch im Jahr 2020 gepflegt, danke fürs Teilen :)
Mark Adamson
19

Proxy kann folgendermaßen funktionieren.

Schritt 1: Konfigurieren Sie den Client für die Verwendung von proxyHost: proxyPort.

Proxy ist ein TCP-Server, der proxyHost: proxyPort überwacht. Der Browser öffnet die Verbindung mit dem Proxy und sendet eine HTTP-Anfrage. Der Proxy analysiert diese Anforderung und versucht, den "Host" -Header zu erkennen. Dieser Header teilt Proxy mit, wo die Verbindung geöffnet werden soll.

Schritt 2: Proxy öffnet die Verbindung zu der im "Host" -Header angegebenen Adresse. Anschließend wird eine HTTP-Anforderung an diesen Remote-Server gesendet. Liest die Antwort.

Schritt 3: Nachdem die Antwort vom Remote-HTTP-Server gelesen wurde, sendet der Proxy die Antwort über eine zuvor geöffnete TCP-Verbindung mit dem Browser.

Schematisch sieht es so aus:

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
quelle
14

Wenn Sie nur den Datenverkehr abfangen möchten, können Sie den Fiddler-Kern verwenden, um einen Proxy zu erstellen ...

http://fiddler.wikidot.com/fiddlercore

Führen Sie Fiddler zuerst mit der Benutzeroberfläche aus, um zu sehen, was es tut. Es ist ein Proxy, mit dem Sie den http / https-Verkehr debuggen können. Es ist in c # geschrieben und hat einen Kern, den Sie in Ihre eigenen Anwendungen einbauen können.

Beachten Sie, dass FiddlerCore für kommerzielle Anwendungen nicht kostenlos ist.

Dean North
quelle
5

Stimmen Sie dr böse zu, wenn Sie HTTPListener verwenden, werden Sie viele Probleme haben, Sie müssen Anfragen analysieren und werden mit Headern und ...

  1. Verwenden Sie den TCP-Listener, um Browseranforderungen abzuhören
  2. Analysieren Sie nur die erste Zeile der Anforderung und lassen Sie die Hostdomäne und den Port eine Verbindung herstellen
  3. Senden Sie die genaue Rohanforderung in der ersten Zeile der Browseranforderung an den gefundenen Host
  4. Empfangen Sie die Daten von der Zielwebsite (ich habe ein Problem in diesem Abschnitt)
  5. Senden Sie die genauen vom Host empfangenen Daten an den Browser

Sie sehen, dass Sie nicht einmal wissen müssen, was in der Browseranforderung enthalten ist, und sie analysieren müssen. Holen Sie sich nur die Adresse der Zielwebsite aus der ersten Zeile. Die erste Zeile gefällt normalerweise. GET http://google.com HTTP1.1 oder CONNECT facebook.com: 443 (dies ist für SSL-Anfragen)

Alireza Rinan
quelle
4

Socks4 ist ein sehr einfach zu implementierendes Protokoll. Sie warten auf die erste Verbindung, stellen eine Verbindung zu dem vom Client angeforderten Host / Port her, senden den Erfolgscode an den Client und leiten die ausgehenden und eingehenden Streams über Sockets weiter.

Wenn Sie sich für HTTP entscheiden, müssen Sie einige HTTP-Header lesen und möglicherweise festlegen / entfernen, damit dies etwas mehr Arbeit bedeutet.

Wenn ich mich richtig erinnere, funktioniert SSL über HTTP- und Socks-Proxys hinweg. Für einen HTTP-Proxy implementieren Sie das Verb CONNECT, das ähnlich wie die oben beschriebenen socks4 funktioniert. Anschließend öffnet der Client die SSL-Verbindung über den Proxy-TCP-Stream.

CM
quelle
2

Der Browser ist mit dem Proxy verbunden, sodass die Daten, die der Proxy vom Webserver erhält, nur über dieselbe Verbindung gesendet werden, die der Browser an den Proxy initiiert hat.

Stephen Caldwell
quelle
1

Hier ist eine asynchrone C # -Beispielimplementierung, die auf HttpListener und HttpClient basiert (ich verwende sie, um Chrome auf Android-Geräten mit IIS Express zu verbinden, das ist der einzige Weg, den ich gefunden habe ...).

Und wenn Sie HTTPS-Unterstützung benötigen, sollte nicht mehr Code erforderlich sein, sondern nur die Zertifikatskonfiguration: Httplistener mit HTTPS-Unterstützung

// 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
quelle