diff --git a/.github/workflows/build_and_run_example_test_mdns.yml b/.github/workflows/build_and_run_example_test_mdns.yml index de4aac90e..3d309a6f5 100644 --- a/.github/workflows/build_and_run_example_test_mdns.yml +++ b/.github/workflows/build_and_run_example_test_mdns.yml @@ -13,8 +13,9 @@ jobs: container: espressif/idf:${{ matrix.idf_ver }} steps: - name: Checkout esp-protocols - uses: actions/checkout@master + uses: actions/checkout@v3 with: + submodules: recursive path: esp-protocols - name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }} env: @@ -23,6 +24,7 @@ jobs: run: | . ${IDF_PATH}/export.sh cd $GITHUB_WORKSPACE/esp-protocols/components/mdns/examples/ + idf.py set-target ${{ matrix.idf_target }} cat sdkconfig.ci.eth_def >> sdkconfig.defaults idf.py build rm sdkconfig.defaults @@ -32,4 +34,5 @@ jobs: cat sdkconfig.ci.eth_socket >> sdkconfig.defaults idf.py build cd $GITHUB_WORKSPACE/esp-protocols/components/mdns/tests/test_apps/ + idf.py set-target ${{ matrix.idf_target }} idf.py build diff --git a/.github/workflows/build_and_run_example_test.yml b/.github/workflows/build_and_run_example_test_websocket.yml similarity index 97% rename from .github/workflows/build_and_run_example_test.yml rename to .github/workflows/build_and_run_example_test_websocket.yml index 4f26acd48..678b6d1e6 100644 --- a/.github/workflows/build_and_run_example_test.yml +++ b/.github/workflows/build_and_run_example_test_websocket.yml @@ -13,7 +13,9 @@ jobs: container: espressif/idf:${{ matrix.idf_ver }} steps: - name: Checkout esp-protocols - uses: actions/checkout@v1 + uses: actions/checkout@v3 + with: + submodules: recursive - name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }} env: IDF_TARGET: ${{ matrix.idf_target }} diff --git a/.github/workflows/build_asio.yml b/.github/workflows/build_asio.yml new file mode 100644 index 000000000..83ccf2d94 --- /dev/null +++ b/.github/workflows/build_asio.yml @@ -0,0 +1,28 @@ +name: Build ASIO + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + idf_ver: ["latest"] + idf_target: ["esp32", "esp32s2"] + example: ["asio_chat", "async_request", "socks4", "ssl_client_server", "tcp_echo_server", "udp_echo_server"] + + runs-on: ubuntu-20.04 + container: espressif/idf:${{ matrix.idf_ver }} + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v3 + with: + path: esp-protocols + - name: Build ${{ matrix.example }} with IDF-${{ matrix.idf_ver }} for ${{ matrix.idf_target }} + env: + IDF_TARGET: ${{ matrix.idf_target }} + shell: bash + run: | + . ${IDF_PATH}/export.sh + cd $GITHUB_WORKSPACE/esp-protocols/components/asio/examples/${{ matrix.example }} + idf.py set-target ${{ matrix.idf_target }} + idf.py build diff --git a/.github/workflows/publish-docs-component.yml b/.github/workflows/publish-docs-component.yml index fbc674a2d..0eeda079c 100644 --- a/.github/workflows/publish-docs-component.yml +++ b/.github/workflows/publish-docs-component.yml @@ -39,10 +39,16 @@ jobs: cp -r html_en/. $GITHUB_WORKSPACE/docs/mdns/en cp -r html_zh_CN/. $GITHUB_WORKSPACE/docs/mdns/zh_CN + cd $GITHUB_WORKSPACE/components/asio/docs + ./generate_docs + mkdir -p $GITHUB_WORKSPACE/docs/asio + cp -r html/. $GITHUB_WORKSPACE/docs/asio + cd $GITHUB_WORKSPACE/docs touch .nojekyll echo 'esp-modem
' > index.html echo 'esp-websocket-client
' >> index.html + echo 'ASIO
' >> index.html echo 'mDNS_en
' >> index.html echo 'mDNS_zh_CN
' >> index.html @@ -50,7 +56,7 @@ jobs: - name: Upload components to component service uses: espressif/github-actions/upload_components@master with: - directories: "components/esp_modem;components/esp_websocket_client;components/mdns" + directories: "components/esp_modem;components/esp_websocket_client;components/mdns;components/asio" namespace: "espressif" api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..6ecce627b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "components/asio/asio"] + path = components/asio/asio + url = https://github.com/espressif/asio diff --git a/README.md b/README.md index c886af4b8..c422581ad 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,8 @@ * Brief introduction [README](components/esp_websocket_client/README.md) * Full html [documentation](https://espressif.github.io/esp-protocols/esp_websocket_client/index.html) + +### ASIO port + +* Brief introduction [README](components/asio/README.md) +* Full html [documentation](https://espressif.github.io/esp-protocols/asio/index.html) diff --git a/components/asio/CMakeLists.txt b/components/asio/CMakeLists.txt new file mode 100644 index 000000000..336654597 --- /dev/null +++ b/components/asio/CMakeLists.txt @@ -0,0 +1,44 @@ +if(NOT CONFIG_LWIP_IPV6 AND NOT CMAKE_BUILD_EARLY_EXPANSION) + # note: the component is still included in the build so it can become visible again in config + # without needing to re-run CMake. However no source or header files are built. + message(STATUS "IPV6 support is disabled so the asio component will not be built") + idf_component_register() + return() +endif() + +set(asio_sources "asio/asio/src/asio.cpp") + +if(CONFIG_ASIO_SSL_SUPPORT) + if(CONFIG_ASIO_USE_ESP_OPENSSL) + list(APPEND asio_sources + "port/src/asio_ssl_impl.cpp" + "port/mbedtls/src/mbedtls_context.cpp" + "port/mbedtls/src/mbedtls_engine.cpp") + set(asio_priv_includes "port/mbedtls/include") + endif() + + if(CONFIG_ASIO_USE_ESP_WOLFSSL) + list(APPEND asio_sources + "asio/asio/src/asio_ssl.cpp") + endif() +endif() + +idf_component_register(SRCS ${asio_sources} + INCLUDE_DIRS "asio/asio/include" "port/include" + PRIV_INCLUDE_DIRS ${asio_priv_includes} + REQUIRES lwip) + +if(CONFIG_ASIO_SSL_SUPPORT) + if(CONFIG_ASIO_USE_ESP_WOLFSSL) + idf_component_get_property(wolflib esp-wolfssl COMPONENT_LIB) + idf_component_get_property(wolfdir esp-wolfssl COMPONENT_DIR) + + target_link_libraries(${COMPONENT_LIB} PUBLIC ${wolflib}) + target_include_directories(${COMPONENT_LIB} PUBLIC ${wolfdir}/wolfssl/wolfssl) + endif() + + if(CONFIG_ASIO_USE_ESP_OPENSSL) + idf_component_get_property(mbedtls mbedtls COMPONENT_LIB) + target_link_libraries(${COMPONENT_LIB} PUBLIC ${mbedtls}) + endif() +endif() diff --git a/components/asio/Kconfig b/components/asio/Kconfig new file mode 100644 index 000000000..6754e1aa2 --- /dev/null +++ b/components/asio/Kconfig @@ -0,0 +1,36 @@ +menu "ESP-ASIO" + visible if LWIP_IPV6 + + config ASIO_SSL_SUPPORT + bool "Enable SSL/TLS support of ASIO" + default n + help + Enable support for basic SSL/TLS features, available for mbedTLS/OpenSSL + as well as wolfSSL TLS library. + + choice ASIO_SSL_LIBRARY_CHOICE + prompt "Choose SSL/TLS library for ESP-TLS (See help for more Info)" + default ASIO_USE_ESP_OPENSSL + depends on ASIO_SSL_SUPPORT + help + The ASIO support multiple backend TLS libraries. Currently the mbedTLS with a thin ESP-OpenSSL + port layer (default choice) and WolfSSL are supported. + Different TLS libraries may support different features and have different resource + usage. Consult the ESP-TLS documentation in ESP-IDF Programming guide for more details. + config ASIO_USE_ESP_OPENSSL + bool "esp-openssl" + config ASIO_USE_ESP_WOLFSSL + depends on TLS_STACK_WOLFSSL + bool "wolfSSL (License info in wolfSSL directory README)" + endchoice + + config ASIO_SSL_BIO_SIZE + int "Size of BIO object" + default 1024 + depends on ASIO_SSL_SUPPORT + help + Size in bytes of SSL-BIO implementation. + Reducing the BIO size saves more RAM, but may slow down input output operations due to + fragmentation. + +endmenu diff --git a/components/asio/README.md b/components/asio/README.md new file mode 100644 index 000000000..acfbd5f91 --- /dev/null +++ b/components/asio/README.md @@ -0,0 +1,11 @@ +# ASIO port + +Asio is a cross-platform C++ library, see https://think-async.com/Asio/. It provides a consistent asynchronous model using a modern C++ approach. + +## Examples + +Get started with example test :example:`examples `: + +## Documentation + +* View the full [html documentation](https://espressif.github.io/esp-protocols/asio/index.html) diff --git a/components/asio/asio b/components/asio/asio new file mode 160000 index 000000000..58384fb6a --- /dev/null +++ b/components/asio/asio @@ -0,0 +1 @@ +Subproject commit 58384fb6af92e7952b1534acb3004924f7ea0565 diff --git a/components/asio/docs/Doxyfile b/components/asio/docs/Doxyfile new file mode 100755 index 000000000..1e95d8217 --- /dev/null +++ b/components/asio/docs/Doxyfile @@ -0,0 +1,73 @@ +# This is Doxygen configuration file +# +# Doxygen provides over 260 configuration statements +# To make this file easier to follow, +# it contains only statements that are non-default +# +# NOTE: +# It is recommended not to change defaults unless specifically required +# Test any changes how they affect generated documentation +# Make sure that correct warnings are generated to flag issues with documented code +# +# For the complete list of configuration statements see: +# http://doxygen.nl/manual/config.html + + +PROJECT_NAME = "ESP Protocols Programming Guide" + +## The 'INPUT' statement below is used as input by script 'gen-df-input.py' +## to automatically generate API reference list files heder_file.inc +## These files are placed in '_inc' directory +## and used to include in API reference documentation + + +## Get warnings for functions that have no documentation for their parameters or return value +## +WARN_NO_PARAMDOC = YES + +## Enable preprocessing and remove __attribute__(...) expressions from the INPUT files +## +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +PREDEFINED = \ + $(ENV_DOXYGEN_DEFINES) \ + __DOXYGEN__=1 \ + __attribute__(x)= \ + _Static_assert()= \ + IDF_DEPRECATED(X)= \ + IRAM_ATTR= \ + configSUPPORT_DYNAMIC_ALLOCATION=1 \ + configSUPPORT_STATIC_ALLOCATION=1 \ + configQUEUE_REGISTRY_SIZE=1 \ + configUSE_RECURSIVE_MUTEXES=1 \ + configTHREAD_LOCAL_STORAGE_DELETE_CALLBACKS=1 \ + configNUM_THREAD_LOCAL_STORAGE_POINTERS=1 \ + configUSE_APPLICATION_TASK_TAG=1 \ + configTASKLIST_INCLUDE_COREID=1 \ + "ESP_EVENT_DECLARE_BASE(x)=extern esp_event_base_t x" + +## Do not complain about not having dot +## +HAVE_DOT = NO + +## Generate XML that is required for Breathe +## +GENERATE_XML = YES +XML_OUTPUT = xml + +GENERATE_HTML = NO +HAVE_DOT = NO +GENERATE_LATEX = NO +GENERATE_MAN = YES +GENERATE_RTF = NO + +## Skip distracting progress messages +## +QUIET = YES + +## Enable Section Tags for conditional documentation +## +ENABLED_SECTIONS += \ + DOC_EXCLUDE_HEADER_SECTION \ ## To conditionally remove doc sections from IDF source files without affecting documentation in upstream files. + DOC_SINGLE_GROUP ## To conditionally remove groups from the documentation and create a 'flat' document without affecting documentation in upstream files. diff --git a/components/asio/docs/conf_common.py b/components/asio/docs/conf_common.py new file mode 100644 index 000000000..10d1e997e --- /dev/null +++ b/components/asio/docs/conf_common.py @@ -0,0 +1,21 @@ +from esp_docs.conf_docs import * # noqa: F403,F401 + +extensions += ['sphinx_copybutton', + # Needed as a trigger for running doxygen + 'esp_docs.esp_extensions.dummy_build_system', + 'esp_docs.esp_extensions.run_doxygen', + ] + +# link roles config +github_repo = 'espressif/esp-protocols' + +# context used by sphinx_idf_theme +html_context['github_user'] = 'espressif' +html_context['github_repo'] = 'esp-protocols' + +# Extra options required by sphinx_idf_theme +project_slug = 'esp-idf' # >=5.0 +versions_url = 'https://github.com/espressif/esp-protocols/docs/docs_versions.js' + +idf_targets = ['esp32'] +languages = ['en', 'zh_CN'] diff --git a/components/asio/docs/en/conf.py b/components/asio/docs/en/conf.py new file mode 100644 index 000000000..bba642268 --- /dev/null +++ b/components/asio/docs/en/conf.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# English Language RTD & Sphinx config file +# +# Uses ../conf_common.py for most non-language-specific settings. + +# Importing conf_common adds all the non-language-specific +# parts to this conf module + +try: + from conf_common import * # noqa: F403,F401 +except ImportError: + import os + import sys + sys.path.insert(0, os.path.abspath('../')) + from conf_common import * # noqa: F403,F401 + +# General information about the project. +project = u'ESP-Protocols' +copyright = u'2016 - 2022, Espressif Systems (Shanghai) Co., Ltd' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = 'en' diff --git a/components/asio/docs/en/index.rst b/components/asio/docs/en/index.rst new file mode 100644 index 000000000..56ff694f2 --- /dev/null +++ b/components/asio/docs/en/index.rst @@ -0,0 +1,42 @@ +ASIO port +========= + +Overview +-------- +Asio is a cross-platform C++ library, see https://think-async.com/Asio/. It provides a consistent asynchronous model using a modern C++ approach. + + +ASIO documentation +^^^^^^^^^^^^^^^^^^ +Please refer to the original asio documentation at https://think-async.com/Asio/Documentation. +Asio also comes with a number of examples which could be find under Documentation/Examples on that web site. + +Supported features +^^^^^^^^^^^^^^^^^^ +ESP platform port currently supports only network asynchronous socket operations; does not support serial port. +SSL/TLS support is disabled by default and could be enabled in component configuration menu by choosing TLS library from + +- mbedTLS with OpenSSL translation layer (default option) +- wolfSSL + +SSL support is very basic at this stage and it does include following features: + +- Verification callbacks +- DH property files +- Certificates/private keys file APIs + +Internal asio settings for ESP include + +- EXCEPTIONS are enabled in ASIO if enabled in menuconfig +- TYPEID is enabled in ASIO if enabled in menuconfig + +Application Example +------------------- +ESP examples are based on standard asio :example:`examples <../examples>`: + +- :example:`udp_echo_server <../examples/udp_echo_server>` +- :example:`tcp_echo_server <../examples/tcp_echo_server>` +- :example:`asio_chat <../examples/asio_chat>` +- :example:`ssl_client_server <../examples/ssl_client_server>` + +Please refer to the specific example README.md for details diff --git a/components/asio/docs/generate_docs b/components/asio/docs/generate_docs new file mode 100755 index 000000000..e60c1a8f9 --- /dev/null +++ b/components/asio/docs/generate_docs @@ -0,0 +1,27 @@ +build-docs --target esp32 --language en + +cp -rf _build/en/esp32/html . +rm -rf _build __pycache__ + +# Modifes some version and target fields of index.html +echo "" >> html/index.html + diff --git a/components/asio/docs/zh_CN/conf.py b/components/asio/docs/zh_CN/conf.py new file mode 100644 index 000000000..cc6fbc271 --- /dev/null +++ b/components/asio/docs/zh_CN/conf.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# English Language RTD & Sphinx config file +# +# Uses ../conf_common.py for most non-language-specific settings. + +# Importing conf_common adds all the non-language-specific +# parts to this conf module +try: + from conf_common import * # noqa: F403,F401 +except ImportError: + import os + import sys + sys.path.insert(0, os.path.abspath('..')) + from conf_common import * # noqa: F403,F401 + +import datetime + +current_year = datetime.datetime.now().year + +# General information about the project. +project = u'ESP-IDF 编程指南' +copyright = u'2016 - {} 乐鑫信息科技(上海)股份有限公司'.format(current_year) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +language = 'zh_CN' diff --git a/components/asio/docs/zh_CN/index.rst b/components/asio/docs/zh_CN/index.rst new file mode 100644 index 000000000..d1afa231e --- /dev/null +++ b/components/asio/docs/zh_CN/index.rst @@ -0,0 +1 @@ +.. include:: ../../../en/api-reference/protocols/asio.rst \ No newline at end of file diff --git a/components/asio/examples/asio_chat/CMakeLists.txt b/components/asio/examples/asio_chat/CMakeLists.txt new file mode 100644 index 000000000..2d3c9ecbe --- /dev/null +++ b/components/asio/examples/asio_chat/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.16) + +# (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 ../../../../common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_chat) diff --git a/components/asio/examples/asio_chat/README.md b/components/asio/examples/asio_chat/README.md new file mode 100644 index 000000000..2896e9ab7 --- /dev/null +++ b/components/asio/examples/asio_chat/README.md @@ -0,0 +1,65 @@ +| Supported Targets | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-C3 | +| ----------------- | ----- | -------- | -------- | -------- | + + +# Asio chat client and server examples + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +The application aims to demonstrate a simple use of Asio library in different modes. +In project settings it could be configured to run either a Asio chat server, a Asio chat client, or both. + +## How to use example + +The example is configured by default as an Asio chat client. + +Note that the example uses string representation of IP addresses and ports. + +You can find the upstream asio chat implementation [here] https://github.com/chriskohlhoff/asio/tree/master/asio/src/examples/cpp11/chat + +### Asio Client + +In the client mode, the example connects to the configured address, sends the message, which was inserted as an input in the terminal, and receives a response. + +### Asio Server + +In the server mode, Asio chat server with a specified port number is created and being polled till a connection request from the client arrives. +Chat server echoes a message (received from any client) to all connected clients. + +## Configure the project + +``` +idf.py menuconfig +``` + +Set following parameters under Example Configuration Options: + +* Set `EXAMPLE_CHAT_SERVER` to use the example as an ASIO chat server + * Configure `EXAMPLE_CHAT_SERVER_BIND_PORT` to the port number. + +* Set `EXAMPLE_CHAT_CLIENT` to use the example as an ASIO chat client + * Configure `EXAMPLE_CHAT_CLIENT_CONNECT_ADDRESS` to a string representation of the address to connect the client to. + * Configure `EXAMPLE_CHAT_CLIENT_CONNECT_PORT` to the port number. + +* Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more d etails. + +## Running the example in server mode + +- Configure the example according "Configure the project" section. +- Run `idf.py -p PORT flash monitor` to build and upload the example to your board and connect to it's serial terminal. +- Wait for the board to connect to WiFi or Ethernet (note the IP address). +- Connect to the server using multiple clients, for example using any option below. + - build and run asio chat client on your host machine + - run chat_client asio example on ESP platform + - since chat messages consists of ASCII size and message, it is possible to + netcat `nc IP PORT` and type for example ` 4ABC` to transmit 'ABC\n' + +## Running the example in client mode + +- Configure the example according "Configure the project" section. +- Start chat server either on host machine or as another ESP device running chat_server example. +- Run `idf.py -p PORT flash monitor` to build and upload the example to your board and connect to it's serial terminal. +- Wait for the board to connect to WiFi or Ethernet. +- Receive and send messages to/from other clients on stdin/stdout via serial terminal. + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/components/asio/examples/asio_chat/example_test.py b/components/asio/examples/asio_chat/example_test.py new file mode 100644 index 000000000..7dc5c7c2b --- /dev/null +++ b/components/asio/examples/asio_chat/example_test.py @@ -0,0 +1,29 @@ +# This example code is in the Public Domain (or CC0 licensed, at your option.) + +# Unless required by applicable law or agreed to in writing, this +# software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. + +# -*- coding: utf-8 -*- + +from __future__ import print_function, unicode_literals + +import re + +import ttfw_idf + + +@ttfw_idf.idf_example_test(env_tag='Example_GENERIC') +def test_examples_asio_chat(env, _): # type: (ttfw_idf.TinyFW.Env, None) -> None + msg = 'asio-chat: received hi' + dut = env.get_dut('asio_chat', 'examples/protocols/asio/asio_chat') + # start the test and expect the client to receive back it's original data + dut.start_app() + dut.expect(re.compile(r'{}'.format('Waiting for input')), timeout=30) + dut.write(msg) + dut.write('exit') + dut.expect(re.compile(r'{}'.format(msg)), timeout=30) + + +if __name__ == '__main__': + test_examples_asio_chat() diff --git a/components/asio/examples/asio_chat/main/CMakeLists.txt b/components/asio/examples/asio_chat/main/CMakeLists.txt new file mode 100644 index 000000000..8f1954423 --- /dev/null +++ b/components/asio/examples/asio_chat/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "asio_chat.cpp" + INCLUDE_DIRS ".") diff --git a/components/asio/examples/asio_chat/main/Kconfig.projbuild b/components/asio/examples/asio_chat/main/Kconfig.projbuild new file mode 100644 index 000000000..99f08cdbd --- /dev/null +++ b/components/asio/examples/asio_chat/main/Kconfig.projbuild @@ -0,0 +1,39 @@ +menu "Example Configuration" + + config EXAMPLE_CHAT_SERVER + bool "Asio example chat server" + default n + help + This example will setup a chat server, binds it to the specified address + and starts listening. + + if EXAMPLE_CHAT_SERVER + config EXAMPLE_CHAT_SERVER_BIND_PORT + string "Asio example server bind port" + default "3344" + help + Server listener's socket would be bound to this port. + endif + + config EXAMPLE_CHAT_CLIENT + bool "Asio example chat client" + default y + help + This example will setup an asio chat client. + and sends the data. + + if EXAMPLE_CHAT_CLIENT + config EXAMPLE_CHAT_CLIENT_CONNECT_ADDRESS + string "Client connection address" + default "192.168.0.1" + help + Client's socket would connect to this address/host. + + config EXAMPLE_CHAT_CLIENT_CONNECT_PORT + string "Client connection port" + default "3344" + help + Client's connection port. + endif + +endmenu diff --git a/components/asio/examples/asio_chat/main/asio_chat.cpp b/components/asio/examples/asio_chat/main/asio_chat.cpp new file mode 100644 index 000000000..be2329d15 --- /dev/null +++ b/components/asio/examples/asio_chat/main/asio_chat.cpp @@ -0,0 +1,119 @@ +/* ASIO chat server client example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include "protocol_examples_common.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "server.hpp" +#include "client.hpp" +#include +#include + +using asio::ip::tcp; + +static const char *TAG = "asio-chat"; + +// This variable is necessary for `python test` execution, it provides synchronisation between server/client(as server should be started before client) +std::mutex server_ready; + +#ifdef CONFIG_EXAMPLE_CHAT_CLIENT +static void get_string(char *line, size_t size) +{ + int count = 0; + while (count < size) { + int c = fgetc(stdin); + if (c == '\n') { + line[count] = '\0'; + break; + } else if (c > 0 && c < 127) { + line[count] = c; + ++count; + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void start_client(void) +{ + const std::string port(CONFIG_EXAMPLE_CHAT_CLIENT_CONNECT_PORT); + const std::string name(CONFIG_EXAMPLE_CHAT_CLIENT_CONNECT_ADDRESS); + asio::io_context io_context; + char line[128]; + + tcp::resolver resolver(io_context); + auto endpoints = resolver.resolve(name, port); + chat_client c(io_context, endpoints); +#ifdef CONFIG_EXAMPLE_CHAT_SERVER + std::lock_guard guard(server_ready); +#endif + std::thread t([&io_context]() { try { + io_context.run(); + } catch (const std::exception &e) { + ESP_LOGE(TAG, "Exception occured during client thread execution %s", e.what()); + } + catch (...) { + ESP_LOGE(TAG, "Unknown exception"); + }}); + do { + ESP_LOGI(TAG, "CLIENT: Waiting for input"); + get_string(line, sizeof(line)); + + chat_message msg; + msg.body_length(std::strlen(line)); + std::memcpy(msg.body(), line, msg.body_length()); + msg.encode_header(); + c.write(msg); + sleep(1); + } while (strcmp(line, "exit") != 0); + + c.close(); + t.join(); +} +#endif // CONFIG_EXAMPLE_CHAT_CLIENT + +extern "C" void app_main(void) +{ + ESP_ERROR_CHECK(nvs_flash_init()); + esp_netif_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* 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()); + + try { +#ifdef CONFIG_EXAMPLE_CHAT_SERVER + asio::io_context io_context; + chat_server server(io_context, tcp::endpoint(tcp::v4(), std::atoi(CONFIG_EXAMPLE_CHAT_SERVER_BIND_PORT))); + std::thread t = std::thread([&io_context]() { // Chat server starting here + try { + io_context.run(); + } catch (const std::exception &e) { + ESP_LOGE(TAG, "Exception occured during server thread execution %s", e.what()); + } + catch (...) { + ESP_LOGE(TAG, "Unknown exception"); + }});; +#endif +#ifdef CONFIG_EXAMPLE_CHAT_CLIENT + start_client(); +#endif +#ifdef CONFIG_EXAMPLE_CHAT_SERVER + t.join(); +#endif + } catch (const std::exception &e) { + ESP_LOGE(TAG, "Exception occured during run %s", e.what()); + } catch (...) { + ESP_LOGE(TAG, "Unknown exception"); + } + ESP_ERROR_CHECK(example_disconnect()); +} diff --git a/components/asio/examples/asio_chat/main/chat_message.hpp b/components/asio/examples/asio_chat/main/chat_message.hpp new file mode 100644 index 000000000..a6ffc83b8 --- /dev/null +++ b/components/asio/examples/asio_chat/main/chat_message.hpp @@ -0,0 +1,91 @@ +// +// chat_message.hpp +// ~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2018 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 CHAT_MESSAGE_HPP +#define CHAT_MESSAGE_HPP + +#include +#include +#include + +class chat_message +{ +public: + static constexpr std::size_t header_length = 4; + static constexpr std::size_t max_body_length = 512; + + chat_message() + : body_length_(0) + { + } + + const char* data() const + { + return data_; + } + + char* data() + { + return data_; + } + + std::size_t length() const + { + return header_length + body_length_; + } + + const char* body() const + { + return data_ + header_length; + } + + char* body() + { + return data_ + header_length; + } + + std::size_t body_length() const + { + return body_length_; + } + + void body_length(std::size_t new_length) + { + body_length_ = new_length; + if (body_length_ > max_body_length) + body_length_ = max_body_length; + } + + bool decode_header() + { + char header[header_length + 1] = ""; + std::strncat(header, data_, header_length); + body_length_ = std::atoi(header); + if (body_length_ > max_body_length) + { + body_length_ = 0; + return false; + } + return true; + } + + void encode_header() + { + char header[header_length + 1] = ""; + std::sprintf(header, "%4d", static_cast(body_length_)); + std::memcpy(data_, header, header_length); + } + +private: + char data_[header_length + max_body_length]; + std::size_t body_length_; +}; + +#endif // CHAT_MESSAGE_HPP diff --git a/components/asio/examples/asio_chat/main/client.hpp b/components/asio/examples/asio_chat/main/client.hpp new file mode 100644 index 000000000..09baece98 --- /dev/null +++ b/components/asio/examples/asio_chat/main/client.hpp @@ -0,0 +1,126 @@ +// +// client.hpp +// ~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2018 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 CHAT_CLIENT_HPP +#define CHAT_CLIENT_HPP + +#include +#include "asio.hpp" +#include "chat_message.hpp" + +typedef std::deque chat_message_queue; + +class chat_client +{ +public: + chat_client(asio::io_context& io_context, + const asio::ip::tcp::resolver::results_type& endpoints) + : io_context_(io_context), + socket_(io_context) + { + do_connect(endpoints); + } + + void write(const chat_message& msg) + { + asio::post(io_context_, + [this, msg]() + { + bool write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(msg); + if (!write_in_progress) + { + do_write(); + } + }); + } + + void close() + { + asio::post(io_context_, [this]() { socket_.close(); }); + } + +private: +void do_connect(const asio::ip::tcp::resolver::results_type& endpoints) + { + asio::async_connect(socket_, endpoints, + [this](std::error_code ec, asio::ip::tcp::endpoint) + { + if (!ec) + { + do_read_header(); + } + }); + } + + void do_read_header() + { + asio::async_read(socket_, + asio::buffer(read_msg_.data(), chat_message::header_length), + [this](std::error_code ec, std::size_t /*length*/) + { + if (!ec && read_msg_.decode_header()) + { + do_read_body(); + } + else + { + socket_.close(); + } + }); + } + + void do_read_body() + { + asio::async_read(socket_, + asio::buffer(read_msg_.body(), read_msg_.body_length()), + [this](std::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + do_read_header(); + } + else + { + socket_.close(); + } + }); + } + + void do_write() + { + asio::async_write(socket_, + asio::buffer(write_msgs_.front().data(), + write_msgs_.front().length()), + [this](std::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + socket_.close(); + } + }); + } + +private: + asio::io_context& io_context_; + asio::ip::tcp::socket socket_; + chat_message read_msg_; + chat_message_queue write_msgs_; +}; + +#endif // CHAT_CLIENT_HPP diff --git a/components/asio/examples/asio_chat/main/server.hpp b/components/asio/examples/asio_chat/main/server.hpp new file mode 100644 index 000000000..450e255a4 --- /dev/null +++ b/components/asio/examples/asio_chat/main/server.hpp @@ -0,0 +1,202 @@ +// +// server.hpp +// ~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2018 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 CHAT_SERVER_HPP +#define CHAT_SERVER_HPP + +#include +#include +#include +#include +#include "asio.hpp" +#include "chat_message.hpp" + +//---------------------------------------------------------------------- + +typedef std::deque chat_message_queue; + +extern std::mutex server_ready; + +//---------------------------------------------------------------------- + + +class chat_participant +{ +public: + virtual ~chat_participant() {} + virtual void deliver(const chat_message& msg) = 0; +}; + +typedef std::shared_ptr chat_participant_ptr; + +//---------------------------------------------------------------------- + +class chat_room +{ +public: + void join(chat_participant_ptr participant) + { + participants_.insert(participant); + for (auto msg: recent_msgs_) + participant->deliver(msg); + } + + void leave(chat_participant_ptr participant) + { + participants_.erase(participant); + } + + void deliver(const chat_message& msg) + { + recent_msgs_.push_back(msg); + while (recent_msgs_.size() > max_recent_msgs) + recent_msgs_.pop_front(); + + for (auto participant: participants_) + participant->deliver(msg); + } + +private: + std::set participants_; + enum { max_recent_msgs = 100 }; + chat_message_queue recent_msgs_; +}; + +//---------------------------------------------------------------------- + + +class chat_session + : public chat_participant, + public std::enable_shared_from_this +{ +public: + chat_session(asio::ip::tcp::socket socket, chat_room& room) + : socket_(std::move(socket)), + room_(room) + { + } + + void start() + { + room_.join(shared_from_this()); + do_read_header(); + } + + void deliver(const chat_message& msg) + { + bool write_in_progress = !write_msgs_.empty(); + write_msgs_.push_back(msg); + if (!write_in_progress) + { + do_write(); + } + } + +private: + void do_read_header() + { + auto self(shared_from_this()); + asio::async_read(socket_, + asio::buffer(read_msg_.data(), chat_message::header_length), + [this, self](std::error_code ec, std::size_t /*length*/) + { + if (!ec && read_msg_.decode_header()) + { + do_read_body(); + } + else + { + room_.leave(shared_from_this()); + } + }); + } + + void do_read_body() + { + auto self(shared_from_this()); + asio::async_read(socket_, + asio::buffer(read_msg_.body(), read_msg_.body_length()), + [this, self](std::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + ESP_LOGD("asio-chat:", "%s", read_msg_.body()); + room_.deliver(read_msg_); + do_read_header(); + } + else + { + room_.leave(shared_from_this()); + } + }); + } + + void do_write() + { + auto self(shared_from_this()); + asio::async_write(socket_, + asio::buffer(write_msgs_.front().data(), + write_msgs_.front().length()), + [this, self](std::error_code ec, std::size_t /*length*/) + { + if (!ec) + { + write_msgs_.pop_front(); + if (!write_msgs_.empty()) + { + do_write(); + } + } + else + { + room_.leave(shared_from_this()); + } + }); + } + + asio::ip::tcp::socket socket_; + chat_room& room_; + chat_message read_msg_; + chat_message_queue write_msgs_; + }; + +//---------------------------------------------------------------------- + +class chat_server +{ +public: + chat_server(asio::io_context& io_context, + const asio::ip::tcp::endpoint& endpoint) + : acceptor_(io_context, endpoint) + { + do_accept(); + } + +private: + void do_accept() + { + std::lock_guard guard(server_ready); + acceptor_.async_accept( + [this](std::error_code ec, asio::ip::tcp::socket socket) + { + if (!ec) + { + std::make_shared(std::move(socket), room_)->start(); + } + + do_accept(); + }); + } + + asio::ip::tcp::acceptor acceptor_; + chat_room room_; +}; + +#endif // CHAT_SERVER_HPP diff --git a/components/asio/examples/asio_chat/sdkconfig.ci b/components/asio/examples/asio_chat/sdkconfig.ci new file mode 100644 index 000000000..4829992d0 --- /dev/null +++ b/components/asio/examples/asio_chat/sdkconfig.ci @@ -0,0 +1,6 @@ +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CHAT_CLIENT=y +CONFIG_EXAMPLE_CHAT_SERVER=y +CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y +CONFIG_EXAMPLE_CHAT_CLIENT_CONNECT_ADDRESS="localhost" diff --git a/components/asio/examples/asio_chat/sdkconfig.defaults b/components/asio/examples/asio_chat/sdkconfig.defaults new file mode 100644 index 000000000..3a66e33fd --- /dev/null +++ b/components/asio/examples/asio_chat/sdkconfig.defaults @@ -0,0 +1,2 @@ +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 +CONFIG_COMPILER_CXX_EXCEPTIONS=y diff --git a/components/asio/examples/async_request/CMakeLists.txt b/components/asio/examples/async_request/CMakeLists.txt new file mode 100644 index 000000000..ff659707e --- /dev/null +++ b/components/asio/examples/async_request/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.16) + +# (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 ../../../../common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(async_http_request) diff --git a/components/asio/examples/async_request/README.md b/components/asio/examples/async_request/README.md new file mode 100644 index 000000000..c2b114511 --- /dev/null +++ b/components/asio/examples/async_request/README.md @@ -0,0 +1,52 @@ +| Supported Targets | ESP32 | +| ----------------- | ----- | + +# 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 compose async operations using ASIO to build network protocols and operations. + +# Configure and Building example + +This example doesn't require any configuration, just build it with + +``` +idf.py build +``` + +# Async operations composition and automatic lifetime control + +On this example we compose the operation by starting the next step in the chain inside the completion handler of the +previous operation. Also we pass the `Connection` class itself as the parameter of its final handler to be owned by +the following operation. This is possible due to the control of lifetime by the usage of `std::shared_ptr`. + +The control of lifetime of the class, done by `std::shared_ptr` usage, guarantee that the data will be available for +async operations until it's not needed any more. This makes necessary that all of the async operation class must start +its lifetime as a `std::shared_ptr` due to the usage of `std::enable_shared_from_this`. + + + User creates a shared_ptr──┐ + of AddressResolution and │ + ask for resolve. │ + The handler for the ┌▼─────────────────────┐ + complete operation is sent│ AddressResolution │ In the completion of resolve a connection is created. + └─────────────────┬────┘ AddressResolution is automaticly destroyed since it's + │ no longer needed + ┌─▼────────────────────────────────────┐ + │ Connection │ + └──────┬───────────────────────────────┘ + Http::Session is created once we have a Connection. │ + Connection is passed to Http::Session that holds it │ + avoiding it's destruction. │ + ┌─▼───────────────────────────────┐ + │ Http::Session │ + └────────┬────────────────────────┘ + After the HTTP request is │ + sent the completion handler │ + is called. │ + └────►Completion Handler() + + +The previous diagram shows the process and the life span of each of the tasks in this examples. At each stage the +object responsible for the last action inject itself to the completion handler of the next stage for reuse. diff --git a/components/asio/examples/async_request/main/CMakeLists.txt b/components/asio/examples/async_request/main/CMakeLists.txt new file mode 100644 index 000000000..018c22a0a --- /dev/null +++ b/components/asio/examples/async_request/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "async_http_request.cpp" + INCLUDE_DIRS ".") diff --git a/components/asio/examples/async_request/main/async_http_request.cpp b/components/asio/examples/async_request/main/async_http_request.cpp new file mode 100644 index 000000000..3f2df210b --- /dev/null +++ b/components/asio/examples/async_request/main/async_http_request.cpp @@ -0,0 +1,369 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + * + * ASIO HTTP request example +*/ + +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "protocol_examples_common.h" + +constexpr auto TAG = "async_request"; +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 +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; +}; + +/** @brief Execute a fully async HTTP request + * + * @tparam completion_handler + * @param ctx io context + * @param request + * + * @note : We build this function as a simpler interface to compose the operations of connecting to + * the address and running the HTTP session. The Http::Session class is injected to the completion handler + * for further use. + */ +template +void request_async(asio::io_context &context, const Request &request, CompletionToken &&completion_handler) +{ + /* + * The first step is to resolve the address we want to connect to. + * The AddressResolution itself is injected to the completion handler. + * + * This shared_ptr is destroyed by the end of the scope. Pay attention that this is a non blocking function + * the lifetime of the object is extended by the resolve call + */ + std::make_shared(context)->resolve(request.host(), request.service_port(), + [&context, &request, completion_handler](std::shared_ptr resolver, tcp::resolver::results_type results) { + /* After resolution we create a Connection. + * The completion handler gets a shared_ptr to receive the connection, once the + * connection process is complete. + */ + std::make_shared(context)->start(results, + [&request, completion_handler](std::shared_ptr connection) { + // Now we create a HTTP::Session and inject the necessary connection. + std::make_shared(connection)->send_request(request, completion_handler); + }); + }); +} +}// 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"); + Http::request_async(io_context, request, [](std::shared_ptr session, Http::Response response) { + /* + * We only print the response here but could reuse session for other requests. + */ + 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/components/asio/examples/socks4/CMakeLists.txt b/components/asio/examples/socks4/CMakeLists.txt new file mode 100644 index 000000000..f1ffe9d12 --- /dev/null +++ b/components/asio/examples/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.16) + +# (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 ../../../../common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_sock4) diff --git a/components/asio/examples/socks4/README.md b/components/asio/examples/socks4/README.md new file mode 100644 index 000000000..f9cc88b46 --- /dev/null +++ b/components/asio/examples/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/components/asio/examples/socks4/main/CMakeLists.txt b/components/asio/examples/socks4/main/CMakeLists.txt new file mode 100644 index 000000000..517ab52d9 --- /dev/null +++ b/components/asio/examples/socks4/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "socks4.cpp" + INCLUDE_DIRS ".") diff --git a/components/asio/examples/socks4/main/Kconfig.projbuild b/components/asio/examples/socks4/main/Kconfig.projbuild new file mode 100644 index 000000000..69dd024cd --- /dev/null +++ b/components/asio/examples/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_SERVICE + string "Proxy Service Type" + default "myport" + help + Service type. Due to a limitation of lwip, must + be the port number e.g. "1080". + +endmenu diff --git a/components/asio/examples/socks4/main/socks4.cpp b/components/asio/examples/socks4/main/socks4.cpp new file mode 100644 index 000000000..5bd0011ed --- /dev/null +++ b/components/asio/examples/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_SERVICE, 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/components/asio/examples/socks4/main/socks4.hpp b/components/asio/examples/socks4/main/socks4.hpp new file mode 100644 index 000000000..a039c07ca --- /dev/null +++ b/components/asio/examples/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/components/asio/examples/socks4/sdkconfig.defaults b/components/asio/examples/socks4/sdkconfig.defaults new file mode 100644 index 000000000..a6320e2c3 --- /dev/null +++ b/components/asio/examples/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/components/asio/examples/ssl_client_server/CMakeLists.txt b/components/asio/examples/ssl_client_server/CMakeLists.txt new file mode 100644 index 000000000..73ee939a7 --- /dev/null +++ b/components/asio/examples/ssl_client_server/CMakeLists.txt @@ -0,0 +1,11 @@ +# 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.16) + +# (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 ../../../../common_components/protocol_examples_common) +set(EXCLUDE_COMPONENTS openssl) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_ssl_client_server) diff --git a/components/asio/examples/ssl_client_server/README.md b/components/asio/examples/ssl_client_server/README.md new file mode 100644 index 000000000..fbf2cc15e --- /dev/null +++ b/components/asio/examples/ssl_client_server/README.md @@ -0,0 +1,88 @@ +| Supported Targets | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-C3 | +| ----------------- | ----- | -------- | -------- | -------- | + +# Asio SSL client/server example + +Simple Asio client and server with SSL/TLS transport + +## How to Use Example + +### Hardware Required + +This example can be executed on any ESP platform board. No external connection is required, it is recommended though +to connect to internet or a local network via WiFi or Ethernet to easily exercise features of this example. + +### Configure the project + +* Open the project configuration menu (`idf.py menuconfig`) +* Configure Wi-Fi or Ethernet under "Example Connection Configuration" menu. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. +* Enable the ASIO client and set server's host name to examine client's functionality. +The ASIO client connects to the configured server and sends default payload string "GET / HTTP/1.1" +* Enable the ASIO server to examine server's functionality. The ASIO server listens to connection and echos back what was received. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +### Client connecting to public server + +The below output illustrates the client connecting to a public https server. + +``` +I (1267) example_connect: Waiting for IP(s) +I (2587) wifi:new:<11,0>, old:<1,0>, ap:<255,255>, sta:<11,0>, prof:1 +I (3367) wifi:state: init -> auth (b0) +I (3377) wifi:state: auth -> assoc (0) +I (3387) wifi:state: assoc -> run (10) +I (3397) wifi:security type: 3, phy: bgn, rssi: -49 +I (3397) wifi:pm start, type: 1 +I (3457) wifi:AP's beacon interval = 102400 us, DTIM period = 1 +I (4747) example_connect: Got IPv6 event: Interface "example_connect: sta" address: fe80:0000:0000:0000:260a:xxxx:xxxx:xxxx, type: ESP_IP6_ADDR_IS_LINK_LOCAL +I (5247) esp_netif_handlers: example_connect: sta ip: 192.168.32.69, mask: 255.255.252.0, gw: 192.168.32.3 +I (5247) example_connect: Got IPv4 event: Interface "example_connect: sta" address: 192.168.32.69 +I (5257) example_connect: Connected to example_connect: sta +I (5257) example_connect: - IPv4 address: 192.168.32.69 +I (5267) example_connect: - IPv6 address: fe80:0000:0000:0000:260a:xxxx:xxxx:xxxx, type: ESP_IP6_ADDR_IS_LINK_LOCAL +W (5277) esp32_asio_pthread: pthread_condattr_setclock: not yet supported! +W (5297) esp32_asio_pthread: pthread_condattr_setclock: not yet supported! +Reply: HTTP/1.1 200 OK +D +``` +### Both server and client enabled + +The below output demonstrates the client connecting to the ASIO server via loopback interface, so no WiFi, nor Ethernet connection +was established. +``` +I (0) cpu_start: App cpu up. +I (495) heap_init: Initializing. RAM available for dynamic allocation: +I (502) heap_init: At 3FFAE6E0 len 00001920 (6 KiB): DRAM +I (508) heap_init: At 3FFB5400 len 0002AC00 (171 KiB): DRAM +I (515) heap_init: At 3FFE0440 len 00003AE0 (14 KiB): D/IRAM +I (521) heap_init: At 3FFE4350 len 0001BCB0 (111 KiB): D/IRAM +I (527) heap_init: At 4008BB80 len 00014480 (81 KiB): IRAM +I (534) cpu_start: Pro cpu start user code +I (556) spi_flash: detected chip: gd +I (556) spi_flash: flash io: dio +W (556) spi_flash: Detected size(4096k) larger than the size in the binary image header(2048k). Using the size in the binary image header. +I (566) cpu_start: Starting scheduler on PRO CPU. +I (0) cpu_start: Starting scheduler on APP CPU. +I (600) example_connect: Waiting for IP(s) +W (600) esp32_asio_pthread: pthread_condattr_setclock: not yet supported! +W (1610) esp32_asio_pthread: pthread_condattr_setclock: not yet supported! +W (1610) esp32_asio_pthread: pthread_condattr_setclock: not yet supported! +Server received: GET / HTTP/1.1 + + +Reply: GET / HTTP/1.1 +``` +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/components/asio/examples/ssl_client_server/example_test.py b/components/asio/examples/ssl_client_server/example_test.py new file mode 100644 index 000000000..e819380e3 --- /dev/null +++ b/components/asio/examples/ssl_client_server/example_test.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals + +import ttfw_idf + + +@ttfw_idf.idf_example_test(env_tag='Example_GENERIC', target=['esp32', 'esp32c3']) +def test_examples_asio_ssl(env, extra_data): + + dut = env.get_dut('asio_ssl_client_server', 'examples/protocols/asio/ssl_client_server') + dut.start_app() + + dut.expect('Reply: GET / HTTP/1.1') + + +if __name__ == '__main__': + test_examples_asio_ssl() diff --git a/components/asio/examples/ssl_client_server/main/CMakeLists.txt b/components/asio/examples/ssl_client_server/main/CMakeLists.txt new file mode 100644 index 000000000..f962b470c --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "asio_ssl_main.cpp" + INCLUDE_DIRS "." + EMBED_TXTFILES ca.crt server.key srv.crt) diff --git a/components/asio/examples/ssl_client_server/main/Kconfig.projbuild b/components/asio/examples/ssl_client_server/main/Kconfig.projbuild new file mode 100644 index 000000000..1d03a2af4 --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/Kconfig.projbuild @@ -0,0 +1,36 @@ +menu "Example Configuration" + + config EXAMPLE_CLIENT + bool "Enable TLS client" + default y + help + Choose this option to use ASIO TLS/SSL client functionality + + config EXAMPLE_PORT + string "ASIO port number" + default "443" + help + Port number used by ASIO example. + + config EXAMPLE_SERVER + bool "Enable TLS server" + default n + help + Choose this option to use ASIO TLS/SSL server functionality + + config EXAMPLE_SERVER_NAME + string "ASIO server name or IP" + default "www.google.com" + depends on EXAMPLE_CLIENT + help + Asio example server ip for the ASIO client to connect to. + + config EXAMPLE_CLIENT_VERIFY_PEER + bool "Client to verify peer" + default n + depends on EXAMPLE_CLIENT + help + This option sets client's mode to verify peer, default is + verify-none + +endmenu diff --git a/components/asio/examples/ssl_client_server/main/asio_ssl_main.cpp b/components/asio/examples/ssl_client_server/main/asio_ssl_main.cpp new file mode 100644 index 000000000..8356dc716 --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/asio_ssl_main.cpp @@ -0,0 +1,272 @@ +// +// 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) +// + +#include +#include "protocol_examples_common.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include +#include +#include +#include +#include "asio.hpp" +#include "asio/ssl.hpp" +#include "asio/buffer.hpp" +#include "esp_pthread.h" + +extern const unsigned char server_pem_start[] asm("_binary_srv_crt_start"); +extern const unsigned char server_pem_end[] asm("_binary_srv_crt_end"); + +extern const unsigned char cacert_pem_start[] asm("_binary_ca_crt_start"); +extern const unsigned char cacert_pem_end[] asm("_binary_ca_crt_end"); + +extern const unsigned char prvtkey_pem_start[] asm("_binary_server_key_start"); +extern const unsigned char prvtkey_pem_end[] asm("_binary_server_key_end"); + +static const asio::const_buffer cert_chain(cacert_pem_start, cacert_pem_end - cacert_pem_start); +static const asio::const_buffer privkey(prvtkey_pem_start, prvtkey_pem_end - prvtkey_pem_start); +static const asio::const_buffer server_cert(server_pem_start, server_pem_end - server_pem_start); + +using asio::ip::tcp; + +static const std::size_t max_length = 1024; + +class Client { +public: + Client(asio::io_context &io_context, + asio::ssl::context &context, + const tcp::resolver::results_type &endpoints) + : socket_(io_context, context) + { + +#if CONFIG_EXAMPLE_CLIENT_VERIFY_PEER + socket_.set_verify_mode(asio::ssl::verify_peer); +#else + socket_.set_verify_mode(asio::ssl::verify_none); +#endif // CONFIG_EXAMPLE_CLIENT_VERIFY_PEER + + connect(endpoints); + } + +private: + void connect(const tcp::resolver::results_type &endpoints) + { + asio::async_connect(socket_.lowest_layer(), endpoints, + [this](const std::error_code & error, + const tcp::endpoint & /*endpoint*/) { + if (!error) { + handshake(); + } else { + std::cout << "Connect failed: " << error.message() << "\n"; + } + }); + } + + void handshake() + { + socket_.async_handshake(asio::ssl::stream_base::client, + [this](const std::error_code & error) { + if (!error) { + send_request(); + } else { + std::cout << "Handshake failed: " << error.message() << "\n"; + } + }); + } + + void send_request() + { + size_t request_length = std::strlen(request_); + + asio::async_write(socket_, + asio::buffer(request_, request_length), + [this](const std::error_code & error, std::size_t length) { + if (!error) { + receive_response(length); + } else { + std::cout << "Write failed: " << error.message() << "\n"; + } + }); + } + + void receive_response(std::size_t length) + { + asio::async_read(socket_, + asio::buffer(reply_, length), + [this](const std::error_code & error, std::size_t length) { + if (!error) { + std::cout << "Reply: "; + std::cout.write(reply_, length); + std::cout << "\n"; + } else { + std::cout << "Read failed: " << error.message() << "\n"; + } + }); + } + + asio::ssl::stream socket_; + char request_[max_length] = "GET / HTTP/1.1\r\n\r\n"; + char reply_[max_length]; +}; + +class Session : public std::enable_shared_from_this { +public: + Session(tcp::socket socket, asio::ssl::context &context) + : socket_(std::move(socket), context) + { + } + + void start() + { + do_handshake(); + } + +private: + void do_handshake() + { + auto self(shared_from_this()); + socket_.async_handshake(asio::ssl::stream_base::server, + [this, self](const std::error_code & error) { + if (!error) { + do_read(); + } + }); + } + + void do_read() + { + auto self(shared_from_this()); + socket_.async_read_some(asio::buffer(data_), + [this, self](const std::error_code & ec, std::size_t length) { + if (!ec) { + std::cout << "Server received: "; + std::cout.write(data_, length); + std::cout << std::endl; + do_write(length); + } + }); + } + + void do_write(std::size_t length) + { + auto self(shared_from_this()); + asio::async_write(socket_, asio::buffer(data_, length), + [this, self](const std::error_code & ec, + std::size_t /*length*/) { + if (!ec) { + do_read(); + } + }); + } + + asio::ssl::stream socket_; + char data_[max_length]; +}; + +class Server { +public: + Server(asio::io_context &io_context, unsigned short port) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)), + context_(asio::ssl::context::tls_server) + { + context_.set_options( + asio::ssl::context::default_workarounds + | asio::ssl::context::no_sslv2); + context_.use_certificate_chain(server_cert); + context_.use_private_key(privkey, asio::ssl::context::pem); + + do_accept(); + } + +private: + void do_accept() + { + acceptor_.async_accept( + [this](const std::error_code & error, tcp::socket socket) { + if (!error) { + std::make_shared(std::move(socket), context_)->start(); + } + + do_accept(); + }); + } + + tcp::acceptor acceptor_; + asio::ssl::context context_; +}; + +void set_thread_config(const char *name, int stack, int prio) +{ + auto cfg = esp_pthread_get_default_config(); + cfg.thread_name = name; + cfg.stack_size = stack; + cfg.prio = prio; + esp_pthread_set_cfg(&cfg); +} + +void ssl_server_thread() +{ + asio::io_context io_context; + + Server s(io_context, 443); + + io_context.run(); +} + +void ssl_client_thread() +{ + asio::io_context io_context; + + tcp::resolver resolver(io_context); + std::string server_ip = CONFIG_EXAMPLE_SERVER_NAME; + std::string server_port = CONFIG_EXAMPLE_PORT; + auto endpoints = resolver.resolve(server_ip, server_port); + + asio::ssl::context ctx(asio::ssl::context::tls_client); +#if CONFIG_EXAMPLE_CLIENT_VERIFY_PEER + ctx.add_certificate_authority(cert_chain); +#endif // CONFIG_EXAMPLE_CLIENT_VERIFY_PEER + + Client c(io_context, ctx, endpoints); + + io_context.run(); + +} + + +extern "C" void app_main(void) +{ + ESP_ERROR_CHECK(nvs_flash_init()); + esp_netif_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* 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()); + + /* This helper function configures blocking UART I/O */ + ESP_ERROR_CHECK(example_configure_stdin_stdout()); + std::vector work_threads; + +#if CONFIG_EXAMPLE_SERVER + set_thread_config("Server", 16 * 1024, 5); + work_threads.emplace_back(std::thread(ssl_server_thread)); + std::this_thread::sleep_for(std::chrono::seconds(1)); +#endif // CONFIG_EXAMPLE_SERVER + +#if CONFIG_EXAMPLE_CLIENT + set_thread_config("Client", 16 * 1024, 5); + work_threads.emplace_back(ssl_client_thread); +#endif // CONFIG_EXAMPLE_CLIENT + + for (auto &t : work_threads) { + t.join(); + } + +} diff --git a/components/asio/examples/ssl_client_server/main/ca.crt b/components/asio/examples/ssl_client_server/main/ca.crt new file mode 100644 index 000000000..894f2959b --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/ca.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIUNI5wldYysh6rtCzYmda6H414aRswDQYJKoZIhvcNAQEL +BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJRXNwcmVzc2lmMB4X +DTIwMDEyMTA5MDk0NloXDTI1MDEyMDA5MDk0NlowWTELMAkGA1UEBhMCQVUxEzAR +BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 +IEx0ZDESMBAGA1UEAwwJRXNwcmVzc2lmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAyadSpRnIQBVbEAsbpkrKrOMlBOMIUmA8AfNyOYPLfv0Oa5lBiMAV +3OQDu5tYyFYKwkCUqq65iAm50fPbSH71w1tkja6nZ1yAIM+TvpMlM/WiFGrhY+Tc +kAcLcKUJyPxrv/glzoVslbqUgIhuhCSKA8uk1+ILcn3nWzPcbcowLx31+AHeZj8h +bIAdj6vjqxMCFStp4IcA+ikmCk75LCN4vkkifdkebb/ZDNYCZZhpCBnCHyFAjPc4 +7C+FDVGT3/UUeeTy+Mtn+MqUAhB+W0sPDm1n2h59D4Z/MFm0hl6GQCAKeMJPzssU +BBsRm6zoyPQ4VTqG0uwfNNbORyIfKONMUwIDAQABo1MwUTAdBgNVHQ4EFgQUGYLV +EkgWzxjpltE6texha7zZVxowHwYDVR0jBBgwFoAUGYLVEkgWzxjpltE6texha7zZ +VxowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAb2EF4Zg2XWNb +eZHnzupCDd9jAhwPqkt7F1OXvxJa/RFUSB9+2izGvikGGhuKY4f0iLuqF+bhExD9 +sapDcdFO2Suh4J3onbwEvmKvsv56K3xhapYg8WwPofpkVirnkwFjpQXGzrYxPujg +BPmSy3psQrhvOr/WH7SefJv2qr4ikaugfE+3enY4PL+C1dSQAuNo1QGgWsZIu0c8 +TZybNZ13vNVMA+tgj2CM8FR3Etaabwtu3TTcAnO7aoBTix/bLBTuZoczhN8/MhG3 +GylmDzFI8a6aKxQL3Fi4PsM82hRKWu3gfs39sR1Ci4V22v8uO5EWBPK0QZvDSc1a +KwwxI4zA0w== +-----END CERTIFICATE----- diff --git a/components/asio/examples/ssl_client_server/main/server.key b/components/asio/examples/ssl_client_server/main/server.key new file mode 100644 index 000000000..2a4d650ea --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAlUCywNhVv4RO2y9h/XGKZ1azzk3jzHpSBzIGO9LoiA8trC/p +1ykGaUfYPJllYK4HMhC4fUyE3J7tVL2Eskzl26LNPLbEoaBWZM9NhV3iA1/1EtOu +p6umLx+y3sDfvK35YAOUbjdAlBfhnJ4r8h7oTsxl3J5jZ18zgjJnJi2NEFq/yTpO +MiwHLWPjy25fDFixfV9UzSvbgt1JaGPmC7c4QkhHzjyp0+ikuvRIw0p9BBNeqBV2 +da3qBMB5FtodUJTAz6o6OKWbTalLjQi6C1H6z9TnY7IrJBUOy/FWkQH/sEsLdscD +hHa1Dz2oT203QjhzyOSfnNF95D/1MdNcMt6l0wIDAQABAoIBAC1JJTOoMFRc48RT +myrYQYNbZlEphv3q+2qdfhC2zMFDwbrmCtCy7PQSzYSNkpoEE8DYG/JAvmtmeWJl +4pZrCK9ctWM/nWfhC3WpBL97nfEiM20T94F+bn0L5Cz8XqaULv839th+QUTt/hGU +WIctY5VNJXcMQ+MAmtNdUbjex1d3iuxiKHUo4nDoZ8digKFNdtdP5B5nlMq5chCL +mxNRcsGsx2dDAxbGUapdTVPWHPJKpLOBoSkluDsfd2KZADFU2R1SJpAX9+RYh3HM +5FTUdHTUaISxbKkgeDKlEM0lqk2TtGUwCyEj098ewi7Wzsu9w60IplPPUJx5FRG6 +jp3wzLkCgYEAxKp5T20rf/7ysX7x053I7VCjDXUxAaWOEj1uS3AhOkl0NaZg7Di+ +y53fWNkcHdkt2n2LqMt/43UgMYq3TVVcq2eunPNF11e1bJw8CjDafwDs4omwwyVn +lYhPuB4dK2OAib+vU5Zqpp0kZMoxk2MZVgon8z+s8DW/zmB6aFqAWeUCgYEAwkhC +OgmXKMdjOCVy5t2f5UbY8Y9rV3w8eUATuJ47MMwLr4pGYnKoEn9JB4ltWrHv/u5S +fOv3tIrrCEvnCoCbOILwCsY5LqTNXgqova8FB6RpMUQCzhDd8LHuvdHv0WMnMzX1 +3PKuqwh8JS55m4WqZRhzr5BFKG4fHPVs4IcaJVcCgYAzzCaJSdqUKqTnJOUydDNQ +ddWMHNqccWs62J0tF0pZHLGT089hSAzQejMyJnSmU+Ykzr4y5e44DUg+ZCelIZ93 +saYmxlgVwI8THQ8fLADQRIEfpV4996MRmkZM2vmZzOo03Zyi6lIKsga82Rg3lnk8 +1Q3ynknBNpbfF0AGLhfyFQKBgBYlxJ73HutAJ5hr9HhLBYJOnEaVUehMOlycKGNg +bmD2sdJWEgYBChXpurqIORYguLo4EuE4ySkkuPxeIr14wbkkfBbOWBBwKxUwY+IT +xKAFZxR9q1AwbgyVTCEJgKw/AGX/HcMNS0omEnjunmBTUYRq0C1QZgHg490aQUor +PJjLAoGAevzdTpFlVeuKeYh1oDubGO1LinyXpBv7fPFjl+zu4AVbjojcU6yC4OO6 +QvqopE6SyAECKy8kAOFcESPsGc9Lta2XUvI203z7pIVlNVEcJ0+90mQh3Mn1U46l +sZ49PdRvNwNb5wvkh1UqNsMlGFbRlzMbIk45ou4311kCobowZek= +-----END RSA PRIVATE KEY----- diff --git a/components/asio/examples/ssl_client_server/main/srv.crt b/components/asio/examples/ssl_client_server/main/srv.crt new file mode 100644 index 000000000..29bfa1664 --- /dev/null +++ b/components/asio/examples/ssl_client_server/main/srv.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9DCCAdwCFA1lSIcHwYKdB2UqOrZxZnVgPObTMA0GCSqGSIb3DQEBCwUAMFkx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCUVzcHJlc3NpZjAeFw0yMDA2 +MTIwNjA0MTNaFw0yMjA2MDIwNjA0MTNaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJVAssDYVb+ETtsvYf1ximdW +s85N48x6UgcyBjvS6IgPLawv6dcpBmlH2DyZZWCuBzIQuH1MhNye7VS9hLJM5dui +zTy2xKGgVmTPTYVd4gNf9RLTrqerpi8fst7A37yt+WADlG43QJQX4ZyeK/Ie6E7M +ZdyeY2dfM4IyZyYtjRBav8k6TjIsBy1j48tuXwxYsX1fVM0r24LdSWhj5gu3OEJI +R848qdPopLr0SMNKfQQTXqgVdnWt6gTAeRbaHVCUwM+qOjilm02pS40IugtR+s/U +52OyKyQVDsvxVpEB/7BLC3bHA4R2tQ89qE9tN0I4c8jkn5zRfeQ/9THTXDLepdMC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAnMYGW+idt37bEE4WPgrRorKWuplR+zHD +wJFz53DQzyIZJHmJ2hR5U0jNcHy/nMq7tbdz9LZPrVF4lZJ3TJhnmkOKjMFPCQE8 +YcmsP3il6eXgtGqg53InOi/uJqEQ9TfM54cbpp6xKbnmpwk4uprISBRQt7u2ZLk2 +40ED6zgjFPDTYmSjSpb2AN6KUB6PflgVs+4p9ViHNq4U3AlYV/BM0+3G4aMX2wNl +ZIpQfOyuaYD5MU50mY+O+gDiiypkpYf6a6S4YJ1sMbavDsP7bW5UMnP0jKYR549q +5hF1fdkXq52DfJ9ya2kl3mANFkKssQV+1KCBMxGoeqfakmJfa03xXA== +-----END CERTIFICATE----- diff --git a/components/asio/examples/ssl_client_server/partitions.csv b/components/asio/examples/ssl_client_server/partitions.csv new file mode 100644 index 000000000..f3aa8e2b4 --- /dev/null +++ b/components/asio/examples/ssl_client_server/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1400000, diff --git a/components/asio/examples/ssl_client_server/sdkconfig.ci b/components/asio/examples/ssl_client_server/sdkconfig.ci new file mode 100644 index 000000000..f1c43e8ac --- /dev/null +++ b/components/asio/examples/ssl_client_server/sdkconfig.ci @@ -0,0 +1,6 @@ +CONFIG_EXAMPLE_CLIENT=y +CONFIG_EXAMPLE_SERVER=y +CONFIG_EXAMPLE_SERVER_NAME="localhost" +CONFIG_EXAMPLE_CONNECT_WIFI=n +CONFIG_EXAMPLE_CONNECT_ETHERNET=n +CONFIG_EXAMPLE_CLIENT_VERIFY_PEER=y diff --git a/components/asio/examples/ssl_client_server/sdkconfig.defaults b/components/asio/examples/ssl_client_server/sdkconfig.defaults new file mode 100644 index 000000000..30bba61d4 --- /dev/null +++ b/components/asio/examples/ssl_client_server/sdkconfig.defaults @@ -0,0 +1,10 @@ +CONFIG_ASIO_SSL_SUPPORT=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" + +# +# Partition Table +# +# Leave some room for larger apps without needing to reduce other features +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y diff --git a/components/asio/examples/tcp_echo_server/CMakeLists.txt b/components/asio/examples/tcp_echo_server/CMakeLists.txt new file mode 100644 index 000000000..29e290677 --- /dev/null +++ b/components/asio/examples/tcp_echo_server/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.16) + +# (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 ../../../../common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_tcp_echo_server) diff --git a/components/asio/examples/tcp_echo_server/README.md b/components/asio/examples/tcp_echo_server/README.md new file mode 100644 index 000000000..2c3893550 --- /dev/null +++ b/components/asio/examples/tcp_echo_server/README.md @@ -0,0 +1,22 @@ +| Supported Targets | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-C3 | +| ----------------- | ----- | -------- | -------- | -------- | + +# Asio TCP echo server example + +Simple Asio TCP echo server using WiFi STA or Ethernet. + +## Example workflow + +- Wi-Fi or Ethernet connection is established, and IP address is obtained. +- Asio TCP server is started on port number defined through the project configuration. +- Server receives and echoes back messages transmitted from client. + +## Running the example + +- Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. +- Set server port number in menuconfig, "Example configuration". +- Run `idf.py -p PORT flash monitor` to build and upload the example to your board and connect to it's serial terminal. +- Wait for the board to connect to WiFi or Ethernet (note the IP address). +- You can now send a TCP message and check it is repeated, for example using netcat `nc IP PORT`. + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/components/asio/examples/tcp_echo_server/asio_tcp_server_test.py b/components/asio/examples/tcp_echo_server/asio_tcp_server_test.py new file mode 100644 index 000000000..0293350ab --- /dev/null +++ b/components/asio/examples/tcp_echo_server/asio_tcp_server_test.py @@ -0,0 +1,47 @@ +import os +import re +import socket + +import ttfw_idf + + +@ttfw_idf.idf_example_test(env_tag='Example_WIFI_Protocols') +def test_examples_protocol_asio_tcp_server(env, extra_data): + """ + steps: | + 1. join AP + 2. Start server + 3. Test connects to server and sends a test message + 4. Test evaluates received test message from server + 5. Test evaluates received test message on server stdout + """ + test_msg = b'echo message from client to server' + dut1 = env.get_dut('tcp_echo_server', 'examples/protocols/asio/tcp_echo_server', dut_class=ttfw_idf.ESP32DUT) + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, 'asio_tcp_echo_server.bin') + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance('asio_tcp_echo_server_bin_size', '{}KB'.format(bin_size // 1024)) + # 1. start test + dut1.start_app() + # 2. get the server IP address + data = dut1.expect(re.compile(r' IPv4 address: ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)'), timeout=30) + # 3. create tcp client and connect to server + dut1.expect('ASIO engine is up and running', timeout=1) + cli = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + cli.settimeout(30) + cli.connect((data[0], 2222)) + cli.send(test_msg) + data = cli.recv(1024) + # 4. check the message received back from the server + if (data == test_msg): + print('PASS: Received correct message') + pass + else: + print('Failure!') + raise ValueError('Wrong data received from asi tcp server: {} (expected:{})'.format(data, test_msg)) + # 5. check the client message appears also on server terminal + dut1.expect(test_msg.decode()) + + +if __name__ == '__main__': + test_examples_protocol_asio_tcp_server() diff --git a/components/asio/examples/tcp_echo_server/main/CMakeLists.txt b/components/asio/examples/tcp_echo_server/main/CMakeLists.txt new file mode 100644 index 000000000..fcd492bca --- /dev/null +++ b/components/asio/examples/tcp_echo_server/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "echo_server.cpp" + INCLUDE_DIRS ".") diff --git a/components/asio/examples/tcp_echo_server/main/Kconfig.projbuild b/components/asio/examples/tcp_echo_server/main/Kconfig.projbuild new file mode 100644 index 000000000..795326db9 --- /dev/null +++ b/components/asio/examples/tcp_echo_server/main/Kconfig.projbuild @@ -0,0 +1,9 @@ +menu "Example Configuration" + + config EXAMPLE_PORT + string "Asio example port number" + default "2222" + help + Port number used by Asio example. + +endmenu diff --git a/components/asio/examples/tcp_echo_server/main/echo_server.cpp b/components/asio/examples/tcp_echo_server/main/echo_server.cpp new file mode 100644 index 000000000..5c520aeac --- /dev/null +++ b/components/asio/examples/tcp_echo_server/main/echo_server.cpp @@ -0,0 +1,108 @@ +#include "asio.hpp" +#include +#include +#include "protocol_examples_common.h" +#include "esp_event.h" +#include "nvs_flash.h" + +using asio::ip::tcp; + +class session + : public std::enable_shared_from_this +{ +public: + session(tcp::socket socket) + : socket_(std::move(socket)) + { + } + + void start() + { + do_read(); + } + +private: + void do_read() + { + auto self(shared_from_this()); + socket_.async_read_some(asio::buffer(data_, max_length), + [this, self](std::error_code ec, std::size_t length) + { + if (!ec) + { + data_[length] = 0; + std::cout << data_ << std::endl; + do_write(length); + } + }); + } + + void do_write(std::size_t length) + { + auto self(shared_from_this()); + asio::async_write(socket_, asio::buffer(data_, length), + [this, self](std::error_code ec, std::size_t length) + { + if (!ec) + { + do_read(); + } + }); + } + + tcp::socket socket_; + enum { max_length = 1024 }; + char data_[max_length]; +}; + +class server +{ +public: + server(asio::io_context& io_context, short port) + : acceptor_(io_context, tcp::endpoint(tcp::v4(), port)) + { + do_accept(); + } + +private: + void do_accept() + { + acceptor_.async_accept( + [this](std::error_code ec, tcp::socket socket) + { + if (!ec) + { + std::make_shared(std::move(socket))->start(); + } + + do_accept(); + }); + } + + tcp::acceptor acceptor_; +}; + + +extern "C" void app_main(void) +{ + ESP_ERROR_CHECK(nvs_flash_init()); + esp_netif_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* 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()); + + /* This helper function configures blocking UART I/O */ + ESP_ERROR_CHECK(example_configure_stdin_stdout()); + + asio::io_context io_context; + + server s(io_context, std::atoi(CONFIG_EXAMPLE_PORT)); + + std::cout << "ASIO engine is up and running" << std::endl; + + io_context.run(); +} diff --git a/components/asio/examples/tcp_echo_server/sdkconfig.defaults b/components/asio/examples/tcp_echo_server/sdkconfig.defaults new file mode 100644 index 000000000..b02a3a3ef --- /dev/null +++ b/components/asio/examples/tcp_echo_server/sdkconfig.defaults @@ -0,0 +1,7 @@ +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# +# Partition Table +# +# Leave some room for larger apps without needing to reduce other features +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y diff --git a/components/asio/examples/udp_echo_server/CMakeLists.txt b/components/asio/examples/udp_echo_server/CMakeLists.txt new file mode 100644 index 000000000..2e0c28024 --- /dev/null +++ b/components/asio/examples/udp_echo_server/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.16) + +# (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 ../../../../common_components/protocol_examples_common) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(asio_udp_echo_server) diff --git a/components/asio/examples/udp_echo_server/README.md b/components/asio/examples/udp_echo_server/README.md new file mode 100644 index 000000000..cd384fc39 --- /dev/null +++ b/components/asio/examples/udp_echo_server/README.md @@ -0,0 +1,22 @@ +| Supported Targets | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-C3 | +| ----------------- | ----- | -------- | -------- | -------- | + +# Asio UDP echo server example + +Simple Asio UDP echo server using WiFi STA or Ethernet. + +## Example workflow + +- Wi-Fi or Ethernet connection is established, and IP address is obtained. +- Asio UDP server is started on port number defined through the project configuration +- Server receives and echoes back messages transmitted from client + +## Running the example + +- Open the project configuration menu (`idf.py menuconfig`) to configure Wi-Fi or Ethernet. See "Establishing Wi-Fi or Ethernet Connection" section in [examples/protocols/README.md](../../README.md) for more details. +- Set server port number in menuconfig, "Example configuration". +- Run `idf.py -p PORT flash monitor` to build and upload the example to your board and connect to it's serial terminal. +- Wait for the board to connect to WiFi or Ethernet (note the IP address). +- You can now send a UDP message and check it is repeated, for example using netcat `nc -u IP PORT`. + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/components/asio/examples/udp_echo_server/asio_udp_server_test.py b/components/asio/examples/udp_echo_server/asio_udp_server_test.py new file mode 100644 index 000000000..b2c2a5f75 --- /dev/null +++ b/components/asio/examples/udp_echo_server/asio_udp_server_test.py @@ -0,0 +1,47 @@ +import os +import re +import socket + +import ttfw_idf + + +@ttfw_idf.idf_example_test(env_tag='Example_WIFI_Protocols') +def test_examples_protocol_asio_udp_server(env, extra_data): + """ + steps: | + 1. join AP + 2. Start server + 3. Test connects to server and sends a test message + 4. Test evaluates received test message from server + 5. Test evaluates received test message on server stdout + """ + test_msg = b'echo message from client to server' + dut1 = env.get_dut('udp_echo_server', 'examples/protocols/asio/udp_echo_server', dut_class=ttfw_idf.ESP32DUT) + # check and log bin size + binary_file = os.path.join(dut1.app.binary_path, 'asio_udp_echo_server.bin') + bin_size = os.path.getsize(binary_file) + ttfw_idf.log_performance('asio_udp_echo_server_bin_size', '{}KB'.format(bin_size // 1024)) + # 1. start test + dut1.start_app() + # 2. get the server IP address + data = dut1.expect(re.compile(r' IPv4 address: ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)'), timeout=30) + # 3. create tcp client and connect to server + dut1.expect('ASIO engine is up and running', timeout=1) + cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + cli.settimeout(30) + cli.connect((data[0], 2222)) + cli.send(test_msg) + data = cli.recv(1024) + # 4. check the message received back from the server + if (data == test_msg): + print('PASS: Received correct message') + pass + else: + print('Failure!') + raise ValueError('Wrong data received from asio udp server: {} (expected:{})'.format(data, test_msg)) + # 5. check the client message appears also on server terminal + dut1.expect(test_msg.decode()) + + +if __name__ == '__main__': + test_examples_protocol_asio_udp_server() diff --git a/components/asio/examples/udp_echo_server/main/CMakeLists.txt b/components/asio/examples/udp_echo_server/main/CMakeLists.txt new file mode 100644 index 000000000..c692c0905 --- /dev/null +++ b/components/asio/examples/udp_echo_server/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "udp_echo_server.cpp" + INCLUDE_DIRS ".") diff --git a/components/asio/examples/udp_echo_server/main/Kconfig.projbuild b/components/asio/examples/udp_echo_server/main/Kconfig.projbuild new file mode 100644 index 000000000..795326db9 --- /dev/null +++ b/components/asio/examples/udp_echo_server/main/Kconfig.projbuild @@ -0,0 +1,9 @@ +menu "Example Configuration" + + config EXAMPLE_PORT + string "Asio example port number" + default "2222" + help + Port number used by Asio example. + +endmenu diff --git a/components/asio/examples/udp_echo_server/main/udp_echo_server.cpp b/components/asio/examples/udp_echo_server/main/udp_echo_server.cpp new file mode 100644 index 000000000..5e21d5100 --- /dev/null +++ b/components/asio/examples/udp_echo_server/main/udp_echo_server.cpp @@ -0,0 +1,90 @@ +// +// async_udp_echo_server.cpp +// ~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// Copyright (c) 2003-2018 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) +// + +#include +#include + +#include "asio.hpp" + +#include "protocol_examples_common.h" +#include "esp_event.h" +#include "nvs_flash.h" + + +using asio::ip::udp; + +class server +{ +public: + server(asio::io_context& io_context, short port) + : socket_(io_context, udp::endpoint(udp::v4(), port)) + { + do_receive(); + } + + void do_receive() + { + socket_.async_receive_from( + asio::buffer(data_, max_length), sender_endpoint_, + [this](std::error_code ec, std::size_t bytes_recvd) + { + if (!ec && bytes_recvd > 0) + { + data_[bytes_recvd] = 0; + std::cout << data_ << std::endl; + do_send(bytes_recvd); + } + else + { + do_receive(); + } + }); + } + + void do_send(std::size_t length) + { + socket_.async_send_to( + asio::buffer(data_, length), sender_endpoint_, + [this](std::error_code /*ec*/, std::size_t bytes /*bytes_sent*/) + { + do_receive(); + }); + } + +private: + udp::socket socket_; + udp::endpoint sender_endpoint_; + enum { max_length = 1024 }; + char data_[max_length]; +}; + +extern "C" void app_main(void) +{ + ESP_ERROR_CHECK(nvs_flash_init()); + esp_netif_init(); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + /* 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()); + + /* This helper function configures blocking UART I/O */ + ESP_ERROR_CHECK(example_configure_stdin_stdout()); + + asio::io_context io_context; + + server s(io_context, std::atoi(CONFIG_EXAMPLE_PORT)); + + std::cout << "ASIO engine is up and running" << std::endl; + + io_context.run(); +} diff --git a/components/asio/examples/udp_echo_server/sdkconfig.defaults b/components/asio/examples/udp_echo_server/sdkconfig.defaults new file mode 100644 index 000000000..b02a3a3ef --- /dev/null +++ b/components/asio/examples/udp_echo_server/sdkconfig.defaults @@ -0,0 +1,7 @@ +CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# +# Partition Table +# +# Leave some room for larger apps without needing to reduce other features +CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y diff --git a/components/asio/idf_component.yml b/components/asio/idf_component.yml new file mode 100644 index 000000000..b2e11a570 --- /dev/null +++ b/components/asio/idf_component.yml @@ -0,0 +1,5 @@ +version: "1.0.1" +description: ASIO +dependencies: + idf: + version: ">=5.0" diff --git a/components/asio/port/include/esp_asio_config.h b/components/asio/port/include/esp_asio_config.h new file mode 100644 index 000000000..cf42183a3 --- /dev/null +++ b/components/asio/port/include/esp_asio_config.h @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _ESP_ASIO_CONFIG_H_ +#define _ESP_ASIO_CONFIG_H_ + +// +// Enabling exceptions only when they are enabled in menuconfig +// +# include +# ifndef CONFIG_COMPILER_CXX_EXCEPTIONS +# define ASIO_NO_EXCEPTIONS +# endif // CONFIG_COMPILER_CXX_EXCEPTIONS + +# ifndef CONFIG_COMPILER_RTTI +# define ASIO_NO_TYPEID +# endif // CONFIG_COMPILER_RTTI + +// +// Use system sockets +// +# include "sys/socket.h" + +// +// Specific ASIO feature flags +// +# define ASIO_DISABLE_SERIAL_PORT +# define ASIO_SEPARATE_COMPILATION +# define ASIO_STANDALONE +# define ASIO_HAS_PTHREADS +# define ASIO_DISABLE_CONCEPTS + +# ifdef CONFIG_ASIO_USE_ESP_OPENSSL +# define ASIO_USE_ESP_OPENSSL +# define OPENSSL_NO_ENGINE +# define ASIO_SSL_DETAIL_OPENSSL_TYPES_HPP +# include "openssl_stub.hpp" + +# elif CONFIG_ASIO_USE_ESP_WOLFSSL +# define ASIO_USE_WOLFSSL +# endif // CONFIG_ASIO_USE_ESP_OPENSSL + +#endif // _ESP_ASIO_CONFIG_H_ diff --git a/components/asio/port/include/esp_exception.h b/components/asio/port/include/esp_exception.h new file mode 100644 index 000000000..15cada7a7 --- /dev/null +++ b/components/asio/port/include/esp_exception.h @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _ESP_EXCEPTION_H_ +#define _ESP_EXCEPTION_H_ + +// +// This exception stub is enabled only if exceptions are disabled in menuconfig +// +#if !defined(CONFIG_COMPILER_CXX_EXCEPTIONS) && defined (ASIO_NO_EXCEPTIONS) + +#include "esp_log.h" + +// +// asio exception stub +// +namespace asio { +namespace detail { +template +void throw_exception(const Exception& e) +{ + ESP_LOGE("esp32_asio_exception", "Caught exception: %s!", e.what()); + abort(); +} +}} +#endif // CONFIG_COMPILER_CXX_EXCEPTIONS==1 && defined (ASIO_NO_EXCEPTIONS) + +#endif // _ESP_EXCEPTION_H_ diff --git a/components/asio/port/include/openssl_stub.hpp b/components/asio/port/include/openssl_stub.hpp new file mode 100644 index 000000000..175879542 --- /dev/null +++ b/components/asio/port/include/openssl_stub.hpp @@ -0,0 +1,46 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// + +#pragma once + +// +// Supply OpenSSL macros and flags for asio-ssl header files +// +#define OPENSSL_VERSION_NUMBER 0x10100001L + +#define SSL_R_SHORT_READ 219 +#define SSL_OP_ALL 0 +#define SSL_OP_SINGLE_DH_USE 0 +#define SSL_OP_NO_COMPRESSION 0 + +#define SSL_OP_NO_SSLv2 0x01000000L +#define SSL_OP_NO_SSLv3 0x02000000L +#define SSL_OP_NO_TLSv1 0x04000000L + +#define SSL_VERIFY_NONE 0x00 +#define SSL_VERIFY_PEER 0x01 +#define SSL_VERIFY_FAIL_IF_NO_PEER_CERT 0x02 +#define SSL_VERIFY_CLIENT_ONCE 0x04 + +// +// Implement asio-ssl layer with these three classes in asio::ssl::mbedtls +// +namespace asio { +namespace ssl { +namespace mbedtls { + +class engine; +class bio; +class shared_ctx; +} } } // namespace asio::ssl::mbedtls + +// +// Supply OpenSSL types as aliases to mbedtls classes +// +using X509_STORE_CTX=void; +using BIO=asio::ssl::mbedtls::bio; +using SSL_CTX=asio::ssl::mbedtls::shared_ctx; +using SSL=asio::ssl::mbedtls::engine; diff --git a/components/asio/port/mbedtls/include/mbedtls_bio.hpp b/components/asio/port/mbedtls/include/mbedtls_bio.hpp new file mode 100644 index 000000000..d5348a052 --- /dev/null +++ b/components/asio/port/mbedtls/include/mbedtls_bio.hpp @@ -0,0 +1,113 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "asio/ssl/context_base.hpp" +#include "asio/ssl/context.hpp" +#include "sdkconfig.h" + +namespace asio { +namespace ssl { +namespace mbedtls { + +class bio { + static constexpr int BIO_SIZE = CONFIG_ASIO_SSL_BIO_SIZE; + static constexpr int BIO_FLAGS_READ = 1; + static constexpr int BIO_FLAGS_WRITE = 2; + +public: + int write(const void *buf, int len) + { + if (buf == nullptr || len <= 0) { + // not an error, just empty operation (as in openssl/bio) + return 0; + } + int remaining = size_ - offset_; + if (remaining <= 0) { + flags_ |= BIO_FLAGS_WRITE; + return -1; + } + int len_to_write = len > remaining ? remaining : len; + std::memcpy(&data_[offset_], buf, len_to_write); + offset_ += len_to_write; + dlen_ = offset_; + if (len_to_write == len) { + flags_ &= ~BIO_FLAGS_WRITE; + } + return len_to_write; + } + + int read(void *buf, int len) + { + if (buf == nullptr || len <= 0) { + // not an error, just empty operation (as in openssl/bio) + return 0; + } + int remaining = peer_->dlen_ - peer_->roffset_; + if (remaining <= 0) { + flags_ |= BIO_FLAGS_READ; + return -1; + } + int len_to_read = remaining > len ? len : remaining; + std::memcpy(buf, &peer_->data_[peer_->roffset_], len_to_read); + peer_->roffset_ += len_to_read; + if (len_to_read == len) { + flags_ &= ~BIO_FLAGS_READ; + } + if (peer_->offset_) { + // shift data back to the beginning of the buffer + std::memmove(&peer_->data_[0], &peer_->data_[peer_->roffset_], peer_->offset_ - peer_->roffset_); + peer_->offset_ -= peer_->roffset_; + peer_->roffset_ = 0; + peer_->dlen_ = peer_->offset_; + } + return len_to_read; + } + + size_t wpending() const + { + return dlen_ - roffset_; + } + + size_t ctrl_pending() + { + return peer_->dlen_ - peer_->roffset_; + } + + bool should_write() const + { + return flags_ & BIO_FLAGS_WRITE; + } + + bool should_read() const + { + return flags_ & BIO_FLAGS_READ; + } + + static std::pair, std::shared_ptr> new_pair(const char* error_location) + { + auto b1 = std::shared_ptr(new (std::nothrow) bio); + auto b2 = std::shared_ptr(new (std::nothrow) bio); + if (b1 == nullptr || b2 == nullptr) { + throw_alloc_failure(error_location); + } else { + b1->peer_ = b2; + b2->peer_ = b1; + } + return std::make_pair(b1, b2); + } + +private: + std::array data_ {}; + size_t size_ {BIO_SIZE}; + std::shared_ptr peer_ {nullptr}; + int dlen_ {0}; + size_t offset_ {0}; + size_t roffset_ {0}; + size_t flags_ {0}; +}; + +} } } // namespace asio::ssl::mbedtls diff --git a/components/asio/port/mbedtls/include/mbedtls_context.hpp b/components/asio/port/mbedtls/include/mbedtls_context.hpp new file mode 100644 index 000000000..27d135b5d --- /dev/null +++ b/components/asio/port/mbedtls/include/mbedtls_context.hpp @@ -0,0 +1,105 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "asio/ssl/context_base.hpp" +#include "asio/ssl/context.hpp" + +namespace asio { +namespace error { + +const asio::error_category& get_mbedtls_category(); +} // namespace error + +namespace ssl { +namespace mbedtls { + +void throw_alloc_failure(const char* location); + +const char *error_message(int error_code); + +enum class container { + CERT, CA_CERT, PRIVKEY +}; + +template +inline T* create(const char * location, Args &&... args) +{ + T* t = new (std::nothrow) T(std::forward(args)...); + if (t == nullptr) + { + throw_alloc_failure(location); + } + return t; +} + +class context { +public: + explicit context(context_base::method m): method_(m), options_(0) {} + + const unsigned char *data(container c) const + { + switch (c) { + case container::CERT: + return static_cast(cert_chain_.data()); + case container::CA_CERT: + return static_cast(ca_cert_.data()); + case container::PRIVKEY: + return static_cast(private_key_.data()); + } + return nullptr; + } + + std::size_t size(container c) const + { + switch (c) { + case container::CERT: + return cert_chain_.size(); + case container::CA_CERT: + return ca_cert_.size(); + case container::PRIVKEY: + return private_key_.size(); + } + return 0; + } + + context_base::method method_; + asio::ssl::context::options options_; + const_buffer cert_chain_; + const_buffer private_key_; + const_buffer ca_cert_; +}; + +/** + * @brief Wrapper class around SSL_CTX so we can easily create + * a shared pointer to the context without throwing the default exception. + * This is useful, as we can use asio::detail::throw_error for allocation errors. + */ +class shared_ctx { +public: + static SSL_CTX *create(const char* location, context_base::method m) + { + auto wrapped = asio::ssl::mbedtls::create(location, m); + if (wrapped->ctx_ == nullptr) + { + throw_alloc_failure(location); + } + return wrapped; + } + + std::shared_ptr get() const + { + return ctx_; + } + + explicit shared_ctx(context_base::method m) + :ctx_(std::shared_ptr(new (std::nothrow) context(m))) { } + +private: + std::shared_ptr ctx_; +}; + +} } } // namespace asio::ssl::mbedtls diff --git a/components/asio/port/mbedtls/include/mbedtls_engine.hpp b/components/asio/port/mbedtls/include/mbedtls_engine.hpp new file mode 100644 index 000000000..a52ba459d --- /dev/null +++ b/components/asio/port/mbedtls/include/mbedtls_engine.hpp @@ -0,0 +1,293 @@ +// +// SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "mbedtls/ssl.h" +#include "mbedtls/entropy.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/error.h" +#include "mbedtls/esp_debug.h" +#include "esp_log.h" + +namespace asio { +namespace ssl { +namespace mbedtls { + +const char *error_message(int error_code) +{ + static char error_buf[100]; + mbedtls_strerror(error_code, error_buf, sizeof(error_buf)); + return error_buf; +} + +void throw_alloc_failure(const char* location) +{ + asio::error_code ec( MBEDTLS_ERR_SSL_ALLOC_FAILED, asio::error::get_mbedtls_category()); + asio::detail::throw_error(ec, location); +} + +namespace error_codes { + +bool is_error(int ret) +{ + return ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE; +} + +static bool want_write(int ret) +{ + return ret == MBEDTLS_ERR_SSL_WANT_WRITE; +} + +static bool want_read(int ret) +{ + return ret == MBEDTLS_ERR_SSL_WANT_READ; +} + +} // namespace error_codes + +enum rw_state { + IDLE, READING, WRITING, CLOSED +}; + +class engine { +public: + explicit engine(std::shared_ptr ctx): ctx_(std::move(ctx)), + bio_(bio::new_pair("mbedtls-engine")), state_(IDLE), verify_mode_(0) {} + + void set_verify_mode(asio::ssl::verify_mode mode) + { + verify_mode_ = mode; + } + + bio* ext_bio() const + { + return bio_.second.get(); + } + + rw_state get_state() const + { + return state_; + } + + int shutdown() + { + int ret = mbedtls_ssl_close_notify(&impl_.ssl_); + if (ret) { + impl::print_error("mbedtls_ssl_close_notify", ret); + } + state_ = CLOSED; + return ret; + } + + int connect() + { + return handshake(true); + } + + int accept() + { + return handshake(false); + } + + int write(const void *buffer, int len) + { + int ret = impl_.write(buffer, len); + state_ = ret == len ? IDLE: WRITING; + return ret; + } + + int read(void *buffer, int len) + { + int ret = impl_.read(buffer, len); + state_ = ret == len ? IDLE: READING; + return ret; + } + +private: + int handshake(bool is_client_not_server) + { + if (impl_.before_handshake()) { + impl_.configure(ctx_.get(), is_client_not_server, impl_verify_mode(is_client_not_server)); + } + return do_handshake(); + } + + static int bio_read(void *ctx, unsigned char *buf, size_t len) + { + auto bio = static_cast(ctx); + int read = bio->read(buf, len); + if (read <= 0 && bio->should_read()) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return read; + } + + static int bio_write(void *ctx, const unsigned char *buf, size_t len) + { + auto bio = static_cast(ctx); + int written = bio->write(buf, len); + if (written <= 0 && bio->should_write()) { + return MBEDTLS_ERR_SSL_WANT_WRITE; + } + return written; + } + + int do_handshake() + { + int ret = 0; + mbedtls_ssl_set_bio(&impl_.ssl_, bio_.first.get(), bio_write, bio_read, nullptr); + + while (impl_.ssl_.MBEDTLS_PRIVATE(state) != MBEDTLS_SSL_HANDSHAKE_OVER) { + ret = mbedtls_ssl_handshake_step(&impl_.ssl_); + + if (ret != 0) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + impl::print_error("mbedtls_ssl_handshake_step", ret); + } + if (ret == MBEDTLS_ERR_SSL_WANT_READ) { + state_ = READING; + } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + state_ = WRITING; + } + break; + } + } + return ret; + } + + // Converts OpenSSL verification mode to mbedtls enum + int impl_verify_mode(bool is_client_not_server) const + { + int mode = MBEDTLS_SSL_VERIFY_UNSET; + if (is_client_not_server) { + if (verify_mode_ & SSL_VERIFY_PEER) + mode = MBEDTLS_SSL_VERIFY_REQUIRED; + else if (verify_mode_ == SSL_VERIFY_NONE) + mode = MBEDTLS_SSL_VERIFY_NONE; + } else { + if (verify_mode_ & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + mode = MBEDTLS_SSL_VERIFY_REQUIRED; + else if (verify_mode_ & SSL_VERIFY_PEER) + mode = MBEDTLS_SSL_VERIFY_OPTIONAL; + else if (verify_mode_ == SSL_VERIFY_NONE) + mode = MBEDTLS_SSL_VERIFY_NONE; + } + return mode; + } + + struct impl { + static void print_error(const char* function, int error_code) + { + constexpr const char *TAG="mbedtls-engine-impl"; + ESP_LOGE(TAG, "%s() returned -0x%04X", function, -error_code); + ESP_LOGI(TAG, "-0x%04X: %s", -error_code, error_message(error_code)); + } + + bool before_handshake() const + { + return ssl_.MBEDTLS_PRIVATE(state) == 0; + } + + int write(const void *buffer, int len) + { + int ret = mbedtls_ssl_write(&ssl_, static_cast(buffer), len); + if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + print_error("mbedtls_ssl_write", ret); + } + return ret; + } + + int read(void *buffer, int len) + { + int ret = mbedtls_ssl_read(&ssl_, static_cast(buffer), len); + if (ret < 0 && ret != MBEDTLS_ERR_SSL_WANT_READ) { + print_error("mbedtls_ssl_read", ret); + } + return ret; + } + + impl() + { + const unsigned char pers[] = "asio ssl"; + mbedtls_ssl_init(&ssl_); + mbedtls_ssl_config_init(&conf_); + mbedtls_ctr_drbg_init(&ctr_drbg_); +#ifdef CONFIG_MBEDTLS_DEBUG + mbedtls_esp_enable_debug_log(&conf_, CONFIG_MBEDTLS_DEBUG_LEVEL); +#endif + mbedtls_entropy_init(&entropy_); + mbedtls_ctr_drbg_seed(&ctr_drbg_, mbedtls_entropy_func, &entropy_, pers, sizeof(pers)); + mbedtls_x509_crt_init(&public_cert_); + mbedtls_pk_init(&pk_key_); + mbedtls_x509_crt_init(&ca_cert_); + } + + bool configure(context *ctx, bool is_client_not_server, int mbedtls_verify_mode) + { + mbedtls_x509_crt_init(&public_cert_); + mbedtls_pk_init(&pk_key_); + mbedtls_x509_crt_init(&ca_cert_); + int ret = mbedtls_ssl_config_defaults(&conf_, is_client_not_server ? MBEDTLS_SSL_IS_CLIENT: MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, MBEDTLS_SSL_PRESET_DEFAULT); + if (ret) { + print_error("mbedtls_ssl_config_defaults", ret); + return false; + } + mbedtls_ssl_conf_rng(&conf_, mbedtls_ctr_drbg_random, &ctr_drbg_); + mbedtls_ssl_conf_authmode(&conf_, mbedtls_verify_mode); + if (ctx->cert_chain_.size() > 0 && ctx->private_key_.size() > 0) { + ret = mbedtls_x509_crt_parse(&public_cert_, ctx->data(container::CERT), ctx->size(container::CERT)); + if (ret < 0) { + print_error("mbedtls_x509_crt_parse", ret); + return false; + } + ret = mbedtls_pk_parse_key(&pk_key_, ctx->data(container::PRIVKEY), ctx->size(container::PRIVKEY), + nullptr, 0, mbedtls_ctr_drbg_random, &ctr_drbg_); + if (ret < 0) { + print_error("mbedtls_pk_parse_keyfile", ret); + return false; + } + ret = mbedtls_ssl_conf_own_cert(&conf_, &public_cert_, &pk_key_); + if (ret) { + print_error("mbedtls_ssl_conf_own_cert", ret); + return false; + } + } + + if (ctx->ca_cert_.size() > 0) { + ret = mbedtls_x509_crt_parse(&ca_cert_, ctx->data(container::CA_CERT), ctx->size(container::CA_CERT)); + if (ret < 0) { + print_error("mbedtls_x509_crt_parse", ret); + return false; + } + mbedtls_ssl_conf_ca_chain(&conf_, &ca_cert_, nullptr); + } else { + mbedtls_ssl_conf_ca_chain(&conf_, nullptr, nullptr); + } + ret = mbedtls_ssl_setup(&ssl_, &conf_); + if (ret) { + print_error("mbedtls_ssl_setup", ret); + return false; + } + return true; + } + mbedtls_ssl_context ssl_{}; + mbedtls_entropy_context entropy_{}; + mbedtls_ctr_drbg_context ctr_drbg_{}; + mbedtls_ssl_config conf_{}; + mbedtls_x509_crt public_cert_{}; + mbedtls_pk_context pk_key_{}; + mbedtls_x509_crt ca_cert_{}; + }; + + impl impl_{}; + std::shared_ptr ctx_; + std::pair, std::shared_ptr> bio_; + enum rw_state state_; + asio::ssl::verify_mode verify_mode_; +}; + +} } } // namespace asio::ssl::mbedtls diff --git a/components/asio/port/mbedtls/include/mbedtls_error.hpp b/components/asio/port/mbedtls/include/mbedtls_error.hpp new file mode 100644 index 000000000..083cf6c88 --- /dev/null +++ b/components/asio/port/mbedtls/include/mbedtls_error.hpp @@ -0,0 +1,55 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "asio/detail/config.hpp" +#include "asio/ssl/error.hpp" +#include "asio/ssl/detail/openssl_init.hpp" +#include "mbedtls_context.hpp" + +namespace asio { +namespace error { +namespace detail { + +class mbedtls_category : public asio::error_category +{ +public: + const char* name() const ASIO_ERROR_CATEGORY_NOEXCEPT + { + return "asio.ssl"; + } + + std::string message(int value) const + { + const char* s = asio::ssl::mbedtls::error_message(value); + return s ? s : "asio.mbedtls error"; + } +}; + +} // namespace detail + +const asio::error_category& get_mbedtls_category() +{ + static detail::mbedtls_category instance; + return instance; +} + +const asio::error_category& get_ssl_category() +{ + return asio::error::get_mbedtls_category(); +} + +} // namespace error + +namespace ssl { +namespace error { + +const asio::error_category& get_stream_category() +{ + return asio::error::get_mbedtls_category(); +} + +} } } // namespace asio::ssl::error diff --git a/components/asio/port/mbedtls/include/openssl/conf.h b/components/asio/port/mbedtls/include/openssl/conf.h new file mode 100644 index 000000000..dc024544e --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/conf.h @@ -0,0 +1,6 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once diff --git a/components/asio/port/mbedtls/include/openssl/dh.h b/components/asio/port/mbedtls/include/openssl/dh.h new file mode 100644 index 000000000..dc024544e --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/dh.h @@ -0,0 +1,6 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once diff --git a/components/asio/port/mbedtls/include/openssl/err.h b/components/asio/port/mbedtls/include/openssl/err.h new file mode 100644 index 000000000..dc024544e --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/err.h @@ -0,0 +1,6 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once diff --git a/components/asio/port/mbedtls/include/openssl/rsa.h b/components/asio/port/mbedtls/include/openssl/rsa.h new file mode 100644 index 000000000..dc024544e --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/rsa.h @@ -0,0 +1,6 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once diff --git a/components/asio/port/mbedtls/include/openssl/ssl.h b/components/asio/port/mbedtls/include/openssl/ssl.h new file mode 100644 index 000000000..be147bc00 --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/ssl.h @@ -0,0 +1,8 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once + +#include "openssl_stub.hpp" diff --git a/components/asio/port/mbedtls/include/openssl/x509v3.h b/components/asio/port/mbedtls/include/openssl/x509v3.h new file mode 100644 index 000000000..dc024544e --- /dev/null +++ b/components/asio/port/mbedtls/include/openssl/x509v3.h @@ -0,0 +1,6 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: BSL-1.0 +// +#pragma once diff --git a/components/asio/port/mbedtls/src/mbedtls_context.cpp b/components/asio/port/mbedtls/src/mbedtls_context.cpp new file mode 100644 index 000000000..36ba700dd --- /dev/null +++ b/components/asio/port/mbedtls/src/mbedtls_context.cpp @@ -0,0 +1,115 @@ +// +// SPDX-FileCopyrightText: 2005 Voipster / Indrek dot Juhani at voipster dot com +// SPDX-FileCopyrightText: 2005-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// SPDX-License-Identifier: BSL-1.0 +// +// SPDX-FileContributor: 2021 Espressif Systems (Shanghai) CO LTD +// + +#include "asio/detail/config.hpp" +#include "openssl_stub.hpp" +#include +#include "asio/detail/throw_error.hpp" +#include "asio/error.hpp" +#include "asio/ssl/context.hpp" +#include "asio/ssl/error.hpp" +#include "mbedtls_context.hpp" + + +namespace asio { +namespace ssl { + + +context::context(context::method m) + : handle_(0) +{ + handle_ = mbedtls::shared_ctx::create("mbedtls-context", m); + set_options(no_compression); +} + +context::context(context&& other) +{ + handle_ = other.handle_; + other.handle_ = 0; +} + +context& context::operator=(context&& other) +{ + context tmp(ASIO_MOVE_CAST(context)(*this)); + handle_ = other.handle_; + other.handle_ = 0; + return *this; +} + +context::~context() +{ + delete handle_; +} + +context::native_handle_type context::native_handle() +{ + return handle_; +} + +void context::set_options(context::options o) +{ + asio::error_code ec; + set_options(o, ec); + asio::detail::throw_error(ec, "set_options"); +} + +ASIO_SYNC_OP_VOID context::set_options( + context::options o, asio::error_code& ec) +{ + handle_->get()->options_ = o; + ec = asio::error_code(); + ASIO_SYNC_OP_VOID_RETURN(ec); +} + +void context::add_certificate_authority(const const_buffer& ca) +{ + asio::error_code ec; + add_certificate_authority(ca, ec); + asio::detail::throw_error(ec, "add_certificate_authority"); +} + +ASIO_SYNC_OP_VOID context::add_certificate_authority( + const const_buffer& ca, asio::error_code& ec) +{ + handle_->get()->ca_cert_ = ca; + ASIO_SYNC_OP_VOID_RETURN(asio::error_code()); +} + +void context::use_certificate_chain(const const_buffer& chain) +{ + asio::error_code ec; + use_certificate_chain(chain, ec); + asio::detail::throw_error(ec, "use_certificate_chain"); +} + +ASIO_SYNC_OP_VOID context::use_certificate_chain( + const const_buffer& chain, asio::error_code& ec) +{ + handle_->get()->cert_chain_ = chain; + ASIO_SYNC_OP_VOID_RETURN(asio::error_code()); +} + +void context::use_private_key( + const const_buffer& private_key, context::file_format format) +{ + asio::error_code ec; + use_private_key(private_key, format, ec); + asio::detail::throw_error(ec, "use_private_key"); +} + +ASIO_SYNC_OP_VOID context::use_private_key( + const const_buffer& private_key, context::file_format format, + asio::error_code& ec) +{ + handle_->get()->private_key_ = private_key; + ASIO_SYNC_OP_VOID_RETURN(asio::error_code()); +} + +} // namespace ssl +} // namespace asio diff --git a/components/asio/port/mbedtls/src/mbedtls_engine.cpp b/components/asio/port/mbedtls/src/mbedtls_engine.cpp new file mode 100644 index 000000000..c3ff91842 --- /dev/null +++ b/components/asio/port/mbedtls/src/mbedtls_engine.cpp @@ -0,0 +1,208 @@ +// +// SPDX-FileCopyrightText: 2003-2019 Christopher M. Kohlhoff (chris at kohlhoff dot com) +// +// SPDX-License-Identifier: BSL-1.0 +// +// SPDX-FileContributor: 2021 Espressif Systems (Shanghai) CO LTD +// + +#include "asio/detail/config.hpp" +#include "openssl_stub.hpp" +#include "asio/detail/throw_error.hpp" +#include "asio/error.hpp" +#include "asio/ssl/detail/engine.hpp" +#include "asio/ssl/error.hpp" +#include "asio/ssl/verify_context.hpp" +#include "mbedtls_context.hpp" +#include "mbedtls_bio.hpp" +#include "mbedtls_error.hpp" +#include "mbedtls_engine.hpp" + + +namespace asio { +namespace ssl { +namespace detail { + + +engine::engine(SSL_CTX* context) + : ssl_(nullptr) +{ + ssl_ = mbedtls::create("mbedtls-engine", context->get()); +} + +engine::~engine() +{ + delete ssl_; +} + +SSL* engine::native_handle() +{ + return ssl_; +} + +asio::error_code engine::set_verify_mode( + verify_mode v, asio::error_code& ec) +{ + ssl_->set_verify_mode(v); + return {}; +} + +engine::want engine::handshake( + stream_base::handshake_type type, asio::error_code& ec) +{ + return perform((type == asio::ssl::stream_base::client) + ? &engine::do_connect : &engine::do_accept, 0, 0, ec, 0); +} + +engine::want engine::shutdown(asio::error_code& ec) +{ + return perform(&engine::do_shutdown, 0, 0, ec, 0); +} + +engine::want engine::write(const asio::const_buffer& data, + asio::error_code& ec, std::size_t& bytes_transferred) +{ + if (data.size() == 0) + { + ec = asio::error_code(); + return engine::want_nothing; + } + + return perform(&engine::do_write, + const_cast(data.data()), + data.size(), ec, &bytes_transferred); +} + +engine::want engine::read(const asio::mutable_buffer& data, + asio::error_code& ec, std::size_t& bytes_transferred) +{ + if (data.size() == 0) + { + ec = asio::error_code(); + return engine::want_nothing; + } + + return perform(&engine::do_read, data.data(), + data.size(), ec, &bytes_transferred); +} + +asio::mutable_buffer engine::get_output( + const asio::mutable_buffer& data) +{ + int length = ssl_->ext_bio()->read(data.data(), static_cast(data.size())); + + return asio::buffer(data, + length > 0 ? static_cast(length) : 0); +} + +asio::const_buffer engine::put_input( + const asio::const_buffer& data) +{ + int length = ssl_->ext_bio()->write(data.data(), static_cast(data.size())); + + return asio::buffer(data + + (length > 0 ? static_cast(length) : 0)); +} + +const asio::error_code& engine::map_error_code( + asio::error_code& ec) const +{ + // We only want to map the error::eof code. + if (ec != asio::error::eof) + return ec; + + // If there's data yet to be read, it's an error. + if (ssl_->ext_bio()->wpending()) + { + ec = asio::ssl::error::stream_truncated; + return ec; + } + + // Otherwise, the peer should have negotiated a proper shutdown. + if (ssl_->shutdown() != 0) + { + ec = asio::ssl::error::stream_truncated; + } + + return ec; +} + +// This is a simplified implementation of a generic ssl io operation +// original implementation using openssl's SSL object is in asio/include/asio/ssl/detail/impl/engine.ipp +engine::want engine::perform(int (engine::* op)(void*, std::size_t), + void* data, std::size_t length, asio::error_code& ec, + std::size_t* bytes_transferred) +{ + std::size_t pending_output_before = ssl_->ext_bio()->ctrl_pending(); + int result = (this->*op)(data, length); + + std::size_t pending_output_after = ssl_->ext_bio()->ctrl_pending(); + + if (mbedtls::error_codes::is_error(result)) + { + ec = asio::error_code(result, asio::error::get_mbedtls_category()); + return pending_output_after > pending_output_before ? want_output : want_nothing; + } + + if (result == 0) + { + return pending_output_after > pending_output_before + ? want_output : want_nothing; + } + + if (result > 0 && bytes_transferred) + *bytes_transferred = static_cast(result); + + if (mbedtls::error_codes::want_write(result)) + { + ec = asio::error_code(); + return want_output_and_retry; + } + else if (pending_output_after > pending_output_before) + { + ec = asio::error_code(); + return result > 0 ? want_output : want_output_and_retry; + } + else if (mbedtls::error_codes::want_read(result)) + { + ec = asio::error_code(); + return want_input_and_retry; + } + else if (ssl_->get_state() == mbedtls::CLOSED) + { + ec = asio::error::eof; + return want_nothing; + } + + ec = asio::error_code(); + return want_nothing; +} + +int engine::do_accept(void*, std::size_t) +{ + return ssl_->accept(); +} + +int engine::do_connect(void*, std::size_t) +{ + return ssl_->connect(); +} + +int engine::do_shutdown(void*, std::size_t) +{ + return ssl_->shutdown(); +} + +int engine::do_read(void* data, std::size_t length) +{ + return ssl_->read(data, length < INT_MAX ? static_cast(length) : INT_MAX); +} + +int engine::do_write(void* data, std::size_t length) +{ + return ssl_->write(data, length < INT_MAX ? static_cast(length) : INT_MAX); +} + +} // namespace detail +} // namespace ssl +} // namespace asio diff --git a/components/asio/port/src/asio_ssl_impl.cpp b/components/asio/port/src/asio_ssl_impl.cpp new file mode 100644 index 000000000..8f387dd7b --- /dev/null +++ b/components/asio/port/src/asio_ssl_impl.cpp @@ -0,0 +1,20 @@ +// +// SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD +// +// SPDX-License-Identifier: Apache-2.0 +// + +#include "asio/detail/config.hpp" +#include "asio/ssl/detail/openssl_init.hpp" + +namespace asio { +namespace ssl { +namespace detail { + +// No OpenSSL in this implementation, instance is nullptr +asio::detail::shared_ptr openssl_init_base::instance() +{ + return nullptr; +} + +} } } // namespace asio::ssl::detail