Реализация прокси HTTPS, как обнаружить завершенный запрос - PullRequest
1 голос
/ 01 апреля 2020

Я пытаюсь написать простой asyn c https прокси-сервер в c#.

Я хотел бы знать, как я должен обнаружить / обработать, когда запрос завершен, и как выйти my bActive l oop, если предположить, что al oop подобен этому.

Буду очень признателен за некоторые указания, если мой подход правильный и что я могу сделать, чтобы улучшить логику c.

Проблема, с которой я, похоже, сталкиваюсь, заключается в том, что время, которое требуется конечной точке для ответа наряду с задержкой в ​​сети, означает, что у меня DataAvailable всегда есть данные, но все еще может быть некоторая отправка. Требование сна и другой attmempt, который, в свою очередь, вызывает длительное время завершения в запросах.

  1. Прослушивание TCP-соединения

  2. Извлечение CONNECT заголовок и откройте соединение с запрашиваемым сервером

  3. Скопируйте requestStream в proxyStream

  4. Скопируйте proxyStream в requestStream

  5. Спите в ожидании данных и повторяйте 3-4, пока данные не будут доступны для обоих потоков. Затем вырвитесь из l oop и закройте соединение.

public async Task Start()
{
    listener.Start();

    while (listen)
    {
        if (listener.Pending())
        {
            HandleClient(await listener.AcceptTcpClientAsync());
        }
        else
        {
            await Task.Delay(100); //<--- timeout
        }
    }
}

private static async Task HandleClient(TcpClient clt)
{

    var bytes = new byte[clt.ReceiveBufferSize];
    var hostHeaderAvailable = 0;
    NetworkStream requestStream = null;
    int count;
    const string connectText = "connect";
    const string hostText = "Host: ";
    bool bActive = true;
    List<Task> tasks = new List<Task>();


    try
    {
        using (NetworkStream proxyStream = clt.GetStream())
        using (TcpClient requestClient = new TcpClient())
        {
            proxyStream.ReadTimeout = 100;
            proxyStream.WriteTimeout = 100;


            while (bActive)
            {

                if (proxyStream.DataAvailable && hostHeaderAvailable == 0)
                {
                    count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);

                    var text = Encoding.UTF8.GetString(bytes);
                    Console.WriteLine(text);

                    if (text.ToLower().StartsWith(connectText))
                    {
                        // extract the url and port
                        var host = text.Remove(0, connectText.Length + 1);
                        var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
                        var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
                        // connect to the url and prot supplied
                        await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
                        requestStream = requestClient.GetStream();

                        requestStream.ReadTimeout = 100;
                        requestStream.WriteTimeout = 100;

                        // send 200 response to proxyStream 
                        const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
                        var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
                        await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);

                        // delay here seems to prevent the following proxyStream.read from failing as data is not yet avaiable
                        // without it the loop runs and has to timeout before running again
                        await Task.Delay(1);
                    }
                }
                hostHeaderAvailable++;


                if (requestStream == null || !requestClient.Connected || !clt.Connected)
                {
                    bActive = false;
                    break;
                }

                Console.WriteLine(proxyStream.DataAvailable || requestStream.DataAvailable);

                if (proxyStream.DataAvailable || requestStream.DataAvailable)
                { 
                    Task task = proxyStream.CopyToAsync(requestStream);
                    Task task2 = requestStream.CopyToAsync(proxyStream);

                    tasks.Add(task);
                    tasks.Add(task2);

                    await Task.WhenAll(tasks).ConfigureAwait(false);
                    bActive = false;
                    break;
                }

                await Task.Delay(10);
            }
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }

    clt.Close();
}

Более старая попытка, которая использовала ReadAsync / WriteAsync слишком долго для ответа и все еще имела время ожидания.

  1. Прослушивание TCP-соединения

  2. Извлечение заголовка CONNECT и открытие соединения с запрошенным сервером

  3. Считать данные из requestStream и скопировать в proxyStream

  4. Дождаться проверки наличия данных в любом потоке

  5. Если данные доступны для чтения из proxyStream и записи в requestStream

  6. Если данные доступны для чтения из requestStream и записи в proxyStream

  7. Режим ожидания данных и повтор 5 - 6 до тех пор, пока на eitboth потоках не будет данных. Затем вырвитесь из l oop и закройте соединение.

private static TcpListener listener = new TcpListener(IPAddress.Parse("192.168.0.25"), 13000);
private static bool listen = true;


public async Task Start()
{
    listener.Start();

    while (listen)
    {
        if (listener.Pending())
        {
            await HandleClient(await listener.AcceptTcpClientAsync());
        }
        else
        {
            await Task.Delay(100); 
        }
    }
}


private static async Task HandleClient(TcpClient clt)
{

    var bytes = new byte[clt.ReceiveBufferSize];
    var hostHeaderAvailable = 0;
    NetworkStream requestStream = null;
    int count;
    const string connectText = "connect";
    const string hostText = "Host: ";

    bool bActive = true;

    try
    {
        using (NetworkStream proxyStream = clt.GetStream())
        using (TcpClient requestClient = new TcpClient())
        {
            while (bActive)
            {
                while (proxyStream.DataAvailable)
                {
                    // handle connect
                    if (hostHeaderAvailable == 0)
                    {
                        count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);

                        var text = Encoding.UTF8.GetString(bytes);
                        Console.WriteLine(text);

                        if (text.ToLower().StartsWith(connectText))
                        {
                            // extract the url and port
                            var host = text.Remove(0, connectText.Length + 1);
                            var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
                            var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
                            // connect to the url and prot supplied
                            await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
                            requestStream = requestClient.GetStream();
                            // send 200 response to proxyStream 
                            const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
                            var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
                            await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);

                            // delay here seems to prevent the following proxyStream.read from failing as data is not yet avaiable
                            // without it the loop runs and has to timeout before running again
                            await Task.Delay(20);
                        }
                    }
                    hostHeaderAvailable++;

                    if (requestClient.Connected && hostHeaderAvailable > 1)
                    {
                        count = await proxyStream.ReadAsync(bytes, 0, bytes.Length);
                        await requestStream.WriteAsync(bytes, 0, count);
                    }
                }

                while (requestStream.DataAvailable)
                {
                    count = await requestStream.ReadAsync(bytes, 0, bytes.Length);
                    await proxyStream.WriteAsync(bytes, 0, count);
                }


                // attempt to detect a timeout / end of data avaiable
                var timeout = 0;
                while (!proxyStream.DataAvailable && !requestStream.DataAvailable)
                {
                    if (timeout > 5)
                    {
                        bActive = false;
                        break;
                    }

                    await Task.Delay(10);
                    timeout++;
                }
            }

        }

    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

ОБНОВЛЕНИЕ

Согласно ответу AgentFire, я имею Теперь перейдите к следующему рабочему коду:

public static async Task HandleDisconnect(TcpClient tcp, TcpClient tcp2, CancellationToken cancellationToken)
{
    while (true)
    {
        if (tcp.Client.Poll(0, SelectMode.SelectRead))
        {
            byte[] buff = new byte[1];

            if (tcp.Client.Receive(buff, SocketFlags.Peek) == 0)
            {
                // Client disconnected
                Console.WriteLine("The requesting client has dropped its connection.");
                cancellationToken = new CancellationToken(true);
                break;
            }
        }
        if (tcp2.Client.Poll(0, SelectMode.SelectRead))
        {
            byte[] buff = new byte[1];

            if (tcp2.Client.Receive(buff, SocketFlags.Peek) == 0)
            {
                // Server disconnected
                Console.WriteLine("The destination client has dropped its connection.");
                cancellationToken = new CancellationToken(true);
                break;
            }
        }

        await Task.Delay(1);
    }
}


private static async Task HandleClient(TcpClient clt)
{
    List<Task> tasks            = new List<Task>();
    var bytes                   = new byte[clt.ReceiveBufferSize];
    var hostHeaderAvailable     = 0;
    NetworkStream requestStream = null;
    const string connectText    = "connect";

    try
    {
        using (NetworkStream proxyStream = clt.GetStream())
        using (TcpClient requestClient = new TcpClient())
        {
            proxyStream.ReadTimeout = 100;
            proxyStream.WriteTimeout = 100;

            if (proxyStream.DataAvailable && hostHeaderAvailable == 0)
            {
                await proxyStream.ReadAsync(bytes, 0, bytes.Length);

                var text = Encoding.UTF8.GetString(bytes);
                Console.WriteLine(text);

                if (text.ToLower().StartsWith(connectText))
                {
                    // extract the url and port
                    var host = text.Remove(0, connectText.Length + 1);
                    var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
                    var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
                    // connect to the url and prot supplied
                    await requestClient.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
                    requestStream = requestClient.GetStream();

                    requestStream.ReadTimeout = 100;
                    requestStream.WriteTimeout = 100;

                    // send 200 response to proxyStream 
                    const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
                    var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
                    await proxyStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);
                }
            }
            hostHeaderAvailable++;

            CancellationToken cancellationToken = new CancellationToken(false);

            Task task               = proxyStream.CopyToAsync(requestStream, cancellationToken);
            Task task2              = requestStream.CopyToAsync(proxyStream, cancellationToken);
            Task handleConnection   = HandleDisconnect(clt, requestClient, cancellationToken);

            tasks.Add(task);
            tasks.Add(task2);
            tasks.Add(handleConnection);

            await Task.WhenAll(tasks).ConfigureAwait(false);

            // close conenctions
            clt.Close();
            clt.Dispose();
            requestClient.Close();
            requestClient.Dispose();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.ToString());
    }
}

UPDATE

Попытка использования CancellationTokenSource

CancellationTokenSource source = new CancellationTokenSource();
CancellationToken cancellationToken = source.Token;
TaskFactory factory = new TaskFactory(cancellationToken);

tasks.Add(factory.StartNew(() => {proxyStream.CopyToAsync(requestStream);}, cancellationToken));
tasks.Add(factory.StartNew(() => {requestStream.CopyToAsync(proxyStream);}, cancellationToken));
tasks.Add(factory.StartNew(async () => {
    //wait for this to retur, then cancel the token
    await HandleDisconnect(clt, requestClient);
    source.Cancel();
}, cancellationToken));

try
{
    await factory.ContinueWhenAll(tasks.ToArray(),
                                                 (results) =>
                                                 {
                                                     Console.WriteLine("Tasks complete");
                                                 }, cancellationToken);
}
catch (AggregateException ae)
{
    foreach (Exception e in ae.InnerExceptions)
    {
        if (e is TaskCanceledException)
            Console.WriteLine("Unable to compute mean: {0}",
                              ((TaskCanceledException)e).Message);
        else
            Console.WriteLine("Exception: " + e.GetType().Name);
    }
}
finally
{
    source.Dispose();
}

UPDATE

public static class extensionTcpClient{

    public static bool CheckIfDisconnected(this TcpClient tcp)
    {
        if (tcp.Client.Poll(0, SelectMode.SelectRead))
        {
            byte[] buff = new byte[1];

            if (tcp.Client.Receive(buff, SocketFlags.Peek) == 0)
            {
                // Client disconnected
                return false;
            }
        }

        return true;
    }
}


class ProxyMaintainer
{

    private static TcpListener listener = new TcpListener(IPAddress.Parse("192.168.0.25"), 13000);

    public ProxyMaintainer()
    {
    }

    public async Task Start()
    {
        Console.WriteLine("###############################");
        Console.WriteLine("Listening on 192.168.0.25:13000");
        Console.WriteLine("###############################\n");

        listener.Start();

        while (listen)
        {
            if (listener.Pending())
            {
                HandleClient(await listener.AcceptTcpClientAsync());
            }
            else
            {
                await Task.Delay(100); //<--- timeout
            }
        }
    }


    private static async Task Transport(NetworkStream from, NetworkStream to, Func<bool> isAlivePoller, CancellationToken token)
    {
        byte[] buffer = new byte[4096];

        while (isAlivePoller())
        {
            while (from.DataAvailable)
            {
                int read = await from.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
                await to.WriteAsync(buffer, 0, read, token);
            }

            // Relieve the CPU a bit.
            await Task.Delay(10, token).ConfigureAwait(false);
        }
    }


    private static async Task HandleClient(TcpClient clientFrom)
    {
        var hostHeaderAvailable = 0;
        int count;
        var bytes = new byte[clientFrom.ReceiveBufferSize];
        const string connectText = "connect";
        NetworkStream toStream = null;

        using (var fromStream = clientFrom.GetStream())
        using(TcpClient clientTo = new TcpClient())
        using (var manualStopper = new CancellationTokenSource())
        {
            count = await fromStream.ReadAsync(bytes, 0, bytes.Length);

            var text = Encoding.UTF8.GetString(bytes);
            Console.WriteLine(text);

            if (text.ToLower().StartsWith(connectText))
            {
                // extract the url and port
                var host = text.Remove(0, connectText.Length + 1);
                var hostIndex = host.IndexOf(" ", StringComparison.Ordinal);
                var hostEntry = host.Remove(hostIndex).Split(new[] { ":" }, StringSplitOptions.None);
                // connect to the url and prot supplied
                await clientTo.ConnectAsync(hostEntry[0], Convert.ToInt32(hostEntry[1]));
                toStream = clientTo.GetStream();

                // send 200 response to proxyStream 
                const string sslResponse = "HTTP/1.0 200 Connection established\r\n\r\n";
                var sslResponseBytes = Encoding.UTF8.GetBytes(sslResponse);
                await fromStream.WriteAsync(sslResponseBytes, 0, sslResponseBytes.Length);
            }

            bool Poller() => clientFrom.CheckIfDisconnected() && clientTo.CheckIfDisconnected();

            Task one = Transport(fromStream, toStream, Poller, manualStopper.Token);
            Task two = Transport(toStream, fromStream, Poller, manualStopper.Token);

            await Task.WhenAll(one, two).ConfigureAwait(false);
            //await one; await two; // To get exceptions if you want them and there are any.
            // Alternatively, you can use Task.WhenAll to get exceptions aggregated for you.
        }

        Console.WriteLine("Closing connection");
    }



}

Ответы [ 2 ]

1 голос
/ 06 апреля 2020

Чтобы ответить на ваш другой вопрос

Вы по-прежнему используете Stream.CopyToAsync, который, как я уже говорил вам, не будет успешным, пока какая-либо связывающаяся сторона решит немного подождать, прежде чем отправлять другой кусок data.

Вы также несколько усложняете свое решение.

Я бы сказал так:

async Task Transport(NetworkStream from, NetworkStream to, Func<bool> isAlivePoller, CancellationToken token)
{
    byte[] buffer = new byte[4096];

    while (isAlivePoller())
    {
        while (from.DataAvailable)
        {
            int read = await from.ReadAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
            await to.WriteAsync(buffer, 0, read, token).ConfigureAwait(false);
        }

        // Relieve the CPU a bit.
        await Task.Delay(100, token).ConfigureAwait(false);
    }
}

А затем в вашем основном коде:

using TcpClient clientFrom = ...;
using TcpClient clientTo = ...;
using var fromStream = clientFrom.GetStream();
using var toStream = clientTo.GetStream();
using var manualStopper = new CancellationTokenSource();

bool Poller() => clientFrom.CheckIfDisconnected() && clientTo.CheckIfDisconnected();

Task one = Transport(fromStream, toStream, Poller, stopper.Token);
Task two = Transport(toStream, fromStream, Poller, stopper.Token);

await Task.WhenAny(one, two).ConfigureAwait(false);
//await one; await two; // To get exceptions if you want them and there are any.
// Alternatively, you can use Task.WhenAll to get exceptions aggregated for you.

И вы здесь почти закончили.

1 голос
/ 05 апреля 2020

Ну, скажи что. Доступность данных, когда дело касается HTTP, заключается только в одном параметре (если мы опускаем такие вещи, как WebSocket), который называется Connection и передается в качестве заголовка как одно из двух возможных состояний: Close или Keep-Alive.

Если клиент выбрал Close, сервер обязан закрыть соединение, как только запрос будет обработан, тогда как Keep-Alive сообщает серверу, что, если это не так не хочу, это может оставить соединение открытым для другого запроса.

Давайте рассмотрим оба случая.

Если клиент выберет Keep-Alive, соединение будет сохраняться и работать как задумано, до бесконечности. Но:

Если соединение сбрасывается с любой стороны, это можно легко обнаружить. Этот фрагмент кода был найден в StackOverflow, и ему было сказано, что он по-прежнему работает отлично:

public static bool CheckIfDisconnected(this TcpClient tcp)
{
    if (tcp.Client.Poll(0, SelectMode.SelectRead))
    {
        byte[] buff = new byte[1];

        if (tcp.Client.Receive(buff, SocketFlags.Peek) == 0)
        {
            // Client disconnected
            return true;
        }
    }

    return false;
}

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

PS Теперь вы также спросили об асинхронности.

Я должен добавить, что TCP-соединения считаются полнодуплексными . Это означает, что вы можете создавать две асинхронные c -запускаемые задачи, как чтение, так и запись в свои собственные приемники. Мои мысли, это был бы оптимальный курс действий.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...