From f7b842bbc7e66d5c6492808b9e59c69c68973605 Mon Sep 17 00:00:00 2001 From: Euripedes Rocha Date: Wed, 1 Dec 2021 10:48:49 -0300 Subject: [PATCH] EXAMPLES/ASIO: Adds a SOCKS4 example Creates an example on how to connect using Socks4 based proxy. --- .../async_request/main/async_http_request.cpp | 4 +- examples/protocols/asio/socks4/CMakeLists.txt | 10 + examples/protocols/asio/socks4/README.md | 73 ++++ .../protocols/asio/socks4/main/CMakeLists.txt | 2 + .../asio/socks4/main/Kconfig.projbuild | 16 + .../protocols/asio/socks4/main/socks4.cpp | 393 ++++++++++++++++++ .../protocols/asio/socks4/main/socks4.hpp | 143 +++++++ .../protocols/asio/socks4/sdkconfig.defaults | 3 + tools/ci/check_copyright_ignore.txt | 1 + 9 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 examples/protocols/asio/socks4/CMakeLists.txt create mode 100644 examples/protocols/asio/socks4/README.md create mode 100644 examples/protocols/asio/socks4/main/CMakeLists.txt create mode 100644 examples/protocols/asio/socks4/main/Kconfig.projbuild create mode 100644 examples/protocols/asio/socks4/main/socks4.cpp create mode 100644 examples/protocols/asio/socks4/main/socks4.hpp create mode 100644 examples/protocols/asio/socks4/sdkconfig.defaults diff --git a/examples/protocols/asio/async_request/main/async_http_request.cpp b/examples/protocols/asio/async_request/main/async_http_request.cpp index 579a61fe28..3f2df210b1 100644 --- a/examples/protocols/asio/async_request/main/async_http_request.cpp +++ b/examples/protocols/asio/async_request/main/async_http_request.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD * * SPDX-License-Identifier: CC0-1.0 * @@ -96,7 +96,7 @@ public: * @tparam completion_handler A callable to act as the final handler for the process. * @param host host address * @param port port number - due to a limitation on lwip implementation this should be the number not the - * service name tipically seen in ASIO examples. + * service name typically seen in ASIO examples. * * @note The class could be modified to store the completion handler, as a member variable, instead of * pass it along asynchronous calls to allow the process to run again completely. diff --git a/examples/protocols/asio/socks4/CMakeLists.txt b/examples/protocols/asio/socks4/CMakeLists.txt new file mode 100644 index 0000000000..0482e20224 --- /dev/null +++ b/examples/protocols/asio/socks4/CMakeLists.txt @@ -0,0 +1,10 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +# (Not part of the boilerplate) +# This example uses an extra component for common functions such as Wi-Fi and Ethernet connection. +set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_sock4) diff --git a/examples/protocols/asio/socks4/README.md b/examples/protocols/asio/socks4/README.md new file mode 100644 index 0000000000..f9cc88b462 --- /dev/null +++ b/examples/protocols/asio/socks4/README.md @@ -0,0 +1,73 @@ +| Supported Targets | ESP32 | ESP32-S2 | +| ----------------- | ----- | ----- | + +# Async request using ASIO + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +The application aims to show how to connect to a Socks4 proxy using async operations with ASIO. The SOCKS protocol is +briefly described by the diagram below. + + ┌──────┐ ┌─────┐ ┌──────┐ + │Client│ │Proxy│ │Target│ + └──┬───┘ └──┬──┘ └──┬───┘ + │ │ │ + │ ╔═╧══════════════╗ │ +══════════════════════╪════════════════════════╣ Initialization ╠═══╪════════════════════════════════════════════ + │ ╚═╤══════════════╝ │ + │ │ │ + ╔══════════════════╗│ │ │ + ║We establish a ░║│ Socket Connection │ │ + ║TCP connection ║│ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ > │ + ║and get a socket ║│ │ │ + ╚══════════════════╝│ │ │ + │ │ │ + │ ╔═╧══════════════╗ │ +══════════════════════╪════════════════════════╣ Socks Protocol ╠═══╪════════════════════════════════════════════ + │ ╚═╤══════════════╝ │ + │ │ │ + │ Client Connection Request│ │ + │ ─────────────────────────> │ + │ │ │ + │ │ │ + │ ╔════════════╪═══════╤══════════╪════════════════════════════════╗ + │ ║ TARGET CONNECTION │ │ ║ + │ ╟────────────────────┘ │ ╔═══════════════════╗ ║ + │ ║ │ Socket Connection│ ║Proxy establishes ░║ ║ + │ ║ │ <─ ─ ─ ─ ─ ─ ─ ─ > ║ TCPconnection ║ ║ + │ ║ │ │ ║ with target host ║ ║ + │ ╚════════════╪══════════════════╪══╚═══════════════════╝═════════╝ + │ │ │ + │ Response packet │ │ + │ <───────────────────────── │ + │ │ │ + │ │ │ + │ │ ╔═══════╗ │ +══════════════════════╪══════════════════════════╪══╣ Usage ╠═══════╪════════════════════════════════════════════ + │ │ ╚═══════╝ │ + │ │ │ + ╔═════════════════╗│ │ │ + ║Client uses the ░║│ │ │ + ║ socket opened ║│ │ │ + ║ with proxy ║│ <─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─> + ║to communicate ║│ │ │ + ║ ║│ │ │ + ╚═════════════════╝┴───┐ ┌──┴──┐ ┌──┴───┐ + │Client│ │Proxy│ │Target│ + └──────┘ └─────┘ └──────┘ + + +# Configure and Building example + +This example requires the proxy address to be configured. You can do this using the menuconfig option. +Proxy address and port must be configured in order for this example to work. + +If using Linux ssh can be used as a proxy for testing. + +``` +ssh -N -v -D 0.0.0.0:1080 localhost +``` +# Async operations composition and automatic lifetime control + +For documentation about the structure of this example look into [async\_request README](../async_request/README.md). + diff --git a/examples/protocols/asio/socks4/main/CMakeLists.txt b/examples/protocols/asio/socks4/main/CMakeLists.txt new file mode 100644 index 0000000000..517ab52d9f --- /dev/null +++ b/examples/protocols/asio/socks4/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "socks4.cpp" + INCLUDE_DIRS ".") diff --git a/examples/protocols/asio/socks4/main/Kconfig.projbuild b/examples/protocols/asio/socks4/main/Kconfig.projbuild new file mode 100644 index 0000000000..984b2f20fa --- /dev/null +++ b/examples/protocols/asio/socks4/main/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Example Configuration" + + config EXAMPLE_PROXY_ADDRESS + string "Proxy address" + default "myproxy" + help + Address of the proxy to be used. + + config EXAMPLE_PROXY_PORT + string "Proxy port" + default "myport" + help + Port for the proxy. Due to a limitation of lwip, must + be a number e.g. "1080". + +endmenu diff --git a/examples/protocols/asio/socks4/main/socks4.cpp b/examples/protocols/asio/socks4/main/socks4.cpp new file mode 100644 index 0000000000..b03d788080 --- /dev/null +++ b/examples/protocols/asio/socks4/main/socks4.cpp @@ -0,0 +1,393 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + * + * + * ASIO Socks4 example +*/ + +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "socks4.hpp" +#include "nvs_flash.h" +#include "esp_event.h" +#include "protocol_examples_common.h" + +constexpr auto TAG = "asio_socks4"; +using asio::ip::tcp; + +namespace { + +void esp_init() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_log_level_set("async_request", ESP_LOG_DEBUG); + + /* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig. + * Read "Establishing Wi-Fi or Ethernet Connection" section in + * examples/protocols/README.md for more information about this function. + */ + ESP_ERROR_CHECK(example_connect()); +} + +/** + * @brief Simple class to add the resolver to a chain of actions + * + */ +class AddressResolution : public std::enable_shared_from_this { +public: + explicit AddressResolution(asio::io_context &context) : ctx(context), resolver(ctx) {} + + /** + * @brief Initiator function for the address resolution + * + * @tparam CompletionToken callable responsible to use the results. + * + * @param host Host address + * @param port Port for the target, must be number due to a limitation on lwip. + */ + template + void resolve(const std::string &host, const std::string &port, CompletionToken &&completion_handler) + { + auto self(shared_from_this()); + resolver.async_resolve(host, port, [self, completion_handler](const asio::error_code & error, tcp::resolver::results_type results) { + if (error) { + ESP_LOGE(TAG, "Failed to resolve: %s", error.message().c_str()); + return; + } + completion_handler(self, results); + }); + } + +private: + asio::io_context &ctx; + tcp::resolver resolver; + +}; + +/** + * @brief Connection class + * + * The lowest level dependency on our asynchronous task, Connection provide an interface to TCP sockets. + * A similar class could be provided for a TLS connection. + * + * @note: All read and write operations are written on an explicit strand, even though an implicit strand + * occurs in this example since we run the io context in a single task. + * + */ +class Connection : public std::enable_shared_from_this { +public: + explicit Connection(asio::io_context &context) : ctx(context), strand(context), socket(ctx) {} + + /** + * @brief Start the connection + * + * Async operation to start a connection. As the final act of the process the Connection class pass a + * std::shared_ptr of itself to the completion_handler. + * Since it uses std::shared_ptr as an automatic control of its lifetime this class must be created + * through a std::make_shared call. + * + * @tparam completion_handler A callable to act as the final handler for the process. + * @param host host address + * @param port port number - due to a limitation on lwip implementation this should be the number not the + * service name typically seen in ASIO examples. + * + * @note The class could be modified to store the completion handler, as a member variable, instead of + * pass it along asynchronous calls to allow the process to run again completely. + * + */ + template + void start(tcp::resolver::results_type results, CompletionToken &&completion_handler) + { + connect(results, completion_handler); + } + + /** + * @brief Start an async write on the socket + * + * @tparam data + * @tparam completion_handler A callable to act as the final handler for the process. + * + */ + template + void write_async(const DataType &data, CompletionToken &&completion_handler) + { + asio::async_write(socket, data, asio::bind_executor(strand, completion_handler)); + } + + /** + * @brief Start an async read on the socket + * + * @tparam data + * @tparam completion_handler A callable to act as the final handler for the process. + * + */ + template + void read_async(DataBuffer &&in_data, CompletionToken &&completion_handler) + { + asio::async_read(socket, in_data, asio::bind_executor(strand, completion_handler)); + } + +private: + + template + void connect(tcp::resolver::results_type results, CompletionToken &&completion_handler) + { + auto self(shared_from_this()); + asio::async_connect(socket, results, [self, completion_handler](const asio::error_code & error, [[maybe_unused]] const tcp::endpoint & endpoint) { + if (error) { + ESP_LOGE(TAG, "Failed to connect: %s", error.message().c_str()); + return; + } + completion_handler(self); + }); + } + asio::io_context &ctx; + asio::io_context::strand strand; + tcp::socket socket; +}; + +} + +namespace Socks { + +struct ConnectionData { + ConnectionData(socks4::request::command_type cmd, const asio::ip::tcp::endpoint &endpoint, + const std::string &user_id) : request(cmd, endpoint, user_id) {}; + socks4::request request; + socks4::reply reply; +}; + +template +void async_connect(asio::io_context &context, std::string proxy, std::string proxy_port, std::string host, std::string port, CompletionToken &&completion_handler) +{ + /* + * The first step is to resolve the address of the proxy we want to connect to. + * The AddressResolution itself is injected to the completion handler. + */ + // Resolve proxy + std::make_shared(context)->resolve(proxy, proxy_port, + [&context, host, port, completion_handler](std::shared_ptr resolver, tcp::resolver::results_type proxy_resolution) { + // We also need to resolve the target host address + resolver->resolve(host, port, [&context, proxy_resolution, completion_handler](std::shared_ptr resolver, tcp::resolver::results_type host_resolution) { + // Make connection with the proxy + ESP_LOGI(TAG, "Startig Proxy Connection"); + std::make_shared(context)->start(proxy_resolution, + [resolver, host_resolution, completion_handler](std::shared_ptr connection) { + auto connect_data = std::make_shared(socks4::request::connect, *host_resolution, ""); + ESP_LOGI(TAG, "Sending Request to proxy for host connection."); + connection->write_async(connect_data->request.buffers(), [connection, connect_data, completion_handler](std::error_code error, std::size_t bytes_received) { + if (error) { + ESP_LOGE(TAG, "Proxy request write error: %s", error.message().c_str()); + return; + } + connection->read_async(connect_data->reply.buffers(), [connection, connect_data, completion_handler](std::error_code error, std::size_t bytes_received) { + if (error) { + + ESP_LOGE(TAG, "Proxy response read error: %s", error.message().c_str()); + return; + } + if (!connect_data->reply.success()) { + ESP_LOGE(TAG, "Proxy error: %#x", connect_data->reply.status()); + } + completion_handler(connection); + + }); + + }); + + }); + + }); + }); + +} +} // namespace Socks + +namespace Http { +enum class Method { GET }; + +/** + * @brief Simple HTTP request class + * + * The user needs to write the request information direct to header and body fields. + * + * Only GET verb is provided. + * + */ +class Request { +public: + Request(Method method, std::string host, std::string port, const std::string &target) : host_data(std::move(host)), port_data(std::move(port)) + { + header_data.append("GET "); + header_data.append(target); + header_data.append(" HTTP/1.1"); + header_data.append("\r\n"); + header_data.append("Host: "); + header_data.append(host_data); + header_data.append("\r\n"); + header_data.append("\r\n"); + }; + + void set_header_field(std::string const &field) + { + header_data.append(field); + } + + void append_to_body(std::string const &data) + { + body_data.append(data); + }; + + const std::string &host() const + { + return host_data; + } + + const std::string &service_port() const + { + return port_data; + } + + const std::string &header() const + { + return header_data; + } + + const std::string &body() const + { + return body_data; + } + +private: + std::string host_data; + std::string port_data; + std::string header_data; + std::string body_data; +}; + +/** + * @brief Simple HTTP response class + * + * The response is built from received data and only parsed to split header and body. + * + * A copy of the received data is kept. + * + */ +struct Response { + /** + * @brief Construct a response from a contiguous buffer. + * + * Simple http parsing. + * + */ + template + explicit Response(DataIt data, size_t size) + { + raw_response = std::string(data, size); + + auto header_last = raw_response.find("\r\n\r\n"); + if (header_last != std::string::npos) { + header = raw_response.substr(0, header_last); + } + body = raw_response.substr(header_last + 3); + } + /** + * @brief Print response content. + */ + void print() + { + ESP_LOGI(TAG, "Header :\n %s", header.c_str()); + ESP_LOGI(TAG, "Body : \n %s", body.c_str()); + } + + std::string raw_response; + std::string header; + std::string body; +}; + +/** @brief HTTP Session + * + * Session class to handle HTTP protocol implementation. + * + */ +class Session : public std::enable_shared_from_this { +public: + explicit Session(std::shared_ptr connection_in) : connection(std::move(connection_in)) + { + } + + template + void send_request(const Request &request, CompletionToken &&completion_handler) + { + auto self = shared_from_this(); + send_data = { asio::buffer(request.header()), asio::buffer(request.body()) }; + connection->write_async(send_data, [self, &completion_handler](std::error_code error, std::size_t bytes_transfered) { + if (error) { + ESP_LOGE(TAG, "Request write error: %s", error.message().c_str()); + return; + } + ESP_LOGD(TAG, "Bytes Transfered: %d", bytes_transfered); + self->get_response(completion_handler); + }); + } + +private: + template + void get_response(CompletionToken &&completion_handler) + { + auto self = shared_from_this(); + connection->read_async(asio::buffer(receive_buffer), [self, &completion_handler](std::error_code error, std::size_t bytes_received) { + if (error and error.value() != asio::error::eof) { + return; + } + ESP_LOGD(TAG, "Bytes Received: %d", bytes_received); + if (bytes_received == 0) { + return; + } + Response response(std::begin(self->receive_buffer), bytes_received); + + completion_handler(self, response); + }); + } + /* + * For this example we assumed 2048 to be enough for the receive_buffer + */ + std::array receive_buffer; + /* + * The hardcoded 2 below is related to the type we receive the data to send. We gather the parts from Request, header + * and body, to send avoiding the copy. + */ + std::array send_data; + std::shared_ptr connection; +}; +}// namespace Http + +extern "C" void app_main(void) +{ + // Basic initialization of ESP system + esp_init(); + + asio::io_context io_context; + Http::Request request(Http::Method::GET, "www.httpbin.org", "80", "/get"); + Socks::async_connect(io_context, CONFIG_EXAMPLE_PROXY_ADDRESS, CONFIG_EXAMPLE_PROXY_PORT, request.host(), request.service_port(), + [&request](std::shared_ptr connection) { + // Now we create a HTTP::Session and inject the necessary connection. + std::make_shared(connection)->send_request(request, [](std::shared_ptr session, Http::Response response) { + response.print(); + }); + }); + // io_context.run will block until all the tasks on the context are done. + io_context.run(); + ESP_LOGI(TAG, "Context run done"); + + ESP_ERROR_CHECK(example_disconnect()); +} diff --git a/examples/protocols/asio/socks4/main/socks4.hpp b/examples/protocols/asio/socks4/main/socks4.hpp new file mode 100644 index 0000000000..a039c07cae --- /dev/null +++ b/examples/protocols/asio/socks4/main/socks4.hpp @@ -0,0 +1,143 @@ +// +// socks4.hpp +// ~~~~~~~~~~ +// +// Copyright (c) 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// + +#ifndef SOCKS4_HPP +#define SOCKS4_HPP + +#include +#include +#include +#include + +namespace socks4 { + +const unsigned char version = 0x04; + +class request +{ +public: + enum command_type + { + connect = 0x01, + bind = 0x02 + }; + + request(command_type cmd, const asio::ip::tcp::endpoint& endpoint, + const std::string& user_id) + : version_(version), + command_(cmd), + user_id_(user_id), + null_byte_(0) + { + // Only IPv4 is supported by the SOCKS 4 protocol. + if (endpoint.protocol() != asio::ip::tcp::v4()) + { + throw asio::system_error( + asio::error::address_family_not_supported); + } + + // Convert port number to network byte order. + unsigned short port = endpoint.port(); + port_high_byte_ = (port >> 8) & 0xff; + port_low_byte_ = port & 0xff; + + // Save IP address in network byte order. + address_ = endpoint.address().to_v4().to_bytes(); + } + + std::array buffers() const + { + return + { + { + asio::buffer(&version_, 1), + asio::buffer(&command_, 1), + asio::buffer(&port_high_byte_, 1), + asio::buffer(&port_low_byte_, 1), + asio::buffer(address_), + asio::buffer(user_id_), + asio::buffer(&null_byte_, 1) + } + }; + } + +private: + unsigned char version_; + unsigned char command_; + unsigned char port_high_byte_; + unsigned char port_low_byte_; + asio::ip::address_v4::bytes_type address_; + std::string user_id_; + unsigned char null_byte_; +}; + +class reply +{ +public: + enum status_type + { + request_granted = 0x5a, + request_failed = 0x5b, + request_failed_no_identd = 0x5c, + request_failed_bad_user_id = 0x5d + }; + + reply() + : null_byte_(0), + status_() + { + } + + std::array buffers() + { + return + { + { + asio::buffer(&null_byte_, 1), + asio::buffer(&status_, 1), + asio::buffer(&port_high_byte_, 1), + asio::buffer(&port_low_byte_, 1), + asio::buffer(address_) + } + }; + } + + bool success() const + { + return null_byte_ == 0 && status_ == request_granted; + } + + unsigned char status() const + { + return status_; + } + + asio::ip::tcp::endpoint endpoint() const + { + unsigned short port = port_high_byte_; + port = (port << 8) & 0xff00; + port = port | port_low_byte_; + + asio::ip::address_v4 address(address_); + + return asio::ip::tcp::endpoint(address, port); + } + +private: + unsigned char null_byte_; + unsigned char status_; + unsigned char port_high_byte_; + unsigned char port_low_byte_; + asio::ip::address_v4::bytes_type address_; +}; + +} // namespace socks4 + +#endif // SOCKS4_HPP diff --git a/examples/protocols/asio/socks4/sdkconfig.defaults b/examples/protocols/asio/socks4/sdkconfig.defaults new file mode 100644 index 0000000000..a6320e2c3c --- /dev/null +++ b/examples/protocols/asio/socks4/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_RTTI=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 diff --git a/tools/ci/check_copyright_ignore.txt b/tools/ci/check_copyright_ignore.txt index b440731c43..bec8e7067c 100644 --- a/tools/ci/check_copyright_ignore.txt +++ b/tools/ci/check_copyright_ignore.txt @@ -2341,6 +2341,7 @@ examples/protocols/asio/asio_chat/main/asio_chat.cpp examples/protocols/asio/asio_chat/main/chat_message.hpp examples/protocols/asio/asio_chat/main/client.hpp examples/protocols/asio/asio_chat/main/server.hpp +examples/protocols/asio/socks4/main/socks4.hpp examples/protocols/asio/ssl_client_server/example_test.py examples/protocols/asio/ssl_client_server/main/asio_ssl_main.cpp examples/protocols/asio/tcp_echo_server/asio_tcp_server_test.py