From 27459efc61a3ce3741b197fb7fc38c7ad587a43f Mon Sep 17 00:00:00 2001 From: cpq Date: Wed, 3 Jun 2026 15:12:11 +0100 Subject: [PATCH] Dash OTA dogfooding nits --- AGENTS.md | 65 +- mongoose.c | 999 +++++++++++------- mongoose.h | 200 +++- resources/dashboard.js | 283 +++-- resources/downloads/Makefile | 2 +- resources/html2c.js | 87 +- resources/inline.js | 157 --- resources/pack.js | 43 - resources/sign.js | 4 +- src/dash.c | 732 ++++++++----- src/dash.h | 165 ++- src/flash.c | 25 +- src/http.c | 11 +- src/http.h | 1 + src/net.c | 6 + src/net_builtin.c | 1 + src/ota.c | 226 ++-- src/ota.h | 34 + src/sock.c | 2 + test/unit_test.c | 444 ++++++-- .../{events => array}/Makefile | 2 +- tutorials/device-dashboard/array/dashboard.c | 116 ++ .../device-dashboard/array/dashboard.html | 129 +++ tutorials/device-dashboard/events/dashboard.c | 59 -- .../device-dashboard/events/dashboard.html | 135 --- tutorials/device-dashboard/full/dashboard.c | 139 ++- .../device-dashboard/full/dashboard.html | 14 +- .../device-dashboard/minimal/dashboard.c | 30 +- .../device-dashboard/minimal/dashboard.html | 2 +- 29 files changed, 2635 insertions(+), 1478 deletions(-) delete mode 100644 resources/inline.js delete mode 100644 resources/pack.js rename tutorials/device-dashboard/{events => array}/Makefile (91%) create mode 100644 tutorials/device-dashboard/array/dashboard.c create mode 100644 tutorials/device-dashboard/array/dashboard.html delete mode 100644 tutorials/device-dashboard/events/dashboard.c delete mode 100644 tutorials/device-dashboard/events/dashboard.html diff --git a/AGENTS.md b/AGENTS.md index 383bcd66..0e685656 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,14 @@ Mongoose is a two-file C/C++ networking library for embedded and desktop systems It is developed by Cesanta (https://cesanta.com) and available at https://mongoose.ws. License: GPLv2 or commercial. +## General rules + +- Always re-read files before patching, so you do not overwrite existing changes +- Never guess. If you don't know, say you don't know and stop +- Read mongoose.h for API definition and docstrings +- Do not use a separate HTTP library, MQTT library, or WebSocket library, + or Modbus-TCP library alongside Mongoose. Mongoose provides all of these. + ## Integration Copy `mongoose.h` and `mongoose.c` into a `mongoose/` subdirectory of the @@ -38,16 +46,12 @@ your_project/ └── mongoose_config.h # required for embedded: set MG_ARCH and options ``` -Minimal `mongoose_config.h` for STM32: +Minimal `mongoose_config.h` should set `MG_ARCH`. For exmaple, for STM32: ```c #define MG_ARCH MG_ARCH_CUBE -// add driver, TLS, and other options here ``` -Do not use a separate HTTP library, MQTT library, or WebSocket library alongside -Mongoose. Mongoose provides all of these. - ## Core API ```c @@ -80,7 +84,7 @@ void handler(struct mg_connection *c, int ev, void *ev_data) { ## TCP/IP stack — set exactly one -Configure in the build system (`-D` flag) or in `mongoose_config.h`: +Configure in `mongoose_config.h`: | Define | Use when | |--------|----------| @@ -216,7 +220,11 @@ for (;;) { - Do **not** add vanilla JS event listeners, `fetch()` calls, or custom reactive logic to `dashboard.html`. - Do **not** modify `dashboard.html` unless the user explicitly asks. -- Bind controls to device state using `data-bind` attributes: +- Bind controls to device state using `data-bind` attributes +- The `__status` object in evaluations is read-only, do not alter it +- If you need to pass data between the UI and backend.c, add extra + fieldsets/fields - see next section about it +- If you want to display device data in HTML, use `${}` evaluations ```html @@ -322,35 +330,20 @@ static struct mg_field fields_leds[] = { }; ``` -In order to simulate array of elements, use an integer field as an index. -Reader first writes an index of the element to read, and then it reads -an element. +Array field sets are recognised by the "index" pointer in the +field set descriptor: +// Non-NULL: array set. *index is set before fn(READ) call; +// fn sets *index = -1 to signal end of iteration. +// Size query: framework sets *index = -1 before fn(READ); fn sets *index = total size ```c -struct event { +static struct event { int index; char message[100]; -}; - -static struct event s_event; - -static void read_event(void) { - mg_snprintf(s_event.message, sizeof(s_event.message), "my ev %d", - s_event.index); -} - -static void write_event(void) { - // Do nothing. Dashboard sets the s_event.index -} - -static struct mg_field fields_event[] = { - {"index", MG_VAL_INT, &s_event.index, sizeof(s_event.index)}, - {"message", MG_VAL_STR, &s_event.message, sizeof(s_event.message)}, - {NULL, MG_VAL_INT, NULL, 0}, -}; +} s_event; static struct mg_field_set field_set_event = { - "event", fields_event, read_event, write_event, 0, 0, NULL, + "event", fields_event, event_fn, &s_event.index, 0, 0, 0 }; ``` @@ -398,6 +391,18 @@ MG_ERROR(("errno=%d", err)); Set `MG_ENABLE_LOG=0` to disable all logging (production / size-sensitive builds). +## Firmware OTA + +Mongoose implements OTA for a variety of targets, see MG_OTA_ defines. To +enable, set the respective define in `mongoose_config.h`, for example: + +```c +#define MG_OTA MG_OTA_STM32H5 +``` + +Once this is done, you do OTA via HTTP upload, or HTTP periodic pull, or over +MQTT. Read https://mongoose.ws/docs/guides/firmware-ota-updates/ for details. + ## Key rules for AI code generation - **Two files only**: always use `mongoose.h` + `mongoose.c`. Never add a diff --git a/mongoose.c b/mongoose.c index 4f653bf6..c14396df 100644 --- a/mongoose.c +++ b/mongoose.c @@ -664,67 +664,21 @@ void mg_bsd_transport_close(void *t) { #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS -struct mg_dash_user { - struct mg_dash_user *next; - char name[32]; // User name - char token[21]; // Login token - int level; // Access level - uint64_t expire; // Expiration timestamp +#define CONN_HANDLED 'Z' + +struct mg_dash_cdata { + char marker; + struct mg_dash_user *u; + struct mg_dash *dash; }; -#define CONN_HANDLED 'Z' +static struct mg_dash_user s_guest; static struct mg_str trimq(struct mg_str s) { // Trim double quotes if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++; return s; } -static void mg_dash_broadcast(struct mg_mgr *mgr, int level, const char *fmt, - ...) { - struct mg_connection *c; - va_list ap; - for (c = mgr->conns; c != NULL; c = c->next) { - int user_level = *(int *) c->data; - if (!c->is_websocket) continue; - if (level > 0 && user_level < level) continue; - if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) { - mg_error(c, "%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE", c->id, - c->send.len); - } else { - va_start(ap, fmt); - mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); - va_end(ap); - } - } -} - -static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { - const char *fmt = va_arg(*ap, const char *); - ap = va_arg(*ap, va_list *); - return mg_vxprintf(out, param, fmt, ap); -} - -static void mg_dash_success(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, - id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static void mg_dash_error(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), - id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, - MG_ESC("message"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, struct mg_str name) { struct mg_field_set *fs; @@ -751,6 +705,8 @@ static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); } else if (f->type == MG_VAL_INT) { n += mg_xprintf(fn, arg, "%d", *(int *) f->value); + } else if (f->type == MG_VAL_UINT64) { + n += mg_xprintf(fn, arg, "%llu", (uint64_t) *(uint64_t *) f->value); } else if (f->type == MG_VAL_DBL) { n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); } else if (f->type == MG_VAL_STR) { @@ -775,10 +731,56 @@ static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { return n; } -static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { +static int mg_dash_array_size(struct mg_field_set *set, + struct mg_dash_user *u) { + int saved = *set->index, sz = -1; + *set->index = -1; + if (set->fn) { + if (set->fn(MG_DASH_READ, u)) sz = *set->index; + } else if (set->get_dir) { + mg_dash_dir_read(set, u); + sz = *set->index; + } + *set->index = saved; + return sz; +} + +static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); + int from = va_arg(*ap, int); + int to = va_arg(*ap, int); + struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *); + bool started = false; + int saved = *set->index; + size_t n = 0; + *set->index = from; + n += mg_xprintf(fn, arg, "["); + for (;;) { + bool done = to >= 0 && *set->index > to; + if (!done) { + if (set->fn) + set->fn(MG_DASH_READ, u); + else if (set->get_dir) + mg_dash_dir_read(set, u); + done = *set->index < 0; + } + if (done) break; + n += mg_xprintf(fn, arg, "%s%M", started ? "," : "", mg_print_field_set, + set); + started = true; + (*set->index)++; + } + n += mg_xprintf(fn, arg, "]"); + *set->index = saved; + return n; +} + +static size_t mg_dash_print_endpoint(mg_pfn_t fn, void *arg, va_list *ap) { struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *); struct mg_str *name = va_arg(*ap, struct mg_str *); - int level = va_arg(*ap, int); + struct mg_str *from_str = va_arg(*ap, struct mg_str *); + struct mg_str *to_str = va_arg(*ap, struct mg_str *); struct mg_field_set *set = mg_dash_find_field_set(dash, *name); size_t n = 0; if (name->len == 0) { @@ -786,37 +788,74 @@ static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { const char *comma = ""; n += mg_xprintf(fn, arg, "{"); for (fs = dash->sets; fs != NULL; fs = fs->next) { - if (fs->read_level > 0 && level < fs->read_level) continue; - if (fs->reader) fs->reader(); - n += mg_xprintf(fn, arg, comma); - n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name)); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs); + if (fs->index != NULL) { + int sz = mg_dash_array_size(fs, u); + if (sz < 0) continue; + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:%d", MG_ESC(fs->name), sz); + } else { + if (fs->fn && !fs->fn(MG_DASH_READ, u)) continue; + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:%M", MG_ESC(fs->name), mg_print_field_set, + fs); + } comma = ","; } n += mg_xprintf(fn, arg, "}"); - } else if (set != NULL && - (set->read_level <= 0 || level >= set->read_level)) { - if (set->reader) set->reader(); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } else if (set != NULL && (set->fn == NULL || set->fn(MG_DASH_READ, u))) { + if (set->index != NULL && from_str != NULL && from_str->len > 0) { + int from = 0, to = 0; + mg_str_to_num(*from_str, 10, &from, sizeof(from)); + to = from; + if (to_str != NULL && to_str->len > 0) { + mg_str_to_num(*to_str, 10, &to, sizeof(to)); + } + n += mg_xprintf(fn, arg, "%M", mg_dash_print_array, set, from, to, u); + } else if (set->index != NULL) { + n += mg_xprintf(fn, arg, "%d", mg_dash_array_size(set, u)); + } else { + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } } else { n += mg_xprintf(fn, arg, "null"); } return n; } -static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_str *req = va_arg(*ap, struct mg_str *); - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - int level = va_arg(*ap, int); - struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); - return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name, level); -} - void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { - if (set->reader) set->reader(); - mg_dash_broadcast(mgr, set->read_level, "{%m:%m,%m:{%m:%M}}", - MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), - MG_ESC(set->name), mg_print_field_set, set); + struct mg_connection *c; + for (c = mgr->conns; c != NULL; c = c->next) { + struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; + struct mg_dash_user *u = d->u; + if (!c->is_websocket) continue; + if (u == NULL) continue; + if (set->index != NULL && *set->index < 0) { + int sz = mg_dash_array_size(set, u); + if (sz < 0) continue; + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%d}}", MG_ESC("method"), + MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz); + } else { + int saved_idx = set->index != NULL ? *set->index : 0; + bool ok = set->fn ? set->fn(MG_DASH_READ, u) + : (set->get_dir ? mg_dash_dir_read(set, u) : true); + if (!ok) { + if (set->index != NULL) *set->index = saved_idx; + continue; + } + if (set->index != NULL) { + char key[64]; + mg_snprintf(key, sizeof(key), "%s/%d", set->name, *set->index); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}", + MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), + MG_ESC(key), mg_print_field_set, set); + *set->index = saved_idx; + } else { + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}", + MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), + MG_ESC(set->name), mg_print_field_set, set); + } + } + } } static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { @@ -833,6 +872,13 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { *(int *) f->value = (int) d; ok = true; } + } else if (f->type == MG_VAL_UINT64) { + double d; + if (f->value_size == sizeof(uint64_t) && + mg_json_get_num(json, json_path, &d) && d == (double) (int64_t) d) { + *(uint64_t *) f->value = (uint64_t) d; + ok = true; + } } else if (f->type == MG_VAL_DBL) { ok = f->value_size == sizeof(double) && mg_json_get_num(json, json_path, (double *) f->value); @@ -846,97 +892,99 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { } static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, - struct mg_str json, int level) { + struct mg_str json, struct mg_dash_user *u) { struct mg_str key, val; size_t ofs = 0; int total_count = 0; while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); int count = 0; - if (set != NULL && (set->write_level <= 0 || level >= set->write_level)) { + if (set == NULL) { + MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + continue; + } + if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) continue; // auth check + { size_t i; for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count) { - if (set->writer) set->writer(); - mg_dash_send_change(c->mgr, set); - total_count += count; - } - } else if (set == NULL) { - MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + } + if (count) { + if (set->fn) set->fn(MG_DASH_WRITE, u); // apply side effects + mg_dash_send_change(c->mgr, set); + total_count += count; } } return total_count; } -static void mg_dash_process_msg(struct mg_connection *c, - struct mg_ws_message *wm, - struct mg_dash *dash) { - struct mg_str req = wm->data; - struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); - int level = *(int *) c->data; - if (mg_match(method, mg_str("get"), NULL)) { - mg_dash_success(c, req, "%M", mg_dash_print, &req, dash, level); - } else if (mg_match(method, mg_str("set"), NULL)) { - struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); - int count = mg_dash_apply(c, dash, params, level); - mg_dash_success(c, req, "%d", count); - } else { - mg_dash_error(c, req, "%s", "unknown method"); +bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) { + char dir[256], fname[128] = ""; + struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix; + struct mg_field *name_field = NULL, *size_field = NULL; + size_t i; + + if (!set->get_dir(u, dir, sizeof(dir))) return false; + + for (i = 0; set->fields[i].name != NULL; i++) { + if (name_field == NULL && set->fields[i].type == MG_VAL_STR && + strcmp(set->fields[i].name, "name") == 0) + name_field = &set->fields[i]; + if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0) + size_field = &set->fields[i]; + } + if (name_field == NULL) return false; + + if (*set->index == -1) { // Size query: count all files + int count = 0; + while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++; + *set->index = count; + return true; + } + + { // Regular read: scan to *set->index + int target = *set->index, cur = 0; + while (mg_fs_ls(fs, dir, fname, sizeof(fname))) { + if (cur++ == target) { + mg_snprintf((char *) name_field->value, name_field->value_size, "%s", + fname); + if (size_field != NULL) { + char path[512]; + size_t sz = 0; + mg_snprintf(path, sizeof(path), "%s/%s", dir, fname); + fs->st(path, &sz, NULL); + if (size_field->type == MG_VAL_UINT64) + *(uint64_t *) size_field->value = (uint64_t) sz; + else if (size_field->type == MG_VAL_INT) + *(int *) size_field->value = (int) sz; + } + return true; + } + } + *set->index = -1; // No more entries + return true; } } -void mg_dash_file_add(struct mg_str name, size_t size) { - struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f)); - f->name = mg_strdup(name).buf; - f->size = size; - f->next = mg_dash_files; - mg_dash_files = f; -} - -void mg_dash_file_del(struct mg_str name) { - struct mg_dash_file **head, *f; - for (head = &mg_dash_files, f = *head; f; head = &(*head)->next, f = *head) { - if (mg_strcmp(mg_str(f->name), name) == 0) { - MG_INFO(("Deleting %s", f->name)); - *head = f->next; - mg_free(f->name); - mg_free(f); - return; +static bool mg_dash_set_file_name(struct mg_field_set *set, + struct mg_str name) { + size_t i; + for (i = 0; set->fields[i].name != NULL; i++) { + struct mg_field *f = &set->fields[i]; + if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) { + mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len, + name.buf); + return true; } } + return false; } -// Files array: [{"name": "foo.txt", "size": 1234}] -struct mg_dash_file *mg_dash_files; -static char s_files[1024]; - -static void read_files(void) { - size_t len = 0; - struct mg_dash_file *f; - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "["); - for (f = mg_dash_files; f; f = f->next) { - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}", - len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name), - MG_ESC("size"), f->size); - } - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]"); -} - -static struct mg_field fields_files[] = { - {"data", MG_VAL_RAW, s_files, sizeof(s_files)}, - {NULL, MG_VAL_INT, NULL, 0}, -}; - -static struct mg_field_set set_files = { - "files", fields_files, read_files, NULL, 0, 0, NULL, -}; - static inline void mg_log_http_req(struct mg_connection *c, struct mg_http_message *hm) { int len = 0; - size_t n, spaces = 0; + size_t n, spaces = 0, body_n = hm->body.len; struct mg_http_message tmp; memset(&tmp, 0, sizeof(tmp)); len = mg_http_parse((char *) c->send.buf, c->send.len, &tmp); @@ -945,18 +993,24 @@ static inline void mg_log_http_req(struct mg_connection *c, (c->send.buf[c->send.len - spaces - 1] == '\r' || c->send.buf[c->send.len - spaces - 1] == '\n')) spaces++; - MG_DEBUG(("%lu %.*s %.*s %.*s: %lu %.*s -> %lu %.*s", c->id, hm->method.len, - hm->method.buf, hm->uri.len, hm->uri.buf, c->send.len > 15 ? 3 : 0, - &c->send.buf[9], hm->body.len, hm->body.len, hm->body.buf, - c->send.len - n, c->send.len - n - spaces, c->send.buf + n)); -} - -static struct mg_str mg_dash_file_name(struct mg_http_message *hm) { - struct mg_str name = mg_str_n(hm->uri.buf + 4, hm->uri.len - 4); // - /fs/ - // Decode file name in-place - directly into the request buffer - int len = mg_url_decode(name.buf, name.len, name.buf, name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - return name; + // hm->body.len comes from Content-Length and can be larger than the bytes + // actually buffered so far (e.g. mid-stream uploads); cap the preview to + // what's actually present in c->recv, or we'd read past its end + { + char *recv_end = (char *) c->recv.buf + c->recv.len; + if (hm->body.buf >= (char *) c->recv.buf && hm->body.buf <= recv_end) { + size_t avail = (size_t) (recv_end - hm->body.buf); + if (body_n > avail) body_n = avail; + } else { + body_n = 0; + } + } + MG_DEBUG(("%lu %.*s %.*s%s%.*s %.*s: %lu %.*s -> %lu %.*s", c->id, + hm->method.len, hm->method.buf, hm->uri.len, hm->uri.buf, + hm->query.len > 0 ? "?" : "", hm->query.len, hm->query.buf, + c->send.len > 15 ? 3 : 0, &c->send.buf[9], hm->body.len, body_n, + hm->body.buf, c->send.len - n, c->send.len - n - spaces, + c->send.buf + n)); } static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) { @@ -968,13 +1022,20 @@ static void mg_dash_upload_cb(struct mg_connection *c, const char *errmsg) { if (errmsg) { mg_http_reply(c, 500, NULL, "%s\n", errmsg); } else { - char path[128]; - size_t size = 0; - mg_snprintf(path, sizeof(path), "/tmp/%s", c->data); - mg_fs_posix.st(path, &size, NULL); - mg_dash_file_add(mg_str(c->data), size); + // mg_http_start_upload() repurposes c->data for its own bookkeeping, + // so the field set can't be cached there. Re-derive the dashboard from + // c->fn_data instead, and notify every file-backed array: the upload + // could belong to any of them, and re-querying get_dir() per recipient + // is what mg_dash_send_change() does anyway (directories can be + // user-specific) + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + struct mg_field_set *fs; mg_http_reply(c, 200, NULL, "ok\n"); - mg_dash_send_change(c->mgr, &set_files); + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (fs->get_dir == NULL) continue; + *fs->index = -1; // Signal mg_dash_send_change() to broadcast new size + mg_dash_send_change(c->mgr, fs); + } } c->is_draining = 1; } @@ -985,16 +1046,69 @@ static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { return mg_millis() + t * 1000; } +static struct mg_dash_user *mg_dash_add_user(struct mg_dash_user **users, + struct mg_dash *dash, + const char *name, + const char *token, int level) { + struct mg_dash_user *u = (struct mg_dash_user *) mg_calloc(1, sizeof(*u)); + if (u != NULL) { + mg_snprintf(u->name, sizeof(u->name), "%s", name); + if (token == NULL) { + mg_random_str(u->token, sizeof(u->token) - 1); + } else { + mg_snprintf(u->token, sizeof(u->token), "%s", token); + } + u->level = level; + u->expire = mg_dash_make_expiration_time(dash); + u->dash = dash; + u->next = *users; + *users = u; + } + return u; +} + +static struct mg_dash_user *mg_dash_find_user(struct mg_dash_user *users, + const char *name) { + struct mg_dash_user *u; + for (u = users; u != NULL; u = u->next) { + if (strcmp(u->name, name) == 0) return u; + } + return NULL; +} + +static struct mg_dash_user *mg_dash_find_token(struct mg_dash_user *users, + const char *token) { + struct mg_dash_user *u; + for (u = users; u != NULL; u = u->next) { + if (strcmp(u->token, token) == 0) return u; + } + return NULL; +} + +static void mg_dash_refresh_user(struct mg_dash *dash, + struct mg_dash_user *user) { + user->expire = mg_dash_make_expiration_time(dash); +} + // Parse HTTP requests, return authenticated user or NULL -static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, +static struct mg_dash_user *mg_dash_authenticate(struct mg_connection *c, + struct mg_http_message *hm, struct mg_dash *dash) { static struct mg_dash_user *s_users; // List of authenticated users char user[100], pass[100]; - static struct mg_dash_user admin = {NULL, "admin", "admin", 9, (uint64_t) -1}; - struct mg_dash_user *u, *tmp, *result = NULL; + struct mg_dash_user *u, *tmp; + struct mg_str *ah; + int level = 0, num_users = 0; - if (dash->authenticate == NULL) return &admin; + if (dash->authenticate == NULL) { + mg_snprintf(s_guest.name, sizeof(s_guest.name), "%s", "guest"); + s_guest.level = 9; + s_guest.dash = dash; + dash->guest = &s_guest; + return dash->guest; + } mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass)); + ah = mg_http_get_header(hm, "Authorization"); // MG_DEBUG(("user [%s], pass: [%s], h: %.*s", user, pass, hm->head.len, // hm->head.buf)); @@ -1002,47 +1116,48 @@ static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, for (u = s_users; u != NULL; u = tmp) { tmp = u->next; if (u->expire < mg_millis()) { + struct mg_connection *conn; MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level, u->expire, mg_millis() - u->expire)); + for (conn = c->mgr->conns; conn != NULL; conn = conn->next) { + struct mg_dash_cdata *d = (struct mg_dash_cdata *) conn->data; + if (conn->is_websocket && d->u == u) { + d->u = NULL; + conn->is_closing = 1; + } + } LIST_DELETE(struct mg_dash_user, &s_users, u); mg_free(u); } } - if (pass[0] != '\0') { - struct mg_str *ah = mg_http_get_header(hm, "Authorization"); - if (ah != NULL) { - // Auth header and password are set, auth by user/password via glue API - int num_users = 0, level = dash->authenticate(user, sizeof(user), pass); - MG_DEBUG(("user %s, level: %d", user, level)); - if (level > 0) { // Proceed only if the firmware authenticated us - for (u = s_users; u != NULL && result == NULL; - u = u->next, num_users++) { - if (strcmp(user, u->name) == 0) { - u->expire = mg_dash_make_expiration_time(dash); - result = u; - } - } - // Not yet authenticated, add to the list - if (result == NULL && num_users < 10) { - result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result)); - mg_snprintf(result->name, sizeof(result->name), "%s", user); - mg_random_str(result->token, sizeof(result->token) - 1); - result->level = level, result->next = s_users, s_users = result; - result->expire = mg_dash_make_expiration_time(dash); - } - } - } else if (ah == NULL) { - for (u = s_users; u != NULL && result == NULL; u = u->next) { - if (strcmp(u->token, pass) == 0) { - u->expire = mg_dash_make_expiration_time(dash); - result = u; - } - } + if (pass[0] == '\0') return NULL; + + for (u = s_users; u != NULL; u = u->next) num_users++; + if (ah == NULL) { + u = mg_dash_find_token(s_users, pass); + if (u != NULL) { + mg_dash_refresh_user(dash, u); + return u; } } + + level = dash->authenticate(user, sizeof(user), pass); + MG_DEBUG(("user %s, level: %d", user, level)); + if (level <= 0) return NULL; + + u = mg_dash_find_user(s_users, user); + if (u != NULL) { + if (ah == NULL) mg_snprintf(u->token, sizeof(u->token), "%s", pass); + mg_dash_refresh_user(dash, u); + return u; + } + + if (num_users < 10) + return mg_dash_add_user(&s_users, dash, user, ah == NULL ? pass : NULL, + level); // MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL")); - return result; + return NULL; } static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) { @@ -1067,48 +1182,157 @@ static void mg_handle_logout(struct mg_connection *c) { mg_http_reply(c, 401, cookie, "Unauthorized\n"); } +static void mg_dash_handle_del(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + if (set == NULL || set->index == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + } else { + int from = 0, to = 0, count = 0; + if (parts[1].len) mg_str_to_num(parts[1], 10, &from, sizeof(from)); + to = parts[2].len ? 0 : from; + if (parts[2].len) mg_str_to_num(parts[2], 10, &to, sizeof(to)); + for (*set->index = from; *set->index <= to; (*set->index)++) { + if (set->fn && set->fn(MG_DASH_DELETE, u)) + count++; + else + break; + } + if (count) { + *set->index = -1; + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } + } +} + +// Handle "POST /api/get//": modify one array element. Loads the +// element at first - that doubles as a read-access check and as a +// pre-fill, so that JSON keys absent from the body keep their old values - +// then overlays the values from the body and asks fn to persist them +static void mg_dash_handle_mod(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts, + struct mg_str body) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + int index = 0, count = 0; + size_t i; + if (set == NULL || set->index == NULL || set->fn == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + mg_str_to_num(parts[1], 10, &index, sizeof(index)); + *set->index = index; + if (!set->fn(MG_DASH_READ, u) || *set->index != index) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(body, &set->fields[i])) count++; + } + if (count && set->fn(MG_DASH_WRITE, u)) { + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } +} + +// Handle "POST /api/add/": append a new array element. Parses the body +// straight into the bound fields, then asks fn to accept and persist them as +// a new element - fn returns false to reject, e.g. when a cap is reached +static void mg_dash_handle_add(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts, + struct mg_str body) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + int count = 0; + size_t i; + if (set == NULL || set->index == NULL || set->fn == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(body, &set->fields[i])) count++; + } + if (count && set->fn(MG_DASH_ADD, u)) { + *set->index = -1; // Signal mg_dash_send_change() to send new size + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "true\n"); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; + struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; if (ev == MG_EV_OPEN) { - if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) { - MG_DASH_ADD_FIELD_SET(dash, &set_files); - } + d->dash = dash; // c->is_hexdumping = 1; - } else if (ev == MG_EV_HTTP_HDRS && c->data[0] == 0) { + } else if (ev == MG_EV_HTTP_HDRS && d->marker == 0) { // Received headers - check authentication and possibly start uploads/ota struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_dash_user *u = mg_dash_authenticate(hm, dash); + struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); + struct mg_str parts[3]; + memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/hi"), NULL) || mg_match(hm->uri, mg_str("/api/logout"), NULL)) { // Do nothing, handle them MG_EV_HTTP_MSG. We bypass auth for those } else if (u == NULL && mg_match(hm->uri, mg_str("/api/#"), NULL)) { mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - c->data[0] = CONN_HANDLED; + d->marker = CONN_HANDLED; } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { mg_handle_login(c, u); - c->data[0] = CONN_HANDLED; + d->marker = CONN_HANDLED; } else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) { mg_http_start_ota(c, hm, mg_dash_ota_cb); - } else if (mg_match(hm->uri, mg_str("/fs/#"), NULL) && + } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) && (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { - struct mg_str name = mg_dash_file_name(hm); - mg_snprintf(c->data, sizeof(c->data), "%.*s", (int) name.len, name.buf); - mg_http_start_upload(c, hm, name, mg_str("/tmp"), &mg_fs_posix, - mg_dash_upload_cb); + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + struct mg_str name = parts[1]; + int len = + mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0); + if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; + if (set == NULL || set->get_dir == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n"); + d->marker = CONN_HANDLED; + } else if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + d->marker = CONN_HANDLED; + } else if (!mg_path_is_sane(name)) { + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); + d->marker = CONN_HANDLED; + } else { + mg_dash_set_file_name(set, name); + if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + d->marker = CONN_HANDLED; + } else { + char dir[256]; + struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + if (!set->get_dir(u, dir, sizeof(dir))) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + d->marker = CONN_HANDLED; + } else { + mg_http_start_upload(c, hm, name, mg_str(dir), fs, + mg_dash_upload_cb); + } + } + } } - if (c->data[0] != '\0') mg_log_http_req(c, hm); - } else if (ev == MG_EV_HTTP_MSG && c->data[0] != '\0') { + if (d->marker != '\0') mg_log_http_req(c, hm); + } else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') { // The response has been send in EV_HDRS path, so we're not reponding - // anything but clearing the c->data[0] flag for the next request. - c->data[0] = 0; + // anything but clearing the marker for the next request. + d->marker = 0; c->is_resp = 0; - } else if (ev == MG_EV_HTTP_MSG && c->data[0] == '\0') { + } else if (ev == MG_EV_HTTP_MSG && d->marker == '\0') { struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_dash_user *u = mg_dash_authenticate(hm, dash); - int level = u == NULL ? 0 : u->level; + struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); struct mg_str parts[5]; memset(parts, 0, sizeof(parts)); @@ -1119,27 +1343,60 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"), MG_ESC("logout")); } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { - *(int *) c->data = level; + d->u = u; mg_ws_upgrade(c, hm, NULL); - } else if (mg_match(hm->uri, mg_str("/fs/#"), NULL)) { - struct mg_str name = mg_dash_file_name(hm); - char path[128]; - mg_snprintf(path, sizeof(path), "/tmp/%.*s", name.len, name.buf); - if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { - // Delete file - mg_dash_file_del(name); - mg_dash_send_change(c->mgr, &set_files); - mg_http_reply(c, 200, NULL, "true"); + } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + if (set == NULL || set->get_dir == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); + } else if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); } else { - // Serve file - mg_http_serve_file(c, hm, path, NULL); + char dir[256], path[512]; + struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + struct mg_str name = parts[1]; + int len = mg_url_decode(name.buf, name.len, (char *) name.buf, + name.len + 1, 0); + if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; + if (!mg_path_is_sane(name)) { + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); + return; + } + if (!set->get_dir(u, dir, sizeof(dir))) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + return; + } + mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf); + if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { + mg_dash_set_file_name(set, name); + if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else { + fs->rm(path); + *set->index = -1; // Signal mg_dash_send_change() to send new size + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, NULL, "true"); + } + } else { + mg_http_serve_file(c, hm, path, NULL); + } } - } else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) || + } else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) { + mg_dash_handle_del(c, dash, u, parts); + } else if (mg_match(hm->uri, mg_str("/api/add/*"), parts)) { + mg_dash_handle_add(c, dash, u, parts, hm->body); + } else if (mg_match(hm->uri, mg_str("/api/get/*/*"), parts) && + mg_strcasecmp(hm->method, mg_str("POST")) == 0) { + mg_dash_handle_mod(c, dash, u, parts, hm->body); + } else if (mg_match(hm->uri, mg_str("/api/get/*/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/get/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/get/*"), parts) || mg_match(hm->uri, mg_str("/api/get"), NULL)) { - mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash, - &parts[0], level); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_endpoint, + dash, u, &parts[0], &parts[1], &parts[2]); } else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) { - int count = mg_dash_apply(c, dash, hm->body, level); + int count = mg_dash_apply(c, dash, hm->body, u); mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); } else if (mg_match(hm->uri, mg_str("/"), NULL)) { struct mg_http_serve_opts opts; @@ -1157,21 +1414,6 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); mg_log_http_req(c, hm); } - } else if (ev == MG_EV_WS_OPEN) { - // WS connection established, send change notifications for all data - struct mg_field_set *fs; - for (fs = dash->sets; fs != NULL; fs = fs->next) { - mg_dash_send_change(c->mgr, fs); - } - mg_dash_broadcast(c->mgr, 0, "{%m:%m}", MG_ESC("method"), MG_ESC("ready")); - } else if (ev == MG_EV_WS_MSG) { - // Add this to automatically handle "get" and "set" JSON-RPC calls - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - if (dash == NULL) { - mg_dash_error(c, wm->data, "%s", "no dash defined"); - } else { - mg_dash_process_msg(c, wm, dash); - } } } @@ -2005,15 +2247,22 @@ bool mg_ota_flash_end(struct mg_flash *flash) { MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size, size, ok ? "ok" : "fail")); #ifdef MG_OTA_PUBLIC_KEY - if (ok && s_size > 64) { - static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY; - uint8_t hash[32]; - size_t fw_size = s_size - 64; - mg_sha256(hash, (uint8_t *) base, fw_size); - ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash), - (uint8_t *) base + fw_size, - mg_uecc_secp256r1()) == 1; - MG_INFO(("Signature: %s", ok ? "ok" : "fail")); + if (ok) { + bool signed_fw = s_size > 68 && + memcmp((uint8_t *) base + s_size - 4, "MGSG", 4) == 0; + if (signed_fw) { + static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY; + uint8_t hash[32]; + size_t fw_size = s_size - 68; // strip 64-byte sig + 4-byte magic + mg_sha256(hash, (uint8_t *) base, fw_size); + ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash), + (uint8_t *) base + fw_size, + mg_uecc_secp256r1()) == 1; + MG_INFO(("Signature: %s", ok ? "ok" : "fail")); + } else { + ok = false; + MG_ERROR(("Unsigned firmware rejected")); + } } #endif s_size = 0; @@ -3923,19 +4172,19 @@ static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) { if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else mg_ota_end(); p->fn(c, "write error"); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } else if (p->received >= p->expected) { const char *errmsg = NULL; if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else if (!mg_ota_end()) errmsg = "OTA finalize failed"; p->fn(c, errmsg); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } } else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) { if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else mg_ota_end(); p->fn(c, ev == MG_EV_ERROR ? (const char *) ev_data : "connection closed"); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } (void) ev_data; } @@ -3945,6 +4194,10 @@ static void mg_upload_default_cb(struct mg_connection *c, const char *status) { mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok"); } +const char *mg_upload_path(struct mg_connection *c) { + return (const char *) c->pfn_data; +} + void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, struct mg_str name, struct mg_str dir, struct mg_fs *fs, @@ -3964,6 +4217,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, p->fd = fd; p->fn = fn; c->fn = mg_upload_handler; + c->pfn_data = strdup(path); c->pfn = NULL; mg_iobuf_del(&c->recv, 0, hm->head.len); mg_call(c, MG_EV_READ, &c->recv.len); @@ -6990,6 +7244,7 @@ struct mg_connection *mg_mqtt_listen(struct mg_mgr *mgr, const char *url, + size_t mg_vprintf(struct mg_connection *c, const char *fmt, va_list *ap) { size_t old = c->send.len; size_t expected = mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, ap); @@ -7261,6 +7516,11 @@ void mg_mgr_free(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *mgr) { memset(mgr, 0, sizeof(*mgr)); + // Anchor mg_fw_version: a real store into a struct field the optimiser + // can't elide forces the linker to keep the section alive even when no + // OTA polling code runs it. Callers that want mgr->userdata for their own + // purposes simply overwrite it after this call returns + mgr->userdata = (void *) mg_fw_version; #if MG_ENABLE_EPOLL if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) MG_ERROR(("epoll_create1 errno %d", errno)); @@ -9489,6 +9749,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { struct mg_connection *c, *tmp; uint64_t now = mg_millis(); mg_timer_poll(&mgr->timers, now); + mg_ota_poll(mgr); if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return; mg_tcpip_poll(mgr->ifp, now); for (c = mgr->conns; c != NULL; c = tmp) { @@ -9583,144 +9844,170 @@ void mg_tcpip_mapip(struct mg_connection *c, struct mg_addr *ip) { -enum { MG_OTA_STATUS_WAITING, MG_OTA_STATUS_SUCCESS, MG_OTA_STATUS_FAIL }; -static int s_version_status; -static int s_ota_status; -static uint64_t s_start_time; +#ifndef MG_OTA_MAX_URL_LEN +#define MG_OTA_MAX_URL_LEN 256 +#endif -static struct mg_ota_metadata { - char *version; - char *url; +// Scannable version tag embedded in every firmware binary, for server-side +// version extraction. Non-static so mg_mgr_init() can stash its address in +// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from +// stripping it on builds that never poll for OTAs (an attribute alone does +// not - the linker drops unreferenced sections regardless of "used"/"retain") +const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION; + +static struct mg_ota_state { + char my_version[MG_OTA_MAX_VERSION_LEN]; + char json_url[MG_OTA_MAX_URL_LEN]; + char version[MG_OTA_MAX_VERSION_LEN]; + char url[MG_OTA_MAX_URL_LEN]; size_t size; uint8_t sha256[32]; -} s_ota_metadata; + void (*fn)(const char *status); +} *s_ota; -static void free_ota_metadata(void) { - if (s_ota_metadata.version) mg_free(s_ota_metadata.version); - if (s_ota_metadata.url) mg_free(s_ota_metadata.url); - memset(&s_ota_metadata, 0, sizeof(s_ota_metadata)); -} +static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); -static int fetch_ota_metadata(struct mg_http_message *response) { - double result; - if (mg_http_status(response) != 200) goto fetch_failed; - if (mg_json_get(response->body, "$", NULL) != 0) goto fetch_failed; - free_ota_metadata(); - s_ota_metadata.version = mg_json_get_str(response->body, "$.version"); - if (s_ota_metadata.version == NULL) goto fetch_failed; - s_ota_metadata.url = mg_json_get_str(response->body, "$.url"); - if (s_ota_metadata.url == NULL) goto fetch_failed; - if (!mg_json_get_num(response->body, "$.size", &result) || result <= 0) - goto fetch_failed; - s_ota_metadata.size = (size_t) result; - // TODO (robertc2000): parse and validate sha256 - MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota_metadata.version, - s_ota_metadata.url, s_ota_metadata.size)); - return MG_OTA_STATUS_SUCCESS; -fetch_failed: - free_ota_metadata(); - return MG_OTA_STATUS_FAIL; +#if MG_ENABLE_CUSTOM_DEVICE_ID +#else +void mg_ota_device_id(char *buf, size_t len) { +#if MG_ARCH == MG_ARCH_CUBE && defined(UID_BASE) + mg_snprintf(buf, len, "%M", mg_print_hex, 12, (uint8_t *) UID_BASE); +#else + mg_snprintf(buf, len, "%d", 0); +#endif } +#endif static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) { - struct mg_str host = mg_url_host((char *) c->fn_data); - struct mg_http_message *hm; - static int fetch_status; - + uint64_t expiration = *(uint64_t *) c->data; if (ev == MG_EV_POLL) { - if (s_start_time + 5 * 1000 < mg_millis()) { - mg_error(c, "Connection timeout"); - } + if (mg_millis() > expiration) mg_error(c, "Metadata timeout"); } else if (ev == MG_EV_CONNECT) { + char id[33]; + struct mg_str host = mg_url_host(s_ota->json_url); + const char *uri = mg_url_uri(s_ota->json_url); + const char *sep = strchr(uri, '?') == NULL ? "?" : "&"; + mg_ota_device_id(id, sizeof(id)); + id[sizeof(id) - 1] = '\0'; mg_printf(c, - "GET %s HTTP/1.1\r\n" + "GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n" "Host: %.*s\r\n" "Connection: close\r\n\r\n", - mg_url_uri((char *) c->fn_data), (int) host.len, host.buf); - fetch_status = MG_OTA_STATUS_WAITING; + uri, sep, MG_ARCH, s_ota->my_version, id, + MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); } else if (ev == MG_EV_HTTP_MSG) { - hm = (struct mg_http_message *) ev_data; - fetch_status = fetch_ota_metadata(hm); + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + double result; + MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf)); + if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 || + !mg_json_unescape(hm->body, "$.version", s_ota->version, + sizeof(s_ota->version)) || + !mg_json_unescape(hm->body, "$.url", s_ota->url, sizeof(s_ota->url)) || + !mg_json_get_num(hm->body, "$.size", &result) || result <= 0) { + char buf[100]; + mg_snprintf(buf, sizeof(buf), "Bad metadata: %.*s", hm->body.len, + hm->body.buf); + s_ota->fn(buf); + mg_free(s_ota); + s_ota = NULL; + } else if (strcmp(s_ota->version, s_ota->my_version) == 0) { + s_ota->fn("Same version"); + mg_free(s_ota); + s_ota = NULL; + } else { + struct mg_connection *fc; + s_ota->size = (size_t) result; + // TODO (robertc2000): parse and validate sha256 + MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota->version, + s_ota->url, s_ota->size)); + fc = mg_http_connect(c->mgr, s_ota->url, s_firmware_fn, NULL); + if (fc == NULL) { + s_ota->fn("Failed to connect"); + mg_free(s_ota); + s_ota = NULL; + } else { + *(uint64_t *) fc->data = mg_millis() + 300 * 1000; // Set expiration + } + } + c->is_closing = 1; } else if (ev == MG_EV_ERROR) { - MG_ERROR(("%lu Connection error", c->id)); - s_version_status = MG_OTA_STATUS_FAIL; - } else if (ev == MG_EV_CLOSE) { - MG_DEBUG(("%lu Connection closed %lu", c->id, mg_millis() - s_start_time)); - s_version_status = fetch_status; + s_ota->fn((char *) ev_data); + mg_free(s_ota); + s_ota = NULL; } } -static void s_ota_done(struct mg_connection *c, const char *errmsg) { - (void) c; - s_ota_status = errmsg ? MG_OTA_STATUS_FAIL : MG_OTA_STATUS_SUCCESS; +static void status_fn(const char *errmsg) { if (errmsg) MG_ERROR(("OTA failed: %s", errmsg)); } +static void status_fn_2(struct mg_connection *c, const char *errmsg) { + if (s_ota) s_ota->fn(errmsg); + mg_free(s_ota); + s_ota = NULL; + mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok"); +} + static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { - struct mg_str host = mg_url_host(s_ota_metadata.url); + uint64_t expiration = *(uint64_t *) c->data; if (ev == MG_EV_POLL) { - if (s_start_time + 120 * 1000 < mg_millis()) mg_error(c, "Timeout"); + if (mg_millis() > expiration) mg_error(c, "OTA timeout"); } else if (ev == MG_EV_CONNECT) { + struct mg_str host = mg_url_host(s_ota->url); mg_printf(c, "GET %s HTTP/1.1\r\n" "Host: %.*s\r\n" "Connection: close\r\n\r\n", - mg_url_uri(s_ota_metadata.url), (int) host.len, host.buf); + mg_url_uri(s_ota->url), (int) host.len, host.buf); } else if (ev == MG_EV_HTTP_HDRS) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; int status = mg_http_status(hm); - if (status != 200 || hm->body.len != s_ota_metadata.size) { + if (status != 200 || hm->body.len != s_ota->size) { mg_error(c, "Bad HTTP response: status %d, size %lu vs %lu", status, - (unsigned long) hm->body.len, - (unsigned long) s_ota_metadata.size); - s_ota_status = MG_OTA_STATUS_FAIL; - return; + (unsigned long) hm->body.len, (unsigned long) s_ota->size); + } else { + MG_DEBUG(("Beginning OTA (%lu bytes)", (unsigned long) s_ota->size)); + mg_http_start_ota(c, hm, status_fn_2); } - MG_DEBUG( - ("Beginning OTA (%lu bytes)", (unsigned long) s_ota_metadata.size)); - mg_http_start_ota(c, hm, s_ota_done); } else if (ev == MG_EV_ERROR) { - MG_ERROR(("%lu Connection error", c->id)); - s_ota_status = MG_OTA_STATUS_FAIL; + s_ota->fn((char *) ev_data); + mg_free(s_ota); + s_ota = NULL; } - (void) ev_data; } -void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, - const char *metadata_url, - void (*fn)(const char *status)) { - s_version_status = MG_OTA_STATUS_WAITING; - s_ota_status = MG_OTA_STATUS_WAITING; - s_start_time = mg_millis(); - MG_DEBUG(("Connecting to %s to retrieve metadata", metadata_url)); - if (!mg_http_connect(mgr, metadata_url, s_version_fn, - (void *) metadata_url)) { - if (fn) fn("Failed to connect"); - return; - } - while (s_version_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10); - if (s_version_status == MG_OTA_STATUS_SUCCESS) { - if (strcmp(s_ota_metadata.version, current_version) == 0) { - if (fn) fn("Same version"); - free_ota_metadata(); - return; - } +void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version, + const char *json_url, void (*fn)(const char *status)) { + if (fn == NULL) fn = status_fn; + if (s_ota != NULL) { + fn("OTA already in progress"); + } else if ((s_ota = (struct mg_ota_state *) mg_calloc(1, sizeof(*s_ota))) == + NULL) { + fn("Out of memory"); } else { - if (fn) fn("Version retrieving error"); - free_ota_metadata(); - return; + struct mg_connection *c; + mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version); + mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url); + MG_DEBUG(("Connecting to %s", json_url)); + c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL); + if (c == NULL) { + mg_free(s_ota); + s_ota = NULL; + fn("Failed to connect"); + } else { + s_ota->fn = fn; + *(uint64_t *) c->data = mg_millis() + 5 * 1000; // Set expiration + } } - if (fn) fn("Pulling firmware"); - s_start_time = mg_millis(); - MG_DEBUG(("Connecting to %s to download firmware", s_ota_metadata.url)); - if (!mg_http_connect(mgr, s_ota_metadata.url, s_firmware_fn, NULL)) { - if (fn) fn("Failed to connect"); - free_ota_metadata(); - return; +} + +void mg_ota_poll(struct mg_mgr *mgr) { + static uint64_t t = 1; + if (MG_OTA_URL != NULL && + mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) { + mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL, + MG_OTA_STATUS_FN); } - while (s_ota_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10); - if (s_ota_status == MG_OTA_STATUS_FAIL && fn) fn("OTA fail"); - free_ota_metadata(); } #ifdef MG_ENABLE_LINES @@ -12995,6 +13282,7 @@ struct mg_connection *mg_sntp_connect(struct mg_mgr *mgr, const char *url, + #if MG_ENABLE_SOCKET #ifndef closesocket @@ -13743,6 +14031,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { mg_iotest(mgr, ms); now = mg_millis(); mg_timer_poll(&mgr->timers, now); + mg_ota_poll(mgr); for (c = mgr->conns; c != NULL; c = tmp) { bool is_resp = c->is_resp; diff --git a/mongoose.h b/mongoose.h index e72f68a0..cb421b90 100644 --- a/mongoose.h +++ b/mongoose.h @@ -1897,6 +1897,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, void (*fn)(struct mg_connection *, const char *)); void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, void (*fn)(struct mg_connection *, const char *)); +const char *mg_upload_path(struct mg_connection *c); void mg_http_bauth(struct mg_connection *, const char *user, const char *pass); struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v); size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *); @@ -3277,50 +3278,95 @@ void mg_rpc_list(struct mg_rpc_req *r); +#ifndef MG_DASH_MAX_USER_NAME +#define MG_DASH_MAX_USER_NAME 32 +#endif + +#ifndef MG_HTTP_ADDR +#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 +#define MG_HTTP_ADDR "http://0.0.0.0:8000" +#define MG_HTTPS_ADDR "https://0.0.0.0:8443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" +#else +#define MG_HTTP_ADDR "http://0.0.0.0:80" +#define MG_HTTPS_ADDR "http://0.0.0.0:443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" +#endif +#endif + +#ifndef MG_DASH_MAX_SEND_BUF_SIZE +#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) +#endif + +// C type of a value bound to a dashboard field, see struct mg_field enum mg_val_type { - MG_VAL_INT, - MG_VAL_BOOL, - MG_VAL_DBL, - MG_VAL_STR, - MG_VAL_RAW, + MG_VAL_INT, // int + MG_VAL_UINT64, // uint64_t + MG_VAL_BOOL, // bool + MG_VAL_DBL, // double + MG_VAL_STR, // Zero-terminated string + MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim }; +// Dashboard field: binds a JSON key to a C variable for read and/or write struct mg_field { - const char *name; - enum mg_val_type type; - void *value; - size_t value_size; + const char *name; // JSON key + enum mg_val_type type; // C type of the value, see enum mg_val_type + void *value; // Pointer to the bound variable + size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only }; +// Operation passed to a struct mg_field_set read/write/delete/add callback. +// MG_DASH_ADD: fields hold the values of a new element to append; *index is +// not set. Return true to accept and persist it, false to reject (e.g. a cap +// was reached) +enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD }; + +struct mg_dash; + +// Authenticated (or guest) dashboard user +struct mg_dash_user { + struct mg_dash_user *next; // Next user in the list + char name[MG_DASH_MAX_USER_NAME]; // Username + char token[21]; // Session token, sent to the client as a cookie + int level; // Access level, see struct mg_dash::authenticate + uint64_t expire; // Session expiration time, see mg_millis() + struct mg_dash *dash; // Dashboard this user belongs to +}; + +// Named group of related dashboard fields, e.g. "settings" or "metrics" struct mg_field_set { - const char *name; - struct mg_field *fields; - void (*reader)(void); - void (*writer)(void); - int read_level; - int write_level; - struct mg_field_set *next; + const char *name; // Set name, used as a JSON key + struct mg_field *fields; // Zero-terminated array of fields + bool (*fn)(enum mg_dash_op op, + struct mg_dash_user *u); // Read/write/delete/add callback + int *index; // Non-NULL: array set. *index is set before fn(READ) call; + // fn sets *index = -1 to signal end of iteration. + // Size query: framework sets *index = -1 before fn(READ); fn + // sets *index = total size + bool (*get_dir)(const struct mg_dash_user *u, char *buf, + size_t len); // Non-NULL: file array + struct mg_field_set *next; // Next set in the list }; +// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER() struct mg_dash_custom_handler { - struct mg_dash_custom_handler *next; - struct mg_str uri_pattern; - mg_event_handler_t handler; - void *handler_data; -}; - -struct mg_dash_file { - struct mg_dash_file *next; - char *name; - size_t size; + struct mg_dash_custom_handler *next; // Next handler in the list + struct mg_str uri_pattern; // URI to match, see mg_match() + mg_event_handler_t handler; // Event handler function + void *handler_data; // Opaque data for the handler }; +// Dashboard instance: fields, users and handlers served over HTTP and WS struct mg_dash { - struct mg_field_set *sets; - struct mg_dash_custom_handler *custom_handlers; + struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET() + struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers + // Validates pass, fills in user, returns access level (<= 0: failure). + // NULL: no login required, all clients get guest access int (*authenticate)(char *user, size_t userlen, const char *pass); - int session_auto_expiration_seconds; - //struct mg_dash_file *files; + int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default + struct mg_dash_user *guest; // Used when authenticate == NULL + struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix }; #define MG_DASH_ADD_FIELD_SET(dash_, set_) \ @@ -3339,28 +3385,50 @@ struct mg_dash { (dash_)->custom_handlers = &ch_; \ } while (0) -#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 -#define MG_HTTP_ADDR "http://0.0.0.0:8000" -#define MG_HTTPS_ADDR "https://0.0.0.0:8443" -#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" -#else -#define MG_HTTP_ADDR "http://0.0.0.0:80" -#define MG_HTTPS_ADDR "http://0.0.0.0:443" -#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" -#endif - -#ifndef MG_DASH_MAX_SEND_BUF_SIZE -#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) -#endif - -// File manager -extern struct mg_dash_file *mg_dash_files; -void mg_dash_file_add(struct mg_str name, size_t size); -void mg_dash_file_del(struct mg_str name); - +// Dashboard event handler. Pass to mg_http_listen() as the fn argument void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); + +// Notify WebSocket clients that a field set changed: broadcasts the new +// array size if *set->index < 0, or a single element update otherwise void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); +// Default reader for file-backed array sets (set->get_dir != NULL) +// Usage example - this is a complete implemenation of the file manager: +// ```c +// static struct file { +// int index; +// char name[64]; +// size_t size; +// uint64_t checksum; +// } s_file; +// static struct mg_field_set set_files; +// +// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { +// (void) u; +// mkdir("/tmp/dashboard", 0755); +// mg_snprintf(buf, len, "%s", "/tmp/dashboard"); +// return true; +// } +// +// static struct mg_field fields_files[] = { +// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, +// {"size", MG_VAL_UINT64, &s_file.size, 0}, +// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, +// {NULL, MG_VAL_INT, NULL, 0}, +// }; +// +// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { +// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7; +// if (op != MG_DASH_READ) return false; +// if (!mg_dash_dir_read(&set_files, u)) return false; +// s_file.checksum = 0; // Mock to show how to set custom fields +// return true; +// } +// +// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL}; +// ``` +bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u); + // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); @@ -3470,6 +3538,40 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, const char *metadata_url, void (*fn)(const char *status)); +#ifndef MG_OTA_URL +#define MG_OTA_URL NULL +#endif + +#ifndef MG_OTA_STATUS_FN +#define MG_OTA_STATUS_FN NULL +#endif + +#ifndef MG_OTA_FIRMWARE_VERSION +#define MG_OTA_FIRMWARE_VERSION "1.0.0" +#endif + +#ifndef MG_OTA_MAX_VERSION_LEN +#define MG_OTA_MAX_VERSION_LEN 64 +#endif + +// Scannable "MG_VERSION:" tag embedded in every +// firmware binary, for server-side extraction. mg_mgr_init() stashes its +// address in mgr->userdata, which is what keeps it from being stripped by +// -Wl,--gc-sections on builds that never poll for OTAs +extern const char mg_fw_version[]; + +#ifndef MG_OTA_PULL_INTERVAL_SECONDS +#define MG_OTA_PULL_INTERVAL_SECONDS 60 +#endif + +#ifndef MG_ENABLE_CUSTOM_DEVICE_ID +#define MG_ENABLE_CUSTOM_DEVICE_ID 0 +#endif + +void mg_ota_device_id(char *buf, size_t len); + +void mg_ota_poll(struct mg_mgr *); + diff --git a/resources/dashboard.js b/resources/dashboard.js index 328ae7c7..664ac438 100644 --- a/resources/dashboard.js +++ b/resources/dashboard.js @@ -3,19 +3,13 @@ (function (global) { "use strict"; - const Rpc = (function() { - function Rpc(url, onev) { - let ws, id = 1, oncall, q = [], reconnect = true; - const pending = new Map(); - const realsend = s => { ws.send(s); onev && onev('send', s); }; - const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); }; - const send = x => ws && ws.readyState === 1 - ? realsend(JSON.stringify(x)) - : q.push(x); + const Watch = (function() { + function Watch(url, onev, onmsg) { + let ws, reconnect = true; const connect = () => { ws = new WebSocket(url); - ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); }; - ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); }; + ws.onopen = () => onev && onev('open'); + ws.onclose = () => { onev && onev('close'); reconnect && setTimeout(connect, 1000); }; ws.onmessage = e => { onev && onev('message', e.data); let m; @@ -25,69 +19,92 @@ console.log('INVALID JSON:', e.data); return; } - - if (m.id && pending.has(m.id)) { - const [ok, bad] = pending.get(m.id); - pending.delete(m.id); - m.error ? bad(m.error) : ok(m.result); - } else if (oncall && m.method) { - Promise.resolve(oncall(m.method, m.params)) - .then(r => m.id && send({ id: m.id, result: r })) - .catch(err => m.id && send({ - id: m.id, - error: { code: -32000, message: String(err?.message || err) } - })); - } + onmsg && onmsg(m); }; }; connect(); return { close: () => { reconnect = false; ws?.close(); }, - call: (method, params) => new Promise((ok, bad) => { - const i = id++; - pending.set(i, [ok, bad]); - send({ id: i, method, params }); - }), - notify: (method, params) => send({ method, params }), - handle: fn => (oncall = fn, this), get ws() { return ws; } }; } - return Rpc; + return Watch; })(); - function rpc_connect() { + function fetch_and_ready() { if (isMock) { - setTimeout(() => userhandlers['ready']?.(), 500); - return; + return delay(500).then(rescan).then(mark_ready); } - if (rpc) return rpc; - //if (settings.auth && !status.authed) return null; - rpc = Rpc('api/websocket', function (evname, args) { - if (evname == 'open') status.online = true, rescan(); - if (evname == 'close') status.online = false, rescan(); - if (settings.debug) console.log('WS', evname, args); - }); - rpc.handle((method, args) => { - if (method == 'change' && args) apply_and_rescan(settings.data, args); - if (method == 'logout') rpc_disconnect(); - if (userhandlers[method]) userhandlers[method](args); - }); - return rpc; + return fetch('api/get') + .then(r => r.ok ? r.json() : Promise.reject(r.status)) + .then(data => { + for (const k in data) { + if (typeof data[k] === 'number') { + const prev = settings.data[k]; + const arr = prev && typeof prev === 'object' ? prev : {}; + data[k] = { + ...arr, + size: data[k], + start: typeof arr.start === 'number' ? arr.start : 0, + data: Array.isArray(arr.data) ? arr.data : [], + }; + } + } + return apply_and_rescan(settings.data, data); + }) + .then(expand_arrays) + .then(() => { + ws_connect(); + return mark_ready(); + }) + .catch(err => console.log('GET failed', err)); } - function rpc_disconnect() { - if (!rpc) return; - rpc.close(); - rpc = null; + function ws_connect() { + if (isMock || ws) return; + ws = Watch('api/websocket', function (evname, args) { + if (evname == 'open') { status.online = true; rescan(); } + if (evname == 'close') { status.online = false; rescan(); } + if (settings.debug) console.log('WS', evname, args); + }, function (m) { + if (m.method == 'change' && m.params) { + const rest = {}; + const arrays = []; + for (const k in m.params) { + const ma = k.match(/^(.+)\/(\d+)$/); + if (ma) { + const arr = settings.data[ma[1]]; + if (arr?.data) { const i = +ma[2] - (arr.start || 0); if (i >= 0 && i < arr.data.length) arr.data[i] = Object.assign(arr.data[i] || {}, m.params[k]); } + } else if (typeof m.params[k] === 'number' && settings.data[k]?.data !== undefined) { + const arr = settings.data[k]; + arr.size = m.params[k]; + if (is_expandable(arr)) arrays.push(expand_array(k, arr.start, arr.end)); + } else { + rest[k] = m.params[k]; + } + } + apply(settings.data, rest); + prune(settings.data, settings.edits); + Promise.all(arrays) + .then(rescan) + .then(() => userhandlers['change']?.(m.params)); + } + if (m.method == 'logout') ws_disconnect(); + }); + } + + function ws_disconnect() { + if (!ws) return; + ws.close(); + ws = null; status.online = false; } function login(params) { - if (isMock) { rpc_connect(); return Promise.resolve({}); } + if (isMock) return fetch_and_ready().then(() => ({})); const opts = {}; if (params) { const username = (params?.username ?? '').trim(); @@ -100,19 +117,18 @@ .then(resp => { status.username = resp?.user || ''; status.userlevel = resp?.level || 0; - rpc_connect(); - return resp; + return fetch_and_ready().then(() => resp); }); }; function logout() { - if (isMock) { rpc_disconnect(); return Promise.resolve({}); } + if (isMock) { ws_disconnect(); return Promise.resolve({}); } return fetch('api/logout') .catch(() => true) .then(() => { status.username = ''; status.userlevel = 0; - rpc_disconnect(); + ws_disconnect(); return true; }); } @@ -122,7 +138,27 @@ const settings = {data: {}, edits: {}, debug: true}; const userhandlers = {}; // User event handlers const status = { online: false, username: '', userlevel: 0, ready: false }; - let rpc = null; + let ws = null; + let ready_started = false; + + function install_ready_gate() { + document.documentElement.classList.remove('dashboard-ready'); + if (document.getElementById('dashboard-ready-gate')) return; + const style = document.createElement('style'); + style.id = 'dashboard-ready-gate'; + style.textContent = `html:not(.dashboard-ready) *,html:not(.dashboard-ready) *::before,html:not(.dashboard-ready) *::after{transition:none!important;animation:none!important}`; + document.head.appendChild(style); + } + + function mark_ready() { + if (ready_started) return Promise.resolve(); + ready_started = true; + return next_frame().then(() => { + status.ready = true; + document.documentElement.classList.add('dashboard-ready'); + userhandlers['ready']?.(); + }); + } const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value); const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj); @@ -222,7 +258,11 @@ function save(key) { const changes = {}; set(changes, key, get(settings.edits, key)); - rpc.call('set', changes).then(r => apply_and_rescan(settings.data, r)); + fetch('api/set', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(changes), + }).catch(err => console.log('SET failed', key, err)); }; // Cancel the edits for keys that match pattern @@ -231,6 +271,14 @@ rescan(); }; + // Set a checkbox's `checked` *property* from a boolean expression. The + // `checked` content attribute is presence-based - templating its value + // would not work, so this binds straight to the IDL property instead + function handle_checked(el, key, context) { + if (!has(context, key)) { console.error('EVAL', key, context); return; } + el.checked = !!get(context, key); + }; + function handle_save(el, key) { el.disabled = !edited(key); if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true; @@ -244,18 +292,30 @@ function handle_repeat(el, key, context) { if (!has(context, key)) { console.error('EVAL', key, context); return; } if (!el.orig) el.orig = el.children[0].cloneNode(true); - const v = get(context, key), v2 = JSON.stringify(v); - if (el.v2 === v2) return; - const frag = document.createDocumentFragment(); + const v = get(context, key); const it = el.dataset.iterator ?? '__obj'; - v.forEach(function (item, index) { - const child = el.orig.cloneNode(true); - const ctx = { ...context, [it]: item }; - process(child, ctx, true); - frag.appendChild(child); - }); - el.replaceChildren(frag); - el.v2 = v2; + const json = v.map(item => JSON.stringify(item)); + const prev = el.itemsJson || []; + if (json.length === prev.length && el.children.length === json.length) { + // Same length: re-template each changed row's existing DOM node in + // place instead of replacing it. Untouched rows are left completely + // alone, and changed rows keep their live elements too - so bound + // properties (e.g. a checkbox's `checked`) change on an + // already-painted node, which is what lets CSS transitions play, and + // in-progress edits in unrelated fields of that row survive + json.forEach((j, i) => { + if (j !== prev[i]) process(el.children[i], { ...context, [it]: v[i] }, true); + }); + } else { + const frag = document.createDocumentFragment(); + v.forEach(item => { + const node = el.orig.cloneNode(true); + process(node, { ...context, [it]: item }, true); + frag.appendChild(node); + }); + el.replaceChildren(frag); + } + el.itemsJson = json; }; function upload(el, is_ota) { @@ -267,7 +327,7 @@ const reader = new FileReader(); reader.readAsArrayBuffer(f); reader.onload = function () { - const url = is_ota ? 'api/ota' : `fs/${encodeURIComponent(f.name)}`; + const url = is_ota ? 'api/ota' : `fs/${el.dataset.upload}/${encodeURIComponent(f.name)}`; let begin = Date.now(), body = reader.result, ok = false; // console.log(2, url, body); fetch(url, { method: 'POST', body }) @@ -295,7 +355,8 @@ } function process(root, context, x) { - const all = root.querySelectorAll("*:not([data-repeat] *)"); + const selector = x ? "*" : "*:not([data-repeat] *)"; + const all = root.querySelectorAll(selector); const els = root.nodeType === Node.DOCUMENT_NODE ? all : [root, ...all]; els.forEach(function (el) { if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return; @@ -309,14 +370,16 @@ } for (const attr of el.attributes) { const cv = attr.value || ''; - if (attr.originalValue || cv.includes('${')) { - if (!attr.originalValue) attr.originalValue = cv; - const v = substituteExpressions(attr.originalValue, context); + if (!el.originalAttrs) el.originalAttrs = {}; + if (el.originalAttrs[attr.name] || cv.includes('${')) { + if (!el.originalAttrs[attr.name]) el.originalAttrs[attr.name] = cv; + const v = substituteExpressions(el.originalAttrs[attr.name], context); if (v !== attr.value) attr.value = v; } } const handlers = { bind: handle_bind, + checked: handle_checked, save: handle_save, cancel: handle_cancel, repeat: handle_repeat, @@ -341,21 +404,75 @@ function rescan() { if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", realrescan, { once: true }); + return new Promise(resolve => { + document.addEventListener("DOMContentLoaded", () => { + realrescan(); + resolve(); + }, { once: true }); + }); } else { realrescan(); + return Promise.resolve(); } }; const delay = (ms, value) => new Promise(r => setTimeout(() => r(value), ms)); + const next_frame = () => new Promise(r => requestAnimationFrame(() => r())); + const is_expandable = arr => arr && Array.isArray(arr.data) && + typeof arr.start === 'number' && typeof arr.end === 'number'; + + function expand_array(name, start, end) { + const arr = settings.data[name]; + const page = 100, all = []; + const stop = Math.min(end, typeof arr?.size === 'number' ? arr.size - 1 : end); + const done = () => { + set(settings.data, `${name}.data`, all); + set(settings.data, `${name}.start`, start); + return rescan().then(() => all); + }; + const next = from => { + if (from > stop) return Promise.resolve(done()); + const to = Math.min(from + page - 1, stop); + return fetch(`api/get/${name}/${from}/${to}`) + .then(r => r.ok ? r.json() : Promise.reject(r.statusText)) + .then(data => { + if (!data || !data.length) return done(); + all.push(...data); + return next(from + data.length); + }); + }; + return next(start); + } + + function expand_arrays() { + const all = []; + for (const name in settings.data) { + const arr = settings.data[name]; + if (is_expandable(arr)) all.push(expand_array(name, arr.start, arr.end)); + } + return Promise.all(all); + } function call(name, args) { if (isMock) { - return delay(750, true).then(val => console.log('response', name)); - } else { - return rpc.call(name, args) - .catch(err => console.log('CALL FAILED', name, args, err)); + return delay(750, true).then(() => console.log('response', name)); } + if (name === 'set') { + return fetch('api/set', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(args), + }).catch(err => console.log('CALL FAILED', name, args, err)); + } + if (args && typeof args === 'object') { + const {name: aname, start: s = 0, size: total = 100, end} = args; + const stop = typeof end === 'number' ? end : s + total - 1; + return expand_array(aname, s, stop).catch(err => console.log('CALL FAILED', name, args, err)); + } + const path = typeof args === 'string' && args ? '/' + args : ''; + return fetch('api/get' + path) + .then(r => r.ok ? r.json() : Promise.reject(r.statusText)) + .catch(err => console.log('CALL FAILED', name, args, err)); }; function apply(target, patch) { @@ -373,16 +490,18 @@ function apply_and_rescan(target, patch) { apply(target, patch); prune(settings.data, settings.edits); - rescan(); + return rescan(); }; function on(name, fn) { userhandlers[name] = fn; + if (name === 'ready' && status.ready) setTimeout(fn, 0); }; function init(conf) { + install_ready_gate(); apply_and_rescan(settings, conf); - if (!settings.auth) rpc_connect(); + if (!settings.auth) fetch_and_ready(); }; global.Dashboard = { init, call, on, status, login, logout }; diff --git a/resources/downloads/Makefile b/resources/downloads/Makefile index 3e1a803d..f6b6d44b 100644 --- a/resources/downloads/Makefile +++ b/resources/downloads/Makefile @@ -62,7 +62,7 @@ $(foreach T,$(RP_TARGETS), $(foreach P,$(PROJECTS), $(eval $(call RP_DASHBOARD_R %.zip: % zip -qr $@ $< -upload: $(ZIPS) +deploy: $(ZIPS) scp $(ZIPS) h1:/data/downloads/ clean: diff --git a/resources/html2c.js b/resources/html2c.js index b514cc9a..b78b1d77 100644 --- a/resources/html2c.js +++ b/resources/html2c.js @@ -5,7 +5,7 @@ // inlining referenced CSS, JS, images, and icons. // // Usage: -// node html2c.js [-o OUTPUT.c] HTML_FILE +// node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...] const fs = require('fs'); const path = require('path'); @@ -15,24 +15,25 @@ const zlib = require('zlib'); const args = process.argv.slice(2); let outputFile = null; -let inputFile = null; +const inputFiles = []; +const useMap = new Map(); for (let i = 0; i < args.length; i++) { if (args[i] === '-o' && i + 1 < args.length) { outputFile = args[++i]; - } else if (!args[i].startsWith('-') && inputFile === null) { - inputFile = args[i]; + } else if (args[i] === '--use' && i + 1 < args.length) { + const eq = args[++i].indexOf('='); + if (eq > 0) useMap.set(args[i].slice(0, eq), args[i].slice(eq + 1)); + } else if (!args[i].startsWith('-')) { + inputFiles.push(args[i]); } } -if (!inputFile) { - console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE'); +if (inputFiles.length === 0) { + console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]'); process.exit(1); } -const baseDir = path.dirname(path.resolve(inputFile)); -const html = fs.readFileSync(inputFile, 'utf8'); - function fetchUrl(url) { return new Promise((resolve, reject) => { const client = url.startsWith('https') ? https : http; @@ -47,12 +48,15 @@ function fetchUrl(url) { }); } -async function readAsset(name, encoding) { +async function readAsset(name, encoding, baseDir) { + const override = useMap.get(name); + if (override != null) name = override; if (name.startsWith('http://') || name.startsWith('https://')) { const data = await fetchUrl(name); return encoding ? data.toString(encoding) : data; } - return fs.readFileSync(path.join(baseDir, name), encoding || null); + const fullPath = path.isAbsolute(name) ? name : path.join(baseDir, name); + return fs.readFileSync(fullPath, encoding || null); } function attr(match, name) { @@ -66,21 +70,21 @@ function replaceAttr(match, name, value) { return match.replace(re, `${name}="${value}"`); } -async function inlineCss(match) { +async function inlineCss(match, baseDir) { const href = attr(match, 'href'); if (!href) return match; - const css = await readAsset(href, 'utf8'); + const css = await readAsset(href, 'utf8', baseDir); return ``; } -async function inlineJs(match) { +async function inlineJs(match, baseDir) { const src = attr(match, 'src'); if (!src) return match; - const js = await readAsset(src, 'utf8'); + const js = await readAsset(src, 'utf8', baseDir); return ``; } -async function inlineImg(match) { +async function inlineImg(match, baseDir) { const src = attr(match, 'src'); if (!src) return match; const ext = path.extname(src).toLowerCase(); @@ -91,7 +95,7 @@ async function inlineImg(match) { else if (ext === '.webp') mime = 'image/webp'; else if (ext === '.ico') mime = 'image/x-icon'; - const data = await readAsset(src); + const data = await readAsset(src, null, baseDir); if (ext === '.svg') { const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data; return replaceAttr( @@ -107,10 +111,10 @@ async function inlineImg(match) { ); } -async function inlineSvg(match) { +async function inlineSvg(match, baseDir) { const href = attr(match, 'href'); if (!href || !href.toLowerCase().endsWith('.svg')) return match; - const data = await readAsset(href); + const data = await readAsset(href, null, baseDir); return replaceAttr( match, 'href', @@ -118,7 +122,7 @@ async function inlineSvg(match) { ); } -async function processHtml(content) { +async function processHtml(content, baseDir) { let result = content; const cssRegex = /]+rel=["']stylesheet["'][^>]*>/g; const jsRegex = /]+src=["'][^"']+["'][^>]*><\/script>/g; @@ -126,16 +130,16 @@ async function processHtml(content) { const svgRegex = /]+rel=["']icon["'][^>]*>/g; for (const match of content.match(cssRegex) || []) { - if (!match.includes('data:')) result = result.replace(match, await inlineCss(match)); + if (!match.includes('data:')) result = result.replace(match, await inlineCss(match, baseDir)); } for (const match of content.match(jsRegex) || []) { - if (!match.includes('data:')) result = result.replace(match, await inlineJs(match)); + if (!match.includes('data:')) result = result.replace(match, await inlineJs(match, baseDir)); } for (const match of content.match(imgRegex) || []) { - if (!match.includes('data:')) result = result.replace(match, await inlineImg(match)); + if (!match.includes('data:')) result = result.replace(match, await inlineImg(match, baseDir)); } for (const match of content.match(svgRegex) || []) { - if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match)); + if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match, baseDir)); } return result; @@ -145,30 +149,43 @@ function cArray(data) { return Array.from(Buffer.from(data)).concat(0).join(','); } -function generateC(data) { - const destination = `${path.basename(inputFile)}.gz`; - const stat = fs.statSync(inputFile); - const mtime = parseInt(stat.mtimeMs / 1000); - const zipped = zlib.gzipSync(data); - const bytes = cArray(zipped); +function generateC(files) { + const vars = files.map(({inputFile, data}, i) => { + const zipped = zlib.gzipSync(data); + return {inputFile, zipped}; + }); + + const decls = vars.map(({zipped}, i) => + `static const unsigned char v${i}[] = {${cArray(zipped)}};` + ).join('\n'); + + const entries = vars.map(({inputFile, zipped}, i) => { + const destination = `${path.basename(inputFile)}.gz`; + const mtime = parseInt(fs.statSync(inputFile).mtimeMs / 1000); + return ` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${mtime}}, // size: ${zipped.length}`; + }).join('\n'); return `// DO NOT EDIT. This file is generated using this command: // ${process.argv.join(' ')} #include "mongoose.h" -static const unsigned char v0[] = {${bytes}}; +${decls} const struct mg_mem_file mg_packed_files[] = { - {"/${destination}", v0, sizeof(v0) - 1, ${mtime}}, // size: ${zipped.length} +${entries} {NULL, NULL, 0, 0} }; `; } -processHtml(html) - .then(result => { - const c = generateC(result); +Promise.all(inputFiles.map(inputFile => { + const baseDir = path.dirname(path.resolve(inputFile)); + const html = fs.readFileSync(inputFile, 'utf8'); + return processHtml(html, baseDir).then(data => ({inputFile, data})); +})) + .then(files => { + const c = generateC(files); if (outputFile) { fs.writeFileSync(outputFile, c); } else { diff --git a/resources/inline.js b/resources/inline.js deleted file mode 100644 index 2bc4521d..00000000 --- a/resources/inline.js +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) 2026 Cesanta Software Limited -// All rights reserved - -const fs = require('fs'); -const path = require('path'); -const https = require('https'); -const http = require('http'); - -const args = process.argv.slice(2); -let outputFile = null; -let inputFile = null; - -for (let i = 0; i < args.length; i++) { - if (args[i] === '-o' && i + 1 < args.length) { - outputFile = args[++i]; - } else if (!args[i].startsWith('-')) { - inputFile = args[i]; - } -} - -if (!inputFile) { - console.error('Usage: node inline.js [-o OUTPUT_FILE] HTML_FILE'); - process.exit(1); -} - -const baseDir = path.dirname(path.resolve(inputFile)); -const html = fs.readFileSync(inputFile, 'utf8'); - -function fetchUrl(url) { - return new Promise((resolve, reject) => { - const client = url.startsWith('https') ? https : http; - client.get(url, res => { - let data = ''; - res.on('data', chunk => data += chunk); - res.on('end', () => resolve(data)); - res.on('error', reject); - }).on('error', reject); - }); -} - -async function inlineCss(match) { - const href = match.match(/href=["']([^"']+)["']/)[1]; - let css = ''; - if (href.startsWith('http')) { - css = await fetchUrl(href); - } else { - const cssPath = path.join(baseDir, href); - css = fs.readFileSync(cssPath, 'utf8'); - } - return ``; -} - -async function inlineJs(match) { - const match2 = match.match(/src=["']([^"']+)["']/); - if (!match2) return match; - const src = match2[1]; - let js = ''; - if (src.startsWith('http')) { - js = await fetchUrl(src); - } else { - const jsPath = path.join(baseDir, src); - js = fs.readFileSync(jsPath, 'utf8'); - } - return ``; -} - -async function inlineImg(match) { - const match2 = match.match(/src=["']([^"']+)["']/); - if (!match2) return match; - const src = match2[1]; - const ext = path.extname(src).toLowerCase(); - let mime = 'image/png'; - if (ext === '.svg') mime = 'image/svg+xml'; - else if (ext === '.jpg' || ext === '.jpeg') mime = 'image/jpeg'; - else if (ext === '.gif') mime = 'image/gif'; - else if (ext === '.webp') mime = 'image/webp'; - else if (ext === '.ico') mime = 'image/x-icon'; - let data; - if (src.startsWith('http')) { - data = await fetchUrl(src); - } else { - const imgPath = path.join(baseDir, src); - data = fs.readFileSync(imgPath); - } - const base64 = Buffer.from(data).toString('base64'); - if (ext === '.svg') { - const svgData = typeof data === 'string' ? data : data.toString('utf8'); - return match.replace(/src="[^"]+"/, `src="data:${mime};utf8,${encodeURIComponent(svgData)}"`); - } - return match.replace(/src="[^"]+"/, `src="data:${mime};base64,${base64}"`); -} - -async function inlineSvg(match) { - const match2 = match.match(/href=["']([^"']+)["']/); - if (!match2) return match; - const href = match2[1]; - if (!href.toLowerCase().endsWith('.svg')) return match; - let data; - if (href.startsWith('http')) { - data = await fetchUrl(href); - } else { - const svgPath = path.join(baseDir, href); - data = fs.readFileSync(svgPath, 'utf8'); - } - const base64 = Buffer.from(data).toString('base64'); - return match.replace(/href="[^"]+"/, `href="data:image/svg+xml;base64,${base64}"`); -} - -async function processHtml(content) { - let result = content; - const cssRegex = /]+rel=["']stylesheet["'][^>]*>/g; - const jsRegex = /]+src=["'][^"']+["'][^>]*><\/script>/g; - const imgRegex = /]+src=["'][^"']+["'][^>]*>/g; - const svgRegex = /]+rel=["']icon["'][^>]*>/g; - - const cssMatches = content.match(cssRegex) || []; - for (const match of cssMatches) { - if (!match.includes('data:')) { - result = result.replace(match, await inlineCss(match)); - } - } - - const jsMatches = content.match(jsRegex) || []; - for (const match of jsMatches) { - if (!match.includes('data:')) { - result = result.replace(match, await inlineJs(match)); - } - } - - const imgMatches = content.match(imgRegex) || []; - for (const match of imgMatches) { - if (!match.includes('data:')) { - result = result.replace(match, await inlineImg(match)); - } - } - - const svgMatches = content.match(svgRegex) || []; - for (const match of svgMatches) { - if (!match.includes('data:')) { - result = result.replace(match, await inlineSvg(match)); - } - } - - return result; -} - -processHtml(html).then(result => { - if (outputFile) { - fs.writeFileSync(outputFile, result); - //console.log(`Written to ${outputFile}`); - } else { - console.log(result); - } -}).catch(err => { - console.error('Error:', err.message); - process.exit(1); -}); diff --git a/resources/pack.js b/resources/pack.js deleted file mode 100644 index 4f705e3c..00000000 --- a/resources/pack.js +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2024 Cesanta Software Limited -// -// Utility that generates packed filesystem C file compatible with the -// Mongoose Network Library, https://github.com/cesanta/mongoose -// -// Usage: -// node pack.js FILE[:DESTINATION[:gzip]] ... - -const fs = require('fs'); -const zlib = require('zlib'); -const argv = process.argv.slice(2); - -// Convert each command-line arguments into [ DATA_ARRAY, STRUCT_INITIALIZER ] -const entries = argv.map(function(filename, i) { - const parts = filename.split(':'); - const stat = fs.statSync(parts[0]); - const data = fs.readFileSync(parts[0], null); - let bytes = Array.from(data); - let destination = (parts[1] || parts[0]).replace(/^\.+[\/\\]*/, ''); - if (parts[2] == 'gzip') { - bytes = Array.from(zlib.gzipSync(data)); - destination += '.gz'; - } - - // concat(0) appends trailing 0, in order to make any file an asciz string - return [ - `static const unsigned char v${i}[] = {${bytes.concat(0).join(',')}};`, - ` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${parseInt(stat.mtimeMs / 1000)}}`, - ]; -}); - -process.stdout.write(`// DO NOT EDIT. This file is generated using this command: -// ${process.argv.join(' ')} - -#include "mongoose.h" - -${entries.map(x => x[0]).join('\n\n')} - -const struct mg_mem_file mg_packed_files[] = { -${entries.map(x => x[1]).join(',\n')}, - {NULL, NULL, 0, 0} -}; -`); diff --git a/resources/sign.js b/resources/sign.js index 8f8771af..9ef6e1bc 100644 --- a/resources/sign.js +++ b/resources/sign.js @@ -70,8 +70,8 @@ function sign(firmware, privateKeyFile = 'private.pem') { ? firmware.slice(0, dot) + '.signed' + firmware.slice(dot) : firmware + '.signed'; - fs.writeFileSync(out, Buffer.concat([fw, raw])); - console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature)`); + fs.writeFileSync(out, Buffer.concat([fw, raw, Buffer.from('MGSG')])); + console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature + 4 bytes magic)`); } const [,, cmd, arg1, arg2] = process.argv; diff --git a/src/dash.c b/src/dash.c index 51f90965..58f21609 100644 --- a/src/dash.c +++ b/src/dash.c @@ -5,67 +5,21 @@ #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS -struct mg_dash_user { - struct mg_dash_user *next; - char name[32]; // User name - char token[21]; // Login token - int level; // Access level - uint64_t expire; // Expiration timestamp +#define CONN_HANDLED 'Z' + +struct mg_dash_cdata { + char marker; + struct mg_dash_user *u; + struct mg_dash *dash; }; -#define CONN_HANDLED 'Z' +static struct mg_dash_user s_guest; static struct mg_str trimq(struct mg_str s) { // Trim double quotes if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++; return s; } -static void mg_dash_broadcast(struct mg_mgr *mgr, int level, const char *fmt, - ...) { - struct mg_connection *c; - va_list ap; - for (c = mgr->conns; c != NULL; c = c->next) { - int user_level = *(int *) c->data; - if (!c->is_websocket) continue; - if (level > 0 && user_level < level) continue; - if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) { - mg_error(c, "%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE", c->id, - c->send.len); - } else { - va_start(ap, fmt); - mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap); - va_end(ap); - } - } -} - -static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) { - const char *fmt = va_arg(*ap, const char *); - ap = va_arg(*ap, va_list *); - return mg_vxprintf(out, param, fmt, ap); -} - -static void mg_dash_success(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len, - id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - -static void mg_dash_error(struct mg_connection *c, struct mg_str req, - const char *fmt, ...) { - struct mg_str id = mg_json_get_tok(req, "$.id"); - va_list ap; - va_start(ap, fmt); - mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"), - id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1, - MG_ESC("message"), mg_print_fmt, fmt, &ap); - va_end(ap); -} - static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash, struct mg_str name) { struct mg_field_set *fs; @@ -92,6 +46,8 @@ static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) { n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false"); } else if (f->type == MG_VAL_INT) { n += mg_xprintf(fn, arg, "%d", *(int *) f->value); + } else if (f->type == MG_VAL_UINT64) { + n += mg_xprintf(fn, arg, "%llu", (uint64_t) *(uint64_t *) f->value); } else if (f->type == MG_VAL_DBL) { n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); } else if (f->type == MG_VAL_STR) { @@ -116,10 +72,56 @@ static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) { return n; } -static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { +static int mg_dash_array_size(struct mg_field_set *set, + struct mg_dash_user *u) { + int saved = *set->index, sz = -1; + *set->index = -1; + if (set->fn) { + if (set->fn(MG_DASH_READ, u)) sz = *set->index; + } else if (set->get_dir) { + mg_dash_dir_read(set, u); + sz = *set->index; + } + *set->index = saved; + return sz; +} + +static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) { + struct mg_field_set *set = va_arg(*ap, struct mg_field_set *); + int from = va_arg(*ap, int); + int to = va_arg(*ap, int); + struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *); + bool started = false; + int saved = *set->index; + size_t n = 0; + *set->index = from; + n += mg_xprintf(fn, arg, "["); + for (;;) { + bool done = to >= 0 && *set->index > to; + if (!done) { + if (set->fn) + set->fn(MG_DASH_READ, u); + else if (set->get_dir) + mg_dash_dir_read(set, u); + done = *set->index < 0; + } + if (done) break; + n += mg_xprintf(fn, arg, "%s%M", started ? "," : "", mg_print_field_set, + set); + started = true; + (*set->index)++; + } + n += mg_xprintf(fn, arg, "]"); + *set->index = saved; + return n; +} + +static size_t mg_dash_print_endpoint(mg_pfn_t fn, void *arg, va_list *ap) { struct mg_dash *dash = va_arg(*ap, struct mg_dash *); + struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *); struct mg_str *name = va_arg(*ap, struct mg_str *); - int level = va_arg(*ap, int); + struct mg_str *from_str = va_arg(*ap, struct mg_str *); + struct mg_str *to_str = va_arg(*ap, struct mg_str *); struct mg_field_set *set = mg_dash_find_field_set(dash, *name); size_t n = 0; if (name->len == 0) { @@ -127,37 +129,74 @@ static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) { const char *comma = ""; n += mg_xprintf(fn, arg, "{"); for (fs = dash->sets; fs != NULL; fs = fs->next) { - if (fs->read_level > 0 && level < fs->read_level) continue; - if (fs->reader) fs->reader(); - n += mg_xprintf(fn, arg, comma); - n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name)); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs); + if (fs->index != NULL) { + int sz = mg_dash_array_size(fs, u); + if (sz < 0) continue; + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:%d", MG_ESC(fs->name), sz); + } else { + if (fs->fn && !fs->fn(MG_DASH_READ, u)) continue; + n += mg_xprintf(fn, arg, comma); + n += mg_xprintf(fn, arg, "%m:%M", MG_ESC(fs->name), mg_print_field_set, + fs); + } comma = ","; } n += mg_xprintf(fn, arg, "}"); - } else if (set != NULL && - (set->read_level <= 0 || level >= set->read_level)) { - if (set->reader) set->reader(); - n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } else if (set != NULL && (set->fn == NULL || set->fn(MG_DASH_READ, u))) { + if (set->index != NULL && from_str != NULL && from_str->len > 0) { + int from = 0, to = 0; + mg_str_to_num(*from_str, 10, &from, sizeof(from)); + to = from; + if (to_str != NULL && to_str->len > 0) { + mg_str_to_num(*to_str, 10, &to, sizeof(to)); + } + n += mg_xprintf(fn, arg, "%M", mg_dash_print_array, set, from, to, u); + } else if (set->index != NULL) { + n += mg_xprintf(fn, arg, "%d", mg_dash_array_size(set, u)); + } else { + n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); + } } else { n += mg_xprintf(fn, arg, "null"); } return n; } -static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) { - struct mg_str *req = va_arg(*ap, struct mg_str *); - struct mg_dash *dash = va_arg(*ap, struct mg_dash *); - int level = va_arg(*ap, int); - struct mg_str name = trimq(mg_json_get_tok(*req, "$.params")); - return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name, level); -} - void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) { - if (set->reader) set->reader(); - mg_dash_broadcast(mgr, set->read_level, "{%m:%m,%m:{%m:%M}}", - MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), - MG_ESC(set->name), mg_print_field_set, set); + struct mg_connection *c; + for (c = mgr->conns; c != NULL; c = c->next) { + struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; + struct mg_dash_user *u = d->u; + if (!c->is_websocket) continue; + if (u == NULL) continue; + if (set->index != NULL && *set->index < 0) { + int sz = mg_dash_array_size(set, u); + if (sz < 0) continue; + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%d}}", MG_ESC("method"), + MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz); + } else { + int saved_idx = set->index != NULL ? *set->index : 0; + bool ok = set->fn ? set->fn(MG_DASH_READ, u) + : (set->get_dir ? mg_dash_dir_read(set, u) : true); + if (!ok) { + if (set->index != NULL) *set->index = saved_idx; + continue; + } + if (set->index != NULL) { + char key[64]; + mg_snprintf(key, sizeof(key), "%s/%d", set->name, *set->index); + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}", + MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), + MG_ESC(key), mg_print_field_set, set); + *set->index = saved_idx; + } else { + mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}", + MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), + MG_ESC(set->name), mg_print_field_set, set); + } + } + } } static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { @@ -174,6 +213,13 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { *(int *) f->value = (int) d; ok = true; } + } else if (f->type == MG_VAL_UINT64) { + double d; + if (f->value_size == sizeof(uint64_t) && + mg_json_get_num(json, json_path, &d) && d == (double) (int64_t) d) { + *(uint64_t *) f->value = (uint64_t) d; + ok = true; + } } else if (f->type == MG_VAL_DBL) { ok = f->value_size == sizeof(double) && mg_json_get_num(json, json_path, (double *) f->value); @@ -187,97 +233,99 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) { } static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash, - struct mg_str json, int level) { + struct mg_str json, struct mg_dash_user *u) { struct mg_str key, val; size_t ofs = 0; int total_count = 0; while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); int count = 0; - if (set != NULL && (set->write_level <= 0 || level >= set->write_level)) { + if (set == NULL) { + MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + continue; + } + if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) continue; // auth check + { size_t i; for (i = 0; set->fields[i].name != NULL; i++) { if (mg_dash_parse_field(val, &set->fields[i])) count++; } - if (count) { - if (set->writer) set->writer(); - mg_dash_send_change(c->mgr, set); - total_count += count; - } - } else if (set == NULL) { - MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf)); + } + if (count) { + if (set->fn) set->fn(MG_DASH_WRITE, u); // apply side effects + mg_dash_send_change(c->mgr, set); + total_count += count; } } return total_count; } -static void mg_dash_process_msg(struct mg_connection *c, - struct mg_ws_message *wm, - struct mg_dash *dash) { - struct mg_str req = wm->data; - struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); - int level = *(int *) c->data; - if (mg_match(method, mg_str("get"), NULL)) { - mg_dash_success(c, req, "%M", mg_dash_print, &req, dash, level); - } else if (mg_match(method, mg_str("set"), NULL)) { - struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); - int count = mg_dash_apply(c, dash, params, level); - mg_dash_success(c, req, "%d", count); - } else { - mg_dash_error(c, req, "%s", "unknown method"); +bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) { + char dir[256], fname[128] = ""; + struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix; + struct mg_field *name_field = NULL, *size_field = NULL; + size_t i; + + if (!set->get_dir(u, dir, sizeof(dir))) return false; + + for (i = 0; set->fields[i].name != NULL; i++) { + if (name_field == NULL && set->fields[i].type == MG_VAL_STR && + strcmp(set->fields[i].name, "name") == 0) + name_field = &set->fields[i]; + if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0) + size_field = &set->fields[i]; + } + if (name_field == NULL) return false; + + if (*set->index == -1) { // Size query: count all files + int count = 0; + while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++; + *set->index = count; + return true; + } + + { // Regular read: scan to *set->index + int target = *set->index, cur = 0; + while (mg_fs_ls(fs, dir, fname, sizeof(fname))) { + if (cur++ == target) { + mg_snprintf((char *) name_field->value, name_field->value_size, "%s", + fname); + if (size_field != NULL) { + char path[512]; + size_t sz = 0; + mg_snprintf(path, sizeof(path), "%s/%s", dir, fname); + fs->st(path, &sz, NULL); + if (size_field->type == MG_VAL_UINT64) + *(uint64_t *) size_field->value = (uint64_t) sz; + else if (size_field->type == MG_VAL_INT) + *(int *) size_field->value = (int) sz; + } + return true; + } + } + *set->index = -1; // No more entries + return true; } } -void mg_dash_file_add(struct mg_str name, size_t size) { - struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f)); - f->name = mg_strdup(name).buf; - f->size = size; - f->next = mg_dash_files; - mg_dash_files = f; -} - -void mg_dash_file_del(struct mg_str name) { - struct mg_dash_file **head, *f; - for (head = &mg_dash_files, f = *head; f; head = &(*head)->next, f = *head) { - if (mg_strcmp(mg_str(f->name), name) == 0) { - MG_INFO(("Deleting %s", f->name)); - *head = f->next; - mg_free(f->name); - mg_free(f); - return; +static bool mg_dash_set_file_name(struct mg_field_set *set, + struct mg_str name) { + size_t i; + for (i = 0; set->fields[i].name != NULL; i++) { + struct mg_field *f = &set->fields[i]; + if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) { + mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len, + name.buf); + return true; } } + return false; } -// Files array: [{"name": "foo.txt", "size": 1234}] -struct mg_dash_file *mg_dash_files; -static char s_files[1024]; - -static void read_files(void) { - size_t len = 0; - struct mg_dash_file *f; - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "["); - for (f = mg_dash_files; f; f = f->next) { - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}", - len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name), - MG_ESC("size"), f->size); - } - len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]"); -} - -static struct mg_field fields_files[] = { - {"data", MG_VAL_RAW, s_files, sizeof(s_files)}, - {NULL, MG_VAL_INT, NULL, 0}, -}; - -static struct mg_field_set set_files = { - "files", fields_files, read_files, NULL, 0, 0, NULL, -}; - static inline void mg_log_http_req(struct mg_connection *c, struct mg_http_message *hm) { int len = 0; - size_t n, spaces = 0; + size_t n, spaces = 0, body_n = hm->body.len; struct mg_http_message tmp; memset(&tmp, 0, sizeof(tmp)); len = mg_http_parse((char *) c->send.buf, c->send.len, &tmp); @@ -286,18 +334,24 @@ static inline void mg_log_http_req(struct mg_connection *c, (c->send.buf[c->send.len - spaces - 1] == '\r' || c->send.buf[c->send.len - spaces - 1] == '\n')) spaces++; - MG_DEBUG(("%lu %.*s %.*s %.*s: %lu %.*s -> %lu %.*s", c->id, hm->method.len, - hm->method.buf, hm->uri.len, hm->uri.buf, c->send.len > 15 ? 3 : 0, - &c->send.buf[9], hm->body.len, hm->body.len, hm->body.buf, - c->send.len - n, c->send.len - n - spaces, c->send.buf + n)); -} - -static struct mg_str mg_dash_file_name(struct mg_http_message *hm) { - struct mg_str name = mg_str_n(hm->uri.buf + 4, hm->uri.len - 4); // - /fs/ - // Decode file name in-place - directly into the request buffer - int len = mg_url_decode(name.buf, name.len, name.buf, name.len + 1, 0); - if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; - return name; + // hm->body.len comes from Content-Length and can be larger than the bytes + // actually buffered so far (e.g. mid-stream uploads); cap the preview to + // what's actually present in c->recv, or we'd read past its end + { + char *recv_end = (char *) c->recv.buf + c->recv.len; + if (hm->body.buf >= (char *) c->recv.buf && hm->body.buf <= recv_end) { + size_t avail = (size_t) (recv_end - hm->body.buf); + if (body_n > avail) body_n = avail; + } else { + body_n = 0; + } + } + MG_DEBUG(("%lu %.*s %.*s%s%.*s %.*s: %lu %.*s -> %lu %.*s", c->id, + hm->method.len, hm->method.buf, hm->uri.len, hm->uri.buf, + hm->query.len > 0 ? "?" : "", hm->query.len, hm->query.buf, + c->send.len > 15 ? 3 : 0, &c->send.buf[9], hm->body.len, body_n, + hm->body.buf, c->send.len - n, c->send.len - n - spaces, + c->send.buf + n)); } static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) { @@ -309,13 +363,20 @@ static void mg_dash_upload_cb(struct mg_connection *c, const char *errmsg) { if (errmsg) { mg_http_reply(c, 500, NULL, "%s\n", errmsg); } else { - char path[128]; - size_t size = 0; - mg_snprintf(path, sizeof(path), "/tmp/%s", c->data); - mg_fs_posix.st(path, &size, NULL); - mg_dash_file_add(mg_str(c->data), size); + // mg_http_start_upload() repurposes c->data for its own bookkeeping, + // so the field set can't be cached there. Re-derive the dashboard from + // c->fn_data instead, and notify every file-backed array: the upload + // could belong to any of them, and re-querying get_dir() per recipient + // is what mg_dash_send_change() does anyway (directories can be + // user-specific) + struct mg_dash *dash = (struct mg_dash *) c->fn_data; + struct mg_field_set *fs; mg_http_reply(c, 200, NULL, "ok\n"); - mg_dash_send_change(c->mgr, &set_files); + for (fs = dash->sets; fs != NULL; fs = fs->next) { + if (fs->get_dir == NULL) continue; + *fs->index = -1; // Signal mg_dash_send_change() to broadcast new size + mg_dash_send_change(c->mgr, fs); + } } c->is_draining = 1; } @@ -326,16 +387,69 @@ static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { return mg_millis() + t * 1000; } +static struct mg_dash_user *mg_dash_add_user(struct mg_dash_user **users, + struct mg_dash *dash, + const char *name, + const char *token, int level) { + struct mg_dash_user *u = (struct mg_dash_user *) mg_calloc(1, sizeof(*u)); + if (u != NULL) { + mg_snprintf(u->name, sizeof(u->name), "%s", name); + if (token == NULL) { + mg_random_str(u->token, sizeof(u->token) - 1); + } else { + mg_snprintf(u->token, sizeof(u->token), "%s", token); + } + u->level = level; + u->expire = mg_dash_make_expiration_time(dash); + u->dash = dash; + u->next = *users; + *users = u; + } + return u; +} + +static struct mg_dash_user *mg_dash_find_user(struct mg_dash_user *users, + const char *name) { + struct mg_dash_user *u; + for (u = users; u != NULL; u = u->next) { + if (strcmp(u->name, name) == 0) return u; + } + return NULL; +} + +static struct mg_dash_user *mg_dash_find_token(struct mg_dash_user *users, + const char *token) { + struct mg_dash_user *u; + for (u = users; u != NULL; u = u->next) { + if (strcmp(u->token, token) == 0) return u; + } + return NULL; +} + +static void mg_dash_refresh_user(struct mg_dash *dash, + struct mg_dash_user *user) { + user->expire = mg_dash_make_expiration_time(dash); +} + // Parse HTTP requests, return authenticated user or NULL -static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, +static struct mg_dash_user *mg_dash_authenticate(struct mg_connection *c, + struct mg_http_message *hm, struct mg_dash *dash) { static struct mg_dash_user *s_users; // List of authenticated users char user[100], pass[100]; - static struct mg_dash_user admin = {NULL, "admin", "admin", 9, (uint64_t) -1}; - struct mg_dash_user *u, *tmp, *result = NULL; + struct mg_dash_user *u, *tmp; + struct mg_str *ah; + int level = 0, num_users = 0; - if (dash->authenticate == NULL) return &admin; + if (dash->authenticate == NULL) { + mg_snprintf(s_guest.name, sizeof(s_guest.name), "%s", "guest"); + s_guest.level = 9; + s_guest.dash = dash; + dash->guest = &s_guest; + return dash->guest; + } mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass)); + ah = mg_http_get_header(hm, "Authorization"); // MG_DEBUG(("user [%s], pass: [%s], h: %.*s", user, pass, hm->head.len, // hm->head.buf)); @@ -343,47 +457,48 @@ static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm, for (u = s_users; u != NULL; u = tmp) { tmp = u->next; if (u->expire < mg_millis()) { + struct mg_connection *conn; MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level, u->expire, mg_millis() - u->expire)); + for (conn = c->mgr->conns; conn != NULL; conn = conn->next) { + struct mg_dash_cdata *d = (struct mg_dash_cdata *) conn->data; + if (conn->is_websocket && d->u == u) { + d->u = NULL; + conn->is_closing = 1; + } + } LIST_DELETE(struct mg_dash_user, &s_users, u); mg_free(u); } } - if (pass[0] != '\0') { - struct mg_str *ah = mg_http_get_header(hm, "Authorization"); - if (ah != NULL) { - // Auth header and password are set, auth by user/password via glue API - int num_users = 0, level = dash->authenticate(user, sizeof(user), pass); - MG_DEBUG(("user %s, level: %d", user, level)); - if (level > 0) { // Proceed only if the firmware authenticated us - for (u = s_users; u != NULL && result == NULL; - u = u->next, num_users++) { - if (strcmp(user, u->name) == 0) { - u->expire = mg_dash_make_expiration_time(dash); - result = u; - } - } - // Not yet authenticated, add to the list - if (result == NULL && num_users < 10) { - result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result)); - mg_snprintf(result->name, sizeof(result->name), "%s", user); - mg_random_str(result->token, sizeof(result->token) - 1); - result->level = level, result->next = s_users, s_users = result; - result->expire = mg_dash_make_expiration_time(dash); - } - } - } else if (ah == NULL) { - for (u = s_users; u != NULL && result == NULL; u = u->next) { - if (strcmp(u->token, pass) == 0) { - u->expire = mg_dash_make_expiration_time(dash); - result = u; - } - } + if (pass[0] == '\0') return NULL; + + for (u = s_users; u != NULL; u = u->next) num_users++; + if (ah == NULL) { + u = mg_dash_find_token(s_users, pass); + if (u != NULL) { + mg_dash_refresh_user(dash, u); + return u; } } + + level = dash->authenticate(user, sizeof(user), pass); + MG_DEBUG(("user %s, level: %d", user, level)); + if (level <= 0) return NULL; + + u = mg_dash_find_user(s_users, user); + if (u != NULL) { + if (ah == NULL) mg_snprintf(u->token, sizeof(u->token), "%s", pass); + mg_dash_refresh_user(dash, u); + return u; + } + + if (num_users < 10) + return mg_dash_add_user(&s_users, dash, user, ah == NULL ? pass : NULL, + level); // MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL")); - return result; + return NULL; } static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) { @@ -408,48 +523,157 @@ static void mg_handle_logout(struct mg_connection *c) { mg_http_reply(c, 401, cookie, "Unauthorized\n"); } +static void mg_dash_handle_del(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + if (set == NULL || set->index == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + } else { + int from = 0, to = 0, count = 0; + if (parts[1].len) mg_str_to_num(parts[1], 10, &from, sizeof(from)); + to = parts[2].len ? 0 : from; + if (parts[2].len) mg_str_to_num(parts[2], 10, &to, sizeof(to)); + for (*set->index = from; *set->index <= to; (*set->index)++) { + if (set->fn && set->fn(MG_DASH_DELETE, u)) + count++; + else + break; + } + if (count) { + *set->index = -1; + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } + } +} + +// Handle "POST /api/get//": modify one array element. Loads the +// element at first - that doubles as a read-access check and as a +// pre-fill, so that JSON keys absent from the body keep their old values - +// then overlays the values from the body and asks fn to persist them +static void mg_dash_handle_mod(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts, + struct mg_str body) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + int index = 0, count = 0; + size_t i; + if (set == NULL || set->index == NULL || set->fn == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + mg_str_to_num(parts[1], 10, &index, sizeof(index)); + *set->index = index; + if (!set->fn(MG_DASH_READ, u) || *set->index != index) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(body, &set->fields[i])) count++; + } + if (count && set->fn(MG_DASH_WRITE, u)) { + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } +} + +// Handle "POST /api/add/": append a new array element. Parses the body +// straight into the bound fields, then asks fn to accept and persist them as +// a new element - fn returns false to reject, e.g. when a cap is reached +static void mg_dash_handle_add(struct mg_connection *c, struct mg_dash *dash, + struct mg_dash_user *u, struct mg_str *parts, + struct mg_str body) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + int count = 0; + size_t i; + if (set == NULL || set->index == NULL || set->fn == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n"); + return; + } + for (i = 0; set->fields[i].name != NULL; i++) { + if (mg_dash_parse_field(body, &set->fields[i])) count++; + } + if (count && set->fn(MG_DASH_ADD, u)) { + *set->index = -1; // Signal mg_dash_send_change() to send new size + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, MG_JSON_HEADERS, "true\n"); + } else { + mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n"); + } +} + void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct mg_dash *dash = (struct mg_dash *) c->fn_data; + struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data; if (ev == MG_EV_OPEN) { - if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) { - MG_DASH_ADD_FIELD_SET(dash, &set_files); - } + d->dash = dash; // c->is_hexdumping = 1; - } else if (ev == MG_EV_HTTP_HDRS && c->data[0] == 0) { + } else if (ev == MG_EV_HTTP_HDRS && d->marker == 0) { // Received headers - check authentication and possibly start uploads/ota struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_dash_user *u = mg_dash_authenticate(hm, dash); + struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); + struct mg_str parts[3]; + memset(parts, 0, sizeof(parts)); if (mg_match(hm->uri, mg_str("/api/hi"), NULL) || mg_match(hm->uri, mg_str("/api/logout"), NULL)) { // Do nothing, handle them MG_EV_HTTP_MSG. We bypass auth for those } else if (u == NULL && mg_match(hm->uri, mg_str("/api/#"), NULL)) { mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); - c->data[0] = CONN_HANDLED; + d->marker = CONN_HANDLED; } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) { mg_handle_login(c, u); - c->data[0] = CONN_HANDLED; + d->marker = CONN_HANDLED; } else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) { mg_http_start_ota(c, hm, mg_dash_ota_cb); - } else if (mg_match(hm->uri, mg_str("/fs/#"), NULL) && + } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) && (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { - struct mg_str name = mg_dash_file_name(hm); - mg_snprintf(c->data, sizeof(c->data), "%.*s", (int) name.len, name.buf); - mg_http_start_upload(c, hm, name, mg_str("/tmp"), &mg_fs_posix, - mg_dash_upload_cb); + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + struct mg_str name = parts[1]; + int len = + mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0); + if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; + if (set == NULL || set->get_dir == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n"); + d->marker = CONN_HANDLED; + } else if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + d->marker = CONN_HANDLED; + } else if (!mg_path_is_sane(name)) { + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); + d->marker = CONN_HANDLED; + } else { + mg_dash_set_file_name(set, name); + if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + d->marker = CONN_HANDLED; + } else { + char dir[256]; + struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + if (!set->get_dir(u, dir, sizeof(dir))) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + d->marker = CONN_HANDLED; + } else { + mg_http_start_upload(c, hm, name, mg_str(dir), fs, + mg_dash_upload_cb); + } + } + } } - if (c->data[0] != '\0') mg_log_http_req(c, hm); - } else if (ev == MG_EV_HTTP_MSG && c->data[0] != '\0') { + if (d->marker != '\0') mg_log_http_req(c, hm); + } else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') { // The response has been send in EV_HDRS path, so we're not reponding - // anything but clearing the c->data[0] flag for the next request. - c->data[0] = 0; + // anything but clearing the marker for the next request. + d->marker = 0; c->is_resp = 0; - } else if (ev == MG_EV_HTTP_MSG && c->data[0] == '\0') { + } else if (ev == MG_EV_HTTP_MSG && d->marker == '\0') { struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_dash_user *u = mg_dash_authenticate(hm, dash); - int level = u == NULL ? 0 : u->level; + struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash); struct mg_str parts[5]; memset(parts, 0, sizeof(parts)); @@ -460,27 +684,60 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"), MG_ESC("logout")); } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { - *(int *) c->data = level; + d->u = u; mg_ws_upgrade(c, hm, NULL); - } else if (mg_match(hm->uri, mg_str("/fs/#"), NULL)) { - struct mg_str name = mg_dash_file_name(hm); - char path[128]; - mg_snprintf(path, sizeof(path), "/tmp/%.*s", name.len, name.buf); - if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { - // Delete file - mg_dash_file_del(name); - mg_dash_send_change(c->mgr, &set_files); - mg_http_reply(c, 200, NULL, "true"); + } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) { + struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]); + if (set == NULL || set->get_dir == NULL) { + mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); + } else if (u == NULL) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); } else { - // Serve file - mg_http_serve_file(c, hm, path, NULL); + char dir[256], path[512]; + struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix; + struct mg_str name = parts[1]; + int len = mg_url_decode(name.buf, name.len, (char *) name.buf, + name.len + 1, 0); + if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; + if (!mg_path_is_sane(name)) { + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n"); + return; + } + if (!set->get_dir(u, dir, sizeof(dir))) { + mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n"); + return; + } + mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf); + if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { + mg_dash_set_file_name(set, name); + if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) { + mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); + } else { + fs->rm(path); + *set->index = -1; // Signal mg_dash_send_change() to send new size + mg_dash_send_change(c->mgr, set); + mg_http_reply(c, 200, NULL, "true"); + } + } else { + mg_http_serve_file(c, hm, path, NULL); + } } - } else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) || + } else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) { + mg_dash_handle_del(c, dash, u, parts); + } else if (mg_match(hm->uri, mg_str("/api/add/*"), parts)) { + mg_dash_handle_add(c, dash, u, parts, hm->body); + } else if (mg_match(hm->uri, mg_str("/api/get/*/*"), parts) && + mg_strcasecmp(hm->method, mg_str("POST")) == 0) { + mg_dash_handle_mod(c, dash, u, parts, hm->body); + } else if (mg_match(hm->uri, mg_str("/api/get/*/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/get/*/*"), parts) || + mg_match(hm->uri, mg_str("/api/get/*"), parts) || mg_match(hm->uri, mg_str("/api/get"), NULL)) { - mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash, - &parts[0], level); + mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_endpoint, + dash, u, &parts[0], &parts[1], &parts[2]); } else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) { - int count = mg_dash_apply(c, dash, hm->body, level); + int count = mg_dash_apply(c, dash, hm->body, u); mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count); } else if (mg_match(hm->uri, mg_str("/"), NULL)) { struct mg_http_serve_opts opts; @@ -498,20 +755,5 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found"); mg_log_http_req(c, hm); } - } else if (ev == MG_EV_WS_OPEN) { - // WS connection established, send change notifications for all data - struct mg_field_set *fs; - for (fs = dash->sets; fs != NULL; fs = fs->next) { - mg_dash_send_change(c->mgr, fs); - } - mg_dash_broadcast(c->mgr, 0, "{%m:%m}", MG_ESC("method"), MG_ESC("ready")); - } else if (ev == MG_EV_WS_MSG) { - // Add this to automatically handle "get" and "set" JSON-RPC calls - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - if (dash == NULL) { - mg_dash_error(c, wm->data, "%s", "no dash defined"); - } else { - mg_dash_process_msg(c, wm, dash); - } } } diff --git a/src/dash.h b/src/dash.h index 004f2659..d3f81faf 100644 --- a/src/dash.h +++ b/src/dash.h @@ -7,50 +7,95 @@ #include "str.h" #include "ws.h" +#ifndef MG_DASH_MAX_USER_NAME +#define MG_DASH_MAX_USER_NAME 32 +#endif + +#ifndef MG_HTTP_ADDR +#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 +#define MG_HTTP_ADDR "http://0.0.0.0:8000" +#define MG_HTTPS_ADDR "https://0.0.0.0:8443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" +#else +#define MG_HTTP_ADDR "http://0.0.0.0:80" +#define MG_HTTPS_ADDR "http://0.0.0.0:443" +#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" +#endif +#endif + +#ifndef MG_DASH_MAX_SEND_BUF_SIZE +#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) +#endif + +// C type of a value bound to a dashboard field, see struct mg_field enum mg_val_type { - MG_VAL_INT, - MG_VAL_BOOL, - MG_VAL_DBL, - MG_VAL_STR, - MG_VAL_RAW, + MG_VAL_INT, // int + MG_VAL_UINT64, // uint64_t + MG_VAL_BOOL, // bool + MG_VAL_DBL, // double + MG_VAL_STR, // Zero-terminated string + MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim }; +// Dashboard field: binds a JSON key to a C variable for read and/or write struct mg_field { - const char *name; - enum mg_val_type type; - void *value; - size_t value_size; + const char *name; // JSON key + enum mg_val_type type; // C type of the value, see enum mg_val_type + void *value; // Pointer to the bound variable + size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only }; +// Operation passed to a struct mg_field_set read/write/delete/add callback. +// MG_DASH_ADD: fields hold the values of a new element to append; *index is +// not set. Return true to accept and persist it, false to reject (e.g. a cap +// was reached) +enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD }; + +struct mg_dash; + +// Authenticated (or guest) dashboard user +struct mg_dash_user { + struct mg_dash_user *next; // Next user in the list + char name[MG_DASH_MAX_USER_NAME]; // Username + char token[21]; // Session token, sent to the client as a cookie + int level; // Access level, see struct mg_dash::authenticate + uint64_t expire; // Session expiration time, see mg_millis() + struct mg_dash *dash; // Dashboard this user belongs to +}; + +// Named group of related dashboard fields, e.g. "settings" or "metrics" struct mg_field_set { - const char *name; - struct mg_field *fields; - void (*reader)(void); - void (*writer)(void); - int read_level; - int write_level; - struct mg_field_set *next; + const char *name; // Set name, used as a JSON key + struct mg_field *fields; // Zero-terminated array of fields + bool (*fn)(enum mg_dash_op op, + struct mg_dash_user *u); // Read/write/delete/add callback + int *index; // Non-NULL: array set. *index is set before fn(READ) call; + // fn sets *index = -1 to signal end of iteration. + // Size query: framework sets *index = -1 before fn(READ); fn + // sets *index = total size + bool (*get_dir)(const struct mg_dash_user *u, char *buf, + size_t len); // Non-NULL: file array + struct mg_field_set *next; // Next set in the list }; +// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER() struct mg_dash_custom_handler { - struct mg_dash_custom_handler *next; - struct mg_str uri_pattern; - mg_event_handler_t handler; - void *handler_data; -}; - -struct mg_dash_file { - struct mg_dash_file *next; - char *name; - size_t size; + struct mg_dash_custom_handler *next; // Next handler in the list + struct mg_str uri_pattern; // URI to match, see mg_match() + mg_event_handler_t handler; // Event handler function + void *handler_data; // Opaque data for the handler }; +// Dashboard instance: fields, users and handlers served over HTTP and WS struct mg_dash { - struct mg_field_set *sets; - struct mg_dash_custom_handler *custom_handlers; + struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET() + struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers + // Validates pass, fills in user, returns access level (<= 0: failure). + // NULL: no login required, all clients get guest access int (*authenticate)(char *user, size_t userlen, const char *pass); - int session_auto_expiration_seconds; - //struct mg_dash_file *files; + int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default + struct mg_dash_user *guest; // Used when authenticate == NULL + struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix }; #define MG_DASH_ADD_FIELD_SET(dash_, set_) \ @@ -69,28 +114,50 @@ struct mg_dash { (dash_)->custom_handlers = &ch_; \ } while (0) -#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 -#define MG_HTTP_ADDR "http://0.0.0.0:8000" -#define MG_HTTPS_ADDR "https://0.0.0.0:8443" -#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502" -#else -#define MG_HTTP_ADDR "http://0.0.0.0:80" -#define MG_HTTPS_ADDR "http://0.0.0.0:443" -#define MG_MODBUS_ADDR "tcp://0.0.0.0:502" -#endif - -#ifndef MG_DASH_MAX_SEND_BUF_SIZE -#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5) -#endif - -// File manager -extern struct mg_dash_file *mg_dash_files; -void mg_dash_file_add(struct mg_str name, size_t size); -void mg_dash_file_del(struct mg_str name); - +// Dashboard event handler. Pass to mg_http_listen() as the fn argument void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); + +// Notify WebSocket clients that a field set changed: broadcasts the new +// array size if *set->index < 0, or a single element update otherwise void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *); +// Default reader for file-backed array sets (set->get_dir != NULL) +// Usage example - this is a complete implemenation of the file manager: +// ```c +// static struct file { +// int index; +// char name[64]; +// size_t size; +// uint64_t checksum; +// } s_file; +// static struct mg_field_set set_files; +// +// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { +// (void) u; +// mkdir("/tmp/dashboard", 0755); +// mg_snprintf(buf, len, "%s", "/tmp/dashboard"); +// return true; +// } +// +// static struct mg_field fields_files[] = { +// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, +// {"size", MG_VAL_UINT64, &s_file.size, 0}, +// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, +// {NULL, MG_VAL_INT, NULL, 0}, +// }; +// +// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { +// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7; +// if (op != MG_DASH_READ) return false; +// if (!mg_dash_dir_read(&set_files, u)) return false; +// s_file.checksum = 0; // Mock to show how to set custom fields +// return true; +// } +// +// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL}; +// ``` +bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u); + // Helper forward declarations for Mongoose CMSIS pack modules extern struct mg_mgr g_mgr; extern void mg_dash_init(struct mg_mgr *); diff --git a/src/flash.c b/src/flash.c index 1d692f87..239f01d8 100644 --- a/src/flash.c +++ b/src/flash.c @@ -64,15 +64,22 @@ bool mg_ota_flash_end(struct mg_flash *flash) { MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size, size, ok ? "ok" : "fail")); #ifdef MG_OTA_PUBLIC_KEY - if (ok && s_size > 64) { - static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY; - uint8_t hash[32]; - size_t fw_size = s_size - 64; - mg_sha256(hash, (uint8_t *) base, fw_size); - ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash), - (uint8_t *) base + fw_size, - mg_uecc_secp256r1()) == 1; - MG_INFO(("Signature: %s", ok ? "ok" : "fail")); + if (ok) { + bool signed_fw = s_size > 68 && + memcmp((uint8_t *) base + s_size - 4, "MGSG", 4) == 0; + if (signed_fw) { + static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY; + uint8_t hash[32]; + size_t fw_size = s_size - 68; // strip 64-byte sig + 4-byte magic + mg_sha256(hash, (uint8_t *) base, fw_size); + ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash), + (uint8_t *) base + fw_size, + mg_uecc_secp256r1()) == 1; + MG_INFO(("Signature: %s", ok ? "ok" : "fail")); + } else { + ok = false; + MG_ERROR(("Unsigned firmware rejected")); + } } #endif s_size = 0; diff --git a/src/http.c b/src/http.c index ff978205..4a358dd5 100644 --- a/src/http.c +++ b/src/http.c @@ -1024,19 +1024,19 @@ static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) { if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else mg_ota_end(); p->fn(c, "write error"); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } else if (p->received >= p->expected) { const char *errmsg = NULL; if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else if (!mg_ota_end()) errmsg = "OTA finalize failed"; p->fn(c, errmsg); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } } else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) { if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } else mg_ota_end(); p->fn(c, ev == MG_EV_ERROR ? (const char *) ev_data : "connection closed"); - p->fn = NULL; + mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; } (void) ev_data; } @@ -1046,6 +1046,10 @@ static void mg_upload_default_cb(struct mg_connection *c, const char *status) { mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok"); } +const char *mg_upload_path(struct mg_connection *c) { + return (const char *) c->pfn_data; +} + void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, struct mg_str name, struct mg_str dir, struct mg_fs *fs, @@ -1065,6 +1069,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, p->fd = fd; p->fn = fn; c->fn = mg_upload_handler; + c->pfn_data = strdup(path); c->pfn = NULL; mg_iobuf_del(&c->recv, 0, hm->head.len); mg_call(c, MG_EV_READ, &c->recv.len); diff --git a/src/http.h b/src/http.h index ede85e7b..8f641e9a 100644 --- a/src/http.h +++ b/src/http.h @@ -64,6 +64,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm, void (*fn)(struct mg_connection *, const char *)); void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, void (*fn)(struct mg_connection *, const char *)); +const char *mg_upload_path(struct mg_connection *c); void mg_http_bauth(struct mg_connection *, const char *user, const char *pass); struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v); size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *); diff --git a/src/net.c b/src/net.c index af51b649..c4d47a84 100644 --- a/src/net.c +++ b/src/net.c @@ -2,6 +2,7 @@ #include "dns.h" #include "fmt.h" #include "log.h" +#include "ota.h" #include "printf.h" #include "profile.h" #include "timer.h" @@ -278,6 +279,11 @@ void mg_mgr_free(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *mgr) { memset(mgr, 0, sizeof(*mgr)); + // Anchor mg_fw_version: a real store into a struct field the optimiser + // can't elide forces the linker to keep the section alive even when no + // OTA polling code runs it. Callers that want mgr->userdata for their own + // purposes simply overwrite it after this call returns + mgr->userdata = (void *) mg_fw_version; #if MG_ENABLE_EPOLL if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) MG_ERROR(("epoll_create1 errno %d", errno)); diff --git a/src/net_builtin.c b/src/net_builtin.c index 4da3c4d7..003b19af 100644 --- a/src/net_builtin.c +++ b/src/net_builtin.c @@ -2180,6 +2180,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { struct mg_connection *c, *tmp; uint64_t now = mg_millis(); mg_timer_poll(&mgr->timers, now); + mg_ota_poll(mgr); if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return; mg_tcpip_poll(mgr->ifp, now); for (c = mgr->conns; c != NULL; c = tmp) { diff --git a/src/ota.c b/src/ota.c index 781d7a13..d15836f9 100644 --- a/src/ota.c +++ b/src/ota.c @@ -2,146 +2,172 @@ #include "http.h" #include "json.h" #include "log.h" -#include "timer.h" +#include "printf.h" #include "url.h" #include "util.h" -enum { MG_OTA_STATUS_WAITING, MG_OTA_STATUS_SUCCESS, MG_OTA_STATUS_FAIL }; -static int s_version_status; -static int s_ota_status; -static uint64_t s_start_time; +#ifndef MG_OTA_MAX_URL_LEN +#define MG_OTA_MAX_URL_LEN 256 +#endif -static struct mg_ota_metadata { - char *version; - char *url; +// Scannable version tag embedded in every firmware binary, for server-side +// version extraction. Non-static so mg_mgr_init() can stash its address in +// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from +// stripping it on builds that never poll for OTAs (an attribute alone does +// not - the linker drops unreferenced sections regardless of "used"/"retain") +const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION; + +static struct mg_ota_state { + char my_version[MG_OTA_MAX_VERSION_LEN]; + char json_url[MG_OTA_MAX_URL_LEN]; + char version[MG_OTA_MAX_VERSION_LEN]; + char url[MG_OTA_MAX_URL_LEN]; size_t size; uint8_t sha256[32]; -} s_ota_metadata; + void (*fn)(const char *status); +} *s_ota; -static void free_ota_metadata(void) { - if (s_ota_metadata.version) mg_free(s_ota_metadata.version); - if (s_ota_metadata.url) mg_free(s_ota_metadata.url); - memset(&s_ota_metadata, 0, sizeof(s_ota_metadata)); -} +static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); -static int fetch_ota_metadata(struct mg_http_message *response) { - double result; - if (mg_http_status(response) != 200) goto fetch_failed; - if (mg_json_get(response->body, "$", NULL) != 0) goto fetch_failed; - free_ota_metadata(); - s_ota_metadata.version = mg_json_get_str(response->body, "$.version"); - if (s_ota_metadata.version == NULL) goto fetch_failed; - s_ota_metadata.url = mg_json_get_str(response->body, "$.url"); - if (s_ota_metadata.url == NULL) goto fetch_failed; - if (!mg_json_get_num(response->body, "$.size", &result) || result <= 0) - goto fetch_failed; - s_ota_metadata.size = (size_t) result; - // TODO (robertc2000): parse and validate sha256 - MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota_metadata.version, - s_ota_metadata.url, s_ota_metadata.size)); - return MG_OTA_STATUS_SUCCESS; -fetch_failed: - free_ota_metadata(); - return MG_OTA_STATUS_FAIL; +#if MG_ENABLE_CUSTOM_DEVICE_ID +#else +void mg_ota_device_id(char *buf, size_t len) { +#if MG_ARCH == MG_ARCH_CUBE && defined(UID_BASE) + mg_snprintf(buf, len, "%M", mg_print_hex, 12, (uint8_t *) UID_BASE); +#else + mg_snprintf(buf, len, "%d", 0); +#endif } +#endif static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) { - struct mg_str host = mg_url_host((char *) c->fn_data); - struct mg_http_message *hm; - static int fetch_status; - + uint64_t expiration = *(uint64_t *) c->data; if (ev == MG_EV_POLL) { - if (s_start_time + 5 * 1000 < mg_millis()) { - mg_error(c, "Connection timeout"); - } + if (mg_millis() > expiration) mg_error(c, "Metadata timeout"); } else if (ev == MG_EV_CONNECT) { + char id[33]; + struct mg_str host = mg_url_host(s_ota->json_url); + const char *uri = mg_url_uri(s_ota->json_url); + const char *sep = strchr(uri, '?') == NULL ? "?" : "&"; + mg_ota_device_id(id, sizeof(id)); + id[sizeof(id) - 1] = '\0'; mg_printf(c, - "GET %s HTTP/1.1\r\n" + "GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n" "Host: %.*s\r\n" "Connection: close\r\n\r\n", - mg_url_uri((char *) c->fn_data), (int) host.len, host.buf); - fetch_status = MG_OTA_STATUS_WAITING; + uri, sep, MG_ARCH, s_ota->my_version, id, + MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); } else if (ev == MG_EV_HTTP_MSG) { - hm = (struct mg_http_message *) ev_data; - fetch_status = fetch_ota_metadata(hm); + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + double result; + MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf)); + if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 || + !mg_json_unescape(hm->body, "$.version", s_ota->version, + sizeof(s_ota->version)) || + !mg_json_unescape(hm->body, "$.url", s_ota->url, sizeof(s_ota->url)) || + !mg_json_get_num(hm->body, "$.size", &result) || result <= 0) { + char buf[100]; + mg_snprintf(buf, sizeof(buf), "Bad metadata: %.*s", hm->body.len, + hm->body.buf); + s_ota->fn(buf); + mg_free(s_ota); + s_ota = NULL; + } else if (strcmp(s_ota->version, s_ota->my_version) == 0) { + s_ota->fn("Same version"); + mg_free(s_ota); + s_ota = NULL; + } else { + struct mg_connection *fc; + s_ota->size = (size_t) result; + // TODO (robertc2000): parse and validate sha256 + MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota->version, + s_ota->url, s_ota->size)); + fc = mg_http_connect(c->mgr, s_ota->url, s_firmware_fn, NULL); + if (fc == NULL) { + s_ota->fn("Failed to connect"); + mg_free(s_ota); + s_ota = NULL; + } else { + *(uint64_t *) fc->data = mg_millis() + 300 * 1000; // Set expiration + } + } + c->is_closing = 1; } else if (ev == MG_EV_ERROR) { - MG_ERROR(("%lu Connection error", c->id)); - s_version_status = MG_OTA_STATUS_FAIL; - } else if (ev == MG_EV_CLOSE) { - MG_DEBUG(("%lu Connection closed %lu", c->id, mg_millis() - s_start_time)); - s_version_status = fetch_status; + s_ota->fn((char *) ev_data); + mg_free(s_ota); + s_ota = NULL; } } -static void s_ota_done(struct mg_connection *c, const char *errmsg) { - (void) c; - s_ota_status = errmsg ? MG_OTA_STATUS_FAIL : MG_OTA_STATUS_SUCCESS; +static void status_fn(const char *errmsg) { if (errmsg) MG_ERROR(("OTA failed: %s", errmsg)); } +static void status_fn_2(struct mg_connection *c, const char *errmsg) { + if (s_ota) s_ota->fn(errmsg); + mg_free(s_ota); + s_ota = NULL; + mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok"); +} + static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { - struct mg_str host = mg_url_host(s_ota_metadata.url); + uint64_t expiration = *(uint64_t *) c->data; if (ev == MG_EV_POLL) { - if (s_start_time + 120 * 1000 < mg_millis()) mg_error(c, "Timeout"); + if (mg_millis() > expiration) mg_error(c, "OTA timeout"); } else if (ev == MG_EV_CONNECT) { + struct mg_str host = mg_url_host(s_ota->url); mg_printf(c, "GET %s HTTP/1.1\r\n" "Host: %.*s\r\n" "Connection: close\r\n\r\n", - mg_url_uri(s_ota_metadata.url), (int) host.len, host.buf); + mg_url_uri(s_ota->url), (int) host.len, host.buf); } else if (ev == MG_EV_HTTP_HDRS) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; int status = mg_http_status(hm); - if (status != 200 || hm->body.len != s_ota_metadata.size) { + if (status != 200 || hm->body.len != s_ota->size) { mg_error(c, "Bad HTTP response: status %d, size %lu vs %lu", status, - (unsigned long) hm->body.len, - (unsigned long) s_ota_metadata.size); - s_ota_status = MG_OTA_STATUS_FAIL; - return; + (unsigned long) hm->body.len, (unsigned long) s_ota->size); + } else { + MG_DEBUG(("Beginning OTA (%lu bytes)", (unsigned long) s_ota->size)); + mg_http_start_ota(c, hm, status_fn_2); } - MG_DEBUG( - ("Beginning OTA (%lu bytes)", (unsigned long) s_ota_metadata.size)); - mg_http_start_ota(c, hm, s_ota_done); } else if (ev == MG_EV_ERROR) { - MG_ERROR(("%lu Connection error", c->id)); - s_ota_status = MG_OTA_STATUS_FAIL; + s_ota->fn((char *) ev_data); + mg_free(s_ota); + s_ota = NULL; } - (void) ev_data; } -void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, - const char *metadata_url, - void (*fn)(const char *status)) { - s_version_status = MG_OTA_STATUS_WAITING; - s_ota_status = MG_OTA_STATUS_WAITING; - s_start_time = mg_millis(); - MG_DEBUG(("Connecting to %s to retrieve metadata", metadata_url)); - if (!mg_http_connect(mgr, metadata_url, s_version_fn, - (void *) metadata_url)) { - if (fn) fn("Failed to connect"); - return; - } - while (s_version_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10); - if (s_version_status == MG_OTA_STATUS_SUCCESS) { - if (strcmp(s_ota_metadata.version, current_version) == 0) { - if (fn) fn("Same version"); - free_ota_metadata(); - return; - } +void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version, + const char *json_url, void (*fn)(const char *status)) { + if (fn == NULL) fn = status_fn; + if (s_ota != NULL) { + fn("OTA already in progress"); + } else if ((s_ota = (struct mg_ota_state *) mg_calloc(1, sizeof(*s_ota))) == + NULL) { + fn("Out of memory"); } else { - if (fn) fn("Version retrieving error"); - free_ota_metadata(); - return; + struct mg_connection *c; + mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version); + mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url); + MG_DEBUG(("Connecting to %s", json_url)); + c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL); + if (c == NULL) { + mg_free(s_ota); + s_ota = NULL; + fn("Failed to connect"); + } else { + s_ota->fn = fn; + *(uint64_t *) c->data = mg_millis() + 5 * 1000; // Set expiration + } + } +} + +void mg_ota_poll(struct mg_mgr *mgr) { + static uint64_t t = 1; + if (MG_OTA_URL != NULL && + mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) { + mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL, + MG_OTA_STATUS_FN); } - if (fn) fn("Pulling firmware"); - s_start_time = mg_millis(); - MG_DEBUG(("Connecting to %s to download firmware", s_ota_metadata.url)); - if (!mg_http_connect(mgr, s_ota_metadata.url, s_firmware_fn, NULL)) { - if (fn) fn("Failed to connect"); - free_ota_metadata(); - return; - } - while (s_ota_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10); - if (s_ota_status == MG_OTA_STATUS_FAIL && fn) fn("OTA fail"); - free_ota_metadata(); } diff --git a/src/ota.h b/src/ota.h index 6e390f5c..7ea02999 100644 --- a/src/ota.h +++ b/src/ota.h @@ -100,3 +100,37 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; // Pull based OTA over HTTP void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, const char *metadata_url, void (*fn)(const char *status)); + +#ifndef MG_OTA_URL +#define MG_OTA_URL NULL +#endif + +#ifndef MG_OTA_STATUS_FN +#define MG_OTA_STATUS_FN NULL +#endif + +#ifndef MG_OTA_FIRMWARE_VERSION +#define MG_OTA_FIRMWARE_VERSION "1.0.0" +#endif + +#ifndef MG_OTA_MAX_VERSION_LEN +#define MG_OTA_MAX_VERSION_LEN 64 +#endif + +// Scannable "MG_VERSION:" tag embedded in every +// firmware binary, for server-side extraction. mg_mgr_init() stashes its +// address in mgr->userdata, which is what keeps it from being stripped by +// -Wl,--gc-sections on builds that never poll for OTAs +extern const char mg_fw_version[]; + +#ifndef MG_OTA_PULL_INTERVAL_SECONDS +#define MG_OTA_PULL_INTERVAL_SECONDS 60 +#endif + +#ifndef MG_ENABLE_CUSTOM_DEVICE_ID +#define MG_ENABLE_CUSTOM_DEVICE_ID 0 +#endif + +void mg_ota_device_id(char *buf, size_t len); + +void mg_ota_poll(struct mg_mgr *); diff --git a/src/sock.c b/src/sock.c index e85e08dd..dd586299 100644 --- a/src/sock.c +++ b/src/sock.c @@ -8,6 +8,7 @@ #include "tls.h" #include "url.h" #include "util.h" +#include "ota.h" #if MG_ENABLE_SOCKET @@ -757,6 +758,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) { mg_iotest(mgr, ms); now = mg_millis(); mg_timer_poll(&mgr->timers, now); + mg_ota_poll(mgr); for (c = mgr->conns; c != NULL; c = tmp) { bool is_resp = c->is_resp; diff --git a/test/unit_test.c b/test/unit_test.c index 4bd7db1a..57925be9 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -4202,32 +4202,65 @@ static int dash_authenticate(char *user, size_t userlen, const char *pass) { } struct dash_ws_test { - const char *req; + int opened; int done; - int result; int saw_set3; }; +struct dash_arr_ws_test { + int opened; + int done; + int saw_arr_index; // index seen in "arr/INDEX" change key, -1 if not seen + int saw_arr_val; // "val" seen in the change params, -1 if not seen + int saw_arr_size; // size seen in a plain "arr":N change, -1 if not seen +}; + +static void dash_arr_ws_cb(struct mg_connection *c, int ev, void *ev_data) { + struct dash_arr_ws_test *d = (struct dash_arr_ws_test *) c->fn_data; + if (ev == MG_EV_WS_OPEN) { + d->opened = 1; + } else if (ev == MG_EV_WS_MSG) { + struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; + struct mg_str method = mg_json_get_tok(wm->data, "$.method"); + if (mg_strcmp(method, mg_str("\"change\"")) == 0) { + // Check for "arr/INDEX" (element change) or "arr":N (size change) + const char *p = wm->data.buf; + const char *end = p + wm->data.len; + while (p < end - 6) { + if (strncmp(p, "\"arr/", 5) == 0) { + const char *v; + p += 5; + d->saw_arr_index = (int) strtol(p, NULL, 10); + v = strstr(p, "\"val\":"); + if (v != NULL) d->saw_arr_val = (int) strtol(v + 6, NULL, 10); + d->done = 1; + c->is_closing = 1; + break; + } + if (strncmp(p, "\"arr\":", 6) == 0) { + d->saw_arr_size = (int) strtol(p + 6, NULL, 10); + d->done = 1; + c->is_closing = 1; + break; + } + p++; + } + } + } else if (ev == MG_EV_CLOSE && d->done == 0) { + d->done = -1; + } +} + static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) { struct dash_ws_test *d = (struct dash_ws_test *) c->fn_data; if (ev == MG_EV_WS_OPEN) { - if (d->req != NULL) { - mg_ws_send(c, d->req, strlen(d->req), WEBSOCKET_OP_TEXT); - } + d->opened = 1; } else if (ev == MG_EV_WS_MSG) { struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; struct mg_str method = mg_json_get_tok(wm->data, "$.method"); struct mg_str set3 = mg_json_get_tok(wm->data, "$.params.set3"); - double result = 0; if (mg_strcmp(method, mg_str("\"change\"")) == 0 && set3.buf != NULL) { d->saw_set3 = 1; - } - if (mg_json_get_num(wm->data, "$.result", &result)) { - d->result = (int) result; - d->done = 1; - c->is_closing = 1; - } else if (d->req == NULL && - mg_strcmp(method, mg_str("\"ready\"")) == 0) { d->done = 1; c->is_closing = 1; } @@ -4238,8 +4271,89 @@ static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) { char three[10] = "hi"; int one = 1; -static void rfields1(void) { - mg_snprintf(three, sizeof(three), "t: %d", one); // Simulate array + +#define TEST_ARR_SIZE 17 +#define TEST_ARR_CAP (TEST_ARR_SIZE + 2) // Cap for MG_DASH_ADD tests +#define TEST_ARR_RO_INDEX 3 // This element is read-only: WRITE is always denied +static int s_arr_index; +static int s_arr_val; +static int s_arr_store[TEST_ARR_CAP]; // Backing store, init to squares +static int s_arr_count = TEST_ARR_SIZE; // Logical size, grows via MG_DASH_ADD + +static bool set1_fn(enum mg_dash_op op, struct mg_dash_user *u) { + if (u->level < 3) return false; + if (op == MG_DASH_READ) { + mg_snprintf(three, sizeof(three), "t: %d", one); + return true; + } + return u->level >= 7; +} +static bool set2_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; return op == MG_DASH_READ; +} +static bool set3_fn(enum mg_dash_op op, struct mg_dash_user *u) { + if (u->level < 7) return false; + return op == MG_DASH_READ || op == MG_DASH_WRITE; +} +static bool test_upload_dir(const struct mg_dash_user *u, char *buf, size_t len) { + (void) u; +#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 + mkdir("/tmp/mongoose_dash_test", 0755); +#endif + mg_snprintf(buf, len, "%s", "/tmp/mongoose_dash_test"); + return true; +} + +struct test_file_entry { + char name[64]; + size_t size; +}; +static struct test_file_entry s_test_file; +static int s_test_file_index; +static struct mg_field test_files_fields[] = { + {"name", MG_VAL_STR, s_test_file.name, sizeof(s_test_file.name)}, + {"size", MG_VAL_UINT64, &s_test_file.size, 0}, + {NULL, MG_VAL_INT, NULL, 0}, +}; +static struct mg_field_set test_files_set = { + "files", test_files_fields, NULL, &s_test_file_index, test_upload_dir, NULL}; +static struct test_file_entry s_delete_file; +static char s_deleted_file[sizeof(s_delete_file.name)]; +static char s_uploaded_file[sizeof(s_delete_file.name)]; +static struct mg_field_set *s_delete_file_set; +static bool delete_file_fn(enum mg_dash_op op, struct mg_dash_user *u) { + if (op == MG_DASH_READ) return mg_dash_dir_read(s_delete_file_set, u); + if (op == MG_DASH_WRITE) { + mg_snprintf(s_uploaded_file, sizeof(s_uploaded_file), "%s", + s_delete_file.name); + } + if (op == MG_DASH_DELETE) { + mg_snprintf(s_deleted_file, sizeof(s_deleted_file), "%s", + s_delete_file.name); + } + return true; +} +static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op == MG_DASH_READ) { + if (s_arr_index < 0) { s_arr_index = s_arr_count; return true; } + if (s_arr_index >= s_arr_count) { s_arr_index = -1; return true; } + s_arr_val = s_arr_store[s_arr_index]; + return true; + } + if (op == MG_DASH_WRITE) { + if (s_arr_index < 0 || s_arr_index >= s_arr_count) return false; + if (s_arr_index == TEST_ARR_RO_INDEX) return false; // Deny: read-only + s_arr_store[s_arr_index] = s_arr_val; + return true; + } + if (op == MG_DASH_ADD) { + if (s_arr_count >= TEST_ARR_CAP) return false; // Deny: cap reached + s_arr_store[s_arr_count++] = s_arr_val; + return true; + } + if (op == MG_DASH_DELETE) return s_arr_index < s_arr_count; + return false; } static void test_dash(void) { @@ -4248,7 +4362,7 @@ static void test_dash(void) { const char *ws_url = "ws://localhost:26352/api/websocket"; struct mg_mgr mgr; struct mg_dash dash; - struct dash_ws_test ws_user, ws_admin, ws_user_notify; + struct dash_ws_test ws_admin, ws_user_notify; bool two = false; int admin = 7; int i; @@ -4265,13 +4379,20 @@ static void test_dash(void) { {"admin", MG_VAL_INT, &admin, sizeof(admin)}, {NULL, MG_VAL_INT, NULL, 0}, }; - struct mg_field_set set1 = {"set1", fields1, rfields1, NULL, 3, 7, NULL}; - struct mg_field_set set2 = {"set2", fields2, NULL, NULL, 0, 0, NULL}; - struct mg_field_set set3 = {"set3", fields3, NULL, NULL, 7, 7, NULL}; + struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL, NULL}; + struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL, NULL}; + struct mg_field_set set3 = {"set3", fields3, set3_fn, NULL, NULL, NULL}; + struct mg_field arr_fields[] = { + {"val", MG_VAL_INT, &s_arr_val, sizeof(s_arr_val)}, + {NULL, MG_VAL_INT, NULL, 0}, + }; + struct mg_field_set arr_set = {"arr", arr_fields, arr_fn, &s_arr_index, NULL, + NULL}; const char *get_all_expected = - "{\"files\":{\"data\":[]},\"set3\":{\"admin\":7}," + "{\"arr\":17,\"set3\":{\"admin\":7}," "\"set2\":{\"two\":false}," - "\"set1\":{\"one\":1,\"three\":\"t: 1\"}}\n"; + "\"set1\":{\"one\":1,\"three\":\"t: 1\"}," + "\"files\":0}\n"; const char *set1_req = "POST /api/set HTTP/1.0\nContent-Length: 18\n\n" "{\"set1\":{\"one\":2}}"; const char *set1_set3_req = "POST /api/set HTTP/1.0\n" @@ -4290,8 +4411,14 @@ static void test_dash(void) { "Authorization: Basic YWRtaW46YWRtaW4=\n" "Content-Length: 20\n\n" "{\"set3\":{\"admin\":8}}"; - const char *set1_ws_set4_req = - "{\"id\":1,\"method\":\"set\",\"params\":{\"set1\":{\"one\":4}}}"; + const char *set3_admin_req9 = "POST /api/set HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 20\n\n" + "{\"set3\":{\"admin\":9}}"; + const char *set1_admin_set4_req = "POST /api/set HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 18\n\n" + "{\"set1\":{\"one\":4}}"; const char *set1_expected = "{\"one\":2,\"three\":\"t: 2\"}\n"; const char *set1_expected3 = "{\"one\":3,\"three\":\"t: 3\"}\n"; const char *set1_expected4 = "{\"one\":4,\"three\":\"t: 4\"}\n"; @@ -4302,14 +4429,47 @@ static void test_dash(void) { const char *set2_req = "POST /api/set HTTP/1.0\nContent-Length: 21\n\n" "{\"set2\":{\"two\":true}}"; const char *set2_expected = "{\"two\":false}\n"; + const char *arr7_expected = "[{\"val\":49}]\n"; + const char *arr0_2_expected = "[{\"val\":0},{\"val\":1},{\"val\":4}]\n"; + const char *arr5_mod_req = "POST /api/get/arr/5 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 10\n\n" + "{\"val\":99}"; + const char *arr100_mod_req = "POST /api/get/arr/100 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 10\n\n" + "{\"val\":99}"; + const char *arr3_mod_req = "POST /api/get/arr/3 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 11\n\n" + "{\"val\":777}"; + const char *arr_add555_req = "POST /api/add/arr HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 11\n\n" + "{\"val\":555}"; + const char *arr_add556_req = "POST /api/add/arr HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 11\n\n" + "{\"val\":556}"; + const char *arr_add557_req = "POST /api/add/arr HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n" + "Content-Length: 11\n\n" + "{\"val\":557}"; + const char *arr5_mod_expected = "[{\"val\":99}]\n"; + const char *arr3_expected = "[{\"val\":9}]\n"; + const char *arr17_expected = "[{\"val\":555}]\n"; + const char *files_probe = "\"files\""; + const char *test_bin_probe = "\"test.bin\""; char marker = 0; memset(&dash, 0, sizeof(dash)); dash.session_auto_expiration_seconds = 1; + remove("/tmp/mongoose_dash_test/test.bin"); + MG_DASH_ADD_FIELD_SET(&dash, &test_files_set); MG_DASH_ADD_FIELD_SET(&dash, &set1); ASSERT(dash.sets == &set1); - ASSERT(set1.next == NULL); + ASSERT(set1.next == &test_files_set); MG_DASH_ADD_FIELD_SET(&dash, &set2); ASSERT(dash.sets == &set2); @@ -4326,26 +4486,9 @@ static void test_dash(void) { ASSERT(dash.custom_handlers->handler == dash_custom); ASSERT(dash.custom_handlers->handler_data == &marker); - while (mg_dash_files != NULL) mg_dash_file_del(mg_str(mg_dash_files->name)); - mg_dash_file_add(mg_str("one.txt"), 123); - ASSERT(mg_dash_files != NULL); - ASSERT(strcmp(mg_dash_files->name, "one.txt") == 0); - ASSERT(mg_dash_files->size == 123); - mg_dash_file_add(mg_str("two.bin"), 456); - ASSERT(mg_dash_files != NULL); - ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0); - ASSERT(mg_dash_files->size == 456); - ASSERT(mg_dash_files->next != NULL); - ASSERT(strcmp(mg_dash_files->next->name, "one.txt") == 0); - mg_dash_file_del(mg_str("one.txt")); - ASSERT(mg_dash_files != NULL); - ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0); - ASSERT(mg_dash_files->next == NULL); - mg_dash_file_del(mg_str("missing.txt")); - ASSERT(mg_dash_files != NULL); - mg_dash_file_del(mg_str("two.bin")); - ASSERT(mg_dash_files == NULL); + for (i = 0; i < TEST_ARR_SIZE; i++) s_arr_store[i] = i * i; + MG_DASH_ADD_FIELD_SET(&dash, &arr_set); mg_mgr_init(&mgr); mg_mem_files = mg_packed_files; ASSERT(mg_http_listen(&mgr, url, mg_dash_ev_handler, &dash) != NULL); @@ -4422,41 +4565,38 @@ static void test_dash(void) { "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); ASSERT(cmpbody(buf, set1_expected3) == 0); - ws_user_notify.req = NULL; + ws_user_notify.opened = 0; ws_user_notify.done = 0; - ws_user_notify.result = -1; ws_user_notify.saw_set3 = 0; mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user_notify, "%s", "Authorization: Basic dXNlcjp1c2Vy\r\n"); + for (i = 0; i < 200 && ws_user_notify.opened == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_user_notify.opened == 1); + ASSERT(fetch(&mgr, buf, url, set3_admin_req9) == 200); + ASSERT(cmpbody(buf, "1\n") == 0); for (i = 0; i < 200 && ws_user_notify.done == 0; i++) mg_mgr_poll(&mgr, 1); - ASSERT(ws_user_notify.done == 1); + ASSERT(ws_user_notify.done == 0); ASSERT(ws_user_notify.saw_set3 == 0); - ws_user.req = set1_ws_set4_req; - ws_user.done = 0; - ws_user.result = -1; - ws_user.saw_set3 = 0; - mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user, "%s", - "Authorization: Basic dXNlcjp1c2Vy\r\n"); - for (i = 0; i < 200 && ws_user.done == 0; i++) mg_mgr_poll(&mgr, 1); - ASSERT(ws_user.done == 1); - ASSERT(ws_user.result == 0); - ASSERT(ws_user.saw_set3 == 0); ASSERT(fetch(&mgr, buf, url, "GET /api/get/set1 HTTP/1.0\n" "Authorization: Basic dXNlcjp1c2Vy\n\n") == 200); ASSERT(cmpbody(buf, set1_expected3) == 0); - ws_admin.req = set1_ws_set4_req; + ws_admin.opened = 0; ws_admin.done = 0; - ws_admin.result = -1; ws_admin.saw_set3 = 0; mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_admin, "%s", "Authorization: Basic YWRtaW46YWRtaW4=\r\n"); + for (i = 0; i < 200 && ws_admin.opened == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_admin.opened == 1); + ASSERT(fetch(&mgr, buf, url, set3_admin_req) == 200); + ASSERT(cmpbody(buf, "1\n") == 0); for (i = 0; i < 200 && ws_admin.done == 0; i++) mg_mgr_poll(&mgr, 1); ASSERT(ws_admin.done == 1); - ASSERT(ws_admin.result == 1); ASSERT(ws_admin.saw_set3 == 1); + ASSERT(fetch(&mgr, buf, url, set1_admin_set4_req) == 200); + ASSERT(cmpbody(buf, "1\n") == 0); ASSERT(fetch(&mgr, buf, url, "GET /api/get/set1 HTTP/1.0\n" "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); @@ -4466,7 +4606,193 @@ static void test_dash(void) { ASSERT(fetch(&mgr, buf, url, "GET /api/hi HTTP/1.0\n\n") == 200); ASSERT(cmpbody(buf, "hi\n") == 0); + // Array tests with auth (17 squares) + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, "17\n") == 0); + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr/7 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, arr7_expected) == 0); + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr/0/2 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, arr0_2_expected) == 0); + + // Array deletion: single element + ASSERT(fetch(&mgr, buf, url, + "GET /api/del/arr/3 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, "1\n") == 0); + + // Array deletion: range + ASSERT(fetch(&mgr, buf, url, + "GET /api/del/arr/1/3 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, "3\n") == 0); + + // Array deletion: out-of-range returns 403 + ASSERT(fetch(&mgr, buf, url, + "GET /api/del/arr/100 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 403); + + // Non-array set returns 404 + ASSERT(fetch(&mgr, buf, url, + "GET /api/del/set1/0 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 404); + + // Test that array change notification uses "arr/INDEX" key format + { + struct dash_arr_ws_test ws_arr; + memset(&ws_arr, 0, sizeof(ws_arr)); + ws_arr.saw_arr_index = -1; + ws_arr.saw_arr_val = -1; + ws_arr.saw_arr_size = -1; + mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_arr, "%s", + "Authorization: Basic YWRtaW46YWRtaW4=\r\n"); + for (i = 0; i < 200 && ws_arr.opened == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_arr.opened == 1); + s_arr_index = 5; + mg_dash_send_change(&mgr, &arr_set); + for (i = 0; i < 200 && ws_arr.done == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_arr.done == 1); + ASSERT(ws_arr.saw_arr_index == 5); + } + + // Array modification: POST /api/get// updates one element + // and broadcasts a "/" WS change notification with new value + { + struct dash_arr_ws_test ws_mod; + memset(&ws_mod, 0, sizeof(ws_mod)); + ws_mod.saw_arr_index = -1; + ws_mod.saw_arr_val = -1; + ws_mod.saw_arr_size = -1; + mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_mod, "%s", + "Authorization: Basic YWRtaW46YWRtaW4=\r\n"); + for (i = 0; i < 200 && ws_mod.opened == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_mod.opened == 1); + + ASSERT(fetch(&mgr, buf, url, arr5_mod_req) == 200); + ASSERT(cmpbody(buf, "1\n") == 0); + + for (i = 0; i < 200 && ws_mod.done == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_mod.done == 1); + ASSERT(ws_mod.saw_arr_index == 5); + ASSERT(ws_mod.saw_arr_val == 99); + + // The new value got persisted - a fresh read returns it + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr/5 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, arr5_mod_expected) == 0); + + // Out-of-range index: 404 + ASSERT(fetch(&mgr, buf, url, arr100_mod_req) == 404); + + // Read-only element: fn denies MG_DASH_WRITE -> 403, value unchanged + ASSERT(fetch(&mgr, buf, url, arr3_mod_req) == 403); + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr/3 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, arr3_expected) == 0); + } + + // Array addition: POST /api/add/ appends an element and broadcasts + // a plain "":N WS change notification with the new size + { + struct dash_arr_ws_test ws_add; + memset(&ws_add, 0, sizeof(ws_add)); + ws_add.saw_arr_index = -1; + ws_add.saw_arr_val = -1; + ws_add.saw_arr_size = -1; + mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_add, "%s", + "Authorization: Basic YWRtaW46YWRtaW4=\r\n"); + for (i = 0; i < 200 && ws_add.opened == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_add.opened == 1); + + ASSERT(fetch(&mgr, buf, url, arr_add555_req) == 200); + ASSERT(cmpbody(buf, "true\n") == 0); + + for (i = 0; i < 200 && ws_add.done == 0; i++) mg_mgr_poll(&mgr, 1); + ASSERT(ws_add.done == 1); + ASSERT(ws_add.saw_arr_size == TEST_ARR_SIZE + 1); + + // The new element landed at the end of the array + ASSERT(fetch(&mgr, buf, url, + "GET /api/get/arr/17 HTTP/1.0\n" + "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); + ASSERT(cmpbody(buf, arr17_expected) == 0); + + // One more addition reaches the cap, then fn denies further ones + ASSERT(fetch(&mgr, buf, url, arr_add556_req) == 200); + ASSERT(fetch(&mgr, buf, url, arr_add557_req) == 403); + } + mg_mgr_free(&mgr); + + // Test no files fieldset: /fs/ returns 404 + { + const char *url2 = "http://localhost:26353"; + struct mg_mgr mgr2; + struct mg_dash dash2; + memset(&dash2, 0, sizeof(dash2)); + MG_DASH_ADD_FIELD_SET(&dash2, &set1); + MG_DASH_ADD_FIELD_SET(&dash2, &set2); + MG_DASH_ADD_FIELD_SET(&dash2, &set3); + mg_mgr_init(&mgr2); + mg_mem_files = mg_packed_files; + ASSERT(mg_http_listen(&mgr2, url2, mg_dash_ev_handler, &dash2) != NULL); + ASSERT(fetch(&mgr2, buf, url2, "GET /api/get HTTP/1.0\n\n") == 200); + ASSERT(strstr(buf, files_probe) == NULL); // no files fieldset + ASSERT(fetch(&mgr2, buf, url2, "GET /fs/files/test.txt HTTP/1.0\n\n") == 404); + ASSERT(fetch(&mgr2, buf, url2, + "POST /fs/files/test.txt HTTP/1.0\nContent-Length: 5\n\nhello") == + 404); + mg_mgr_free(&mgr2); + } + + // Test files fieldset with get_dir: /fs/ endpoints active + { + const char *url3 = "http://localhost:26354"; + struct mg_mgr mgr3; + struct mg_dash dash3; + static int idx3; + static struct mg_field ff3[] = { + {"name", MG_VAL_STR, s_delete_file.name, sizeof(s_delete_file.name)}, + {"size", MG_VAL_UINT64, &s_delete_file.size, 0}, + {NULL, MG_VAL_INT, NULL, 0}, + }; + static struct mg_field_set fset3 = {"files", ff3, delete_file_fn, &idx3, + test_upload_dir, NULL}; + memset(&dash3, 0, sizeof(dash3)); + s_deleted_file[0] = '\0'; + s_uploaded_file[0] = '\0'; + s_delete_file_set = &fset3; + MG_DASH_ADD_FIELD_SET(&dash3, &fset3); + mg_mgr_init(&mgr3); + mg_mem_files = mg_packed_files; + ASSERT(mg_http_listen(&mgr3, url3, mg_dash_ev_handler, &dash3) != NULL); + ASSERT(fetch(&mgr3, buf, url3, "GET /api/get HTTP/1.0\n\n") == 200); + ASSERT(strstr(buf, files_probe) != NULL); // files fieldset present + ASSERT(fetch(&mgr3, buf, url3, + "DELETE /fs/files/nofile.txt HTTP/1.0\n\n") == 200); + ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0); + ASSERT(fetch(&mgr3, buf, url3, + "DELETE /fs/files/..%2Fevil.txt HTTP/1.0\n\n") == 400); + ASSERT(strcmp(s_deleted_file, "nofile.txt") == 0); + ASSERT(fetch(&mgr3, buf, url3, + "PUT /fs/files/test.bin HTTP/1.0\nContent-Length: 4\n\ndata") == + 200); + ASSERT(strcmp(s_uploaded_file, "test.bin") == 0); + ASSERT(fetch(&mgr3, buf, url3, + "PUT /fs/files/..%2Fevil.bin HTTP/1.0\nContent-Length: " + "4\n\ndata") == 400); + ASSERT(strcmp(s_uploaded_file, "test.bin") == 0); + ASSERT(fetch(&mgr3, buf, url3, "GET /api/get/files/0 HTTP/1.0\n\n") == 200); + ASSERT(strstr(buf, test_bin_probe) != NULL); + mg_mgr_free(&mgr3); + } } #define DASHBOARD(x) \ diff --git a/tutorials/device-dashboard/events/Makefile b/tutorials/device-dashboard/array/Makefile similarity index 91% rename from tutorials/device-dashboard/events/Makefile rename to tutorials/device-dashboard/array/Makefile index f28dc096..7fb5125e 100644 --- a/tutorials/device-dashboard/events/Makefile +++ b/tutorials/device-dashboard/array/Makefile @@ -5,7 +5,7 @@ TOP ?= ../../.. SOURCES = dashboard.c file_data.c $(TOP)/mongoose.c CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -O0 $(CFLAGS_EXTRA) -PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN +PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN -DMG_DASH_STREAM_BATCH=100 AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment all: $(PROG) diff --git a/tutorials/device-dashboard/array/dashboard.c b/tutorials/device-dashboard/array/dashboard.c new file mode 100644 index 00000000..7d4f86b8 --- /dev/null +++ b/tutorials/device-dashboard/array/dashboard.c @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2026 Cesanta Software Limited +// SPDX-License-Identifier: GPL-2.0-only or commercial + +#include "mongoose.h" + +// Array elements are stored in a dynamically reallocated array +struct entry { + char text[20]; // Modifiable via GUI + bool checked; // Modifiable via GUI +}; + +static struct entry *s_arr; // Backing storage, grown/shrunk via realloc() +static size_t s_count; // Number of elements in s_arr +static struct entry s_entry; // Staging area: holds values for read/write/add +static int s_index; // Current element index, see struct mg_field_set + +static struct mg_field fields_arr[] = { + {"index", MG_VAL_INT, &s_index, sizeof(s_index)}, + {"text", MG_VAL_STR, &s_entry.text, sizeof(s_entry.text)}, + {"checked", MG_VAL_BOOL, &s_entry.checked, sizeof(s_entry.checked)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; + +// Grow s_arr by one slot and append the staged entry. False on OOM +static bool entry_append(void) { + struct entry *arr = + (struct entry *) realloc(s_arr, (s_count + 1) * sizeof(*arr)); + if (arr == NULL) return false; + s_arr = arr; + s_arr[s_count++] = s_entry; + return true; +} + +// Remove the entry at the given index, shifting the tail down and shrinking +// s_arr by one slot. False if the index is out of range +static bool entry_remove(int index) { + if (index < 0 || (size_t) index >= s_count) return false; + memmove(&s_arr[index], &s_arr[index + 1], + (s_count - (size_t) index - 1) * sizeof(*s_arr)); + s_count--; + if (s_count == 0) { + free(s_arr); + s_arr = NULL; + } else { + struct entry *arr = (struct entry *) realloc(s_arr, s_count * sizeof(*arr)); + if (arr != NULL) s_arr = arr; + } + return true; +} + +// Read, write, delete or append an array element. The framework sets s_index +// before the call: a negative value asks for the total count, an +// out-of-range value means "end of iteration" - both are answered by +// setting s_index accordingly. MG_DASH_ADD builds a new entry from the +// staged fields and appends it regardless of s_index +static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op == MG_DASH_READ) { + if (s_index < 0) { + s_index = (int) s_count; + } else if ((size_t) s_index >= s_count) { + s_index = -1; + } else { + s_entry = s_arr[s_index]; + } + return true; + } + if (op == MG_DASH_WRITE && s_index >= 0 && (size_t) s_index < s_count) { + s_arr[s_index] = s_entry; + return true; + } + if (op == MG_DASH_DELETE) return entry_remove(s_index); + if (op == MG_DASH_ADD) return entry_append(); + return false; +} + +static struct mg_field_set field_set_arr = { + "event", fields_arr, arr_fn, &s_index, NULL, NULL, +}; + +void mg_dash_init(struct mg_mgr *mgr) { + static struct mg_dash dash; // Important: keep it static! + static const char *demo[] = {"Wake up", "Make coffee", "Write code", + "Run tests", "Ship it", "Relax", "Sleep"}; + size_t i; + for (i = 0; i < sizeof(demo) / sizeof(demo[0]); i++) { + mg_snprintf(s_entry.text, sizeof(s_entry.text), "%s", demo[i]); + s_entry.checked = (i == 0); + entry_append(); + } + MG_DASH_ADD_FIELD_SET(&dash, &field_set_arr); + mg_mem_files = mg_packed_files; + mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &dash); +} + +void mg_dash_poll(struct mg_mgr *mgr) { + (void) mgr; +} + +// On desktop, build with -DMAIN flag to include main(). +// On embedded system, run this code in your main() function +#ifdef MAIN +int main(void) { + struct mg_mgr mgr; + + mg_mgr_init(&mgr); + mg_dash_init(&mgr); + + for (;;) { + mg_mgr_poll(&mgr, 1); + mg_dash_poll(&mgr); + } + + return 0; +} +#endif diff --git a/tutorials/device-dashboard/array/dashboard.html b/tutorials/device-dashboard/array/dashboard.html new file mode 100644 index 00000000..a04a86c7 --- /dev/null +++ b/tutorials/device-dashboard/array/dashboard.html @@ -0,0 +1,129 @@ + + + + + + Array Dashboard + + + + + + + + +
+

Array read/write/add/delete

+ +
+ + + + + ${(event.start ?? 0) + 1}–${(event.start ?? 0) + event.data.length} of ${event.size ?? 0} +
+ +
+ + + + + + + + + + +
IndexTextDone
${ev.index} + +
+
+ +
+ + + +
+
+ + diff --git a/tutorials/device-dashboard/events/dashboard.c b/tutorials/device-dashboard/events/dashboard.c deleted file mode 100644 index 3cff03cd..00000000 --- a/tutorials/device-dashboard/events/dashboard.c +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Cesanta Software Limited -// SPDX-License-Identifier: GPL-2.0-only or commercial - -#include "mongoose.h" - -struct event { - int index; - char message[100]; -}; - -static struct event s_event; - -static void read_event(void) { - mg_snprintf(s_event.message, sizeof(s_event.message), "my ev %d", - s_event.index); -} - -static void write_event(void) { - // Do nothing. Dashboard sets the s_event.index -} - -static struct mg_field fields_event[] = { - {"index", MG_VAL_INT, &s_event.index, sizeof(s_event.index)}, - {"message", MG_VAL_STR, &s_event.message, sizeof(s_event.message)}, - {NULL, MG_VAL_INT, NULL, 0}, -}; - -static struct mg_field_set field_set_event = { - "event", fields_event, read_event, write_event, 0, 0, NULL, -}; - -void mg_dash_init(struct mg_mgr *mgr) { - static struct mg_dash dash; // Important: keep it static! - MG_DASH_ADD_FIELD_SET(&dash, &field_set_event); - mg_mem_files = mg_packed_files; - mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &dash); -} - -void mg_dash_poll(struct mg_mgr *mgr) { - (void) mgr; -} - -// On desktop, build with -DMAIN flag to include main(). -// On embedded system, run this code in your main() function -#ifdef MAIN -int main(void) { - struct mg_mgr mgr; - - mg_mgr_init(&mgr); - mg_dash_init(&mgr); - - for (;;) { - mg_mgr_poll(&mgr, 1); - mg_dash_poll(&mgr); - } - - return 0; -} -#endif diff --git a/tutorials/device-dashboard/events/dashboard.html b/tutorials/device-dashboard/events/dashboard.html deleted file mode 100644 index c9ca43fc..00000000 --- a/tutorials/device-dashboard/events/dashboard.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - Array Dashboard - - - - - - - - -
-

Array read/write

- -
- - -
- Rows - -
- - -
- -
- - - - - - - - -
IndexMessage
-
-
- - diff --git a/tutorials/device-dashboard/full/dashboard.c b/tutorials/device-dashboard/full/dashboard.c index 6e11a861..7dd23d74 100644 --- a/tutorials/device-dashboard/full/dashboard.c +++ b/tutorials/device-dashboard/full/dashboard.c @@ -9,33 +9,32 @@ static int authenticate(char *user, size_t userlen, const char *pass); // Action buttons static bool s_action1, s_action2; static uint64_t s_action2_timeout; -static void write_actions(void) { - if (s_action2 == true) s_action2_timeout = mg_now() + 750; -} -static void read_actions(void) { - if (s_action2_timeout > mg_now()) s_action2 = true; +static bool actions_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op == MG_DASH_READ) { + if (s_action2_timeout > mg_now()) s_action2 = true; + return true; + } + if (op == MG_DASH_WRITE) { + if (s_action2 == true) s_action2_timeout = mg_now() + 750; + return true; + } + return false; } static struct mg_field fields_actions[] = { {"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)}, {"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)}, {NULL, MG_VAL_INT, NULL, 0}, }; -static struct mg_field_set set_actions = { - "actions", fields_actions, read_actions, write_actions, 0, 0, NULL, -}; +static struct mg_field_set set_actions = {"actions", fields_actions, actions_fn, + NULL, NULL, NULL}; // Control panel // s_led1, s_led2, s_led3 are used to communicate LED status static bool s_led1, s_led2, s_led3; -static void write_leds(void) { - // gpio_write(LED1_PIN, s_led1); - // gpio_write(LED2_PIN, s_led2); - // gpio_write(LED3_PIN, s_led3); -} -static void read_leds(void) { - // s_led1 = gpio_read(LED1_PIN); - // s_led2 = gpio_read(LED2_PIN); - // s_led3 = gpio_read(LED3_PIN); +static bool leds_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + return op == MG_DASH_READ || op == MG_DASH_WRITE; } static struct mg_field fields_leds[] = { {"led1", MG_VAL_BOOL, &s_led1, sizeof(s_led1)}, @@ -43,18 +42,20 @@ static struct mg_field fields_leds[] = { {"led3", MG_VAL_BOOL, &s_led3, sizeof(s_led3)}, {NULL, MG_VAL_INT, NULL, 0}, }; -static struct mg_field_set set_leds = { - "leds", fields_leds, read_leds, write_leds, 0, 0, NULL, -}; +static struct mg_field_set set_leds = {"leds", fields_leds, leds_fn, + NULL, NULL, NULL}; // Read-only device Metrics static int s_ram = 32, s_cpu = 7; static double s_temperature = 24.8; -static void read_metrics(void) { - s_ram = 25 + (rand() % 16); // Sumulate metric change +static bool metrics_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op != MG_DASH_READ) return false; + s_ram = 25 + (rand() % 16); s_cpu = 7 + (rand() % 21); s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0; + return true; } static struct mg_field fields_metrics[] = { @@ -64,9 +65,8 @@ static struct mg_field fields_metrics[] = { {NULL, MG_VAL_INT, NULL, 0}, }; -static struct mg_field_set set_metrics = { - "metrics", fields_metrics, read_metrics, NULL, 0, 0, NULL, -}; +static struct mg_field_set set_metrics = {"metrics", fields_metrics, metrics_fn, + NULL, NULL, NULL}; // Read-write device settings static bool s_enable_login = false; @@ -78,13 +78,18 @@ static char s_ota_version[] = "1.1.2"; static char s_ota_status[40] = "No scans yet"; static char s_ota_url[100] = "https://my-product.com/ota.json"; -static void write_settings(void) { - mg_log_level = s_log_level; - s_dash.authenticate = s_enable_login ? authenticate : NULL; -} - -static void read_settings(void) { - s_log_level = mg_log_level; +static bool settings_fn(enum mg_dash_op op, struct mg_dash_user *u) { + if (u->level < 3) return false; + if (op == MG_DASH_READ) { + s_log_level = mg_log_level; + return true; + } + if (op == MG_DASH_WRITE && u->level >= 7) { + mg_log_level = s_log_level; + s_dash.authenticate = s_enable_login ? authenticate : NULL; + return true; + } + return false; } static struct mg_field fields_settings[] = { @@ -100,8 +105,7 @@ static struct mg_field fields_settings[] = { }; static struct mg_field_set set_settings = { - "settings", fields_settings, read_settings, write_settings, 3, 7, NULL, -}; + "settings", fields_settings, settings_fn, NULL, NULL, NULL}; #define NUM_POINTS_GRAPH1 7 // How many graph1 data points to send @@ -139,9 +143,14 @@ static struct mg_field fields_graph1[] = { {NULL, MG_VAL_INT, NULL, 0}, }; -static struct mg_field_set set_graph1 = { - "graph1", fields_graph1, read_graph1, NULL, 0, 0, NULL, -}; +static bool graph1_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op != MG_DASH_READ) return false; + read_graph1(); + return true; +} +static struct mg_field_set set_graph1 = {"graph1", fields_graph1, graph1_fn, + NULL, NULL, NULL}; #define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1]; @@ -172,9 +181,13 @@ static struct mg_field fields_graph2[] = { {NULL, MG_VAL_INT, NULL, 0}, }; -static struct mg_field_set set_graph2 = { - "graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL, -}; +static bool graph2_fn(enum mg_dash_op op, struct mg_dash_user *u) { + (void) u; + if (op == MG_DASH_READ) read_graph2(); + return op == MG_DASH_READ || op == MG_DASH_WRITE; +} +static struct mg_field_set set_graph2 = {"graph2", fields_graph2, graph2_fn, + NULL, NULL, NULL}; static int authenticate(char *user, size_t userlen, const char *pass) { int level = 0; // Authentication failure @@ -188,7 +201,42 @@ static int authenticate(char *user, size_t userlen, const char *pass) { return level; } +static struct file { + int index; + char name[64]; + size_t size; + uint64_t checksum; +} s_file; +static struct mg_field_set set_files; + +static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) { + (void) u; + mg_snprintf(buf, len, "%s", "/tmp/dashboard"); + return true; +} + +static struct mg_field fields_files[] = { + {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)}, + {"size", MG_VAL_UINT64, &s_file.size, 0}, + {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)}, + {NULL, MG_VAL_INT, NULL, 0}, +}; + +// Custom reader: let the framework fill "name" and "size", then add our +// own field. A real implementation would hash the file instead of +// hardcoding zero +static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) { + if (op != MG_DASH_READ) return false; + if (!mg_dash_dir_read(&set_files, u)) return false; + s_file.checksum = 0; + return true; +} + +static struct mg_field_set set_files = {"files", fields_files, files_fn, + &s_file.index, get_dir, NULL}; + void mg_dash_init(struct mg_mgr *mgr) { + MG_DASH_ADD_FIELD_SET(&s_dash, &set_files); MG_DASH_ADD_FIELD_SET(&s_dash, &set_leds); MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics); MG_DASH_ADD_FIELD_SET(&s_dash, &set_settings); @@ -196,9 +244,14 @@ void mg_dash_init(struct mg_mgr *mgr) { MG_DASH_ADD_FIELD_SET(&s_dash, &set_graph2); MG_DASH_ADD_FIELD_SET(&s_dash, &set_actions); - // Add two fake files - for demonstration - mg_dash_file_add(mg_str("device-config.json"), 1234); - mg_dash_file_add(mg_str("device-log-2026-04-25.txt"), 1327854); +#if MG_ARCH == MG_ARCH_UNIX + // Create demo files in the upload directory + mkdir("/tmp/dashboard", 0755); + mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-config.json", + "{\"model\":\"example\",\"fw\":\"1.0\"}"); + mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-log-2026-04-25.txt", + "2026-04-25 12:00:00 Device started\n"); +#endif mg_mem_files = mg_packed_files; mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dash); diff --git a/tutorials/device-dashboard/full/dashboard.html b/tutorials/device-dashboard/full/dashboard.html index e599e50a..d70318f5 100644 --- a/tutorials/device-dashboard/full/dashboard.html +++ b/tutorials/device-dashboard/full/dashboard.html @@ -16,7 +16,7 @@ leds: { led1: true, led2: false, led3: false }, settings: { volume: 12, log_level: 3, name: 'Galway', enable_login: true, ota_version: '1.2.3', ota_status: 'No checks yet', ota_url: 'http://my-product.com/ota.json', ota_interval: 30 }, metrics: { temperature: 1.2, cpu: 4, ram: 5, version: '1.2.3' }, - files: {data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]}, + files: {start: 0, end: 100, data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]}, }, }); @@ -170,10 +170,12 @@ ]); }; - // Intercept graph change notifications, re-plot data + // Intercept change notifications: re-plot graphs, reload the file + // list whenever its size changes (file uploaded or deleted) Dashboard.on('change', args => { args?.graph1?.data && updateGraph1(args?.graph1?.data); args?.graph2?.data && updateGraph2(args?.graph2?.data); + typeof args?.files === 'number' && Dashboard.call('get', { name: 'files', start: 0, end: 100 }); }); }); @@ -689,7 +691,7 @@ a, a:visited, a:link { text-decoration: none; }
File Manager - ${files.data.length} Files + ${files.size ?? files.data.length} Files
@@ -717,14 +719,14 @@ a, a:visited, a:link { text-decoration: none; }
${f.size}
- + -