SensESP 3.4.1-alpha
Universal Signal K sensor toolkit ESP32
Loading...
Searching...
No Matches
http_server.h
Go to the documentation of this file.
1#ifndef SENSESP_NET_HTTP_SERVER_H_
2#define SENSESP_NET_HTTP_SERVER_H_
3
4#include "sensesp.h"
5
6#include <ESPmDNS.h>
7#include <IPAddress.h>
8#include <esp_http_server.h>
9#include <functional>
10#include <list>
11#include <memory>
12
15#include "sensesp_base_app.h"
16
17// HTTP server worker task stack. Measured worst case on an ESP32-C3 was ~3.4 KB
18// used by a config PUT (max-nesting JSON deserialize + SPIFFS save, with a long
19// request URI on the same frame). The digest-auth path (a 1024-byte
20// Authorization buffer + MD5/String temporaries) runs and returns before the
21// handler, so it never stacks on top of it. 6144 leaves ~2.7 KB of headroom
22// over the deepest measured path.
23#ifndef HTTP_SERVER_STACK_SIZE
24#define HTTP_SERVER_STACK_SIZE 6144
25#endif
26
27namespace sensesp {
28
29#ifndef HTTP_DEFAULT_PORT
30#define HTTP_DEFAULT_PORT 80
31#endif
32
33// Maximum simultaneous client sockets the HTTP server will accept.
34//
35// Kept at the esp_http_server default (7, of which 3 are reserved internally).
36// The web UI stalls modern browsers suffered with this default were caused by
37// keep-alive socket starvation, not the cap itself, and are addressed by
38// enabling `lru_purge_enable` (see the HTTPServer constructor) rather than by
39// claiming more of the lwIP socket budget — which the Signal K WebSocket
40// client, mDNS, and SNTP also draw from.
41//
42// Raising this requires `CONFIG_LWIP_MAX_SOCKETS >= HTTP_SERVER_MAX_OPEN_SOCKETS
43// + 3`; the espidf framework defaults that to 10, so values above 7 also need
44// `CONFIG_LWIP_MAX_SOCKETS` raised in sdkconfig.defaults or httpd_start() fails.
45#ifndef HTTP_SERVER_MAX_OPEN_SOCKETS
46#define HTTP_SERVER_MAX_OPEN_SOCKETS 7
47#endif
48
49#include <ctype.h>
50#include <stdlib.h>
51
52void urldecode2(char* dst, const char* src);
53String get_content_type(httpd_req_t* req);
54esp_err_t call_request_dispatcher(httpd_req_t* req);
55
56class HTTPServer;
57
63 public:
64 HTTPRequestHandler(uint32_t method_mask, String match_uri,
65 std::function<esp_err_t(httpd_req_t*)> handler_func)
66 : method_mask_(method_mask),
67 match_uri_(match_uri),
68 handler_func_(handler_func) {}
69
70 const uint32_t method_mask_;
71 const String match_uri_;
72
73 esp_err_t call(httpd_req_t* req) { return this->handler_func_(req); }
74
75 protected:
76 const std::function<esp_err_t(httpd_req_t*)> handler_func_;
77};
78
84 public:
86 const String& config_path = "/system/httpserver")
87 : FileSystemSaveable(config_path), config_(HTTPD_DEFAULT_CONFIG()) {
88 config_.server_port = port;
90 config_.max_uri_handlers = 20;
91 config_.max_open_sockets = HTTP_SERVER_MAX_OPEN_SOCKETS;
92 // The embedded SPA is served over HTTP/1.1 keep-alive, so a browser holds
93 // several connections open. Without LRU purge, once every socket slot is
94 // camped by an idle keep-alive connection the server defers (starves) any
95 // new connection — a late chunk, a retried fetch, a second tab — until one
96 // times out. On high-latency links (e.g. esp_hosted SDIO WiFi) that window
97 // is long enough to fail the SPA's parallel /api/* requests outright.
98 // Enabling LRU purge evicts the least-recently-used (idle) socket to admit
99 // the newcomer instead, which the browser simply reopens.
100 config_.lru_purge_enable = true;
101 config_.uri_match_fn = httpd_uri_match_wildcard;
102 String auth_realm_ = "Login required for " + SensESPBaseApp::get_hostname();
103 load();
104 if (auth_required_) {
105 authenticator_ = std::unique_ptr<HTTPDigestAuthenticator>(
108 username_, ha1_, auth_realm_));
109 }
110 event_loop()->onDelay(0, [this]() {
111 esp_err_t error = httpd_start(&server_, &config_);
112 if (error != ESP_OK) {
113 ESP_LOGE(__FILENAME__, "Error starting HTTP server: %s",
114 esp_err_to_name(error));
115 // Registering handlers or announcing mDNS against a failed handle would
116 // leave the device advertising a dead web UI with no further signal.
117 return;
118 }
119 ESP_LOGI(__FILENAME__, "HTTP server started");
120
121 // register the request dispatcher for all methods and all URIs
122 httpd_uri_t uri = {
123 .uri = "/*",
124 .method = HTTP_GET,
125 .handler = call_request_dispatcher,
126 .user_ctx = this,
127 };
128 httpd_register_uri_handler(server_, &uri);
129 uri.method = HTTP_HEAD;
130 httpd_register_uri_handler(server_, &uri);
131 uri.method = HTTP_POST;
132 httpd_register_uri_handler(server_, &uri);
133 uri.method = HTTP_PUT;
134 httpd_register_uri_handler(server_, &uri);
135 uri.method = HTTP_DELETE;
136 httpd_register_uri_handler(server_, &uri);
137
138 // announce the server over mDNS
139 MDNS.addService("http", "tcp", 80);
140 });
141 };
142
143 void stop() { httpd_stop(server_); }
144
145 void set_auth_credentials(const String& username, const String& password,
146 bool auth_required = true) {
147 username_ = username;
148 String realm = "Login required for " + SensESPBaseApp::get_hostname();
149 ha1_ = MD5(username + ":" + realm + ":" + password);
150 auth_required_ = auth_required;
151 }
152
161 void set_captive_portal(bool captive_portal,
162 IPAddress ap_ip = IPAddress()) {
163 captive_portal_ = captive_portal;
164 captive_portal_ap_ip_ = ap_ip;
165 }
166
167 virtual bool to_json(JsonObject& config) override {
168 config["auth_required"] = auth_required_;
169 config["username"] = username_;
170 config["ha1"] = ha1_;
171
172 return true;
173 }
174
175 virtual bool from_json(const JsonObject& config) override {
176 if (config["auth_required"].is<bool>()) {
177 auth_required_ = config["auth_required"];
178 }
179 if (config["username"].is<String>()) {
180 username_ = config["username"].as<String>();
181 }
182 if (config["ha1"].is<String>()) {
183 ha1_ = config["ha1"].as<String>();
184 } else if (config["password"].is<String>()) {
185 // Migrate from old plaintext password format
186 String password = config["password"].as<String>();
187 String realm = "Login required for " + SensESPBaseApp::get_hostname();
188 ha1_ = MD5(username_ + ":" + realm + ":" + password);
189 save();
190 }
191 return true;
192 }
193
194 void add_handler(std::shared_ptr<HTTPRequestHandler>& handler) {
195 handlers_.push_back(handler);
196 }
197
198 protected:
199 bool captive_portal_ = false;
201 httpd_handle_t server_ = nullptr;
202 httpd_config_t config_;
203 String username_;
204 String ha1_;
205 bool auth_required_ = false;
206 std::unique_ptr<HTTPAuthenticator> authenticator_;
207
209 std::function<esp_err_t(httpd_req_t*)> handler,
210 httpd_req_t* req);
211
219 esp_err_t dispatch_request(httpd_req_t* req);
220
228 bool handle_captive_portal(httpd_req_t* req);
229
230 std::list<std::shared_ptr<HTTPRequestHandler>> handlers_;
231
232 friend esp_err_t call_request_dispatcher(httpd_req_t* req);
233};
234
235inline const String ConfigSchema(const HTTPServer& obj) {
236 return "null";
237}
238
239inline bool ConfigRequiresRestart(const HTTPServer& obj) { return true; }
240
241} // namespace sensesp
242
243#endif // SENSESP_NET_HTTP_SERVER_H_
virtual bool load() override
Load and populate the object from a persistent storage.
Definition saveable.cpp:8
virtual bool save() override
Save the object to a persistent storage.
Definition saveable.cpp:40
HTTP Authenticator base class.
HTTP Digest Authenticator class.
HTTP request handler storage class.
Definition http_server.h:62
const uint32_t method_mask_
Definition http_server.h:70
HTTPRequestHandler(uint32_t method_mask, String match_uri, std::function< esp_err_t(httpd_req_t *)> handler_func)
Definition http_server.h:64
const std::function< esp_err_t(httpd_req_t *)> handler_func_
Definition http_server.h:76
esp_err_t call(httpd_req_t *req)
Definition http_server.h:73
HTTP server class wrapping the esp-idf http server.
Definition http_server.h:83
esp_err_t dispatch_request(httpd_req_t *req)
Dispatcher method that captures all requests and forwards them to the appropriate handlers.
String ha1_
Pre-computed MD5(username:realm:password)
virtual bool to_json(JsonObject &config) override
std::list< std::shared_ptr< HTTPRequestHandler > > handlers_
httpd_handle_t server_
void set_auth_credentials(const String &username, const String &password, bool auth_required=true)
IPAddress captive_portal_ap_ip_
bool authenticate_request(HTTPAuthenticator *auth, std::function< esp_err_t(httpd_req_t *)> handler, httpd_req_t *req)
void set_captive_portal(bool captive_portal, IPAddress ap_ip=IPAddress())
Enable or disable the captive-portal redirect path.
friend esp_err_t call_request_dispatcher(httpd_req_t *req)
httpd_config_t config_
virtual bool from_json(const JsonObject &config) override
void add_handler(std::shared_ptr< HTTPRequestHandler > &handler)
HTTPServer(int port=HTTP_DEFAULT_PORT, const String &config_path="/system/httpserver")
Definition http_server.h:85
std::unique_ptr< HTTPAuthenticator > authenticator_
bool handle_captive_portal(httpd_req_t *req)
Check if the request is for the captive portal and handle it if it.
static String get_hostname()
Get the current hostname.
#define HTTP_DEFAULT_PORT
Definition http_server.h:30
#define HTTP_SERVER_STACK_SIZE
Definition http_server.h:24
#define HTTP_SERVER_MAX_OPEN_SOCKETS
Definition http_server.h:46
const String ConfigSchema(const SmartSwitchController &obj)
std::shared_ptr< reactesp::EventLoop > event_loop()
Definition sensesp.cpp:9
String MD5(const String &payload_str)
MD5 hash function.
Definition hash.cpp:45
esp_err_t call_request_dispatcher(httpd_req_t *req)
void urldecode2(char *dst, const char *src)
bool ConfigRequiresRestart(const HTTPServer &obj)
String get_content_type(httpd_req_t *req)
Construct with a pre-computed HA1 hash (avoids storing plaintext password)