#include "PsychicHttpServer.h" #include "PsychicEndpoint.h" #include "PsychicHandler.h" #include "PsychicJson.h" #include "PsychicStaticFileHandler.h" #include "PsychicWebHandler.h" #include "PsychicWebSocket.h" #ifndef CONFIG_IDF_TARGET_ESP32H2 #include "WiFi.h" #endif PsychicHttpServer::PsychicHttpServer(uint16_t port) { maxRequestBodySize = MAX_REQUEST_BODY_SIZE; maxUploadSize = MAX_UPLOAD_SIZE; defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, ""); onNotFound(PsychicHttpServer::defaultNotFoundHandler); // for a regular server config = HTTPD_DEFAULT_CONFIG(); config.open_fn = PsychicHttpServer::openCallback; config.close_fn = PsychicHttpServer::closeCallback; config.global_user_ctx = this; config.global_user_ctx_free_fn = PsychicHttpServer::destroy; config.uri_match_fn = MATCH_WILDCARD; // new internal endpoint matching - do not change this!!! config.stack_size = 4608; // default stack is just a little bit too small. // our internal matching function for endpoints _uri_match_fn = MATCH_WILDCARD; // use this change the endpoint matching function. #ifdef ENABLE_ASYNC // It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS // Why? This leaves at least one socket still available to handle // quick synchronous requests. Otherwise, all the sockets will // get taken by the long async handlers, and your server will no // longer be responsive. config.max_open_sockets = ASYNC_WORKER_COUNT + 1; config.lru_purge_enable = true; #endif setPort(port); } PsychicHttpServer::~PsychicHttpServer() { _esp_idf_endpoints.clear(); for (auto* client : _clients) delete (client); _clients.clear(); for (auto* endpoint : _endpoints) delete (endpoint); _endpoints.clear(); for (auto* handler : _handlers) delete (handler); _handlers.clear(); for (auto* rewrite : _rewrites) delete (rewrite); _rewrites.clear(); delete defaultEndpoint; delete _chain; } void PsychicHttpServer::destroy(void* ctx) { // do not release any resource for PsychicHttpServer in order to be able to restart it after stopping } void PsychicHttpServer::setPort(uint16_t port) { this->config.server_port = port; } uint16_t PsychicHttpServer::getPort() { return this->config.server_port; } esp_err_t PsychicHttpServer::start() { if (_running) return ESP_OK; esp_err_t ret; #ifdef ENABLE_ASYNC // start workers start_async_req_workers(); #endif // one URI handler for each http_method config.max_uri_handlers = supported_methods.size() + _esp_idf_endpoints.size(); // fire it up. ret = _startServer(); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret)); return ret; } // some handlers (aka websockets) need actual endpoints in esp-idf http_server for (auto& endpoint : _esp_idf_endpoints) { ESP_LOGD(PH_TAG, "Adding endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method)); // Register endpoint with ESP-IDF server esp_err_t ret = httpd_register_uri_handler(this->server, &endpoint); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); } // Register a handler for each http_method method - it will match all requests with that URI/method for (auto& method : supported_methods) { ESP_LOGD(PH_TAG, "Adding %s meta endpoint", http_method_str((http_method)method)); httpd_uri_t my_uri; my_uri.uri = "*"; my_uri.method = method; my_uri.handler = PsychicHttpServer::requestHandler; my_uri.is_websocket = false; my_uri.supported_subprotocol = ""; // Register endpoint with ESP-IDF server esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); } // Register handler ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret)); ESP_LOGI(PH_TAG, "Server started on port %" PRIu16, getPort()); _running = true; return ret; } esp_err_t PsychicHttpServer::_startServer() { return httpd_start(&this->server, &this->config); } esp_err_t PsychicHttpServer::stop() { if (!_running) return ESP_OK; // some handlers (aka websockets) need actual endpoints in esp-idf http_server for (auto& endpoint : _esp_idf_endpoints) { ESP_LOGD(PH_TAG, "Removing endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method)); // Unregister endpoint with ESP-IDF server esp_err_t ret = httpd_unregister_uri_handler(this->server, endpoint.uri, endpoint.method); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret)); } // Unregister a handler for each http_method method - it will match all requests with that URI/method for (auto& method : supported_methods) { ESP_LOGD(PH_TAG, "Removing %s meta endpoint", http_method_str((http_method)method)); // Unregister endpoint with ESP-IDF server esp_err_t ret = httpd_unregister_uri_handler(this->server, "*", method); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret)); } esp_err_t ret = _stopServer(); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "Server stop failed (%s)", esp_err_to_name(ret)); return ret; } ESP_LOGI(PH_TAG, "Server stopped"); _running = false; return ret; } esp_err_t PsychicHttpServer::_stopServer() { return httpd_stop(this->server); } void PsychicHttpServer::reset() { if (_running) stop(); for (auto* client : _clients) delete (client); _clients.clear(); for (auto* endpoint : _endpoints) delete (endpoint); _endpoints.clear(); for (auto* handler : _handlers) delete (handler); _handlers.clear(); for (auto* rewrite : _rewrites) delete (rewrite); _rewrites.clear(); _esp_idf_endpoints.clear(); onNotFound(PsychicHttpServer::defaultNotFoundHandler); _onOpen = nullptr; _onClose = nullptr; } httpd_uri_match_func_t PsychicHttpServer::getURIMatchFunction() { return _uri_match_fn; } void PsychicHttpServer::setURIMatchFunction(httpd_uri_match_func_t match_fn) { _uri_match_fn = match_fn; } PsychicHandler* PsychicHttpServer::addHandler(PsychicHandler* handler) { _handlers.push_back(handler); return handler; } void PsychicHttpServer::removeHandler(PsychicHandler* handler) { _handlers.remove(handler); } PsychicRewrite* PsychicHttpServer::addRewrite(PsychicRewrite* rewrite) { _rewrites.push_back(rewrite); return rewrite; } void PsychicHttpServer::removeRewrite(PsychicRewrite* rewrite) { _rewrites.remove(rewrite); } PsychicRewrite* PsychicHttpServer::rewrite(const char* from, const char* to) { return addRewrite(new PsychicRewrite(from, to)); } PsychicEndpoint* PsychicHttpServer::on(const char* uri) { return on(uri, HTTP_GET); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method) { PsychicWebHandler* handler = new PsychicWebHandler(); return on(uri, method, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler* handler) { return on(uri, HTTP_GET, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHandler* handler) { // make our endpoint PsychicEndpoint* endpoint = new PsychicEndpoint(this, method, uri); // set our handler endpoint->setHandler(handler); // websockets need a real endpoint in esp-idf if (handler->isWebSocket()) { // URI handler structure httpd_uri_t my_uri; my_uri.uri = uri; my_uri.method = HTTP_GET; my_uri.handler = PsychicEndpoint::requestCallback; my_uri.user_ctx = endpoint; my_uri.is_websocket = handler->isWebSocket(); my_uri.supported_subprotocol = handler->getSubprotocol(); // save it to our 'real' handlers for later. _esp_idf_endpoints.push_back(my_uri); } // if this is a method we haven't added yet, do it. if (method != HTTP_ANY) { if (!(std::find(supported_methods.begin(), supported_methods.end(), (http_method)method) != supported_methods.end())) { ESP_LOGD(PH_TAG, "Adding %s to server.supported_methods", http_method_str((http_method)method)); supported_methods.push_back((http_method)method); } } // add it to our meta endpoints _endpoints.push_back(endpoint); return endpoint; } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallback fn) { return on(uri, HTTP_GET, fn); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHttpRequestCallback fn) { // these basic requests need a basic web handler PsychicWebHandler* handler = new PsychicWebHandler(); handler->onRequest(fn); return on(uri, method, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallback fn) { return on(uri, HTTP_GET, fn); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicJsonRequestCallback fn) { // these basic requests need a basic web handler PsychicJsonHandler* handler = new PsychicJsonHandler(); handler->onRequest(fn); return on(uri, method, handler); } bool PsychicHttpServer::removeEndpoint(const char* uri, int method) { // some handlers (aka websockets) need actual endpoints in esp-idf http_server // don't return from here, because its added to the _endpoints list too. for (auto& endpoint : _esp_idf_endpoints) { if (!strcmp(endpoint.uri, uri) && method == endpoint.method) { ESP_LOGD(PH_TAG, "Unregistering endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method)); // Register endpoint with ESP-IDF server esp_err_t ret = httpd_register_uri_handler(this->server, &endpoint); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); } } // loop through our endpoints and see if anyone matches for (auto* endpoint : _endpoints) { if (endpoint->uri().equals(uri) && method == endpoint->_method) return removeEndpoint(endpoint); } return false; } bool PsychicHttpServer::removeEndpoint(PsychicEndpoint* endpoint) { _endpoints.remove(endpoint); return true; } PsychicHttpServer* PsychicHttpServer::addFilter(PsychicRequestFilterFunction fn) { _filters.push_back(fn); return this; } bool PsychicHttpServer::_filter(PsychicRequest* request) { // run through our filter chain. for (auto& filter : _filters) { if (!filter(request)) return false; } return true; } PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddleware* middleware) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(middleware); return this; } PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddlewareCallback fn) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(fn); return this; } void PsychicHttpServer::removeMiddleware(PsychicMiddleware* middleware) { if (_chain) { _chain->removeMiddleware(middleware); } } void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn) { PsychicWebHandler* handler = new PsychicWebHandler(); handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn); this->defaultEndpoint->setHandler(handler); } bool PsychicHttpServer::_rewriteRequest(PsychicRequest* request) { for (auto* r : _rewrites) { if (r->match(request)) { request->_setUri(r->toUrl().c_str()); return true; } } return false; } esp_err_t PsychicHttpServer::requestHandler(httpd_req_t* req) { PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); PsychicRequest request(server, req); // process any URL rewrites server->_rewriteRequest(&request); // run it through our global server filter list if (!server->_filter(&request)) { ESP_LOGD(PH_TAG, "Request %s refused by global filter", request.uri().c_str()); return request.response()->send(400); } // then runs the request through the filter chain esp_err_t ret; if (server->_chain) { ret = server->_chain->runChain(&request, [server, &request]() { return server->_process(&request); }); } else { ret = server->_process(&request); } ESP_LOGD(PH_TAG, "Request %s processed by global middleware: %s", request.uri().c_str(), esp_err_to_name(ret)); if (ret == HTTPD_404_NOT_FOUND) { return PsychicHttpServer::notFoundHandler(req, HTTPD_404_NOT_FOUND); } return ret; } esp_err_t PsychicHttpServer::_process(PsychicRequest* request) { // loop through our endpoints and see if anyone wants it. for (auto* endpoint : _endpoints) { if (endpoint->matches(request->uri().c_str())) { if (endpoint->_method == request->method() || endpoint->_method == HTTP_ANY) { request->setEndpoint(endpoint); return endpoint->process(request); } } } // loop through our global handlers and see if anyone wants it for (auto* handler : _handlers) { esp_err_t ret = handler->process(request); if (ret != HTTPD_404_NOT_FOUND) return ret; } return HTTPD_404_NOT_FOUND; } esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t* req, httpd_err_code_t err) { PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); PsychicRequest request(server, req); // pull up our default handler / endpoint PsychicHandler* handler = server->defaultEndpoint->handler(); if (!handler) return request.response()->send(404); esp_err_t ret = handler->process(&request); if (ret != HTTPD_404_NOT_FOUND) return ret; // not sure how we got this far. return request.response()->send(404); } esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "That URI does not exist."); } void PsychicHttpServer::onOpen(PsychicClientCallback handler) { this->_onOpen = handler; } esp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd) { ESP_LOGD(PH_TAG, "New client connected %d", sockfd); // get our global server reference PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); // lookup our client PsychicClient* client = server->getClient(sockfd); if (client == NULL) { client = new PsychicClient(hd, sockfd); server->addClient(client); } // user callback if (server->_onOpen != NULL) server->_onOpen(client); return ESP_OK; } void PsychicHttpServer::onClose(PsychicClientCallback handler) { this->_onClose = handler; } void PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd) { ESP_LOGD(PH_TAG, "Client disconnected %d", sockfd); PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); // lookup our client PsychicClient* client = server->getClient(sockfd); if (client != NULL) { // give our handlers a chance to handle a disconnect first for (PsychicEndpoint* endpoint : server->_endpoints) { PsychicHandler* handler = endpoint->handler(); handler->checkForClosedClient(client); } // do we have a callback attached? if (server->_onClose != NULL) server->_onClose(client); // remove it from our list server->removeClient(client); } else ESP_LOGE(PH_TAG, "No client record %d", sockfd); // finally close it out. close(sockfd); } PsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control) { PsychicStaticFileHandler* handler = new PsychicStaticFileHandler(uri, fs, path, cache_control); this->addHandler(handler); return handler; } void PsychicHttpServer::addClient(PsychicClient* client) { _clients.push_back(client); } void PsychicHttpServer::removeClient(PsychicClient* client) { _clients.remove(client); delete client; } PsychicClient* PsychicHttpServer::getClient(int socket) { for (PsychicClient* client : _clients) if (client->socket() == socket) return client; return NULL; } PsychicClient* PsychicHttpServer::getClient(httpd_req_t* req) { return getClient(httpd_req_to_sockfd(req)); } bool PsychicHttpServer::hasClient(int socket) { return getClient(socket) != NULL; } const std::list& PsychicHttpServer::getClientList() { return _clients; } bool ON_STA_FILTER(PsychicRequest* request) { #if defined(CONFIG_IDF_TARGET_ESP32H2) return false; #else return WiFi.localIP() == request->client()->localIP(); #endif } bool ON_AP_FILTER(PsychicRequest* request) { #if defined(CONFIG_IDF_TARGET_ESP32H2) return false; #else return WiFi.softAPIP() == request->client()->localIP(); #endif } String urlDecode(const char* encoded) { size_t length = strlen(encoded); char* decoded = (char*)malloc(length + 1); if (!decoded) { return ""; } size_t i, j = 0; for (i = 0; i < length; ++i) { if (encoded[i] == '%' && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) { // Valid percent-encoded sequence int hex; sscanf(encoded + i + 1, "%2x", &hex); decoded[j++] = (char)hex; i += 2; // Skip the two hexadecimal characters } else if (encoded[i] == '+') { // Convert '+' to space decoded[j++] = ' '; } else { // Copy other characters as they are decoded[j++] = encoded[i]; } } decoded[j] = '\0'; // Null-terminate the decoded string String output(decoded); free(decoded); return output; } bool psychic_uri_match_simple(const char* uri1, const char* uri2, size_t len2) { return strlen(uri1) == len2 && // First match lengths (strncmp(uri1, uri2, len2) == 0); // Then match actual URIs } #ifdef PSY_ENABLE_REGEX bool psychic_uri_match_regex(const char* uri1, const char* uri2, size_t len2) { std::regex pattern(uri1); std::smatch matches; std::string s(uri2); // len2 is passed in to tell us to match up to a point. if (s.length() > len2) s = s.substr(0, len2); return std::regex_search(s, matches, pattern); } #endif