| 
									
										
										
										
											2021-09-20 15:25:02 -03:00
										 |  |  | /*
 | 
					
						
							| 
									
										
										
										
											2021-12-01 10:48:49 -03:00
										 |  |  |  * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD | 
					
						
							| 
									
										
										
										
											2021-09-20 15:25:02 -03:00
										 |  |  |  * | 
					
						
							|  |  |  |  * SPDX-License-Identifier: CC0-1.0 | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  *  ASIO HTTP request example | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #include <string>
 | 
					
						
							|  |  |  | #include <array>
 | 
					
						
							|  |  |  | #include <asio.hpp>
 | 
					
						
							|  |  |  | #include <memory>
 | 
					
						
							|  |  |  | #include <system_error>
 | 
					
						
							|  |  |  | #include <utility>
 | 
					
						
							|  |  |  | #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<AddressResolution> { | 
					
						
							|  |  |  | 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<class CompletionToken> | 
					
						
							|  |  |  |     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<Connection> { | 
					
						
							|  |  |  | 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 | 
					
						
							| 
									
										
										
										
											2021-12-01 10:48:49 -03:00
										 |  |  |      *                           service name typically seen in ASIO examples. | 
					
						
							| 
									
										
										
										
											2021-09-20 15:25:02 -03:00
										 |  |  |      * | 
					
						
							|  |  |  |      * @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<class CompletionToken> | 
					
						
							|  |  |  |     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<class DataType, class CompletionToken> | 
					
						
							|  |  |  |     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<class DataBuffer, class CompletionToken> | 
					
						
							|  |  |  |     void read_async(DataBuffer &&in_data, CompletionToken &&completion_handler) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         asio::async_read(socket, in_data, asio::bind_executor(strand, completion_handler)); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | private: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     template<class CompletionToken> | 
					
						
							|  |  |  |     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<class DataIt> | 
					
						
							|  |  |  |     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<Session> { | 
					
						
							|  |  |  | public: | 
					
						
							|  |  |  |     explicit Session(std::shared_ptr<Connection> connection_in) : connection(std::move(connection_in)) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     template<class CompletionToken> | 
					
						
							|  |  |  |     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<class CompletionToken> | 
					
						
							|  |  |  |     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<char, 2048> 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<asio::const_buffer, 2> send_data; | 
					
						
							|  |  |  |     std::shared_ptr<Connection> 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<class CompletionToken> | 
					
						
							|  |  |  | 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<AddressResolution>(context)->resolve(request.host(), request.service_port(), | 
					
						
							|  |  |  |     [&context, &request, completion_handler](std::shared_ptr<AddressResolution> resolver, tcp::resolver::results_type results) { | 
					
						
							|  |  |  |         /* After resolution we create a Connection.
 | 
					
						
							|  |  |  |         *  The completion handler gets a shared_ptr<Connection> to receive the connection, once the | 
					
						
							|  |  |  |         *  connection process is complete. | 
					
						
							|  |  |  |         */ | 
					
						
							|  |  |  |         std::make_shared<Connection>(context)->start(results, | 
					
						
							|  |  |  |         [&request, completion_handler](std::shared_ptr<Connection> connection) { | 
					
						
							|  |  |  |             // Now we create a HTTP::Session and inject the necessary connection.
 | 
					
						
							|  |  |  |             std::make_shared<Session>(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<Http::Session> 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()); | 
					
						
							|  |  |  | } |