REDAC HybridController
Firmware for LUCIDAC/REDAC Teensy
Loading...
Searching...
No Matches
server.cpp
Go to the documentation of this file.
1// Copyright (c) 2023 anabrid GmbH
2// Contact: https://www.anabrid.com/licensing/
3// SPDX-License-Identifier: MIT OR GPL-2.0-or-later
4
5#include <web/server.h>
6
7#include <net/auth.h>
8#include <net/ethernet.h>
9#include <protocol/protocol.h>
10#include <utils/logging.h>
11#include <web/assets.h>
12#include <web/websockets.h>
13
14#include <build/distributor.h>
15#include <build/distributor_generated.h>
16
17#define ENABLE_AWOT_NAMESPACE
18#include "web/aWOT.h"
19
20// more readable c string algebra
21bool contains(const char *haystack, const char *needle) { return strstr(haystack, needle) != NULL; }
22
23bool equals(const char *a, const char *b) { return strcmp(a, b) == 0; }
24
25using namespace web;
26
27#define SERVER_VERSION "LucidacWebServer/" FIRMWARE_VERSION
28
29// TODO: Redeem this pseudo singleton architecture
30
32 net::EthernetClient *client;
33 LucidacWebServer *server;
35};
36
37awot::Application webapp;
38
44FLASHMEM void allocate_interesting_headers(awot::Application &app) {
45 constexpr int max_allocated_headers = 10;
46 constexpr int maxval = 80; // maximum string length
47
48 int i = 0;
49
50 // This is the storage for the headers. Don't confuse it with the linked
51 // list managed internally by awot::Appplication which only stores string
52 // pointers back to this storage.
53 static char header[max_allocated_headers][maxval];
54
55 app.header("Host", header[i++], maxval);
56 app.header("Origin", header[i++], maxval);
57 app.header("Connection", header[i++], maxval);
58 app.header("Upgrade", header[i++], maxval);
59 app.header("Sec-WebSocket-Version", header[i++], maxval);
60 app.header("Sec-Websocket-Key", header[i++], maxval);
61
62 // programmer, please ensure at this line i < max_allocated_headers.
63}
64
65FLASHMEM void notfound(awot::Request &req, awot::Response &res) {
66 res.status(404);
67 res.set("Server", SERVER_VERSION);
68 res.set("Content-Type", "text/html");
69 res.println("<h1>Not found</h1><p>The ressource:<pre>");
70 res.println(req.path());
71 res.println("</pre><p>was not found on this server.");
72 res.println("<hr><p><i>" SERVER_VERSION "</i>");
73}
74
75FLASHMEM void serve_static(const web::StaticFile &file, awot::Response &res) {
76 LOGMEV("Serving static file %s with %d bytes\n", file.filename, file.size);
77 res.status(200);
78 // copying the headers (dumb!)
79 for (int i = 0; i < file.max_headers; i++) {
80 if (file.headers[i].name)
81 res.set(file.headers[i].name, file.headers[i].value);
82 }
83 // this would have been smarter but aWOT is not very smart
84 // res.print(file.prefab_http_headers);
85 // res.endHeaders();
86 // // Note on the note: This use case is possible with the aWOT API, cf.
87 // // https://awot.net/en/3x/api.html#res-beginHeaders
88 for (uint32_t written = 0; written != file.size;) {
89 // The folling line requires anabrid/aWOT@3.5.1 as upstream aWOT 3.5.0
90 // has a severe bug. Fix is sketched in
91 // https://github.com/lasselukkari/aWOT/compare/master...lfarrand:aWOT:master
92 // and adopted by us.
93 uint32_t singleshot = res.write((uint8_t *)file.start + written, file.size - written);
94 // LOGMEV("Written %d, plus %d", written, singleshot);
95 written += singleshot;
96 }
97 res.flush();
98}
99
100FLASHMEM void serve_static(awot::Request &req, awot::Response &res) {
101 // as a little workaround for aWOT, determine whether this runs *after*
102 // some successful route by inspecting whether something has been sent out or not.
103 // Handle only if nothing has been sent so far, i.e. no previous middleware/endpoint
104 // replied to this http query.
105 if (res.bytesSent() || res.ended())
106 return;
107
108 LOGMEV("%s", req.path());
109 const char *path_without_leading_slash = req.path() + 1;
110 const StaticFile *file = StaticAttic().get_by_filename(path_without_leading_slash);
111 if (file) {
112 serve_static(*file, res);
113 res.end();
114 } else {
115 LOGMEV("No suitable static file found for path %s\n", req.path());
116 notfound(req, res);
117 }
118}
119
120FLASHMEM void index(awot::Request &req, awot::Response &res) {
121 const StaticFile *file = StaticAttic().get_by_filename("index.html");
122 if (file) {
123 res.status(200);
124 serve_static(*file, res);
125 } else {
126 res.status(501);
127 res.set("Content-Type", "text/html");
128 res.println(
129 "<h1>LUCIDAC: Welcome</h1>"
130 "<p>Your LUCIDAC can reply to messages on the web via an elevated JSONL API endpoint. "
131 "However. the Single Page Application (SPA) static files have not been installed on the firmware. "
132 "This means you cannot use the beautiful and easy web interface straight out of the way. However, "
133 "you can still use it if you have sufficiently permissive CORS settings active and use a public "
134 "hosted version of the LUCIDAC GUI (SPA)."
135 "<hr><p><i>" SERVER_VERSION "</i>");
136 }
137}
138
139namespace web {
140FLASHMEM
141void convertToJson(const StaticFile &file, JsonVariant serialized) {
142 auto j = serialized.to<JsonObject>();
143 j["filename"] = file.filename;
144 j["lastmod"] = file.lastmod;
145 j["size"] = (int)file.size;
146 j["start_in_memory"] = (size_t)file.start;
147}
148} // namespace web
149
150// Provide some information about the built in assets
151// TODO: Should actually integrate information about webserver status
152// in the general {'type':'status'} query.
153FLASHMEM void about_static(awot::Request &req, awot::Response &res) {
154 res.set("Server", SERVER_VERSION);
155 res.set("Content-Type", "application/json");
156
157 StaticAttic attic;
158
159 StaticJsonDocument<1024> j;
160 j["has_any_static_files"] = attic.number_of_files != 0;
161 for (size_t i = 0; i < attic.number_of_files; i++) {
162 j["static_files"][i] = attic.files[i].filename;
163 }
164
165 // Note that this dump is already truncated by JSON Document size.
166 // Question: Do we even need this functionality?
167
168 // Could also show other web server status/config options
169 // such as: CORS, all available routes, etc.
170
171 // or some build flags around the assets, such as an asset bundle version or so.
172
173 // TODO: enable again.
174 // serializeJson(j, res);
175}
176
177FLASHMEM void set_cors(awot::Request &req, awot::Response &res) {
178 res.set("Server", SERVER_VERSION);
179 res.set("Content-Type", "application/json");
180 // Disable CORS for the time being
181 // Note the maximum number of headers of the lib (can be avoided by directly printing)
182 res.set("Access-Control-Allow-Origin", net::auth::Gatekeeper::get().access_control_allow_origin.c_str());
183 res.set("Access-Control-Allow-Credentials", "true");
184 res.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
185 res.set("Access-Control-Allow-Headers", "Origin, Cookie, Set-Cookie, Content-Type, Server");
186}
187
188FLASHMEM void api_preflight(awot::Request &req, awot::Response &res) {
189 res.status(200);
190 set_cors(req, res);
191}
192
193FLASHMEM void api(awot::Request &req, awot::Response &res) {
194 set_cors(req, res);
195
196 // the following mimics the function
197 // msg::JsonLinesProtocol::get().process_tcp_input(req, net::auth::AuthentificationContext());
198 // which does not work because it assumes input stream = output sream.
199
200 auto error = deserializeJson(*msg::JsonLinesProtocol().get().envelope_in, req);
201 if (error == DeserializationError::Code::EmptyInput) {
202 res.status(400);
203 res.println("{'type':'error', 'msg':'Empty input. Expecting a Lucidac query in JSON fomat'}");
204 } else if (error) {
205 res.status(400);
206 res.println("{'type':'error', 'msg': 'Error parsing JSON'}");
207 // Serial.println(error.c_str());
208 } else {
209 res.status(200);
210 static net::auth::AuthentificationContext c;
211 msg::JsonLinesProtocol().get().handleMessage(c, res);
212 // serializeJson(msg::JsonLinesProtocol().get().envelope_out->as<JsonObject>(), Serial);
213 // serializeJson(msg::JsonLinesProtocol().get().envelope_out->as<JsonObject>(), res);
214 }
215}
216
217#define ERR(msg) \
218 { \
219 if (!has_errors) { \
220 has_errors = true; \
221 res.status(400); \
222 res.set("Content-Type", "text/plain"); \
223 res.println("This URL is only for Websocket Upgrade."); \
224 } \
225 res.println("Error: " msg); \
226 }
227
228FLASHMEM void websocket_upgrade(awot::Request &req, awot::Response &res) {
229 res.set("Server", SERVER_VERSION);
230 auto ctx = (HTTPContext *)req.context;
231
232 // Important TODO:
233 // Check req.get("Origin") if it is allowed to connect.
234 // Similar as in the CORS, have to keep a user setting of allowed origins.
235
236 bool has_errors = false;
237 if (!req.get("Connection"))
238 ERR("Missing 'Connection' header");
239 if (!contains(req.get("Connection"), "Upgrade"))
240 ERR("Connection header contains not 'Upgrade'");
241 if (!req.get("Upgrade"))
242 ERR("Missing 'Upgrade' header");
243 if (!equals(req.get("Upgrade"), "websocket"))
244 ERR("Upgrade header is not 'websocket'");
245 if (!req.get("Sec-WebSocket-Version"))
246 ERR("Missing Sec-Websocket-Version");
247 if (!equals(req.get("Sec-WebSocket-Version"), "13"))
248 ERR("Only Sec-Websocket-Version=13 supported");
249 if (!req.get("Sec-WebSocket-Key"))
250 ERR("Missing Sec-Websocket-Key");
251 if (has_errors)
252 return;
253
254 if (ctx->server->clients.size() == net::StartupConfig::get().max_connections) {
255 LOG3("Cannot accept new websocket connection because maximum number of connections (",
256 net::StartupConfig::get().max_connections, ") already reached.");
257 res.status(500);
258 res.set("Content-Type", "text/plain");
259 res.println("Maximum number of concurrent websocket connections reached");
260 return;
261 }
262
263 auto serverAccept = websocketsHandshakeEncodeKey(req.get("Sec-WebSocket-Key"));
264
265 res.set("Connection", "Upgrade");
266 res.set("Upgrade", "websocket");
267 res.set("Sec-WebSocket-Version", "13");
268 res.set("Sec-Websocket-Accept", serverAccept.c_str());
269 res.status(101); // Switching Protocols
270 res.flush();
271 res.end(); // avoid the catchall handler to get active
272
273 ctx->convert_to_websocket = true;
274 // Control to WebsocketsClient is passed at further LucidacWebServer::loop iterations.
275}
276
277FLASHMEM void web::LucidacWebServer::begin() {
278 ethserver.begin(net::StartupConfig::get().webserver_port);
279
281
282 webapp.get("/", &index);
283
284 webapp.get("/api", &api);
285 webapp.post("/api", &api);
286 webapp.options("/api", &api_preflight);
287
288 if (net::StartupConfig::get().enable_websockets) {
289 webapp.get("/websocket", &websocket_upgrade);
290 webapp.options("/websocket", &api_preflight);
291 } else {
292 // should spill out some error explaining what is up
293 }
294
299
300 // This is more for testing, not really a good endpoint.
301 webapp.get("/webserver-status", &about_static);
302
303 // register static file handler as last one, this will also fall back to 404.
304 webapp.get(serve_static);
305}
306
307// ws initialization looks a bit bonkers, so let me explain it:
308// The communciation/src/websockets package is alien and not integrated into the codebase.
309// It comes with its own Socket-handling logic. There, there is a TcpClient which holds a pointer
310// to our QNEthernet client. However, the TcpClient itself is a shared pointer for whatever reason.
311// So for letting websocket clients lookup client context informations, we have this link-loop
312FLASHMEM web::LucidacWebsocketsClient::LucidacWebsocketsClient(net::EthernetClient other)
313 : socket(std::move(other)), ws(std::make_shared<websockets::network::TcpClient>(this)) {
314 user_context.set_remote_identifier(net::auth::RemoteIdentifier{socket.remoteIP()});
315}
316
317/*
318 Note that this data handling is *REALLY* not performant, as it requires a websocket message to be completely
319 read into a string, then passed over to the JSON parser which again parses it into its buffer (note that
320 ArduinoJSON is *actually* streamlined for reading things from/to buffers) and then all the way back.
321 However: It works, at least.
322
323 Potential caveat: Users sending really large messages. Check if there is some safety net for that in
324 our websockets library.
325*/
326FLASHMEM void onWebsocketMessageCallback(websockets::WebsocketsClient &wsclient,
327 websockets::WebsocketsMessage msg) {
328 LOGMEV("ws Role=%d, data=%s", msg.role(), msg.data().c_str());
329 if (!msg.isComplete()) {
330 LOG_ALWAYS("webSockets: Ignoring incomplete message");
331 return;
332 }
333 if (!msg.isText()) {
334 LOG_ALWAYS("webSockets: Ignoring non-text message");
335 return;
336 }
337
338 std::string envelope_out_str;
339 msg::JsonLinesProtocol::get().process_string_input(msg.data(), envelope_out_str,
340 wsclient.client()->context->user_context);
341 wsclient.send(envelope_out_str);
342}
343
344FLASHMEM void web::LucidacWebServer::loop() {
345 net::EthernetClient client_socket = ethserver.accept();
346
347 // Warning: This probably allows also to deadlock the device by opening a connection
348 // but not sending anything.
349
350 if (client_socket) {
351 // incoming new HTTP client.
352 LOG4("Web Client request from ", client_socket.remoteIP(), ":", client_socket.remotePort());
353
354 HTTPContext ctx{.client = &client_socket, .server = this, .convert_to_websocket = false};
355
356 webapp.process(&client_socket, &ctx);
357
358 if (ctx.convert_to_websocket) {
359 LOG_ALWAYS("Accepting Websocket connection.");
360
361 // we use emplace + a constructor because we have to be super careful about having pointers
362 // referencing to the correct socket.
363 clients.emplace_back(std::move(client_socket));
364 auto &client = clients.back();
365
366 LOG_ALWAYS("Pushed to clients list.");
367
368 // Don't use masking from server to client (according to RFC)
369 client.ws.setUseMasking(false);
370
371 // register callback for receiving messages.
372 client.ws.onMessage(onWebsocketMessageCallback);
373
374 // Send a hello world or so
375 client.ws.send("{'hello':'client'}\n");
376
377 // TODO: Consider adding to JsonLinesProtocol::get().broadcast,
378 // however would require some callback because message needs to be wrapped
379 // into websocket protocol.
380
381 LOG_ALWAYS("Done accepting Websocket connection.");
382 } else {
383 // As we currently do not support keep-alive, we need to close the
384 // connection, otherwise browser wait for more data.
385 client_socket.close();
386 }
387 }
388
389 // using iterator instead range-based loop because EthernetClient lacks == operator
390 // so we use list::erase instead of list::remove.
391 for (auto client = clients.begin(); client != clients.end(); client++) {
392 const auto client_idx = std::distance(clients.begin(), client);
393 if (client->socket.connected()) {
394 if (client->ws.available()) {
395 // msg::JsonLinesProtocol::get().process_tcp_input(client->socket, client->user_context);
396 // webapp.process(&client->socket);
397 client->ws.poll();
398 // note that actual message handling is done via the onWebsocketMessageCallback.
399 client->last_contact.reset();
400 } else if (!client->socket.connected()) {
401 client->socket.stop();
402 } else if (client->last_contact.expired(net::StartupConfig::get().connection_timeout_ms)) {
403 LOG5("Web client ", client_idx, ", timed out after ", net::StartupConfig::get().connection_timeout_ms,
404 " ms of idling");
405 client->ws.close(websockets::CloseReason_GoingAway);
406 // consider...
407 // client->socket.stop();
408 }
409 } else {
410 LOG5("Websocket Client ", client_idx, ", was ", client->socket.remoteIP(), ", disconnected");
411 client->socket.close();
412 // JsonLinesProtocol::get().broadcast.remove(&client->socket);
413 clients.erase(client);
414 return; // iterator invalidated, better start loop() freshly.
415 }
416 }
417}
uint32_t
Definition flasher.cpp:195
FLASHMEM void convertToJson(const StaticFile &file, JsonVariant serialized)
Definition server.cpp:141
bool convert_to_websocket
Definition server.cpp:34
net::EthernetClient * client
Definition server.cpp:32
LucidacWebServer * server
Definition server.cpp:33
FLASHMEM void allocate_interesting_headers(awot::Application &app)
aWOT preallocates everything, therefore request headers are not dynamically parsed but instead only s...
Definition server.cpp:44
#define ERR(msg)
Definition server.cpp:217
#define SERVER_VERSION
Definition server.cpp:27
FLASHMEM void onWebsocketMessageCallback(websockets::WebsocketsClient &wsclient, websockets::WebsocketsMessage msg)
Definition server.cpp:326
FLASHMEM void index(awot::Request &req, awot::Response &res)
Definition server.cpp:120
FLASHMEM void notfound(awot::Request &req, awot::Response &res)
Definition server.cpp:65
FLASHMEM void api_preflight(awot::Request &req, awot::Response &res)
Definition server.cpp:188
bool equals(const char *a, const char *b)
Definition server.cpp:23
FLASHMEM void about_static(awot::Request &req, awot::Response &res)
Definition server.cpp:153
awot::Application webapp
Definition server.cpp:37
FLASHMEM void api(awot::Request &req, awot::Response &res)
Definition server.cpp:193
bool contains(const char *haystack, const char *needle)
Definition server.cpp:21
FLASHMEM void serve_static(const web::StaticFile &file, awot::Response &res)
Definition server.cpp:75
FLASHMEM void set_cors(awot::Request &req, awot::Response &res)
Definition server.cpp:177
FLASHMEM void websocket_upgrade(awot::Request &req, awot::Response &res)
Definition server.cpp:228