¿Cómo crear un proxy simple en C #?

143

Hace unas semanas descargué Privoxy y, por diversión, tenía curiosidad por saber cómo se puede hacer una versión simple.

Entiendo que necesito configurar el navegador (cliente) para enviar la solicitud al proxy. El proxy envía la solicitud a la web (digamos que es un proxy http). El proxy recibirá la respuesta ... pero, ¿cómo puede enviar la solicitud al navegador (cliente)?

He buscado en la web C # y proxy HTTP, pero no he encontrado algo que me permita entender cómo funciona correctamente detrás de escena. (Creo que no quiero un proxy inverso pero no estoy seguro).

¿Alguno de ustedes tiene alguna explicación o alguna información que me permita continuar con este pequeño proyecto?

Actualizar

Esto es lo que entiendo (ver gráfico a continuación).

Paso 1 Configuro el cliente (navegador) para que todas las solicitudes se envíen a 127.0.0.1 en el puerto que escucha Proxy. De esta manera, la solicitud no se enviará a Internet directamente, sino que será procesada por el proxy.

Paso 2 El proxy ve una nueva conexión, lee el encabezado HTTP y ve la solicitud que debe ejecutar. Él ejecuta la solicitud.

Paso 3 El proxy recibe una respuesta de la solicitud. Ahora debe enviar la respuesta de la web al cliente, pero ¿cómo?

texto alternativo

Enlace útil

Proxy Mentalis : He encontrado este proyecto que es un proxy (pero más de lo que me gustaría). Podría verificar la fuente, pero realmente quería algo básico para entender más el concepto.

Proxy ASP : también podría obtener información aquí.

Solicitar reflector : este es un ejemplo simple.

Aquí hay un repositorio de Git Hub con un proxy HTTP simple .

Patrick Desjardins
fuente
No tengo una captura de pantalla de 2008 en 2015. Lo siento.
Patrick Desjardins
En realidad, resulta que archive.org lo tiene . Perdón por molestarte.
Ilmari Karonen

Respuestas:

35

Puede crear uno con la HttpListenerclase para escuchar las solicitudes entrantes y la HttpWebRequestclase para retransmitir las solicitudes.

Mark Cidade
fuente
¿Dónde retransmito? ¿Cómo puedo saber dónde enviar la información? El navegador envía a deja dicho 127.0.0.1:9999 el cliente en 9999 recibe la solicitud y la envió a la web. Obtenga una respuesta ... QUE lo que hace el cliente? ¿Enviar a qué dirección?
Patrick Desjardins
2
Si está utilizando HttpListener, simplemente escriba la respuesta a HttpListener.GetContext (). Response.OutputStream. No hay que preocuparse por la dirección.
OregonGhost
Interesante, lo comprobaré de esta manera.
Patrick Desjardins
8
No usaría HttpListener para esto. En su lugar, cree una aplicación ASP.NET y hospedela dentro de IIS. Cuando usa HttpListener, está renunciando al modelo de proceso proporcionado por IIS. Esto significa que pierde cosas como la administración de procesos (inicio, detección de fallas, reciclaje), administración de agrupaciones de hilos, etc.
Mauricio Scheffer
2
Es decir, si tiene la intención de usarlo para muchas computadoras cliente ... para un proxy de juguete, HttpListener está bien ...
Mauricio Scheffer
94

No usaría HttpListener o algo así, de esa manera te encontrarás con muchos problemas.

Lo más importante será un gran dolor de apoyo:

  • Proxy Keep-Alives
  • SSL no funcionará (de forma correcta, obtendrá ventanas emergentes)
  • Las bibliotecas .NET siguen estrictamente las RFC, lo que hace que algunas solicitudes fallen (a pesar de que IE, FF y cualquier otro navegador del mundo funcionará).

Lo que debes hacer es:

  • Escucha un puerto TCP
  • Analizar la solicitud del navegador
  • Extraer Host conectado a ese host en el nivel TCP
  • Reenvíe todo de un lado a otro a menos que desee agregar encabezados personalizados, etc.

Escribí 2 proxies HTTP diferentes en .NET con diferentes requisitos y puedo decirte que esta es la mejor manera de hacerlo.

Mental está haciendo esto, pero su código es "delegar espagueti", peor que GoTo :)

Dr. mal
fuente
1
¿Qué clase (s) usaste para las conexiones TCP?
Cameron
8
@cameron TCPListener y SslStream.
dr. mal
2
¿Podría compartir su experiencia sobre por qué HTTPS no funcionará?
Restuta
10
@Restuta para que SSL funcione, debe reenviar la conexión sin tocarla realmente en el nivel TCP y HttpListener no puede hacer eso. Puede leer cómo funciona SSL y verá que es necesario autenticarse en el servidor de destino. Por lo tanto, el cliente intentará conectarse a google.com, pero en realidad conectará su Httplistener, que no es google.com, y obtendrá un error de falta de coincidencia de cert y, dado que su oyente no utilizará un certificado firmado, obtendrá un cert incorrecto, etc. sin embargo, instalando una CA en la computadora que usará el cliente. Es una solución bastante sucia.
dr. mal
1
@ dr.evil: +++ 1 gracias por consejos sorprendentes, pero tengo curiosidad por cómo enviar datos al cliente (navegador), digamos que tengo TcpClient, ¿cómo debo enviar la respuesta al cliente?
saber
26

Recientemente he escrito un proxy ligero en c # .net usando TcpListener y TcpClient .

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

Es compatible con HTTP seguro de la manera correcta, la máquina del cliente necesita confiar en el certificado raíz utilizado por el proxy. También es compatible con la retransmisión de WebSockets. Todas las características de HTTP 1.1 son compatibles, excepto la canalización. La mayoría de los navegadores modernos no utilizan la canalización de todos modos. También es compatible con la autenticación de Windows (simple, resumen).

Puede conectar su aplicación haciendo referencia al proyecto y luego ver y modificar todo el tráfico. (Solicitud y respuesta).

En cuanto al rendimiento, lo he probado en mi máquina y funciona sin ningún retraso notable.

justcoding121
fuente
y aún se mantiene en 2020, gracias por compartir :)
Mark Adamson
20

Proxy puede funcionar de la siguiente manera.

Paso 1, configure el cliente para usar proxyHost: proxyPort.

Proxy es un servidor TCP que está escuchando en proxyHost: proxyPort. El navegador abre la conexión con Proxy y envía la solicitud Http. Proxy analiza esta solicitud e intenta detectar el encabezado "Host". Este encabezado le indicará a Proxy dónde abrir la conexión.

Paso 2: el proxy abre la conexión a la dirección especificada en el encabezado "Host". Luego envía una solicitud HTTP a ese servidor remoto. Lee la respuesta.

Paso 3: Después de leer la respuesta del servidor HTTP remoto, Proxy envía la respuesta a través de una conexión TCP abierta anterior con el navegador.

Esquemáticamente se verá así:

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

Si solo está buscando interceptar el tráfico, puede usar el núcleo de Fiddler para crear un proxy ...

http://fiddler.wikidot.com/fiddlercore

ejecute fiddler primero con la interfaz de usuario para ver qué hace, es un proxy que le permite depurar el tráfico http / https. Está escrito en C # y tiene un núcleo que puede construir en sus propias aplicaciones.

Tenga en cuenta que FiddlerCore no es gratuito para aplicaciones comerciales.

Dean North
fuente
6

Acepte dr evil si usa HTTPListener, tendrá muchos problemas, tendrá que analizar las solicitudes y se comprometerá con los encabezados y ...

  1. Use tcp listener para escuchar las solicitudes del navegador
  2. analizar solo la primera línea de la solicitud y obtener el dominio del host y el puerto para conectarse
  3. enviar la solicitud sin procesar exacta al host encontrado en la primera línea de solicitud del navegador
  4. recibir los datos del sitio de destino (tengo un problema en esta sección)
  5. enviar los datos exactos recibidos del host al navegador

ves que ni siquiera necesitas saber qué hay en la solicitud del navegador y analizarlo, solo obtén la dirección del sitio de destino de la primera línea, la primera línea generalmente le gusta GET http://google.com HTTP1.1 o CONNECT facebook.com: 443 (esto es para solicitudes SSL)

Alireza Rinan
fuente
5

Socks4 es un protocolo muy simple de implementar. Escucha la conexión inicial, se conecta al host / puerto solicitado por el cliente, envía el código de éxito al cliente y luego reenvía las secuencias entrantes y salientes a través de los sockets.

Si utiliza HTTP, tendrá que leer y posiblemente configurar / eliminar algunos encabezados HTTP, por lo que es un poco más de trabajo.

Si no recuerdo mal, SSL funcionará en los servidores proxy HTTP y Socks. Para un proxy HTTP, implementa el verbo CONNECT, que funciona de manera similar a los socks4 como se describió anteriormente, luego el cliente abre la conexión SSL a través del flujo tcp proxy.

CM
fuente
2

El navegador está conectado al proxy, por lo que los datos que el proxy obtiene del servidor web se envían a través de la misma conexión que el navegador inició con el proxy.

Stephen Caldwell
fuente
2

Para lo que vale, aquí hay una implementación asíncrona de muestra de C # basada en HttpListener y HttpClient (lo uso para poder conectar Chrome en dispositivos Android a IIS Express, esa es la única forma que encontré ...).

Y si necesita soporte HTTPS, no debería requerir más código, solo configuración de certificado: Httplistener con soporte 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
fuente