Como criar um proxy simples em c #?

143

Eu baixei o Privoxy há algumas semanas e, por diversão, fiquei curioso para saber como uma versão simples dele pode ser feita.

Entendo que preciso configurar o navegador (cliente) para enviar a solicitação ao proxy. O proxy envia a solicitação para a web (digamos que seja um proxy http). O proxy receberá a resposta ... mas como o proxy pode enviar de volta a solicitação ao navegador (cliente)?

Eu pesquisei na Web por proxy C # e http, mas não encontrei algo que me permita entender como funciona corretamente nos bastidores. (Acredito que não quero um proxy reverso, mas não tenho certeza).

Algum de vocês tem alguma explicação ou alguma informação que me permita continuar este pequeno projeto?

Atualizar

É isso que eu entendo (veja o gráfico abaixo).

Etapa 1 Eu configuro o cliente (navegador) para que toda solicitação seja enviada para 127.0.0.1 na porta que o Proxy escuta. Dessa forma, a solicitação não será enviada diretamente à Internet, mas será processada pelo proxy.

Etapa 2 O proxy vê uma nova conexão, lê o cabeçalho HTTP e vê a solicitação que ele deve executar. Ele executa a solicitação.

Etapa 3 O proxy recebe uma resposta da solicitação. Agora ele deve enviar a resposta da web para o cliente, mas como ???

texto alternativo

Link útil

Mentalis Proxy : Encontrei este projeto que é um proxy (mas mais do que gostaria). Posso verificar a fonte, mas realmente queria algo básico para entender mais o conceito.

Proxy ASP : Talvez eu também consiga obter algumas informações por aqui.

Solicitar refletor : Este é um exemplo simples.

Aqui está um repositório do Git Hub com um proxy HTTP simples .

Patrick Desjardins
fonte
Não tenho uma captura de tela de 2008 em 2015. Desculpe.
Patrick Desjardins
Na verdade, verifica-se que o archive.org possui . Desculpe incomodá-lo.
Ilmari Karonen

Respostas:

35

Você pode criar um com a HttpListenerclasse para escutar solicitações recebidas e a HttpWebRequestclasse para retransmitir as solicitações.

Mark Cidade
fonte
Onde eu retransmito? Como posso saber para onde enviar as informações? O navegador envia para lets 127.0.0.1:9999, o cliente em 9999 obtém a solicitação e a envia para a web. Obtenha uma resposta ... DO QUE o cliente faz? Enviar para qual endereço?
Patrick Desjardins
2
Se você estiver usando HttpListener, basta escrever a resposta em HttpListener.GetContext (). Response.OutputStream. Não há necessidade de cuidar do endereço.
OregonGhost 22/10/08
Interessante, vou verificar dessa maneira.
Patrick Desjardins
8
Eu não usaria HttpListener para isso. Em vez disso, crie um aplicativo ASP.NET e hospede-o no IIS. Ao usar o HttpListener, você está desistindo do modelo de processo fornecido pelo IIS. Isso significa que você perde coisas como gerenciamento de processos (inicialização, detecção de falhas, reciclagem), gerenciamento de pool de threads, etc.
Mauricio Scheffer
2
Isto é, se você pretende usá-lo para muitos computadores clientes ... para um proxy de brinquedo HttpListener é ok ...
Mauricio Scheffer
94

Eu não usaria o HttpListener ou algo assim, dessa maneira você encontrará tantos problemas.

Mais importante ainda, será uma grande dor apoiar:

  • Manter-vivo de proxy
  • O SSL não funcionará (da maneira correta, você receberá pop-ups)
  • As bibliotecas .NET seguem rigorosamente as RFCs, o que causa a falha de algumas solicitações (mesmo que o IE, FF e qualquer outro navegador do mundo funcione.)

O que você precisa fazer é:

  • Ouça uma porta TCP
  • Analisar a solicitação do navegador
  • Extrair host conectar-se a esse host no nível TCP
  • Encaminhe tudo para frente e para trás, a menos que você queira adicionar cabeçalhos personalizados etc.

Escrevi 2 proxies HTTP diferentes no .NET com requisitos diferentes e posso dizer que essa é a melhor maneira de fazer isso.

Mentalis fazendo isso, mas o código deles é "delegar espaguete", pior que o GoTo :)

dr. mal
fonte
1
Quais classes você usou para as conexões TCP?
Cameron
8
@cameron TCPListener e SslStream.
dr. mal
2
Você poderia compartilhar sua experiência sobre por que o HTTPS não funciona?
Restuta
10
@Restuta para que o SSL funcione, você deve encaminhar a conexão sem realmente tocá-la no nível TCP e o HttpListener não pode fazer isso. Você pode ler como o SSL funciona e verá que é necessário se autenticar no servidor de destino. Portanto, o cliente tentará se conectar ao google.com mas na verdade conectará seu Httplistener que não é google.com e receberá um erro de incompatibilidade de certificado e, como o ouvinte não usará o certificado assinado, receberá um certificado incorreto etc. Você pode corrigir instalando uma CA no computador que o cliente usará. É uma solução bastante suja.
dr. mal
1
@ dr.evil: +++ 1 Obrigado por dicas incríveis, mas estou curioso para enviar dados ao cliente (navegador), digamos que eu tenha o TcpClient, como devo enviar uma resposta ao cliente?
Sabre
26

Eu escrevi recentemente um proxy leve em c # .net usando TcpListener e TcpClient .

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

Ele suporta HTTP seguro da maneira correta, a máquina cliente precisa confiar no certificado raiz usado pelo proxy. Também suporta retransmissão WebSockets. Todos os recursos do HTTP 1.1 são suportados, exceto o pipelining. O pipelining não é usado pela maioria dos navegadores modernos. Também suporta autenticação do Windows (simples, resumo).

Você pode conectar seu aplicativo fazendo referência ao projeto e, em seguida, ver e modificar todo o tráfego. (Pedido e resposta).

Quanto ao desempenho, testei-o na minha máquina e funciona sem nenhum atraso perceptível.

justcoding121
fonte
e ainda mantida em 2020, obrigado por compartilhar :)
Mark Adamson
20

O proxy pode funcionar da seguinte maneira.

Etapa 1, configure o cliente para usar proxyHost: proxyPort.

Proxy é um servidor TCP que está ouvindo no proxyHost: proxyPort. O navegador abre a conexão com o Proxy e envia a solicitação HTTP. O proxy analisa essa solicitação e tenta detectar o cabeçalho "Host". Este cabeçalho dirá ao Proxy onde abrir a conexão.

Etapa 2: o proxy abre a conexão com o endereço especificado no cabeçalho "Host". Em seguida, ele envia a solicitação HTTP para esse servidor remoto. Lê a resposta.

Etapa 3: Após a resposta da leitura do servidor HTTP remoto, o Proxy envia a resposta por meio de uma conexão TCP aberta anteriormente com o navegador.

Esquematicamente, será assim:

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

Se você apenas deseja interceptar o tráfego, pode usar o núcleo do violinista para criar um proxy ...

http://fiddler.wikidot.com/fiddlercore

execute o violinista primeiro com a interface do usuário para ver o que faz, é um proxy que permite depurar o tráfego http / https. Está escrito em c # e possui um núcleo que você pode criar em seus próprios aplicativos.

Lembre-se de que o FiddlerCore não é gratuito para aplicativos comerciais.

Dean North
fonte
6

Concorde com o dr evil se você usar o HTTPListener, terá muitos problemas, terá que analisar solicitações e estará envolvido em cabeçalhos e ...

  1. Use o ouvinte tcp para ouvir solicitações do navegador
  2. analise apenas a primeira linha da solicitação e faça com que o domínio e a porta do host se conectem
  3. envie a solicitação bruta exata para o host encontrado na primeira linha de solicitação do navegador
  4. receber os dados do site de destino (tenho problemas nesta seção)
  5. envie os dados exatos recebidos do host para o navegador

você perceber que não precisa nem saber o que está na solicitação do navegador e analisá-lo, apenas obtenha o endereço do site de destino na primeira linha. A primeira linha geralmente gosta desse GET http://google.com HTTP1.1 ou CONNECT facebook.com: 443 (isto é para solicitações de SSL)

Alireza Rinan
fonte
5

O Socks4 é um protocolo muito simples de implementar. Você ouve a conexão inicial, conecta-se ao host / porta solicitada pelo cliente, envia o código de sucesso ao cliente e encaminha os fluxos de saída e entrada pelos soquetes.

Se você optar pelo HTTP, terá que ler e possivelmente definir / remover alguns cabeçalhos HTTP, para que isso funcione um pouco mais.

Se bem me lembro, o SSL funcionará nos proxies HTTP e Socks. Para um proxy HTTP, você implementa o verbo CONNECT, que funciona como o socks4, conforme descrito acima, e o cliente abre a conexão SSL no fluxo tcp proxy.

CM
fonte
2

O navegador está conectado ao proxy, para que os dados que o proxy obtém do servidor da Web sejam enviados apenas pela mesma conexão que o navegador iniciou no proxy.

Stephen Caldwell
fonte
2

Para o que vale a pena, aqui está uma implementação assíncrona de exemplo de C # baseada em HttpListener e HttpClient (eu uso para conectar o Chrome em dispositivos Android ao IIS Express, foi a única maneira que encontrei ...).

E se você precisar de suporte HTTPS, não deverá exigir mais código, apenas a configuração do certificado: Httplistener with HTTPS support

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