Как создать простой прокси в C #?

143

Я скачал Privoxy несколько недель назад, и мне было любопытно узнать, как сделать простую версию.

Я понимаю, что мне нужно настроить браузер (клиент) для отправки запроса на прокси. Прокси-сервер отправляет запрос в Интернет (скажем, это http-прокси). Прокси-сервер получит ответ ... но как прокси-сервер может отправить запрос обратно в браузер (клиент)?

Я ищу в Интернете прокси C # и http, но не нашел чего-то, что позволило бы мне понять, как это работает за кулисами правильно. (Я считаю, что я не хочу обратного прокси, но я не уверен).

Есть ли у вас какие-либо объяснения или информация, которая позволит мне продолжить этот небольшой проект?

Обновить

Это то, что я понимаю (см. Рисунок ниже).

Шаг 1 Я настраиваю клиент (браузер) для отправки всех запросов на 127.0.0.1 через порт, который прослушивает Прокси. Таким образом, запрос не будет отправлен напрямую в Интернет, а будет обработан прокси.

Шаг 2 Прокси видит новое соединение, читает заголовок HTTP и видит запрос, который он должен выполнить. Он выполняет запрос.

Шаг 3 Прокси-сервер получает ответ от запроса. Теперь он должен отправить ответ из Интернета клиенту, но как ???

альтернативный текст

Полезная ссылка

Mentalis Proxy : Я нашел этот проект, который является прокси (но больше, что я хотел бы). Я мог бы проверить источник, но я действительно хотел чего-то простого, чтобы лучше понять концепцию.

ASP Proxy : Я мог бы также получить некоторую информацию здесь.

Отражатель запроса : это простой пример.

Вот Git Hub Repository с простым Http-прокси .

Patrick Desjardins
источник
У меня нет скриншота 2008 года в 2015 году. Извините.
Патрик Дежарден
На самом деле, оказывается, что archive.org имеет его . Извините, что беспокою вас.
Илмари Каронен

Ответы:

35

Вы можете создать его с помощью HttpListenerкласса для прослушивания входящих запросов и HttpWebRequestкласса для ретрансляции запросов.

Марк Сидаде
источник
Где я могу передать? Как я могу узнать, куда отправить информацию? Браузер отправляет на указанный 127.0.0.1:9999 клиент в 9999 получает запрос и отправляет его в Интернет. Получите ответ ... Чем занимается клиент? Отправить на какой адрес?
Патрик Дежарден
2
Если вы используете HttpListener, вы просто пишете ответ в HttpListener.GetContext (). Response.OutputStream. Не нужно заботиться об адресе.
OregonGhost
Интересно, я проверю таким образом.
Патрик Дежарден
8
Я бы не использовал HttpListener для этого. Вместо этого создайте приложение ASP.NET и разместите его в IIS. При использовании HttpListener вы отказываетесь от модели процесса, предоставляемой IIS. Это означает, что вы теряете такие вещи, как управление процессами (запуск, обнаружение сбоев, утилизация), управление пулом потоков и т. Д.
Маурисио Шеффер
2
То есть, если вы намереваетесь использовать его для многих клиентских компьютеров ... для игрушечного прокси HttpListener в порядке ...
Маурисио Шеффер
94

Я бы не использовал HttpListener или что-то в этом роде, таким образом, вы столкнетесь с таким количеством проблем.

Самое главное, это будет огромная боль для поддержки:

  • Proxy Keep-Alives
  • SSL не будет работать (правильно, вы получите всплывающие окна)
  • Библиотеки .NET строго следуют RFC, что приводит к сбою некоторых запросов (даже если IE, FF и любой другой браузер в мире будут работать.)

Что вам нужно сделать, это:

  • Слушайте порт TCP
  • Разобрать запрос браузера
  • Извлечь узел подключиться к этому узлу на уровне TCP
  • Пересылать все назад и вперед, если вы не хотите добавлять собственные заголовки и т. Д.

Я написал 2 разных HTTP прокси в .NET с разными требованиями и могу вам сказать, что это лучший способ сделать это.

Mentalis делают это, но их код "делегат спагетти", хуже, чем GoTo :)

др. злой
источник
1
Какой класс (ы) вы использовали для соединений TCP?
Кэмерон
8
@cameron TCPListener и SslStream.
доктор зло
2
Не могли бы вы поделиться своим опытом о том, почему HTTPS не будет работать?
Рестута
10
@Restuta для работы SSL, вы должны переадресовать соединение, не касаясь его на уровне TCP, и HttpListener не может этого сделать. Вы можете прочитать, как работает SSL, и увидите, что он требует аутентификации на целевом сервере. Таким образом, клиент попытается подключиться к google.com, но на самом деле подключит ваш Httplistener, который не является google.com, и получит ошибку несоответствия сертификата, а так как ваш слушатель не будет использовать подписанный сертификат, получит неправильный сертификат и т. Д. Вы можете исправить это путем установки CA на компьютер, который клиент будет использовать, хотя. Это довольно грязное решение.
доктор зло
1
@ dr.evil: +++ 1 спасибо за удивительные советы, но мне интересно, как отправить данные обратно клиенту (браузеру), скажем, у меня TcpClient, как мне отправить ответ клиенту?
сабля
26

Недавно я написал облегченный прокси в c # .net, используя TcpListener и TcpClient .

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

Он поддерживает безопасный HTTP правильно, клиентский компьютер должен доверять корневому сертификату, используемому прокси. Также поддерживает ретрансляцию WebSockets. Поддерживаются все функции HTTP 1.1, кроме конвейерной. В любом случае конвейерная обработка не используется большинством современных браузеров. Также поддерживает проверку подлинности Windows (обычный, дайджест).

Вы можете подключить свое приложение, ссылаясь на проект, а затем просматривать и изменять весь трафик. (Запрос и ответ).

Что касается производительности, я проверил ее на своей машине и работает без каких-либо заметных задержек.

justcoding121
источник
и до сих пор поддерживается в 2020 году, спасибо за обмен :)
Марк Адамсон
20

Прокси может работать следующим образом.

Шаг 1, настройте клиент для использования proxyHost: proxyPort.

Прокси-сервер - это TCP-сервер, который прослушивает proxyHost: proxyPort. Браузер открывает соединение с прокси и отправляет Http-запрос. Прокси анализирует этот запрос и пытается определить заголовок «Host». Этот заголовок скажет Прокси, где открыть соединение.

Шаг 2: Прокси открывает соединение по адресу, указанному в заголовке «Хост». Затем он отправляет HTTP-запрос на этот удаленный сервер. Читает ответ.

Шаг 3. После считывания ответа с удаленного HTTP-сервера Proxy отправляет ответ через ранее открытое TCP-соединение с браузером.

Схематически это будет выглядеть так:

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

Если вы просто хотите перехватить трафик, вы можете использовать ядро ​​Fiddler для создания прокси ...

http://fiddler.wikidot.com/fiddlercore

Сначала запустите fiddler с пользовательским интерфейсом, чтобы увидеть, что он делает, это прокси, который позволяет отлаживать трафик http / https. Он написан на C # и имеет ядро, которое вы можете встроить в свои собственные приложения.

Имейте в виду, FiddlerCore не является бесплатным для коммерческих приложений.

Дин Норт
источник
6

Согласитесь с dr evil, если вы будете использовать HTTPListener, у вас будет много проблем, вам придется анализировать запросы, и вы будете заняты заголовками и ...

  1. Используйте tcp listener для прослушивания запросов браузера
  2. парсит только первую строку запроса и получит домен хоста и порт для подключения
  3. отправьте точный необработанный запрос найденному хосту в первой строке запроса браузера
  4. получить данные с целевого сайта (у меня есть проблемы в этом разделе)
  5. отправить точные данные, полученные от хоста в браузер

вы видите, что вам даже не нужно знать, что находится в запросе браузера, и анализировать его, только получить адрес целевого сайта из первой строки, первой строке обычно нравится GET http://google.com HTTP1.1 или CONNECT facebook.com: 443 (это для запросов ssl)

Алиреза Ринан
источник
5

Socks4 - очень простой для реализации протокол. Вы прослушиваете начальное соединение, подключаетесь к хосту / порту, запрошенному клиентом, отправляете клиенту код успеха, а затем пересылаете исходящий и входящий потоки через сокеты.

Если вы используете HTTP, вам придется читать и, возможно, устанавливать / удалять некоторые заголовки HTTP, так что это немного больше работы.

Если я правильно помню, SSL будет работать через HTTP и Socks прокси. Для HTTP-прокси вы реализуете глагол CONNECT, который работает так же, как socks4, как описано выше, затем клиент открывает SSL-соединение через прокси-поток tcp.

СМ
источник
2

Браузер подключен к прокси, поэтому данные, которые прокси получает с веб-сервера, просто отправляются через то же соединение, которое браузер инициировал для прокси.

Стивен Колдуэлл
источник
2

Для этого стоит пример асинхронной реализации C # на основе HttpListener и HttpClient (я использую его для подключения Chrome на устройствах Android к IIS Express, это единственный способ, который я нашел ...).

И если вам нужна поддержка HTTPS, для этого не нужно больше кода, просто настройка сертификата: Httplistener с поддержкой 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;
    }
}
Саймон Мурье
источник