Ver código fonte

refactor kbf::http

Bence Balint 3 anos atrás
pai
commit
c6593bcc6b

+ 3 - 2
CMakeLists.txt

@@ -5,8 +5,9 @@ idf_component_register(
         "src/kbf.cpp"
         "src/kbf_adc.cpp"
         "src/kbf_gpio.cpp"
-        "src/kbf_http_client.cpp"
-        "src/kbf_http_server.cpp"
+        "src/http/client.cpp"
+        "src/http/common.cpp"
+        "src/http/server.cpp"
         "src/kbf_net.cpp"
         "src/kbf_nvs.cpp"
         "src/kbf_spiffs.cpp"

+ 102 - 0
include/http/client.h

@@ -0,0 +1,102 @@
+#ifndef KBF_HTTP_CLIENT_H
+#define KBF_HTTP_CLIENT_H
+
+#include <string>
+#include <memory>
+
+#include <esp_err.h>
+#include <esp_http_client.h>
+
+#include "kbf_http.h"
+
+using std::string;
+using std::shared_ptr;
+
+namespace kbf::http {
+    /**
+     * @brief Asynchronous HTTP Client.
+     *
+     * @note May only perform 1 request at a time, but can be reused afterwards.
+     */
+    class Client {
+    public:
+        /** @brief Tag used for logging. */
+        static constexpr const char *TAG = "kbf::http::Client";
+
+        /**
+         * @brief Constructor.
+         *
+         * @warning Async mode is only supported by IDF for HTTPS.
+         *
+         * @param async perform request asynchronously; default is false
+         */
+        explicit Client(bool async = false);
+
+        /**
+         * @brief Destructor.
+         *
+         * Calls esp_http_client_cleanup().
+         */
+        ~Client();
+
+        /**
+         * @brief Performs an HTTP GET.
+         *
+         * Calls onSuccess() when a response is received (even if it's a HTTP 4XX), or onError() on failure
+         * (e.g. socket error)
+         *
+         * @param url_param url to fetch
+         */
+        shared_ptr<Response> get(const string &url_param);
+
+        /**
+         * @brief Called on HTTP_EVENT_ERROR event.
+         *
+         * @note This will be called for low-level errors (e.g. socket error). If the request went through normally,
+         * but the server responded with an error code (HTTP 4XX), onSuccess() will be called instead.
+         *
+         * @param client reference to the Client that issued the request
+         */
+        void (*onError)(Client &client) = nullptr;
+
+        /**
+         * @brief Called after the response is received.
+         *
+         * @note "Success" here means that the request went through and a response was received.
+         * This method will be called even if the server returned an error status (HTTP 4XX)
+         *
+         * @param client reference to the Client that issued the request
+         * @param response struct containing the HTTP response data
+         */
+        void (*onSuccess)(Client &client, const Response &response) = nullptr;
+
+        /**
+         * @brief Returns whether a request is currently in progress.
+         *
+         * @return true if a request is currently in progress; false otherwise
+         */
+        [[nodiscard]] bool isRunning() const { return running; }
+
+    private:
+        void init();
+
+        static void updateResponse(Client &client);
+
+        /**
+         * @brief Processes events from esp_http_client.
+         *
+         * @return
+         */
+        static esp_err_t handleHttpEvent(esp_http_client_event_t *);
+
+        esp_http_client_handle_t handle{};
+        string                   buffer;
+        shared_ptr<Response>     response;
+        bool                     running = false;
+        bool                     retry   = false;
+        bool                     async;
+    };
+
+}
+
+#endif //KBF_HTTP_CLIENT_H

+ 96 - 0
include/http/server.h

@@ -0,0 +1,96 @@
+#ifndef KBF_HTTP_SERVER_H
+#define KBF_HTTP_SERVER_H
+
+#include <string>
+#include <vector>
+#include <map>
+
+#include <esp_http_server.h>
+
+#include "kbf_http.h"
+
+using std::string;
+using std::vector;
+using std::map;
+
+namespace kbf::http {
+    /** @brief HTTP Server */
+    class Server {
+    public:
+        /** @brief Tag used for logging. */
+        static constexpr const char *TAG = "kbf::http::Server";
+
+        /**
+         * @brief Destructor.
+         *
+         * Will call stop() if #running.
+         */
+        ~Server();
+
+        /**
+         * @brief Holds information about a route and it's handler.
+         */
+        struct Route {
+        public:
+            /** @brief HTTP method */
+            http::Method method;
+
+            /**
+             * @brief URI
+             *
+             * @example "/"
+             * @example "/foo"
+             */
+            const string uri;
+
+            /**
+             * @brief Request handler function.
+             *
+             * @param request Request sent by a client
+             * @return Response to be sent to the client
+             */
+            Response (*handler)(const Request &request);
+        };
+
+        /**
+         * @brief Adds a Route handler.
+         *
+         * @note All routes must be added before calling start().
+         *
+         * @note Removing routes is not yet supported.
+         *
+         * @param route Route to add
+         * @return *this (to enable builder-like syntax)
+         */
+        Server &route(Route route);
+
+        /**
+         * @brief Starts the HTTP server.
+         *
+         * @param port TCP port, default is 80
+         * @return *this (to enable builder-like syntax)
+         */
+        Server &start(int port = 80);
+
+        /**
+         * @brief Stops the HTTP server.
+         */
+        void stop();
+
+        /**
+         * @brief Returns whether the server is running.
+         *
+         * @return true if server is running; false otherwise
+         */
+        [[nodiscard]] bool isRunning() const { return running; }
+
+    private:
+        bool           running = false;
+        httpd_handle_t handle  = nullptr;
+        vector<Route>  routes;
+
+        static esp_err_t handleHttpRequest(httpd_req_t *httpdRequest);
+    };
+}
+
+#endif //KBF_HTTP_SERVER_H

+ 140 - 0
include/kbf_http.h

@@ -0,0 +1,140 @@
+#ifndef KBF_HTTP_H
+#define KBF_HTTP_H
+
+#include <string>
+#include <vector>
+#include <map>
+
+#include <esp_http_server.h>
+
+using std::string;
+using std::vector;
+using std::map;
+
+/**
+ * @brief HTTP functions
+ */
+namespace kbf::http {
+    /** @brief Tag used for logging. */
+    static constexpr const char *TAG = "kbf::http";
+
+    /**
+     * @brief Supported HTTP methods.
+     *
+     * Mostly used for syntax consistency; values match that of httpd_method_t.
+     */
+    enum Method {
+        GET     = 1,
+        POST    = 3,
+        OPTIONS = 6,
+    };
+
+    /**
+     * @brief Base class for HTTP Request and Response objects.
+     */
+    class HTTPObject {
+    public:
+        /**
+         * @brief HTTP headers
+         *
+         * @warning Parsing headers from the incoming response is not supported in kbf::http::Client (yet).
+         *
+         * @note Only the headers listed in acceptedHeaders will be parsed automatically for incoming
+         * requests. For parsing custom headers, use readHeader().
+         */
+        map<string, string> headers;
+
+        /** @brief body */
+        string body;
+
+        /**
+         * @brief POST data
+         */
+        map<string, string> postData;
+
+    protected:
+        HTTPObject() = default;
+
+        explicit HTTPObject(string body);
+
+        static void parseToMap(map<string, string> &target, const string &buffer);
+    };
+
+    /**
+     * @brief Handles a HTTP request.
+     */
+    class Request : public HTTPObject {
+    public:
+        /**
+         * @brief Creates instance from a httpd_req_t object.
+         *
+         * @param httpdRequest
+         */
+        explicit Request(httpd_req_t *httpdRequest);
+
+        /**
+         * @brief Vector of headers automatically parsed for incoming requests.
+         */
+        static inline const vector<string> acceptedHeaders = { // NOLINT(cert-err58-cpp)
+                "Host",
+                "User-Agent",
+                "Content-Type",
+        };
+
+        /** @brief number of retry attempts in case of a socket read timeout */
+        static inline const int MAX_READ_RETRY = 3;
+
+        /**
+         * @brief Parse a header from the httpd_req_t object. The key-value pair will also be added to the #headers map.
+         *
+         * @warning Can only be used on incoming requests.
+         *
+         * @param header
+         * @return header string
+         */
+        string readHeader(const string &header);
+
+        /** @brief HTTP method */
+        Method method;
+
+        /** @brief request URI */
+        string uri;
+
+        /** @brief query parameters */
+        map<string, string> query{};
+
+
+    private:
+        httpd_req_t *httpdRequest;
+
+        void readQuery();
+
+        void readBody();
+    };
+
+    /**
+     * @brief Stores a HTTP response.
+     */
+    class Response : public HTTPObject {
+    public:
+        /**
+         * @brief Default constructor.
+         *
+         * @param body response body; default is ""
+         * @param status HTTP status code; default is 200
+         * @param contentType value of Content-Type header; default is "text/html"
+         */
+        explicit Response(string body = "", int status = 200, string contentType = "text/html");
+
+        /** @brief HTTP status code */
+        int status; // TODO enumerate me
+
+        /** @brief HTTP status text */
+        string statusText = "OK";
+
+        /** @brief value of Content-Type header */
+        string contentType;
+    };
+}
+
+#endif //KBF_HTTP_H

+ 0 - 77
include/kbf_http_client.h

@@ -1,77 +0,0 @@
-#ifndef KBF_HTTP_H
-#define KBF_HTTP_H
-
-#include <string>
-#include <string_view>
-
-#include <esp_err.h>
-#include <esp_http_client.h>
-
-#include "kbf.h" // TODO can we include this only from the .cpp?
-
-using std::string;
-
-namespace kbf::http {
-    /**
-     * Asynchronous HTTP Client.
-     * May only perform 1 request at a time, but can be reused afterwards.
-     */
-    class Client {
-    public:
-        static constexpr const char *TAG = "kbf::http::AsyncClient";
-        Client();
-
-        /**
-         * Performs an HTTP GET.
-         *
-         * @param url_param url to fetch
-         */
-        void get(const string &url_param);
-
-        /**
-         * Holds HTTP response data.
-         *
-         * @note body is a weak reference to the buffer and will be overwritten by subsequent requests!
-         */
-        struct Response {
-            int status;
-            int contentLength;
-            std::string_view body;
-        };
-
-        /**
-         * Called on HTTP_EVENT_ERROR event.
-         *
-         * @param client reference to the AsyncClient that issued the request
-         */
-        void (*onError)(Client &client) = nullptr;
-
-        /**
-         * Called after the response is received.
-         *
-         * @param client reference to the AsyncClient that issued the request
-         * @param response struct containing the HTTP response data
-         */
-        void (*onFinish)(Client &client, Response response) = nullptr;
-
-    private:
-        /**
-         * RTOS task handler
-         */
-        static void task(void *);
-
-        /**
-         * Processes events from esp_http_client.
-         *
-         * @return
-         */
-        static esp_err_t handleHttpEvent(esp_http_client_event_t *);
-
-        esp_http_client_handle_t handle{};
-        string url;
-        string buffer;
-    };
-
-}
-
-#endif //KBF_HTTP_H

+ 0 - 92
include/kbf_http_server.h

@@ -1,92 +0,0 @@
-#ifndef KBF_HTTP_SERVER_H
-#define KBF_HTTP_SERVER_H
-
-#include <string>
-#include <vector>
-#include <map>
-
-#include <esp_http_server.h>
-
-using std::string;
-using std::vector;
-using std::map;
-
-namespace kbf::http {
-    /**
-     * Supported HTTP methods.
-     * Mostly used for syntax consistency; values match that of httpd_method_t.
-     */
-    enum Method {
-        GET = 1,
-        POST = 3,
-        OPTIONS = 6
-    };
-
-    class Request {
-    public:
-        explicit Request(httpd_req_t *httpdRequest);
-
-        static inline const vector<string> acceptedHeaders = {
-                "Host",
-                "User-Agent",
-                "Content-Type",
-        };
-
-        static inline const int MAX_READ_RETRY = 3;
-
-        Method method;
-        string uri;
-        map<string, string> query;
-        map<string, string> headers;
-        string body;
-        map<string, string> postData;
-    private:
-        httpd_req_t *httpdRequest;
-
-        void readHeader(const string &header);
-
-        void readQuery();
-
-        void readBody();
-
-        static void parseToMap(map<string, string> &target, const string &buffer);
-    };
-
-    class Response {
-    public:
-        explicit Response(string body = "", int status = 200, string contentType = "text/html");
-
-        string body;
-        int status; // TODO enumerate me
-        string statusText = "OK";
-        string contentType;
-        map<string, string> headers;
-    };
-
-    class Server {
-    public:
-        struct Route {
-            Method method;
-            const string &uri;
-
-            Response (*handler)(const Request &request);
-        };
-
-        Server &route(Route route);
-
-        void start(int port = 80);
-
-        void stop();
-
-        [[nodiscard]] bool isRunning() const { return running; }
-
-    private:
-        bool running = false;
-        httpd_handle_t handle = nullptr;
-        vector<Route> routes;
-
-        static esp_err_t handleHttpRequest(httpd_req_t *httpdRequest);
-    };
-}
-
-#endif //KBF_HTTP_SERVER_H

+ 2 - 4
include/kbf_wifi.h

@@ -35,11 +35,9 @@ namespace kbf::wifi {
     /**
      * @brief Starts WiFi in STA mode.
      *
-     * @see #kbf::wifi::AP::create()
-     *
-     * @param sta STA pointer
+     * @param sta STA pointer; if nullptr, kbf::wifi::STA::create() will be called.
      */
-    void start(shared_ptr<STA> sta);
+    void start(shared_ptr<STA> sta = nullptr);
 
     /**
      * @brief Starts WiFi in AP+STA dual mode.

+ 131 - 0
src/http/client.cpp

@@ -0,0 +1,131 @@
+#include "http/client.h"
+
+#include <freertos/task.h>
+#include <esp_log.h>
+
+#include "kbf_assert.h"
+
+using namespace kbf;
+using std::make_shared;
+
+http::Client::Client(bool async) : buffer(), async(async) {
+    ESP_LOGD(TAG, "Client()");
+    init();
+}
+
+void http::Client::init() {
+    ESP_LOGD(TAG, "init()");
+    esp_http_client_config_t config{};
+    config.event_handler         = handleHttpEvent;
+    config.user_data             = this;
+    config.host                  = "localhost";
+    config.path                  = "/";
+    config.disable_auto_redirect = false; // do not set to true, IDF bug :(
+    config.max_redirection_count = 0;
+    config.is_async              = async;
+    handle = esp_http_client_init(&config);
+}
+
+
+http::Client::~Client() {
+    ESP_LOGD(TAG, "~Client()");
+    CHECK(esp_http_client_cleanup(handle));
+}
+
+shared_ptr<http::Response> http::Client::get(const string &url) {
+    ESP_LOGD(TAG, "get(%s)", url.c_str());
+
+    if (running) {
+        ESP_LOGE(TAG, "request already in progress");
+        return nullptr;
+    }
+    running = true;
+
+    response = shared_ptr<Response>(new Response()); // TODO why doesn't make_shared work here? :/
+    buffer.clear();
+
+    esp_http_client_set_method(handle, HTTP_METHOD_GET);
+    esp_http_client_set_url(handle, url.c_str());
+    auto err = esp_http_client_perform(handle);
+
+    if (err == ESP_ERR_HTTP_MAX_REDIRECT) {
+        // TODO this seems get triggered on HTTP 4XX
+        ESP_LOGW(TAG, "reached redirect limit");
+        updateResponse(*this);
+        err = ESP_OK;
+    }
+
+    if (err == ESP_ERR_HTTP_EAGAIN) {
+        ESP_LOGD(TAG, "async request in progress, returning nullptr");
+        return nullptr;
+    } else if (err == ESP_OK) {
+        ESP_LOGD(TAG, "success");
+        return response;
+    } else if (err == ESP_ERR_HTTP_FETCH_HEADER && !retry) {
+        ESP_LOGD(TAG, "fetching headers failed, maybe the connection was dropped? retrying...");
+        retry = true;
+        esp_http_client_cleanup(handle);
+        init();
+        return get(url);
+    } else {
+        ESP_LOGE(TAG, "HTTP GET failed: %s", esp_err_to_name(err));
+        ABORT("debug me");
+    }
+}
+
+esp_err_t http::Client::handleHttpEvent(esp_http_client_event_t *event) {
+    auto client = (Client *) event->user_data;
+
+    switch (event->event_id) {
+        case HTTP_EVENT_ERROR:
+            ESP_LOGW(TAG, "fixme: unhandled error event: HTTP_EVENT_ERROR");
+            if (client->onError) client->onError(*client);
+            client->running = false;
+            client->retry   = false;
+            break;
+        case HTTP_EVENT_ON_CONNECTED:
+            ESP_LOGD(TAG, "connected");
+            break;
+        case HTTP_EVENT_HEADERS_SENT:
+            ESP_LOGD(TAG, "request sent");
+            break;
+        case HTTP_EVENT_ON_HEADER:
+            ESP_LOGD(TAG, "header received: %s: %s", event->header_key, event->header_value);
+            client->response->headers[event->header_key] = event->header_value;
+            break;
+        case HTTP_EVENT_ON_DATA:
+            ESP_LOGD(TAG, "data received");
+            if (esp_http_client_is_chunked_response(client->handle)) {
+                ESP_LOGE(TAG, "received chunked data");
+                ABORT("not implemented");
+            }
+            client->buffer.append((char *) event->data, event->data_len);
+            break;
+        case HTTP_EVENT_ON_FINISH:
+            ESP_LOGD(TAG, "finished");
+            updateResponse(*client);
+            break;
+        case HTTP_EVENT_DISCONNECTED:
+            ESP_LOGW(TAG, "disconnected");
+            client->running = false;
+            client->retry   = false;
+            break;
+    }
+
+    return ESP_OK;
+}
+
+void http::Client::updateResponse(http::Client &client) {
+    client.response->status = esp_http_client_get_status_code(client.handle);
+    client.response->body   = client.buffer;
+
+    ESP_LOGD(TAG, "HTTP status: %d, Content-Length: %s", client.response->status,
+             client.response->headers["Content-Length"].c_str());
+    ESP_LOG_BUFFER_HEXDUMP(TAG, client.response->body.data(), client.response->body.length(),
+                           ESP_LOG_VERBOSE);
+
+    if (client.onSuccess) { client.onSuccess(client, *client.response); }
+    client.running = false;
+    client.retry   = false;
+}
+

+ 100 - 0
src/http/common.cpp

@@ -0,0 +1,100 @@
+#include "kbf_http.h"
+
+#include <regex>
+#include <utility>
+
+#include <esp_log.h>
+#include <esp_http_server.h>
+
+#include "kbf_assert.h"
+
+
+void kbf::http::HTTPObject::parseToMap(map<string, string> &target, const string &buffer) {
+    auto queryString = string(buffer);
+
+    std::regex  regex("([\\w]+)=([\\w]+)");
+    std::smatch submatch;
+    while (std::regex_search(queryString, submatch, regex)) {
+        ESP_LOGD(TAG, "  parsed: %s = %s", submatch[1].str().c_str(), submatch[2].str().c_str());
+        target[submatch[1]] = submatch[2];
+        queryString = submatch.suffix();
+    }
+}
+
+kbf::http::Request::Request(httpd_req_t *httpdRequest) {
+    this->httpdRequest = httpdRequest;
+    method = static_cast<Method>(httpdRequest->method);
+    uri    = httpdRequest->uri;
+
+    for (const auto &name : acceptedHeaders) {
+        readHeader(name);
+    }
+    readQuery();
+    readBody();
+}
+
+string kbf::http::Request::readHeader(const string &header) {
+    auto len = httpd_req_get_hdr_value_len(httpdRequest, header.c_str());
+    if (len == 0) {
+        ESP_LOGD(TAG, "  header not found: %s", header.c_str());
+        return "";
+    }
+    char buffer[len + 1];
+    CHECK(httpd_req_get_hdr_value_str(httpdRequest, header.c_str(), buffer, len + 1));
+    ESP_LOGD(TAG, "  found header: %s: %s", header.c_str(), buffer);
+    headers[header] = buffer;
+    return buffer;
+}
+
+void kbf::http::Request::readQuery() {
+    auto queryLen = httpd_req_get_url_query_len(httpdRequest);
+    char buffer[queryLen + 1];
+
+    auto err = httpd_req_get_url_query_str(httpdRequest, buffer, queryLen + 1);
+    if (err == ESP_ERR_NOT_FOUND) {
+        ESP_LOGD(TAG, "no query found");
+        return;
+    } else
+        CHECK(err);
+
+    ESP_LOGD(TAG, "found query: %s", buffer);
+    parseToMap(query, buffer);
+}
+
+void kbf::http::Request::readBody() {
+    if (httpdRequest->content_len == 0) {
+        return;
+    }
+    ESP_LOGD(TAG, "reading body, length = %d", httpdRequest->content_len);
+
+    // TODO check if we have enough memory for the content!
+    char buffer[httpdRequest->content_len];
+
+    for (int i = 1; i <= MAX_READ_RETRY; i++) {
+        int ret;
+        if ((ret = httpd_req_recv(httpdRequest, buffer, httpdRequest->content_len)) <= 0) {
+            if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
+                if (i == MAX_READ_RETRY) {
+                    ESP_LOGE(TAG, "socket timeout, giving up");
+                    break;
+                }
+                ESP_LOGW(TAG, "socket timeout, retrying (attempt %d)", i);
+                continue;
+            }
+            ESP_LOGE(TAG, "socket read failed: %d (%s)", ret, esp_err_to_name(ret));
+        } else {
+            body = string(buffer, httpdRequest->content_len);
+            ESP_LOGD(TAG, "  read: \"%s\"", body.c_str());
+            parseToMap(postData, body);
+            break;
+        }
+    }
+}
+
+kbf::http::Response::Response(string body, int status, string contentType) : HTTPObject(std::move(body)),
+                                                                             status(status),
+                                                                             contentType(std::move(contentType)) {
+}
+
+kbf::http::HTTPObject::HTTPObject(string body) : body(std::move(body)) {
+}

+ 87 - 0
src/http/server.cpp

@@ -0,0 +1,87 @@
+#include "http/server.h"
+
+#include <esp_log.h>
+#include <memory>
+
+#include "kbf_assert.h"
+
+using namespace kbf;
+
+
+http::Server &http::Server::route(http::Server::Route route) {
+    ESP_LOGD(TAG, "adding route: %s", route.uri.c_str());
+    routes.push_back(route);
+    return *this;
+}
+
+http::Server &http::Server::start(int port) {
+    ESP_LOGI(TAG, "starting HTTP server on port %d", port);
+    if (running) {
+        ESP_LOGE(TAG, "server already running");
+        ABORT("fix me");
+    }
+
+    handle = nullptr;
+    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+    config.server_port = port;
+    config.stack_size  = 16384;
+    CHECK(httpd_start(&handle, &config));
+
+    ESP_LOGI(TAG, "registering URI handlers");
+    for (auto &route : routes) {
+        ESP_LOGI(TAG, "  method: %d; uri: %s", static_cast<int>(route.method), route.uri.c_str());
+        httpd_uri_t uriHandler = {
+                .uri = route.uri.c_str(),
+                .method = static_cast<httpd_method_t>(route.method),
+                .handler = handleHttpRequest,
+                .user_ctx = &route
+        };
+        CHECK(httpd_register_uri_handler(handle, &uriHandler));
+    }
+
+    running = true;
+    return *this;
+}
+
+void http::Server::stop() {
+    CHECK(httpd_stop(handle));
+    running = false;
+}
+
+// TODO handle all status codes; also maybe use macro or constexpr or something?
+static string getDefaultStatusText(int status) {
+    if (status == 200) return "OK";
+    if (status == 204) return "No Content";
+    if (status == 400) return "Bad Request";
+    if (status == 500) return "Internal Server Error";
+    return "Unknown HTTP Status";
+}
+
+esp_err_t http::Server::handleHttpRequest(httpd_req_t *httpdRequest) {
+    auto route = static_cast<Route *>(httpdRequest->user_ctx);
+    ESP_LOGD(TAG, "incoming request: method %d, path %s", httpdRequest->method, route->uri.c_str());
+
+    auto request  = Request(httpdRequest);
+    auto response = route->handler(request);
+
+    CHECK(httpd_resp_set_hdr(httpdRequest, "Server", "kbf_http_server/0.1"));
+    CHECK(httpd_resp_set_type(httpdRequest, response.contentType.c_str()));
+    for (const auto &[name, value] : response.headers) {
+        CHECK(httpd_resp_set_hdr(httpdRequest, name.c_str(), value.c_str()));
+    }
+
+    if (response.statusText.empty()) {
+        response.statusText = getDefaultStatusText(response.status);
+    }
+
+    string status = std::to_string(response.status) + " " + response.statusText;
+    ESP_LOGD(TAG, "sending response with status: %s", status.c_str());
+    CHECK(httpd_resp_set_status(httpdRequest, status.c_str()));
+
+    CHECK(httpd_resp_send(httpdRequest, response.body.c_str(), response.body.length()));
+    return ESP_OK;
+}
+
+http::Server::~Server() {
+    if (running) stop();
+}

+ 0 - 95
src/kbf_http_client.cpp

@@ -1,95 +0,0 @@
-#include "kbf_http_client.h"
-
-#include <string_view>
-
-#include <freertos/task.h>
-#include <esp_log.h>
-#include <kbf_assert.h>
-
-kbf::http::Client::Client() : buffer() {
-    ESP_LOGI(TAG, "initializing");
-    esp_http_client_config_t config {};
-    config.event_handler = handleHttpEvent;
-    config.user_data = this;
-    config.host = "localhost";
-    config.path = "/";
-    config.disable_auto_redirect = true;
-    handle = esp_http_client_init(&config);
-}
-
-void kbf::http::Client::get(const string &url_param) {
-    ESP_LOGI(TAG, "creating GET task");
-    this->url = url_param;
-    xTaskCreate(task, "kbf_http_ac", 4096, this, 5, nullptr);
-}
-
-void kbf::http::Client::task(void *arg) {
-    ESP_LOGI(TAG, "begin GET task");
-    auto client = (Client *) arg;
-
-    esp_http_client_set_method(client->handle, HTTP_METHOD_GET);
-    esp_http_client_set_url(client->handle, client->url.c_str());
-
-    auto err = esp_http_client_perform(client->handle);
-    if (err == ESP_ERR_HTTP_MAX_REDIRECT) {
-        // TODO this seems get triggered on HTTP 405... is this a bug in IDF?
-        ESP_LOGW(TAG, "reached redirect limit");
-        err = ESP_OK;
-    }
-
-    if (err == ESP_OK) {
-        Response response{};
-        response.status = esp_http_client_get_status_code(client->handle);
-        response.body   = std::string_view(client->buffer);
-
-        ESP_LOGI(TAG, "HTTP status: %d, content-length: %d", response.status, response.contentLength);
-        ESP_LOG_BUFFER_HEXDUMP(TAG, response.body.data(), response.body.length(), ESP_LOG_INFO);
-
-        if (client->onFinish) {
-            client->onFinish(*client, response);
-        }
-    } else {
-        ESP_LOGE(TAG, "HTTP GET failed: %s", esp_err_to_name(err));
-        ABORT("debug me");
-    }
-    ESP_LOGI(TAG, "finished GET task");
-    vTaskDelete(nullptr);
-}
-
-esp_err_t kbf::http::Client::handleHttpEvent(esp_http_client_event_t *event) {
-    auto client = (Client *) event->user_data;
-
-    switch (event->event_id) {
-        case HTTP_EVENT_ERROR:
-            ESP_LOGI(TAG, "fixme: unhandled error event: HTTP_EVENT_ERROR");
-            if (client->onError) {
-                client->onError(*client);
-            }
-            break;
-        case HTTP_EVENT_ON_CONNECTED:
-            ESP_LOGI(TAG, "connected");
-            break;
-        case HTTP_EVENT_HEADERS_SENT:
-            ESP_LOGI(TAG, "request sent");
-            break;
-        case HTTP_EVENT_ON_HEADER:
-            break;
-        case HTTP_EVENT_ON_DATA:
-            ESP_LOGI(TAG, "data received");
-            if (esp_http_client_is_chunked_response(client->handle)) {
-                ESP_LOGE(TAG, "received chunked data");
-                ABORT("not implemented");
-            }
-            client->buffer.append((char *) event->data, event->data_len);
-            break;
-        case HTTP_EVENT_ON_FINISH:
-            ESP_LOGI(TAG, "finished");
-            break;
-        case HTTP_EVENT_DISCONNECTED:
-            ESP_LOGI(TAG, "disconnected");
-            break;
-    }
-
-    return ESP_OK;
-}
-

+ 0 - 167
src/kbf_http_server.cpp

@@ -1,167 +0,0 @@
-#include "kbf_http_server.h"
-
-#include <regex>
-#include <utility>
-
-#include <esp_log.h>
-
-#include "kbf_assert.h"
-
-static constexpr const char *TAG = "kbf::http::Server";
-
-kbf::http::Server &kbf::http::Server::route(kbf::http::Server::Route route) {
-    routes.push_back(route);
-    return *this;
-}
-
-void kbf::http::Server::start(int port) {
-    ESP_LOGI(TAG, "starting HTTP server on port %d", port);
-    if (running) {
-        ESP_LOGE(TAG, "server already running");
-        ABORT("fix me");
-    }
-
-    handle = nullptr;
-    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
-    config.server_port = port;
-    config.stack_size = 16384;
-    CHECK(httpd_start(&handle, &config));
-
-    ESP_LOGI(TAG, "registering URI handlers");
-    for (auto &route : routes) {
-        ESP_LOGI(TAG, "  method: %d; uri: %s", static_cast<int>(route.method), route.uri.c_str());
-        httpd_uri_t uriHandler = {
-                .uri = route.uri.c_str(),
-                .method = static_cast<httpd_method_t>(route.method),
-                .handler = handleHttpRequest,
-                .user_ctx = &route
-        };
-        httpd_register_uri_handler(handle, &uriHandler);
-    }
-}
-
-void kbf::http::Server::stop() {
-    ABORT("not implemented");
-}
-
-// TODO handle all status codes; also maybe use macro or constexpr or something?
-static string getDefaultStatusText(int status) {
-    if (status == 200) return "OK";
-    if (status == 204) return "No Content";
-    if (status == 400) return "Bad Request";
-    if (status == 500) return "Internal Server Error";
-    return "Unknown HTTP Status";
-}
-
-esp_err_t kbf::http::Server::handleHttpRequest(httpd_req_t *httpdRequest) {
-    auto route = static_cast<Route *>(httpdRequest->user_ctx);
-    ESP_LOGI(TAG, "incoming request: method %d, path %s", httpdRequest->method, route->uri.c_str());
-
-    auto request = Request(httpdRequest);
-    auto response = route->handler(request);
-
-    CHECK(httpd_resp_set_hdr(httpdRequest, "Server", "kbf_http_server/0.1"));
-    CHECK(httpd_resp_set_type(httpdRequest, response.contentType.c_str()));
-    for (const auto &[name, value] : response.headers) {
-        CHECK(httpd_resp_set_hdr(httpdRequest, name.c_str(), value.c_str()));
-    }
-
-    if (response.statusText.empty()) {
-        response.statusText = getDefaultStatusText(response.status);
-    }
-
-    string status = std::to_string(response.status) + " " + response.statusText;
-    ESP_LOGI(TAG, "sending response with status: %s", status.c_str());
-    CHECK(httpd_resp_set_status(httpdRequest, status.c_str()));
-
-    CHECK(httpd_resp_send(httpdRequest, response.body.c_str(), response.body.length()));
-    return ESP_OK;
-}
-
-kbf::http::Request::Request(httpd_req_t *httpdRequest) {
-    this->httpdRequest = httpdRequest;
-    method = static_cast<Method>(httpdRequest->method);
-    uri = httpdRequest->uri;
-
-    for (const auto &name : acceptedHeaders) {
-        readHeader(name);
-    }
-    readQuery();
-
-    readBody();
-}
-
-void kbf::http::Request::readHeader(const string &header) {
-    auto len = httpd_req_get_hdr_value_len(httpdRequest, header.c_str());
-    if (len == 0) {
-        ESP_LOGI(TAG, "  header not found: %s", header.c_str());
-        return;
-    }
-    char buffer[len + 1];
-    CHECK(httpd_req_get_hdr_value_str(httpdRequest, header.c_str(), buffer, len + 1));
-    ESP_LOGI(TAG, "  found header: %s: %s", header.c_str(), buffer);
-    headers[header] = buffer;
-}
-
-void kbf::http::Request::readQuery() {
-    auto queryLen = httpd_req_get_url_query_len(httpdRequest);
-    char buffer[queryLen + 1];
-
-    auto err = httpd_req_get_url_query_str(httpdRequest, buffer, queryLen + 1);
-    if (err == ESP_ERR_NOT_FOUND) {
-        ESP_LOGI(TAG, "no query found");
-        return;
-    } else
-        CHECK(err);
-
-    ESP_LOGI(TAG, "found query: %s", buffer);
-    parseToMap(query, buffer);
-
-}
-
-void kbf::http::Request::parseToMap(map<string, string> &target, const string &buffer) {
-    auto queryString = string(buffer);
-
-    std::regex regex("([\\w]+)=([\\w]+)");
-    std::smatch submatch;
-    while (std::regex_search(queryString, submatch, regex)) {
-        ESP_LOGI(TAG, "  parsed: %s = %s", submatch[1].str().c_str(), submatch[2].str().c_str());
-        target[submatch[1]] = submatch[2];
-        queryString = submatch.suffix();
-    }
-}
-
-void kbf::http::Request::readBody() {
-    if (httpdRequest->content_len == 0) {
-        return;
-    }
-    ESP_LOGI(TAG, "reading body, length = %d", httpdRequest->content_len);
-
-    // TODO check if we have enough memory for the content!
-    char buffer[httpdRequest->content_len];
-
-    for (int i = 1; i <= MAX_READ_RETRY; i++) {
-        int ret;
-        if ((ret = httpd_req_recv(httpdRequest, buffer, httpdRequest->content_len)) <= 0) {
-            if (ret == HTTPD_SOCK_ERR_TIMEOUT) {
-                if (i == MAX_READ_RETRY) {
-                    ESP_LOGE(TAG, "socket timeout, giving up");
-                    break;
-                }
-                ESP_LOGW(TAG, "socket timeout, retrying (attempt %d)", i);
-                continue;
-            }
-            ESP_LOGE(TAG, "socket read failed: %d (%s)", ret, esp_err_to_name(ret));
-        } else {
-            body = string(buffer, httpdRequest->content_len);
-            ESP_LOGI(TAG, "  read: \"%s\"", body.c_str());
-            parseToMap(postData, body);
-            break;
-        }
-    }
-}
-
-kbf::http::Response::Response(string body, int status, string contentType) : body(std::move(body)),
-                                                                             status(status),
-                                                                             contentType(std::move(contentType)) {
-}

+ 6 - 1
src/wifi/driver.cpp

@@ -63,7 +63,12 @@ void kbf::wifi::start(shared_ptr<STA> sta) {
 
     init();
 
-    s_sta = std::move(sta);
+    if (sta == nullptr) {
+        s_sta = STA::create();
+    } else {
+        s_sta = std::move(sta);
+    }
+
     s_sta->netif = esp_netif_create_default_wifi_sta();
     s_sta->registerEventHandlers();
 

+ 72 - 45
test/test_http.cpp

@@ -2,18 +2,18 @@
 
 #include <atomic>
 
+#include "kbf.h"
 #include "kbf_wifi.h"
-#include "kbf_http_server.h"
-#include "kbf_http_client.h"
+#include "kbf_http.h"
+#include "http/server.h"
+#include "http/client.h"
 
 using namespace kbf;
-using std::atomic;
 
-atomic<bool> finished;
+std::atomic<bool> asyncFinished = {false};
 
 TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
-    wifi::start(wifi::STA::create());
-
+    wifi::start();
     auto server = http::Server();
 
     http::Response (*handleGet)(const http::Request &) = {[](const http::Request &request) {
@@ -28,7 +28,7 @@ TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
     }};
     server.route({http::POST, "/post-only", handlePost});
 
-    http::Response (*handleBoth)(const http::Request &) = {[](const http::Request &request) {
+    http::Response (*handleGetAndPost)(const http::Request &) = {[](const http::Request &request) {
         if (request.method == http::GET) {
             return http::Response("GET");
         } else if (request.method == http::POST) {
@@ -38,73 +38,100 @@ TEST_CASE("HTTP server <--> client / GET, POST, 404, 405", "[kbf_http]") {
             return http::Response("fail"); // unreachable but the compiler moans otherwise
         }
     }};
-    server.route({http::GET, "/both", handleBoth});
-    server.route({http::POST, "/both", handleBoth});
+    server.route({http::GET, "/get-and-post", handleGetAndPost});
+    server.route({http::POST, "/get-and-post", handleGetAndPost});
 
     server.start();
 
+    auto client   = http::Client();
+    auto response = client.get("http://localhost/get-only");
+    TEST_ASSERT_EQUAL(200, response->status);
+    TEST_ASSERT_EQUAL_STRING("OK", response->body.c_str());
 
-    auto client = new http::Client();
-    finished = false;
-    client->onFinish = {[](http::Client &, http::Client::Response response) {
-        TEST_ASSERT_EQUAL(response.status, 200);
-        TEST_ASSERT_EQUAL_STRING("OK", response.body.data());
-        finished = true;
-    }};
-    client->get("http://localhost/get-only");
-    while (!finished);
-    delete client;
+    response = client.get("http://localhost/non-existent");
+    TEST_ASSERT_EQUAL(404, response->status);
+    TEST_ASSERT_EQUAL_STRING("This URI does not exist", response->body.c_str());
 
+    response = client.get("http://localhost/post-only");
+    TEST_ASSERT_EQUAL(405, response->status);
+    TEST_ASSERT_EQUAL_STRING("Request method for this URI is not handled by server", response->body.c_str());
 
-    client = new http::Client();
-    finished = false;
-    client->onFinish = {[](http::Client &, http::Client::Response response) {
-        TEST_ASSERT_EQUAL(response.status, 405);
-        TEST_ASSERT_EQUAL_STRING("", response.body.data());
-        finished = true;
-    }};
-    client->get("http://localhost/post-only");
-    while (!finished);
-    delete client;
-
-
-    client = new http::Client();
-    finished = false;
-    client->onFinish = {[](http::Client &, http::Client::Response response) {
-        TEST_ASSERT_EQUAL(response.status, 200);
-        TEST_ASSERT_EQUAL_STRING("GET", response.body.data());
-        finished = true;
-    }};
-    client->get("http://localhost/both");
-    while (!finished);
+    response = client.get("http://localhost/get-and-post");
+    TEST_ASSERT_EQUAL(200, response->status);
+    TEST_ASSERT_EQUAL_STRING("GET", response->body.c_str());
 
     // TODO test POST once the client can do POST
+
+    wifi::stop();
 }
 
 TEST_CASE("HTTP server <--> client / custom headers", "[kbf_http]") {
+    wifi::start();
     auto server = http::Server();
 
     http::Response (*handleContentTypeTest)(const http::Request &) = {[](const http::Request &request) {
         auto response = http::Response("OK");
-        if (request.query.at("type") == "txt") {
+        if (!request.query.empty() && request.query.at("type") == "txt") {
             response.contentType = "text/plain";
         }
         return response;
     }};
-    server.route({http::GET, "/test-content-type", handleContentTypeTest});
+    server.route({http::GET, "/content-type-test", handleContentTypeTest});
 
     http::Response (*handleCustomHeaderTest)(const http::Request &) = {[](const http::Request &request) {
         auto response = http::Response("OK");
         response.headers["X-Secret"] = request.query.at("secret");
         return response;
     }};
-    server.route({http::GET, "/test-custom-header", handleCustomHeaderTest});
+    server.route({http::GET, "/custom-header-test", handleCustomHeaderTest});
+
+    server.start();
+    auto client = http::Client();
+
+    auto response = client.get("http://localhost/content-type-test");
+    TEST_ASSERT_EQUAL_STRING("text/html", response->headers.at("Content-Type").c_str());
+    TEST_ASSERT_EQUAL_STRING("OK", response->body.data());
+
+    response = client.get("http://localhost/content-type-test?type=txt");
+    TEST_ASSERT_EQUAL_STRING("text/plain", response->headers.at("Content-Type").c_str());
+    TEST_ASSERT_EQUAL_STRING("OK", response->body.data());
+
+    response = client.get("http://localhost/custom-header-test?secret=THISis1337");
+    TEST_ASSERT_EQUAL_STRING("THISis1337", response->headers.at("X-Secret").c_str());
+    TEST_ASSERT_EQUAL_STRING("OK", response->body.data());
 
-    // TODO finish test after adding header support to Client
+    wifi::stop();
+}
+
+TEST_CASE("HTTP server <--> client / async", "[kbf_http]") {
+    wifi::start();
+
+    http::Response (*handleRequest)(const http::Request &) = {[](const http::Request &request) {
+        auto response = http::Response("OK");
+        kbf::sleep(100);
+        return response;
+    }};
+    auto server = http::Server()
+            .route({http::GET, "/", handleRequest})
+            .start();
+
+    auto client = http::Client(true);
+    client.onSuccess = {[](http::Client &client, const http::Response &response) {
+        TEST_ASSERT_EQUAL_STRING("OK", response.body.data());
+        asyncFinished = true;
+    }};
+
+    auto response = client.get("http://localhost/");
+    TEST_ASSERT_NULL(response);
+    TEST_ASSERT_EQUAL(false, asyncFinished);
+    kbf::sleep(200);
+    TEST_ASSERT_EQUAL(true, asyncFinished);
+
+    wifi::stop();
 }
 
 TEST_CASE("HTTP server <--> client / CORS", "[kbf_http]") {
-    // TODO finish test after adding header support to Client
+    TEST_FAIL_MESSAGE("not yet implemented");
 }
 
 TEST_CASE("HTTP server <--> client / SPIFFS static route", "[kbf_http]") {