Jak stworzyć proste proxy w C #?

143

Pobrałem Privoxy kilka tygodni temu i dla zabawy byłem ciekaw, jak można zrobić prostą wersję.

Rozumiem, że muszę skonfigurować przeglądarkę (klienta), aby wysyłała żądanie do proxy. Serwer proxy wysyła żądanie do sieci (powiedzmy, że jest to serwer proxy http). Proxy otrzyma odpowiedź ... ale w jaki sposób proxy może odesłać żądanie do przeglądarki (klienta)?

Wyszukałem w Internecie C # i proxy HTTP, ale nie znalazłem czegoś, co pozwoliłoby mi zrozumieć, jak to działa poprawnie za kulisami. (Uważam, że nie chcę odwrotnego serwera proxy, ale nie jestem pewien).

Czy ktoś z was ma jakieś wyjaśnienia lub informacje, które pozwolą mi kontynuować ten mały projekt?

Aktualizacja

To właśnie rozumiem (patrz grafika poniżej).

Krok 1 Konfiguruję klienta (przeglądarkę) tak, aby wszystkie żądania były wysyłane do 127.0.0.1 na porcie nasłuchiwanym przez Proxy. W ten sposób żądanie nie zostanie wysłane bezpośrednio do Internetu, ale będzie przetwarzane przez proxy.

Krok 2 Proxy widzi nowe połączenie, czyta nagłówek HTTP i widzi żądanie, które musi wykonać. Wykonuje żądanie.

Krok 3 Proxy otrzymuje odpowiedź z żądania. Teraz musi wysłać odpowiedź z sieci do klienta, ale jak ???

tekst alternatywny

Przydatny link

Mentalis Proxy : Znalazłem ten projekt, który jest proxy (ale bardziej niż bym chciał). Mogę sprawdzić źródło, ale naprawdę chciałem czegoś podstawowego, aby lepiej zrozumieć koncepcję.

ASP Proxy : Mogę tu również uzyskać informacje.

Poproś o odbłyśnik : to jest prosty przykład.

Oto repozytorium Git Hub z prostym serwerem proxy HTTP .

Patrick Desjardins
źródło
Nie mam zrzutu ekranu z 2008 roku z 2015 roku. Przepraszamy.
Patrick Desjardins
Właściwie okazuje się, że archive.org ją ma . Przepraszam, że przeszkadzam.
Ilmari Karonen

Odpowiedzi:

35

Możesz zbudować HttpListenerklasę, która nasłuchuje żądań przychodzących i HttpWebRequestklasę, która będzie przekazywać żądania.

Mark Cidade
źródło
Gdzie mam przekazać? Skąd mam wiedzieć, gdzie odesłać informacje? Przeglądarka wysyła do 127.0.0.1:9999, aby klient pod numerem 9999 odebrał żądanie i wysłał je do sieci. Uzyskaj odpowiedź ... NIŻ to, co robi klient? Wysłać na jaki adres?
Patrick Desjardins,
2
Jeśli używasz HttpListener, po prostu napisz odpowiedź do HttpListener.GetContext (). Response.OutputStream. Nie musisz dbać o adres.
OregonGhost
Ciekawe, sprawdzę w ten sposób.
Patrick Desjardins
8
Nie użyłbym do tego HttpListener. Zamiast tego utwórz aplikację ASP.NET i hostuj ją w usługach IIS. Korzystając z HttpListener, rezygnujesz z modelu procesu udostępnianego przez usługi IIS. Oznacza to, że tracisz takie elementy, jak zarządzanie procesami (uruchamianie, wykrywanie awarii, recykling), zarządzanie pulą wątków itp.
Mauricio Scheffer
2
To znaczy, jeśli zamierzasz używać go na wielu komputerach klienckich ... dla zabawkowego proxy HttpListener jest w porządku ...
Mauricio Scheffer
93

Nie użyłbym HttpListener lub czegoś podobnego, w ten sposób napotkasz tak wiele problemów.

A co najważniejsze, wspieranie:

  • Proxy Keep-Alives
  • SSL nie działa (w prawidłowy sposób otrzymasz wyskakujące okienka)
  • Biblioteki .NET ściśle przestrzegają specyfikacji RFC, co powoduje, że niektóre żądania kończą się niepowodzeniem (nawet jeśli IE, FF i każda inna przeglądarka na świecie będą działać).

Musisz:

  • Nasłuchuj portu TCP
  • Przeanalizuj żądanie przeglądarki
  • Wyodrębnij połączenie hosta z tym hostem na poziomie TCP
  • Przesyłaj dalej wszystko tam iz powrotem, chyba że chcesz dodać niestandardowe nagłówki itp.

Napisałem 2 różne proxy HTTP w .NET z różnymi wymaganiami i mogę powiedzieć, że jest to najlepszy sposób na zrobienie tego.

Mentalis to robi, ale ich kod brzmi „deleguj spaghetti”, gorzej niż GoTo :)

dr. zło
źródło
1
Jakich klas używasz do połączeń TCP?
Cameron,
8
@cameron TCPListener i SslStream.
dr. zły
2
Czy mógłbyś podzielić się swoim doświadczeniem, dlaczego protokół HTTPS nie działa?
Restuta
10
@Restuta, aby SSL działał, powinieneś przekazywać połączenie bez dotykania go na poziomie TCP, a HttpListener nie może tego zrobić. Możesz przeczytać, jak działa SSL, a zobaczysz, że wymaga uwierzytelnienia na serwerze docelowym. Więc klient spróbuje połączyć się z google.com, ale w rzeczywistości połączy się z twoim Httplistener, który nie jest google.com i otrzyma błąd niezgodności certyfikatów, a ponieważ twój odbiornik nie będzie używał podpisanego certyfikatu, otrzyma nieprawidłowy certyfikat itp. Możesz naprawić instalując urząd certyfikacji na komputerze, którego klient będzie jednak używał. To dość brudne rozwiązanie.
dr. zły
1
@ dr.evil: +++ 1 dzięki za niesamowite wskazówki, ale jestem ciekawy, jak odesłać dane do klienta (przeglądarki), powiedzmy, że mam TcpClient, jak mam wysłać odpowiedź z powrotem do klienta?
szabla
26

Niedawno napisałem lekki serwer proxy w c # .net przy użyciu TcpListener i TcpClient .

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

Obsługuje bezpieczny HTTP we właściwy sposób, komputer kliencki musi ufać certyfikatowi głównemu używanemu przez proxy. Obsługuje również przekaźnik WebSockets. Obsługiwane są wszystkie funkcje protokołu HTTP 1.1 z wyjątkiem potokowania. W większości nowoczesnych przeglądarek i tak nie używa potoków. Obsługuje również uwierzytelnianie systemu Windows (zwykłe, skrót).

Możesz podłączyć swoją aplikację, odwołując się do projektu, a następnie przeglądać i modyfikować cały ruch. (Wniosek i odpowiedź).

Jeśli chodzi o wydajność, przetestowałem go na moim komputerze i działa bez zauważalnego opóźnienia.

justcoding121
źródło
i nadal utrzymywany w 2020 roku, dzięki za udostępnienie :)
Mark Adamson
19

Proxy może działać w następujący sposób.

Krok 1, skonfiguruj klienta do korzystania z proxyHost: proxyPort.

Proxy to serwer TCP, który nasłuchuje na proxyHost: proxyPort. Przeglądarka otwiera połączenie z Proxy i wysyła żądanie HTTP. Proxy analizuje to żądanie i próbuje wykryć nagłówek „Host”. Ten nagłówek powie Proxy, gdzie otworzyć połączenie.

Krok 2: Proxy otwiera połączenie z adresem podanym w nagłówku „Host”. Następnie wysyła żądanie HTTP do tego zdalnego serwera. Czyta odpowiedź.

Krok 3: Po odczytaniu odpowiedzi ze zdalnego serwera HTTP, Proxy wysyła odpowiedź przez wcześniej otwarte połączenie TCP z przeglądarką.

Schematycznie będzie wyglądać następująco:

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
źródło
14

Jeśli chcesz tylko przechwycić ruch, możesz użyć rdzenia skrzypka do utworzenia serwera proxy ...

http://fiddler.wikidot.com/fiddlercore

najpierw uruchom program Fiddler z interfejsem użytkownika, aby zobaczyć, co robi, jest to proxy, które umożliwia debugowanie ruchu http / https. Jest napisany w języku C # i ma rdzeń, który można wbudować we własne aplikacje.

Należy pamiętać, że FiddlerCore nie jest darmowy do zastosowań komercyjnych.

Dean North
źródło
5

Zgadzam się dr evil jeśli używasz HTTPListener, będziesz miał wiele problemów, będziesz musiał analizować żądania i będziesz zaangażowany w nagłówki i ...

  1. Użyj nasłuchiwania TCP, aby nasłuchiwać żądań przeglądarki
  2. analizuje tylko pierwszą linię żądania i uzyskuje domenę hosta i port do połączenia
  3. wyślij dokładne surowe żądanie do znalezionego hosta w pierwszym wierszu żądania przeglądarki
  4. odbierz dane ze strony docelowej (mam problem w tej sekcji)
  5. przesłać dokładne dane otrzymane od hosta do przeglądarki

widzisz, że nie musisz nawet wiedzieć, co jest w żądaniu przeglądarki i analizować go, tylko pobierz adres witryny docelowej z pierwszego wiersza, pierwszy wiersz zwykle lubi to GET http://google.com HTTP1.1 lub CONNECT facebook.com: 443 (dotyczy żądań SSL)

Alireza Rinan
źródło
4

Socks4 to bardzo prosty w implementacji protokół. Nasłuchujesz początkowego połączenia, łączysz się z hostem / portem, którego zażądał klient, wysyłasz kod sukcesu do klienta, a następnie przesyłasz dalej wychodzące i przychodzące strumienie przez gniazda.

Jeśli korzystasz z protokołu HTTP, będziesz musiał przeczytać i prawdopodobnie ustawić / usunąć niektóre nagłówki HTTP, więc to trochę więcej pracy.

Jeśli dobrze pamiętam, SSL będzie działał na serwerach proxy HTTP i Socks. W przypadku proxy HTTP zaimplementujesz czasownik CONNECT, który działa podobnie do socks4, jak opisano powyżej, a następnie klient otwiera połączenie SSL przez proxowany strumień tcp.

CM
źródło
2

Przeglądarka jest połączona z serwerem proxy, więc dane, które otrzymuje proxy z serwera WWW, są po prostu wysyłane za pośrednictwem tego samego połączenia, które przeglądarka zainicjowała do serwera proxy.

Stephen Caldwell
źródło
1

Warto, oto przykładowa implementacja asynchroniczna w C # oparta na HttpListener i HttpClient (używam jej do łączenia Chrome na urządzeniach z Androidem z IIS Express, to jedyny sposób, jaki znalazłem ...).

A jeśli potrzebujesz obsługi HTTPS, nie powinno wymagać więcej kodu, wystarczy konfiguracja certyfikatu: Httplistener z obsługą 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
źródło