Come creare un proxy semplice in C #?


143

Ho scaricato Privoxy poche settimane fa e per divertimento ero curioso di sapere come si può fare una versione semplice di esso.

Comprendo che devo configurare il browser (client) per inviare la richiesta al proxy. Il proxy invia la richiesta al web (supponiamo che sia un proxy http). Il proxy riceverà la risposta ... ma come può il proxy rispedire la richiesta al browser (client)?

Ho cercato sul web C # e proxy HTTP ma non ho trovato qualcosa che mi permetta di capire come funziona correttamente dietro la scena. (Credo di non voler un proxy inverso ma non ne sono sicuro).

Qualcuno di voi ha qualche spiegazione o alcune informazioni che mi permetteranno di continuare questo piccolo progetto?

Aggiornare

Questo è quello che ho capito (vedi grafico sotto).

Passaggio 1 Configuro il client (browser) affinché tutte le richieste vengano inviate a 127.0.0.1 sulla porta in ascolto del proxy. In questo modo, la richiesta non verrà inviata direttamente a Internet ma verrà elaborata dal proxy.

Step2 Il proxy vede una nuova connessione, legge l'intestazione HTTP e vede la richiesta che deve eseguire. Esegue la richiesta.

Passaggio 3 Il proxy riceve una risposta dalla richiesta. Ora deve inviare la risposta dal web al client ma come ???

testo alternativo

Link utile

Proxy Mentalis : ho trovato questo progetto che è un proxy (ma più che mi piacerebbe). Potrei controllare la fonte ma volevo davvero qualcosa di base per capire di più il concetto.

Proxy ASP : potrei essere in grado di ottenere alcune informazioni anche qui.

Richiedi riflettore : questo è un semplice esempio.

Ecco un repository Git Hub con un proxy HTTP semplice .


Non ho uno screenshot del 2008 nel 2015. Siamo spiacenti.
Patrick Desjardins,

In realtà, risulta che archive.org ce l'ha . Mi dispiace disturbarla.
Ilmari Karonen,

Risposte:


35

Puoi crearne uno con la HttpListenerclasse per ascoltare le richieste in arrivo e la HttpWebRequestclasse per inoltrare le richieste.


Dove devo inoltrare? Come posso sapere dove inviare le informazioni? Il browser invia a consente a 127.0.0.1:9999 di dire che il client a 9999 ottiene la richiesta e la invia al web. Ottieni una risposta ... CHE cosa fa il cliente? Invia a quale indirizzo?
Patrick Desjardins,

2
Se stai usando HttpListener, scrivi semplicemente la risposta a HttpListener.GetContext (). Response.OutputStream. Non è necessario occuparsi dell'indirizzo.
OregonGhost,

Interessante, controllerò in questo modo.
Patrick Desjardins,

8
Non userei HttpListener per questo. Invece, crea un'app ASP.NET e ospitala all'interno di IIS. Quando si utilizza HttpListener, si rinuncia al modello di processo fornito da IIS. Ciò significa che si perdono cose come la gestione dei processi (avvio, rilevamento guasti, riciclaggio), la gestione del pool di thread, ecc.
Mauricio Scheffer,

2
Cioè, se hai intenzione di usarlo per molti computer client ... per un proxy giocattolo HttpListener è ok ...
Mauricio Scheffer,

94

Non userei HttpListener o qualcosa del genere, in questo modo incontrerai così tanti problemi.

Soprattutto, sarà un grande dolore supportare:

  • Proxy Keep-Alives
  • SSL non funzionerà (in modo corretto, otterrai popup)
  • Le librerie .NET seguono rigorosamente le RFC che causano il fallimento di alcune richieste (anche se IE, FF e qualsiasi altro browser al mondo funzioneranno).

Quello che devi fare è:

  • Ascolta una porta TCP
  • Analizza la richiesta del browser
  • Estrai Host connettiti a quell'host a livello TCP
  • Inoltra tutto avanti e indietro a meno che tu non voglia aggiungere intestazioni personalizzate ecc.

Ho scritto 2 diversi proxy HTTP in .NET con requisiti diversi e posso dirti che questo è il modo migliore per farlo.

Mentalis lo fa, ma il loro codice è "delegate spaghetti", peggio di GoTo :)


1
Quali classi hai usato per le connessioni TCP?
Cameron,

8
@cameron TCPListener e SslStream.
dott. male

2
Potresti condividere la tua esperienza sul perché HTTPS non funzionerà?
Restuta,

10
@Restuta per il funzionamento di SSL dovresti inoltrare la connessione senza toccarla effettivamente a livello TCP e HttpListener non può farlo. Puoi leggere come funziona SSL e vedrai che richiede l'autenticazione sul server di destinazione. Quindi il client proverà a connettersi a google.com ma in realtà connetterà il tuo Httplistener che non è google.com e riceverà un errore di mancata corrispondenza del certificato e poiché il tuo ascoltatore non utilizzerà il certificato firmato, riceverà un certificato errato ecc. Puoi correggere installando una CA sul computer che il client utilizzerà comunque. È una soluzione piuttosto sporca.
dott. male il

1
@ dr.evil: +++ 1 grazie per suggerimenti incredibili, ma sono curioso di come inviare i dati al client (browser), diciamo che ho TcpClient come devo inviare la risposta al client?
sciabola

26

Di recente ho scritto un proxy leggero in c # .net usando TcpListener e TcpClient .

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

Supporta HTTP sicuro nel modo corretto, la macchina client deve fidarsi del certificato radice utilizzato dal proxy. Supporta anche il relay WebSocket. Tutte le funzionalità di HTTP 1.1 sono supportate ad eccezione del pipelining. Il pipelining non è comunque utilizzato dalla maggior parte dei browser moderni. Supporta anche l'autenticazione di Windows (semplice, digest).

È possibile collegare l'applicazione facendo riferimento al progetto, quindi vedere e modificare tutto il traffico. (Richiesta e risposta).

Per quanto riguarda le prestazioni, l'ho testato sulla mia macchina e funziona senza alcun ritardo evidente.


e ancora mantenuto nel 2020, grazie per la condivisione :)
Mark Adamson,

20

Il proxy può funzionare nel modo seguente.

Step1, configura il client per usare proxyHost: proxyPort.

Proxy è un server TCP in ascolto su proxyHost: proxyPort. Il browser apre la connessione con il proxy e invia la richiesta HTTP. Il proxy analizza questa richiesta e tenta di rilevare l'intestazione "Host". Questa intestazione dirà al proxy dove aprire la connessione.

Passaggio 2: il proxy apre la connessione all'indirizzo specificato nell'intestazione "Host". Quindi invia la richiesta HTTP a quel server remoto. Legge la risposta.

Passaggio 3: Dopo aver letto la risposta dal server HTTP remoto, il proxy invia la risposta tramite una connessione TCP aperta in precedenza con il browser.

Schematicamente sarà simile a questo:

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

Se stai solo cercando di intercettare il traffico, potresti utilizzare il nucleo di violinista per creare un proxy ...

http://fiddler.wikidot.com/fiddlercore

esegui prima il violinista con l'interfaccia utente per vedere cosa fa, è un proxy che ti consente di eseguire il debug del traffico http / https. È scritto in c # e ha un nucleo che puoi integrare nelle tue applicazioni.

Tieni presente che FiddlerCore non è gratuito per applicazioni commerciali.


6

Accetta il malefico se usi HTTPListener avrai molti problemi, dovrai analizzare le richieste e sarai coinvolto nelle intestazioni e ...

  1. Utilizzare il listener tcp per ascoltare le richieste del browser
  2. analizzare solo la prima riga della richiesta e ottenere il dominio host e la porta per connettersi
  3. invia l'esatta richiesta non elaborata all'host trovato sulla prima riga della richiesta del browser
  4. ricevere i dati dal sito di destinazione (ho un problema in questa sezione)
  5. invia i dati esatti ricevuti dall'host al browser

vedi che non devi nemmeno sapere cosa c'è nella richiesta del browser e analizzarlo, ottieni solo l'indirizzo del sito di destinazione dalla prima riga prima linea di solito piace questo GET http://google.com HTTP1.1 o CONNECT facebook.com: 443 (questo è per le richieste SSL)



5

Socks4 è un protocollo molto semplice da implementare. Ascolti la connessione iniziale, ti connetti all'host / porta richiesta dal client, invii il codice di successo al client, quindi inoltri i flussi in uscita e in entrata attraverso i socket.

Se vai con HTTP dovrai leggere e possibilmente impostare / rimuovere alcune intestazioni HTTP, quindi è un po 'più di lavoro.

Se ricordo bene, SSL funzionerà su proxy HTTP e Socks. Per un proxy HTTP si implementa il verbo CONNECT, che funziona in modo molto simile a socks4 come descritto sopra, quindi il client apre la connessione SSL attraverso il flusso tcp proxy.


2

Il browser è collegato al proxy in modo che i dati che il proxy ottiene dal server Web vengano inviati tramite la stessa connessione che il browser ha avviato al proxy.


2

Per quello che vale, ecco un'implementazione asincrona di esempio C # basata su HttpListener e HttpClient (lo uso per poter connettere Chrome nei dispositivi Android a IIS Express, è l'unico modo che ho trovato ...).

E se hai bisogno del supporto HTTPS, non dovrebbe richiedere più codice, ma solo la configurazione del certificato: Httplistener con supporto 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;
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.