5#include "proto/main.pb.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>
14#include <build/distributor.h>
15#include <build/distributor_generated.h>
17#define ENABLE_AWOT_NAMESPACE
21bool contains(
const char *haystack,
const char *needle) {
return strstr(haystack, needle) != NULL; }
23bool equals(
const char *a,
const char *b) {
return strcmp(a, b) == 0; }
27#define SERVER_VERSION "LucidacWebServer/" FIRMWARE_VERSION
45 constexpr int max_allocated_headers = 10;
46 constexpr int maxval = 80;
53 static char header[max_allocated_headers][maxval];
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);
65void notfound(awot::Request &req, awot::Response &res) {
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.");
75void serve_static(
const web::StaticFile &file, awot::Response &res) {
76 LOGMEV(
"Serving static file %s with %d bytes\n", file.filename, file.size);
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);
88 for (
uint32_t written = 0; written != file.size;) {
93 uint32_t singleshot = res.write((uint8_t *)file.start + written, file.size - written);
95 written += singleshot;
105 if (res.bytesSent() || res.ended())
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);
115 LOGMEV(
"No suitable static file found for path %s\n", req.path());
120void index(awot::Request &req, awot::Response &res) {
121 const StaticFile *file = StaticAttic().get_by_filename(
"index.html");
127 res.set(
"Content-Type",
"text/html");
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)."
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;
155 res.set(
"Content-Type",
"application/json");
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;
177void set_cors(awot::Request &req, awot::Response &res) {
179 res.set(
"Content-Type",
"application/json");
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");
193void api(awot::Request &req, awot::Response &res) {
223 res.set("Content-Type", "text/plain"); \
224 res.println("This URL is only for Websocket Upgrade."); \
226 res.println("Error: " msg); \
237 bool has_errors =
false;
238 if (!req.get(
"Connection"))
239 ERR(
"Missing 'Connection' header");
240 if (!
contains(req.get(
"Connection"),
"Upgrade"))
241 ERR(
"Connection header contains not 'Upgrade'");
242 if (!req.get(
"Upgrade"))
243 ERR(
"Missing 'Upgrade' header");
244 if (!
equals(req.get(
"Upgrade"),
"websocket"))
245 ERR(
"Upgrade header is not 'websocket'");
246 if (!req.get(
"Sec-WebSocket-Version"))
247 ERR(
"Missing Sec-Websocket-Version");
248 if (!
equals(req.get(
"Sec-WebSocket-Version"),
"13"))
249 ERR(
"Only Sec-Websocket-Version=13 supported");
250 if (!req.get(
"Sec-WebSocket-Key"))
251 ERR(
"Missing Sec-Websocket-Key");
255 if (ctx->server->clients.size() == net::StartupConfig::get().max_connections) {
256 LOG3(
"Cannot accept new websocket connection because maximum number of connections (",
257 net::StartupConfig::get().max_connections,
") already reached.");
259 res.set(
"Content-Type",
"text/plain");
260 res.println(
"Maximum number of concurrent websocket connections reached");
264 auto serverAccept = websocketsHandshakeEncodeKey(req.get(
"Sec-WebSocket-Key"));
266 res.set(
"Connection",
"Upgrade");
267 res.set(
"Upgrade",
"websocket");
268 res.set(
"Sec-WebSocket-Version",
"13");
269 res.set(
"Sec-Websocket-Accept", serverAccept.c_str());
274 ctx->convert_to_websocket =
true;
278void web::LucidacWebServer::begin() {
279 ethserver.begin(net::StartupConfig::get().webserver_port);
289 if (net::StartupConfig::get().enable_websockets) {
313web::LucidacWebsocketsClient::LucidacWebsocketsClient(net::EthernetClient other)
314 : socket(std::move(other)), ws(std::make_shared<
websockets::network::TcpClient>(this)) {
315 user_context.set_remote_identifier(net::auth::RemoteIdentifier{socket.remoteIP()});
328 websockets::WebsocketsMessage
msg) {
329 LOGMEV(
"ws Role=%d, data=%s",
msg.role(),
msg.data().c_str());
330 if (!
msg.isComplete()) {
331 LOG_ALWAYS(
"webSockets: Ignoring incomplete message");
335 LOG_ALWAYS(
"webSockets: Ignoring non-text message");
339 std::string envelope_out_str;
342 wsclient.send(envelope_out_str);
345void web::LucidacWebServer::loop() {
346 net::EthernetClient client_socket = ethserver.accept();
353 LOG4(
"Web Client request from ", client_socket.remoteIP(),
":", client_socket.remotePort());
355 HTTPContext ctx{.client = &client_socket, .server =
this, .convert_to_websocket =
false};
357 webapp.process(&client_socket, &ctx);
359 if (ctx.convert_to_websocket) {
360 LOG_ALWAYS(
"Accepting Websocket connection.");
364 clients.emplace_back(std::move(client_socket));
365 auto &client = clients.back();
367 LOG_ALWAYS(
"Pushed to clients list.");
370 client.ws.setUseMasking(
false);
376 client.ws.send(
"{'hello':'client'}\n");
382 LOG_ALWAYS(
"Done accepting Websocket connection.");
386 client_socket.close();
392 for (
auto client = clients.begin(); client != clients.end(); client++) {
393 const auto client_idx = std::distance(clients.begin(), client);
394 if (client->socket.connected()) {
395 if (client->ws.available()) {
400 client->last_contact.reset();
401 }
else if (!client->socket.connected()) {
402 client->socket.stop();
403 }
else if (client->last_contact.expired(net::StartupConfig::get().connection_timeout_ms)) {
404 LOG5(
"Web client ", client_idx,
", timed out after ", net::StartupConfig::get().connection_timeout_ms,
406 client->ws.close(websockets::CloseReason_GoingAway);
411 LOG5(
"Websocket Client ", client_idx,
", was ", client->socket.remoteIP(),
", disconnected");
412 client->socket.close();
414 clients.erase(client);
void convertToJson(const StaticFile &file, JsonVariant serialized)
bool convert_to_websocket
net::EthernetClient * client
LucidacWebServer * server
void websocket_upgrade(awot::Request &req, awot::Response &res)
void index(awot::Request &req, awot::Response &res)
void about_static(awot::Request &req, awot::Response &res)
void set_cors(awot::Request &req, awot::Response &res)
void serve_static(const web::StaticFile &file, awot::Response &res)
void allocate_interesting_headers(awot::Application &app)
aWOT preallocates everything, therefore request headers are not dynamically parsed but instead only s...
bool equals(const char *a, const char *b)
void api_preflight(awot::Request &req, awot::Response &res)
bool contains(const char *haystack, const char *needle)
void notfound(awot::Request &req, awot::Response &res)
void api(awot::Request &req, awot::Response &res)
void onWebsocketMessageCallback(websockets::WebsocketsClient &wsclient, websockets::WebsocketsMessage msg)