SensESP 3.6.1-alpha
Universal Signal K sensor toolkit ESP32
Loading...
Searching...
No Matches
base_command_handler.cpp
Go to the documentation of this file.
2
3#include <ETH.h>
4#include <WiFi.h>
5#include <freertos/FreeRTOS.h>
6#include <freertos/task.h>
7
8#include <cerrno>
9#include <cstdint>
10#include <cstdlib>
11#include <memory>
12#include <new>
13
17#include "sensesp_app.h"
18
19namespace sensesp {
20
21namespace {
22
23String strip_port(const String& authority) {
24 int colon = authority.lastIndexOf(':');
25 return colon < 0 ? authority : authority.substring(0, colon);
26}
27
28bool is_own_interface_ip(const IPAddress& ip) {
29 // A disabled soft-AP or absent Ethernet interface reports 0.0.0.0; a Host of
30 // 0.0.0.0 must not match one of those, so reject it before comparing.
31 if (ip == IPAddress(0, 0, 0, 0)) {
32 return false;
33 }
34 return ip == WiFi.localIP() || ip == WiFi.softAPIP() || ip == ETH.localIP();
35}
36
37bool is_own_host(const String& host) {
38 String hostname = SensESPBaseApp::get_hostname();
39 if (hostname.length() > 0 && host.equalsIgnoreCase(hostname + ".local")) {
40 return true;
41 }
42 IPAddress ip;
43 return ip.fromString(host) && is_own_interface_ip(ip);
44}
45
46} // namespace
47
48// An Origin or Host that does not fit the buffer is rejected rather than
49// treated as absent: a legitimate same-origin request to this device is
50// always short, so an over-long header can only be a forgery attempt and
51// must fail closed.
52bool check_origin(httpd_req_t* req) {
53 if (httpd_req_get_hdr_value_len(req, "Origin") == 0) {
54 return true;
55 }
56
57 char origin[128] = {0};
58 char host[128] = {0};
59 if (httpd_req_get_hdr_value_str(req, "Origin", origin, sizeof(origin)) !=
60 ESP_OK ||
61 httpd_req_get_hdr_value_str(req, "Host", host, sizeof(host)) != ESP_OK) {
62 httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
63 "Cross-origin request rejected");
64 return false;
65 }
66
67 String origin_str(origin);
68 int scheme_end = origin_str.indexOf("://");
69 String origin_authority =
70 scheme_end < 0 ? origin_str : origin_str.substring(scheme_end + 3);
71
72 String host_str(host);
73
74 // Origin must agree with Host (classic CSRF defense) *and* Host must name
75 // an address the device actually owns. A DNS-rebinding page can make its
76 // own hostname resolve to the device's local IP after the fact, so its
77 // Origin and Host headers still agree with each other on that hostname;
78 // the Origin==Host comparison alone would pass it straight through. Only
79 // cross-checking Host against the device's real mDNS name/interface IPs
80 // catches that case.
81 if (origin_authority == host_str && is_own_host(strip_port(host_str))) {
82 return true;
83 }
84
85 httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
86 "Cross-origin request rejected");
87 return false;
88}
89
90void add_http_reset_handler(std::shared_ptr<HTTPServer>& server) {
91 auto reset_handler = std::make_shared<HTTPRequestHandler>(
92 1 << HTTP_POST, "/api/device/reset", [](httpd_req_t* req) {
93 if (!check_origin(req)) {
94 return ESP_FAIL;
95 }
96 httpd_resp_sendstr(req,
97 "Resetting device back to factory defaults. "
98 "You may have to reconfigure the WiFi settings.");
99 event_loop()->onDelay(500, []() { SensESPBaseApp::get()->reset(); });
100 return ESP_OK;
101 });
102 server->add_handler(reset_handler);
103}
104
105void add_http_restart_handler(std::shared_ptr<HTTPServer>& server) {
106 auto restart_handler = std::make_shared<HTTPRequestHandler>(
107 1 << HTTP_POST, "/api/device/restart", [](httpd_req_t* req) {
108 if (!check_origin(req)) {
109 return ESP_FAIL;
110 }
111 httpd_resp_sendstr(req, "Restarting device");
112 event_loop()->onDelay(500, []() { ESP.restart(); });
113 return ESP_OK;
114 });
115 server->add_handler(restart_handler);
116}
117
118void add_http_info_handler(std::shared_ptr<HTTPServer>& server) {
119 auto info_handler = std::make_shared<HTTPRequestHandler>(
120 1 << HTTP_GET, "/api/info", [](httpd_req_t* req) {
121 auto status_page_items = StatusPageItemBase::get_status_page_items();
122
123 JsonDocument json_doc;
124 JsonArray info_items = json_doc.to<JsonArray>();
125
126 for (auto info_item = status_page_items->begin();
127 info_item != status_page_items->end(); ++info_item) {
128 info_items.add(info_item->second->as_json());
129 }
130
131 // Per-task minimum-ever free stack, grouped under "Task stack free
132 // (bytes)", for spotting over- or under-provisioned task stacks.
133 // Computed live per request; needs the FreeRTOS trace facility, which
134 // the Arduino/ESP-IDF default config enables.
135#if defined(CONFIG_FREERTOS_USE_TRACE_FACILITY) && \
136 CONFIG_FREERTOS_USE_TRACE_FACILITY
137 // Over-allocate a few slots so a task created between the count and the
138 // snapshot can't undersize the array: uxTaskGetSystemState returns 0 on
139 // an undersized buffer, which would drop the whole section that request.
140 UBaseType_t task_slots = uxTaskGetNumberOfTasks() + 4;
141 std::unique_ptr<TaskStatus_t[]> tasks(
142 new (std::nothrow) TaskStatus_t[task_slots]);
143 if (tasks) {
144 UBaseType_t n = uxTaskGetSystemState(tasks.get(), task_slots, nullptr);
145 for (UBaseType_t i = 0; i < n; i++) {
146 JsonDocument item;
147 item["name"] = tasks[i].pcTaskName;
148 item["value"] = static_cast<uint32_t>(
149 tasks[i].usStackHighWaterMark * sizeof(StackType_t));
150 item["group"] = "Task stack free (bytes)";
151 item["order"] = kUIOutputDefaultOrder;
152 info_items.add(item);
153 }
154 }
155#endif
156
157 String response;
158 serializeJson(json_doc, response);
159 httpd_resp_set_type(req, "application/json");
160 httpd_resp_sendstr(req, response.c_str());
161 return ESP_OK;
162 });
163 server->add_handler(info_handler);
164}
165
166void add_http_log_handler(std::shared_ptr<HTTPServer>& server) {
167 auto log_handler = std::make_shared<HTTPRequestHandler>(
168 1 << HTTP_GET, "/api/log", [](httpd_req_t* req) {
169 LogBuffer* log_buffer = LogBuffer::instance();
170 httpd_resp_set_type(req, "application/json; charset=utf-8");
171 if (log_buffer == nullptr) {
172 httpd_resp_sendstr(
173 req, "{\"session\":0,\"next\":0,\"gap\":false,\"lines\":[]}");
174 return ESP_OK;
175 }
176
177 // Access control: none of its own, like /api/info. The only gate is the
178 // dispatcher's HTTP auth, which is off by default — so /api/log mirrors
179 // the serial log to anyone who can reach the web server unless web auth
180 // is enabled. check_origin() is anti-CSRF for destructive POSTs and
181 // gives no read protection, so it is not used here.
182 uint32_t since = 0;
183 bool has_since = false;
184 size_t query_len = httpd_req_get_url_query_len(req) + 1;
185 if (query_len > 1 && query_len <= 64) {
186 char query[64];
187 if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK) {
188 char value[16];
189 if (httpd_query_key_value(query, "since", value, sizeof(value)) ==
190 ESP_OK) {
191 // Accept only a clean, in-range unsigned integer; ignore garbage
192 // so a stale or crafted cursor cannot corrupt the client's view.
193 char* end = nullptr;
194 errno = 0;
195 unsigned long parsed = strtoul(value, &end, 10);
196 if (end != value && *end == '\0' && errno == 0 &&
197 parsed <= UINT32_MAX) {
198 since = static_cast<uint32_t>(parsed);
199 has_since = true;
200 }
201 }
202 }
203 }
204
205 LogSnapshot snapshot = log_buffer->snapshot_since(since, has_since);
206
207 JsonDocument json_doc;
208 json_doc["session"] = snapshot.session_id;
209 json_doc["next"] = snapshot.next;
210 json_doc["gap"] = snapshot.gap;
211 JsonArray lines = json_doc["lines"].to<JsonArray>();
212 for (const auto& line : snapshot.lines) {
213 lines.add(line.c_str());
214 }
215
216 String response;
217 serializeJson(json_doc, response);
218 httpd_resp_sendstr(req, response.c_str());
219 return ESP_OK;
220 });
221 server->add_handler(log_handler);
222}
223
224void add_routes_handlers(std::shared_ptr<HTTPServer>& server) {
225 std::vector<RouteDefinition> routes;
226
227 routes.push_back(RouteDefinition("Status", "/status", "StatusPage"));
228 routes.push_back(RouteDefinition("System", "/system", "SystemPage"));
229 routes.push_back(RouteDefinition("Log", "/log", "LogPage"));
230 routes.push_back(RouteDefinition("WiFi", "/wifi", "WiFiConfigPage"));
231 routes.push_back(RouteDefinition("Signal K", "/signalk", "SignalKPage"));
232 routes.push_back(
233 RouteDefinition("Configuration", "/configuration", "ConfigurationPage"));
234 routes.push_back(RouteDefinition("Control", "/control", "ControlPage"));
235
236 // Pre-render the response
237 JsonDocument json_doc;
238 JsonArray routes_json = json_doc.to<JsonArray>();
239
240 for (auto it = routes.begin(); it != routes.end(); ++it) {
241 routes_json.add(it->as_json());
242 }
243
244 String response;
245
246 serializeJson(routes_json, response);
247
248 auto routes_handler = std::make_shared<HTTPRequestHandler>(
249 1 << HTTP_GET, "/api/routes", [response](httpd_req_t* req) {
250 httpd_resp_set_type(req, "application/json");
251 httpd_resp_sendstr(req, response.c_str());
252 return ESP_OK;
253 });
254 server->add_handler(routes_handler);
255
256 // Find the root page
257
258 StaticFileData* root_page = nullptr;
259 for (int i = 0; i < sizeof(kFrontendFiles) / sizeof(StaticFileData); i++) {
260 if (strcmp(kFrontendFiles[i].url, "/") == 0) {
261 root_page = (StaticFileData*)&kFrontendFiles[i];
262 break;
263 }
264 }
265 if (root_page == nullptr) {
266 ESP_LOGE(__FILENAME__, "Root page not found in kWebUIFiles");
267 return;
268 }
269
270 // Add a handler for each route that returns the root page
271
272 for (auto it = routes.begin(); it != routes.end(); ++it) {
273 String path = it->get_path();
274 auto route_handler = std::make_shared<HTTPRequestHandler>(
275 1 << HTTP_GET, path.c_str(), [root_page](httpd_req_t* req) {
276 httpd_resp_set_type(req, root_page->content_type);
277 if (root_page->content_encoding != nullptr) {
278 httpd_resp_set_hdr(req, kContentEncoding,
279 root_page->content_encoding);
280 }
281 httpd_resp_send(req, root_page->content, root_page->content_length);
282 return ESP_OK;
283 });
284 server->add_handler(route_handler);
285 }
286}
287
288void add_base_app_http_command_handlers(std::shared_ptr<HTTPServer>& server) {
291 add_http_info_handler(server);
292 add_http_log_handler(server);
293 add_routes_handlers(server);
294}
295
296} // namespace sensesp
Captures ESP_LOGx output into a bounded RAM buffer for the web UI.
Definition log_buffer.h:97
LogSnapshot snapshot_since(uint32_t since, bool has_since, uint32_t now_ms)
Return retained lines newer than since.
static LogBuffer * instance()
Accessor used by the static vprintf trampoline.
Definition log_buffer.h:137
static String get_hostname()
Get the current hostname.
static const std::shared_ptr< SensESPBaseApp > & get()
Get the singleton instance of the SensESPBaseApp.
static const std::map< String, StatusPageItemBase * > * get_status_page_items()
std::shared_ptr< reactesp::EventLoop > event_loop()
Definition sensesp.cpp:9
void add_http_info_handler(std::shared_ptr< HTTPServer > &server)
bool check_origin(httpd_req_t *req)
constexpr int kUIOutputDefaultOrder
const StaticFileData kFrontendFiles[]
void add_http_restart_handler(std::shared_ptr< HTTPServer > &server)
void add_http_reset_handler(std::shared_ptr< HTTPServer > &server)
void add_http_log_handler(std::shared_ptr< HTTPServer > &server)
void add_base_app_http_command_handlers(std::shared_ptr< HTTPServer > &server)
void add_routes_handlers(std::shared_ptr< HTTPServer > &server)
Result of a snapshot_since() query, ready to serialize for the web UI.
Definition log_buffer.h:66
std::vector< std::string > lines
Definition log_buffer.h:70
const unsigned int content_length