Comment créer un proxy simple en C #?

143

J'ai téléchargé Privoxy il y a quelques semaines et pour le plaisir, j'étais curieux de savoir comment une version simple de celui-ci peut être réalisée.

Je comprends que je dois configurer le navigateur (client) pour envoyer la demande au proxy. Le proxy envoie la requête au Web (disons que c'est un proxy http). Le proxy recevra la réponse ... mais comment le proxy peut-il renvoyer la demande au navigateur (client)?

J'ai effectué une recherche sur le Web pour les proxy C # et http, mais je n'ai pas trouvé quelque chose qui me permette de comprendre comment cela fonctionne correctement dans les coulisses. (Je crois que je ne veux pas de proxy inverse mais je ne suis pas sûr).

L'un de vous a-t-il des explications ou des informations qui me permettront de poursuivre ce petit projet?

Mettre à jour

C'est ce que je comprends (voir graphique ci-dessous).

Étape 1 Je configure le client (navigateur) pour que toutes les demandes soient envoyées à 127.0.0.1 sur le port d'écoute du proxy. De cette façon, la demande ne sera pas envoyée directement à Internet mais sera traitée par le proxy.

Étape 2 Le proxy voit une nouvelle connexion, lit l'en-tête HTTP et voit la requête qu'il doit exécuter. Il exécute la demande.

Étape 3 Le mandataire reçoit une réponse de la demande. Maintenant, il doit envoyer la réponse du Web au client mais comment ???

texte alternatif

Lien utile

Mentalis Proxy : J'ai trouvé ce projet qui est un proxy (mais plus que j'aimerais). Je pourrais vérifier la source mais je voulais vraiment quelque chose de basique pour mieux comprendre le concept.

Proxy ASP : Je pourrais peut-être obtenir des informations ici aussi.

Réflecteur de demande : Ceci est un exemple simple.

Voici un référentiel Git Hub avec un proxy Http simple .

Patrick Desjardins
la source
Je n'ai pas de capture d'écran de 2008 en 2015. Désolé.
Patrick Desjardins
En fait, il s'avère que archive.org l'a . Désolé de vous déranger.
Ilmari Karonen

Réponses:

35

Vous pouvez en créer un avec la HttpListenerclasse pour écouter les demandes entrantes et la HttpWebRequestclasse pour relayer les demandes.

Mark Cidade
la source
Où dois-je relayer? Comment puis-je savoir où renvoyer les informations? Le navigateur envoie à permet à 127.0.0.1:9999 que le client à 9999 reçoive la demande et l'envoie sur le Web. Obtenez une réponse ... QUE ce que fait le client? Envoyer à quelle adresse?
Patrick Desjardins
2
Si vous utilisez HttpListener, vous écrivez simplement la réponse dans HttpListener.GetContext (). Response.OutputStream. Pas besoin de s'occuper de l'adresse.
OregonGhost
Intéressant, je vais vérifier de cette façon.
Patrick Desjardins
8
Je n'utiliserais pas HttpListener pour cela. Au lieu de cela, créez une application ASP.NET et hébergez-la dans IIS. Lorsque vous utilisez HttpListener, vous abandonnez le modèle de processus fourni par IIS. Cela signifie que vous perdez des éléments tels que la gestion des processus (démarrage, détection des échecs, recyclage), la gestion du pool de threads, etc.
Mauricio Scheffer
2
Autrement dit, si vous avez l'intention de l'utiliser pour de nombreux ordinateurs clients ... pour un proxy de jouet HttpListener est ok ...
Mauricio Scheffer
94

Je n'utiliserais pas HttpListener ou quelque chose comme ça, de cette façon vous rencontrerez tant de problèmes.

Plus important encore, ce sera une énorme douleur à soutenir:

  • Proxy Keep-Alives
  • SSL ne fonctionnera pas (de manière correcte, vous obtiendrez des fenêtres contextuelles)
  • Les bibliothèques .NET suivent strictement les RFC, ce qui entraîne l'échec de certaines requêtes (même si IE, FF et tout autre navigateur dans le monde fonctionnent.)

Ce que vous devez faire est:

  • Écouter un port TCP
  • Analyser la demande du navigateur
  • Extraire l'hôte se connecter à cet hôte au niveau TCP
  • Transférez tout dans les deux sens sauf si vous souhaitez ajouter des en-têtes personnalisés, etc.

J'ai écrit 2 proxys HTTP différents en .NET avec des exigences différentes et je peux vous dire que c'est la meilleure façon de le faire.

Mentalis fait cela, mais leur code est "déléguer spaghetti", pire que GoTo :)

dr. mal
la source
1
Quelle (s) classe (s) avez-vous utilisé pour les connexions TCP?
Cameron
8
@cameron TCPListener et SslStream.
dr. evil
2
Pourriez-vous s'il vous plaît partager votre expérience sur les raisons pour lesquelles HTTPS ne fonctionne pas?
Restuta
10
@Restuta pour que SSL fonctionne, vous devez transférer la connexion sans la toucher au niveau TCP et HttpListener ne peut pas le faire. Vous pouvez lire le fonctionnement de SSL et vous verrez qu'il est nécessaire de s'authentifier auprès du serveur cible. Ainsi, le client essaiera de se connecter à google.com mais connectera en fait votre Httplistener qui n'est pas google.com et obtiendra une erreur de non-concordance de certificat et puisque votre auditeur n'utilisera pas de certificat signé, il obtiendra un certificat incorrect, etc. il en installant une autorité de certification sur l'ordinateur que le client utilisera cependant. C'est une solution assez sale.
dr. evil
1
@ dr.evil: +++ 1 merci pour des conseils incroyables, mais je suis curieux de savoir comment renvoyer des données au client (navigateur), disons que j'ai TcpClient, comment dois-je renvoyer la réponse au client?
sabre du
26

J'ai récemment écrit un proxy léger en c # .net en utilisant TcpListener et TcpClient .

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

Il prend en charge HTTP sécurisé de la bonne manière, la machine cliente doit faire confiance au certificat racine utilisé par le proxy. Prend également en charge le relais WebSockets. Toutes les fonctionnalités de HTTP 1.1 sont prises en charge à l'exception du pipelining. Le pipelining n'est de toute façon pas utilisé par la plupart des navigateurs modernes. Prend également en charge l'authentification Windows (simple, condensé).

Vous pouvez connecter votre application en référençant le projet, puis voir et modifier tout le trafic. (Demande et réponse).

En ce qui concerne les performances, je l'ai testé sur ma machine et fonctionne sans aucun retard notable.

justcoding121
la source
et toujours maintenu en 2020, merci pour le partage :)
Mark Adamson
20

Le proxy peut fonctionner de la manière suivante.

Étape 1, configurez le client pour utiliser proxyHost: proxyPort.

Proxy est un serveur TCP qui écoute sur proxyHost: proxyPort. Le navigateur ouvre la connexion avec le proxy et envoie une requête Http. Le proxy analyse cette demande et essaie de détecter l'en-tête "Host". Cet en-tête indiquera à Proxy où ouvrir la connexion.

Étape 2: Le proxy ouvre la connexion à l'adresse spécifiée dans l'en-tête "Host". Ensuite, il envoie une requête HTTP à ce serveur distant. Lit la réponse.

Étape 3: Une fois la réponse lue à partir du serveur HTTP distant, le proxy envoie la réponse via une connexion TCP ouverte précédemment avec le navigateur.

Schématiquement, cela ressemblera à ceci:

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
la source
14

Si vous cherchez simplement à intercepter le trafic, vous pouvez utiliser le noyau Fiddler pour créer un proxy ...

http://fiddler.wikidot.com/fiddlercore

lancez d'abord fiddler avec l'interface utilisateur pour voir ce qu'il fait, c'est un proxy qui vous permet de déboguer le trafic http / https. Il est écrit en c # et possède un noyau que vous pouvez intégrer à vos propres applications.

Gardez à l'esprit que FiddlerCore n'est pas gratuit pour les applications commerciales.

Dean North
la source
6

Acceptez de dr evil si vous utilisez HTTPListener vous aurez de nombreux problèmes, vous devrez analyser les demandes et serez engagé dans les en-têtes et ...

  1. Utilisez tcp listener pour écouter les requêtes du navigateur
  2. analyser uniquement la première ligne de la demande et obtenir le domaine hôte et le port pour se connecter
  3. envoyer la requête brute exacte à l'hôte trouvé sur la première ligne de la requête du navigateur
  4. recevoir les données du site cible (j'ai un problème dans cette section)
  5. envoyer les données exactes reçues de l'hôte au navigateur

vous voyez que vous n'avez même pas besoin de savoir ce qu'il y a dans la demande du navigateur et de l'analyser, n'obtenez l'adresse du site cible qu'à partir de la première ligne. La première ligne aime généralement ceci GET http://google.com HTTP1.1 ou CONNECT facebook.com: 443 (ceci est pour les requêtes SSL)

Alireza Rinan
la source
5

Socks4 est un protocole très simple à mettre en œuvre. Vous écoutez la connexion initiale, vous vous connectez à l'hôte / au port demandé par le client, envoyez le code de réussite au client, puis transférez les flux sortants et entrants sur les sockets.

Si vous utilisez HTTP, vous devrez lire et éventuellement définir / supprimer certains en-têtes HTTP, ce qui représente un peu plus de travail.

Si je me souviens bien, SSL fonctionnera sur les proxys HTTP et Socks. Pour un proxy HTTP, vous implémentez le verbe CONNECT, qui fonctionne un peu comme le socks4 comme décrit ci-dessus, puis le client ouvre la connexion SSL à travers le flux tcp proxy.

CM
la source
2

Le navigateur est connecté au proxy afin que les données que le proxy obtient du serveur Web soient simplement envoyées via la même connexion que le navigateur a initiée au proxy.

Stephen Caldwell
la source
2

Pour ce que ça vaut, voici un exemple d'implémentation asynchrone C # basé sur HttpListener et HttpClient (je l'utilise pour pouvoir connecter Chrome dans les appareils Android à IIS Express, c'est le seul moyen que j'ai trouvé ...).

Et si vous avez besoin du support HTTPS, cela ne devrait pas nécessiter plus de code, juste une configuration de certificat: Httplistener avec support 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
la source