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

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

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

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

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

Обновить

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

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

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

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

alt text

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

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

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

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

Вот репозиторий Git Hub с простым Http-прокси.

Ответ 1

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

Ответ 2

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

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

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

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

  • Прослушать порт TCP
  • Анализ запроса браузера
  • Extract Host подключиться к этому хосту на уровне TCP
  • Переместите все взад и вперед, если вы не хотите добавлять пользовательские заголовки и т.д.

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

Mentalis делает это, но их код - "делегировать спагетти", хуже GoTo:)

Ответ 3

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

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

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

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

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

Ответ 4

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

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

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

Шаг 2: Прокси открывает соединение с адресом, указанным в заголовке "Host". Затем он отправляет HTTP-запрос на этот удаленный сервер. Считывает ответ.

Шаг 3: После того, как ответ считывается с удаленного HTTP-сервера, прокси отправляет ответ через ранее открытое 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

Ответ 5

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

http://fiddler.wikidot.com/fiddlercore

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

Я также написал простой, но мощный обратный прокси для asp.net/web api.

Вы можете найти его здесь: https://github.com/SharpTools/SharpReverseProxy

Просто добавьте свой проект через nuget, и вам хорошо идти. Вы даже можете изменить "на лету" запрос, ответ или отказаться от пересылки из-за сбоя аутентификации.

Взгляните на исходный код, его очень легко реализовать:)

Ответ 11

Для чего это стоит, вот пример асинхронной реализации С#, основанной на 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 does not 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;
    }
}