Отладка соединения WebSocket через прокси с использованием Boost Asio и Beast на C++

Я написал сценарий с использованием Boost Asio и Boost Beast, чтобы включить связь WebSocket через прокси-сервер. Мне удалось разработать рабочий прототип следующим образом Как подключиться с помощью boost::asio к HTTPS-серверу с помощью прокси? и несколько официальных примеров coro-ssl. Однако я не уверен, почему определенные изменения в моем коде сделали программу успешной. Первоначально моему сценарию не удалось выполнить SSL-подтверждение после успешной (?) отправки запроса HTTP CONNECT на прокси. Изменение способа обработки ответа HTTP CONNECT, похоже, решило проблему. Могут ли остаточные данные в буфере от исходной обработки HTTP-ответа мешать SSL-квитированию? Как это изменение повлияет на общую настройку WebSocket?

Вот мой оригинальный (неработающий) код:

#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/certify/https_verification.hpp>
#include <iostream>
#include <string>
#include <cstdlib> // for std::getenv

namespace beast = boost::beast;
namespace http = beast::http;    
namespace websocket = beast::websocket;
namespace net = boost::asio;
namespace ssl = net::ssl;
using tcp = boost::asio::ip::tcp;

void fail(beast::error_code ec, char const* what)
{
    std::cerr << what << ": " << ec.message() << "\n";
}

void do_session(
    std::string host,
    std::string const& port,
    std::string const& stream_path,
    net::io_context& ioc,
    ssl::context& ctx,
    net::yield_context yield)
{
    beast::error_code ec;
    tcp::resolver resolver(ioc);

    websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws(ioc, ctx);

    std::cout << "Starting session for host: " << host << ", port: " << port << ", stream_path: " << stream_path << std::endl;

    // Get proxy server address
    const char* proxy_env = std::getenv("https_proxy");
    if (!proxy_env) {
        std::cerr << "https_proxy environment variable is not set.\n";
        return;
    }
    std::string proxy_uri = proxy_env;
    std::string proxy_host, proxy_port;
    if (proxy_uri.substr(0, 7) == "http://") {
        proxy_uri = proxy_uri.substr(7);
        std::cout << "Using proxy: " << proxy_uri << std::endl;
    }
    auto pos = proxy_uri.find(':');
    if (pos != std::string::npos) {
        proxy_host = proxy_uri.substr(0, pos);
        proxy_port = proxy_uri.substr(pos + 1);
    } else {
        std::cerr << "Invalid https_proxy format. Expected http://host:port\n";
        return;
    }

    // Resolve the proxy server
    std::cout << "Resolving proxy host..." << std::endl;
    std::cout << "Proxy host: " << proxy_host << ", Proxy port: " << proxy_port << std::endl;
    auto const proxy_results = resolver.async_resolve(proxy_host, proxy_port, yield[ec]);
    if (ec) return fail(ec, "resolve_proxy");
    std::cout << "Proxy host resolved." << std::endl;

    // Connect to the proxy server
    std::cout << "Connecting to proxy endpoint..." << std::endl;
    for (auto const& result : proxy_results) {
        std::cout << "Trying proxy endpoint: " << result.endpoint() << std::endl;
    }
    auto ep = beast::get_lowest_layer(ws).async_connect(proxy_results, yield[ec]);
    if (ec) {
        std::cout << "Failed to connect to proxy endpoint: " << ec.message() << std::endl;
        return fail(ec, "connect_proxy");
    }
    std::cout << "Connected to proxy endpoint." << std::endl;

    // Set tcp::no_delay to reduce latency
    beast::get_lowest_layer(ws).socket().set_option(tcp::no_delay(true));

    // Send HTTP CONNECT request to proxy
    std::cout << "Sending HTTP CONNECT request to proxy..." << std::endl;
    http::request<http::empty_body> req{http::verb::connect, host + ":" + port, 11};
    req.set(http::field::host, host + ":" + port);
    req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
    req.set(http::field::proxy_connection, "keep-alive");
    req.set(http::field::connection, "keep-alive");

    std::cout << "HTTP CONNECT request: " << req << std::endl;
    http::write(beast::get_lowest_layer(ws), req, ec);
    if (ec) return fail(ec, "write_connect");

    // Read HTTP CONNECT response from proxy
    std::cout << "Reading HTTP CONNECT response from proxy..." << std::endl;
    beast::flat_buffer buffer;
    http::response<http::empty_body> res;
    http::read(beast::get_lowest_layer(ws), buffer, res, ec);
    if (ec) return fail(ec, "read_connect");

    if (res.result() != http::status::ok) {
        std::cerr << "Proxy failed to CONNECT: " << res.result_int() << std::endl;
        return;
    }
    std::cout << "HTTP CONNECT response received." << std::endl;
    std::cout << "HTTP CONNECT response: " << res << std::endl;

    // Perform SSL handshake
    std::cout << "Performing SSL handshake..." << std::endl;
    ws.next_layer().async_handshake(ssl::stream_base::client, yield[ec]);
    if (ec) {
        std::cerr << "SSL handshake failed: " << ec.message() << std::endl;
        return fail(ec, "ssl_handshake");
    }
    std::cout << "SSL handshake completed." << std::endl;

    host += ":" + port;

    // Perform WebSocket handshake
    std::cout << "Performing WebSocket handshake..." << std::endl;
    ws.async_handshake(host, stream_path, yield[ec]);
    if (ec) return fail(ec, "handshake");
    std::cout << "WebSocket handshake completed." << std::endl;

    // Set timeout settings for the websocket
    ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));

    std::cout << "Entering read loop..." << std::endl;

    // Loop to read messages
    for (;;)
    {
        std::cout << "Reading message..." << std::endl;
        ws.async_read(buffer, yield[ec]);
        if (ec) return fail(ec, "read");

        std::cout << "Received message: " << beast::make_printable(buffer.data()) << std::endl;

        // Optionally send a pong frame to keep the connection alive
        ws.async_pong({}, yield[ec]);
    }
}

int main(int argc, char** argv)
{
    if (argc != 4)
    {
        std::cerr << "Usage: websocket-client-coro-ssl <host> <port> <stream>\n" <<
                     "Example:\n" <<
                     "    websocket-client-coro-ssl fstream.binance.com 443 /ws/bnbusdt@aggTrade\n" <<
                     "    websocket-client-coro-ssl fstream.binance.com 443 /stream?streams=bnbusdt@aggTrade/btcusdt@markPrice\n";
        return EXIT_FAILURE;
    }
    auto const host = argv[1];
    auto const port = argv[2];
    auto const stream_path = argv[3];

    std::cout << "Starting client for host: " << host << ", port: " << port << ", stream_path: " << stream_path << std::endl;

    net::io_context ioc;
    // boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
    ssl::context ctx{ssl::context::sslv23_client};
    // ctx.load_verify_file("/path/to/cacert.pem");
    ctx.set_verify_mode(boost::asio::ssl::verify_peer);
    ctx.load_verify_file("/usr/lib/ssl/certs/ca-certificates.crt"); // Change to the actual path of your CA cert

    // boost::certify::enable_native_https_server_verification(ctx); // from lib https://github.com/djarek/certify
    ctx.set_options(boost::asio::ssl::context::default_workarounds |
                    boost::asio::ssl::context::no_sslv2 |
                    boost::asio::ssl::context::no_sslv3);

    boost::asio::spawn(ioc, std::bind(
        &do_session,
        std::string(host),
        std::string(port),
        std::string(stream_path),
        std::ref(ioc),
        std::ref(ctx),
        std::placeholders::_1));

    std::cout << "Running IO context..." << std::endl;
    ioc.run();

    std::cout << "Client exited." << std::endl;
    return EXIT_SUCCESS;
}

Это вывод командной строки:

./proxy_client fstream.binance.com 443 /ws/bnbusdt@trade
Starting client for host: fstream.binance.com, port: 443, stream_path: /ws/bnbusdt@trade
Running IO context...
Starting session for host: fstream.binance.com, port: 443, stream_path: /ws/bnbusdt@trade
Using proxy: 127.0.0.1:7890
Resolving proxy host...
Proxy host: 127.0.0.1, Proxy port: 7890
Proxy host resolved.
Connecting to proxy endpoint...
Trying proxy endpoint: 127.0.0.1:7890
Connected to proxy endpoint.
Sending HTTP CONNECT request to proxy...
HTTP CONNECT request: CONNECT fstream.binance.com:443 HTTP/1.1
Host: fstream.binance.com:443
User-Agent: Boost.Beast/300
Proxy-Connection: keep-alive
Connection: keep-alive

Reading HTTP CONNECT response from proxy...
HTTP CONNECT response received.
HTTP CONNECT response: HTTP/1.1 200 Connection established

Performing SSL handshake...
SSL handshake failed: stream truncated
ssl_handshake: stream truncated
Client exited.

Когда я изменил часть кода, которая читает ответ HTTP, на:

    {
        // Read HTTP CONNECT response from proxy
        std::cout << "Reading HTTP CONNECT response from proxy..." << std::endl;
        beast::flat_buffer buffer;
        http::response_parser<http::empty_body> p;
        http::read_header(beast::get_lowest_layer(ws), buffer, p, ec);
        if (ec) return fail(ec, "read_connect");
        http::response<http::empty_body> proxy_response = std::move(p.get());
        
        assert(buffer.size() == 0);
        
        if (proxy_response.result() != http::status::ok) {
            std::cerr << "Proxy failed to CONNECT: " << proxy_response.result_int() << std::endl;
            return;
        }
        std::cout << "HTTP CONNECT response received." << std::endl;
        // output the response and the body
        std::cout << "HTTP CONNECT response: " << proxy_response << std::endl;
    }

затем программа заработала успешно. Я не понимаю причину этого.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

остаточные данные в буфере от исходной обработки HTTP-ответа будут мешать SSL-квитированию

Да. Конечно.

Вторая версия правильно сообщает синтаксическому анализатору не читать тело. (В противном случае прокси-сервер должен будет сообщить вам (Различает ли HTTP пустое тело и отсутствие тела?), но это характерно для HTTP-прокси).

Вторая версия также неверна, поскольку в обеих версиях buffer может содержать лишние данные, отправленные сервером.

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

Решение проблемы

Вы смешиваете синхронные и асинхронные операции. Это кажется неразумным. Я не думаю, что зверь::websocket::stream это поддерживает. Так, например. использовать

http::async_write(beast::get_lowest_layer(ws), req, yield[ec]);
// ...
http::async_read(beast::get_lowest_layer(ws), buffer, res, yield[ec]);

Затем передайте буфер из предыдущей операции чтения в подтверждение SSL:

auto used = ws.next_layer().async_handshake(ssl::stream_base::client, buffer.data(), yield[ec]);
if (ec) {
    std::cerr << "SSL handshake failed: " << ec.message() << std::endl;
    return fail(ec, "ssl_handshake");
}
buffer.consume(used);

На этом этапе вы можете быть уверены в том, что буфер пуст.

Живая демонстрация

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/beast/websocket/ssl.hpp>
//#include <boost/certify/https_verification.hpp>
#include <cstdlib> // for std::getenv
#include <iostream>
#include <string>

namespace beast     = boost::beast;
namespace http      = beast::http;
namespace websocket = beast::websocket;
namespace net       = boost::asio;
namespace ssl       = net::ssl;
using tcp           = boost::asio::ip::tcp;

void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; }

void do_session(std::string host, std::string const& port, std::string const& stream_path,
                net::io_context& ioc, ssl::context& ctx, net::yield_context yield) {
    beast::error_code ec;
    tcp::resolver     resolver(ioc);

    websocket::stream<beast::ssl_stream<beast::tcp_stream>> ws(ioc, ctx);

    std::cout << "Starting session for host: " << host << ", port: " << port
              << ", stream_path: " << stream_path << std::endl;

    // Get proxy server address
    char const* proxy_env = std::getenv("https_proxy");
    if (!proxy_env) {
        std::cerr << "https_proxy environment variable is not set.\n";
        return;
    }
    std::string proxy_uri = proxy_env;
    std::string proxy_host, proxy_port;
    if (proxy_uri.substr(0, 7) == "http://") {
        proxy_uri = proxy_uri.substr(7);
        std::cout << "Using proxy: " << proxy_uri << std::endl;
    }
    auto pos = proxy_uri.find(':');
    if (pos != std::string::npos) {
        proxy_host = proxy_uri.substr(0, pos);
        proxy_port = proxy_uri.substr(pos + 1);
    } else {
        std::cerr << "Invalid https_proxy format. Expected http://host:port\n";
        return;
    }

    // Resolve the proxy server
    std::cout << "Resolving proxy host..." << std::endl;
    std::cout << "Proxy host: " << proxy_host << ", Proxy port: " << proxy_port << std::endl;
    auto proxy_results = resolver.async_resolve(proxy_host, proxy_port, yield[ec]);
    if (ec)
        return fail(ec, "resolve_proxy");
    std::cout << "Proxy host resolved." << std::endl;

    // Connect to the proxy server
    std::cout << "Connecting to proxy endpoint..." << std::endl;
    for (auto const& result : proxy_results) {
        std::cout << "Trying proxy endpoint: " << result.endpoint() << std::endl;
    }
    /*auto ep =*/beast::get_lowest_layer(ws).async_connect(proxy_results, yield[ec]);
    if (ec) {
        std::cout << "Failed to connect to proxy endpoint: " << ec.message() << std::endl;
        return fail(ec, "connect_proxy");
    }
    std::cout << "Connected to proxy endpoint." << std::endl;

    // Set tcp::no_delay to reduce latency
    beast::get_lowest_layer(ws).socket().set_option(tcp::no_delay(true));

    // Send HTTP CONNECT request to proxy
    std::cout << "Sending HTTP CONNECT request to proxy..." << std::endl;
    http::request<http::empty_body> req{http::verb::connect, host + ":" + port, 11};
    req.set(http::field::host, host + ":" + port);
    req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
    req.set(http::field::proxy_connection, "keep-alive");
    req.set(http::field::connection, "keep-alive");

    std::cout << "HTTP CONNECT request: " << req << std::endl;
    http::async_write(beast::get_lowest_layer(ws), req, yield[ec]);
    if (ec)
        return fail(ec, "write_connect");

    // Read HTTP CONNECT response from proxy
    std::cout << "Reading HTTP CONNECT response from proxy..." << std::endl;
    beast::flat_buffer               buffer;
    http::response<http::empty_body> res;
    {
        http::response_parser<http::empty_body> p;
        http::async_read_header(beast::get_lowest_layer(ws), buffer, p, yield[ec]);
        if (ec)
            return fail(ec, "read_connect");

        res = std::move(p.get());
    }
    if (res.result() != http::status::ok) {
        std::cerr << "Proxy failed to CONNECT: " << res.result_int() << std::endl;
        return;
    }
    std::cout << "HTTP CONNECT response received." << std::endl;
    std::cout << "HTTP CONNECT response: " << res << std::endl;

    // Perform SSL handshake
    std::cout << "Performing SSL handshake..." << std::endl;
    {
        auto used = ws.next_layer().async_handshake(ssl::stream_base::client, buffer.data(), yield[ec]);
        if (ec) {
            std::cerr << "SSL handshake failed: " << ec.message() << std::endl;
            return fail(ec, "ssl_handshake");
        }
        buffer.consume(used);
    }
    std::cout << "SSL handshake completed." << std::endl;

    host += ":" + port;

    assert(buffer.size() == 0);

    // Perform WebSocket handshake
    std::cout << "Performing WebSocket handshake..." << std::endl;
    ws.async_handshake(host, stream_path, yield[ec]);
    if (ec)
        return fail(ec, "handshake");
    std::cout << "WebSocket handshake completed." << std::endl;

    // Set timeout settings for the websocket
    ws.set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));

    std::cout << "Entering read loop..." << std::endl;

    // Loop to read messages
    for (;;) {
        std::cout << "Reading message..." << std::endl;
        ws.async_read(buffer, yield[ec]);
        if (ec)
            return fail(ec, "read");

        std::cout << "Received message: " << beast::make_printable(buffer.data()) << std::endl;

        // Optionally send a pong frame to keep the connection alive
        ws.async_pong({}, yield[ec]);
    }
}

int main(int argc, char** argv) {
    if (argc != 4) {
        std::cerr << "Usage: websocket-client-coro-ssl <host> <port> <stream>\n"
                  << "Example:\n"
                  << "    websocket-client-coro-ssl fstream.binance.com 443 /ws/bnbusdt@aggTrade\n"
                  << "    websocket-client-coro-ssl fstream.binance.com 443 "
                     "/stream?streams=bnbusdt@aggTrade/btcusdt@markPrice\n";
        return EXIT_FAILURE;
    }
    auto const host        = argv[1];
    auto const port        = argv[2];
    auto const stream_path = argv[3];

    std::cout << "Starting client for host: " << host << ", port: " << port
              << ", stream_path: " << stream_path << std::endl;

    net::io_context ioc;
    // boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
    ssl::context ctx{ssl::context::sslv23_client};
    // ctx.load_verify_file("/path/to/cacert.pem");
    ctx.set_verify_mode(boost::asio::ssl::verify_peer);
    ctx.load_verify_file(
        "/usr/lib/ssl/certs/ca-certificates.crt"); // Change to the actual path of your CA cert

    // boost::certify::enable_native_https_server_verification(ctx); // from lib
    // https://github.com/djarek/certify
    ctx.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 |
                    boost::asio::ssl::context::no_sslv3);

    spawn(ioc,
          std::bind(&do_session, std::string(host), std::string(port), std::string(stream_path),
                    std::ref(ioc), std::ref(ctx), std::placeholders::_1),
          boost::asio::detached);

    std::cout << "Running IO context..." << std::endl;
    ioc.run();

    std::cout << "Client exited." << std::endl;
    return EXIT_SUCCESS;
}

Использование tinyproxy на порту 7777 локально:

Спасибо! У меня есть еще несколько вопросов: 1. Однако метод синхронизации (http::write(beast::get_lowest_layer(ws), req, yield[ec]);) действительно сработал. Может быть, это потому, что зверь::get_lowest_layer(ws) возвращает tcp_stream? 2. Действительно, в моем окончательном коде (который работает успешно) buffer находится в скобках. Процесс установления связи SSL в моем коде не имеет ничего общего с buffer. Однако это сработало. Так действительно ли необходима передача buffer.data() в ws.next_layer().async_handshake()?

Arandott 29.07.2024 05:09

3. То есть, как правило, ответы от прокси-серверов не имеют тела? Раньше я надолго застревал при чтении ответа (с версией кода 1), но в конце концов он все равно возвращал 200 ОК (не знаю почему). Я не передал buffer.data() методу установления связи SSL. Могут ли в таких случаях данные в буфере привести к сбою установления связи SSL?

Arandott 29.07.2024 05:12

Другие вопросы по теме

Похожие вопросы

Как я могу связать статическую библиотеку, которая зависит от других библиотек в cmake?
Как написать ctor для класса, владеющего типами nocopy-nomove, один из которых должен быть инициализирован из другого, который может быть одним из нескольких типов?
Почему const Type&& не приводится к Type&&?
Графический интерфейс не отображает все компоненты (C++)
GlGetTexImage возвращает 0 для текстуры, даже если текстура загружена и правильно заполнена в памяти графического процессора
Нужно ли объявлять оператор < перед вызовом std::lexicographical_compare?
Как правильно использовать OpenMP Target Enter/Exit/Update для неструктурированных асинхронных вычислений на стороне устройства?
Конкатенация строк препроцессора макросов C++
Почему в конце двусвязного циклического связанного списка в C++ стоит нуль?
Объявите несколько переменных-членов разных типов в шаблонном классе в соответствии с параметром шаблона