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);
65FLASHMEM
void 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.");
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);
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());
120FLASHMEM
void 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;
177FLASHMEM
void 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");
193FLASHMEM
void api(awot::Request &req, awot::Response &res) {
200 auto error = deserializeJson(*msg::JsonLinesProtocol().get().envelope_in, req);
201 if (error == DeserializationError::Code::EmptyInput) {
203 res.println(
"{'type':'error', 'msg':'Empty input. Expecting a Lucidac query in JSON fomat'}");
206 res.println(
"{'type':'error', 'msg': 'Error parsing JSON'}");
210 static net::auth::AuthentificationContext c;
211 msg::JsonLinesProtocol().get().handleMessage(c, res);
222 res.set("Content-Type", "text/plain"); \
223 res.println("This URL is only for Websocket Upgrade."); \
225 res.println("Error: " msg); \
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");
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.");
258 res.set(
"Content-Type",
"text/plain");
259 res.println(
"Maximum number of concurrent websocket connections reached");
263 auto serverAccept = websocketsHandshakeEncodeKey(req.get(
"Sec-WebSocket-Key"));
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());
273 ctx->convert_to_websocket =
true;
277FLASHMEM
void web::LucidacWebServer::begin() {
278 ethserver.begin(net::StartupConfig::get().webserver_port);
288 if (net::StartupConfig::get().enable_websockets) {
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()});
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");
334 LOG_ALWAYS(
"webSockets: Ignoring non-text message");
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);
344FLASHMEM
void web::LucidacWebServer::loop() {
345 net::EthernetClient client_socket = ethserver.accept();
352 LOG4(
"Web Client request from ", client_socket.remoteIP(),
":", client_socket.remotePort());
354 HTTPContext ctx{.client = &client_socket, .server =
this, .convert_to_websocket =
false};
356 webapp.process(&client_socket, &ctx);
358 if (ctx.convert_to_websocket) {
359 LOG_ALWAYS(
"Accepting Websocket connection.");
363 clients.emplace_back(std::move(client_socket));
364 auto &client = clients.back();
366 LOG_ALWAYS(
"Pushed to clients list.");
369 client.ws.setUseMasking(
false);
375 client.ws.send(
"{'hello':'client'}\n");
381 LOG_ALWAYS(
"Done accepting Websocket connection.");
385 client_socket.close();
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()) {
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,
405 client->ws.close(websockets::CloseReason_GoingAway);
410 LOG5(
"Websocket Client ", client_idx,
", was ", client->socket.remoteIP(),
", disconnected");
411 client->socket.close();
413 clients.erase(client);
FLASHMEM void convertToJson(const StaticFile &file, JsonVariant serialized)
bool convert_to_websocket
net::EthernetClient * client
LucidacWebServer * server
FLASHMEM void allocate_interesting_headers(awot::Application &app)
aWOT preallocates everything, therefore request headers are not dynamically parsed but instead only s...
FLASHMEM void onWebsocketMessageCallback(websockets::WebsocketsClient &wsclient, websockets::WebsocketsMessage msg)
FLASHMEM void index(awot::Request &req, awot::Response &res)
FLASHMEM void notfound(awot::Request &req, awot::Response &res)
FLASHMEM void api_preflight(awot::Request &req, awot::Response &res)
bool equals(const char *a, const char *b)
FLASHMEM void about_static(awot::Request &req, awot::Response &res)
FLASHMEM void api(awot::Request &req, awot::Response &res)
bool contains(const char *haystack, const char *needle)
FLASHMEM void serve_static(const web::StaticFile &file, awot::Response &res)
FLASHMEM void set_cors(awot::Request &req, awot::Response &res)
FLASHMEM void websocket_upgrade(awot::Request &req, awot::Response &res)