Як створити простий проксі в C #?


143

Я завантажив Privoxy кілька тижнів тому, і для розваги мені було цікаво дізнатись, як можна зробити просту версію.

Я розумію, що мені потрібно налаштувати браузер (клієнт) для надсилання запиту на проксі. Проксі-сервер надсилає запит до Інтернету (скажімо, це http-проксі). Проксі-сервер отримає відповідь ... але як проксі може надіслати запит назад до браузера (клієнта)?

Я в Інтернеті шукаю проксі C # і http, але не знайшов щось, що дозволило б зрозуміти, як це працює за сценою правильно. (Я вважаю, що я не хочу зворотного проксі, але я не впевнений).

Хтось із вас має якусь пояснення чи якусь інформацію, яка дозволить мені продовжувати цей невеликий проект?

Оновлення

Це я розумію (див. Графіку нижче).

Крок 1. Я налаштовую клієнт (браузер) на всі запити, що надсилаються на 127.0.0.1 на порт прослуховування проксі. Таким чином, запит не надсилатиметься до Інтернету безпосередньо, але обробляється проксі.

Крок 2. Проксі-сервер бачить нове з'єднання, читає заголовок HTTP і бачить запит, який він повинен виконати. Він виконує запит.

Step3 Проксі-сервер отримує відповідь із запиту. Тепер він повинен надіслати відповідь з Інтернету клієнту, але як ???

alt текст

Корисне посилання

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

Проксі-сервер ASP : Я також міг би отримати тут інформацію.

Запит рефлектора : Це простий приклад.

Ось сховище Git Hub із простим Http-проксі .


У мене немає скріншоту 2008 року в 2015 році. Вибачте.
Патрік Дежардінс

Власне, виявляється, що архів.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 це нормально ...
Маурісіо Шеффер

93

Я б не використовував HttpListener чи щось подібне, таким чином ви натрапите на стільки проблем.

Найголовніше, що це буде великий біль, щоб підтримати:

  • Проксі утримуйте живі
  • 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, і отримає помилку невідповідності cert, а оскільки ваш слухач не буде використовувати підписаний cert, отримає неправильну сертифікацію тощо. Ви можете виправити це, встановивши CA на комп'ютер, який клієнт буде використовувати. Це досить брудне рішення.
д-р. зло

1
@ dr.evil: +1
шабля

26

Нещодавно я написав проксі-сервер на невелику вагу в c # .net, використовуючи TcpListener і TcpClient .

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

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

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

Щодо продуктивності, я перевірив це на своїй машині і працює без помітної затримки.


і досі підтримується в 2020 році, дякую за спільний доступ :)
Марк Адамсон

19

Проксі може працювати наступним чином.

Крок 1, налаштуйте клієнта на використання proxyHost: proxyPort.

Проксі - це сервер TCP, який слухає на proxyHost: proxyPort. Браузер відкриває з'єднання з проксі і надсилає Http запит. Проксі аналізує цей запит і намагається виявити заголовок "Хост". Цей заголовок підкаже проксі, де відкрити з'єднання.

Крок 2: Проксі відкриває з'єднання з адресою, вказаною у заголовку "Хост". Потім він надсилає запит 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

14

Якщо ви просто хочете перехопити трафік, ви можете використовувати ядро ​​Fiddler для створення проксі ...

http://fiddler.wikidot.com/fiddlercore

спочатку запустіть Fiddler за допомогою інтерфейсу користувача, щоб побачити, що він робить, це проксі-сервер, який дозволяє налагоджувати http / https-трафік. Він написаний c # і має серцевину, яку можна вбудувати у власні програми.

Майте на увазі, що FiddlerCore не є безкоштовним для комерційних програм.



5

Погодьтеся, що д-р зла, якщо ви використовуєте HTTPListener, у вас виникнуть багато проблем, вам доведеться розбирати запити і будете залучатися до заголовків і ...

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

ви бачите, що вам навіть не потрібно знати, що є у запиті веб-переглядача, і аналізувати його, отримуйте лише цільову адресу сайту з першого рядка. Перший рядок зазвичай подобається цьому GET http://google.com HTTP1.1 або CONNECT facebook.com: 443 (це для ssl запитів)


4

Socks4 - дуже простий у виконанні протокол. Ви прослуховуєте початкове з'єднання, підключаєтесь до хоста / порту, який просив клієнт, відправляєте клієнтові код успіху, а потім пересилаєте вихідні та вхідні потоки через сокети.

Якщо ви користуєтеся HTTP, вам доведеться прочитати та, можливо, встановити / видалити деякі заголовки HTTP, так що це трохи більше роботи.

Якщо я добре пам’ятаю, SSL працюватиме через HTTP та Socks проксі. Для HTTP-проксі ви реалізуєте дієслово CONNECT, яке працює так само, як і socks4, як описано вище, тоді клієнт відкриває SSL-з'єднання через проксі-потік tcp.


2

Браузер підключений до проксі, тому дані, які проксі отримує від веб-сервера, просто надсилаються через те саме з'єднання, яке браузер ініціював до проксі.


1

Для чого це варто, ось реалізація асинхронної вибірки 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;
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.