Merge pull request #3594 from cesanta/dash

Dash OTA dogfooding nits
This commit is contained in:
Sergey Lyubka authored and GitHub committed 2026-06-09 12:20:50 +01:00
commit 73f4e39b19
29 files changed
+2635 -1478

No files matched your search

+35 -30
View File
@@ -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. It is developed by Cesanta (https://cesanta.com) and available at https://mongoose.ws.
License: GPLv2 or commercial. 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 ## Integration
Copy `mongoose.h` and `mongoose.c` into a `mongoose/` subdirectory of the 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 └── 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 ```c
#define MG_ARCH MG_ARCH_CUBE #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 ## Core API
```c ```c
@@ -80,7 +84,7 @@ void handler(struct mg_connection *c, int ev, void *ev_data) {
## TCP/IP stack — set exactly one ## 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 | | Define | Use when |
|--------|----------| |--------|----------|
@@ -216,7 +220,11 @@ for (;;) {
- Do **not** add vanilla JS event listeners, `fetch()` calls, or custom - Do **not** add vanilla JS event listeners, `fetch()` calls, or custom
reactive logic to `dashboard.html`. reactive logic to `dashboard.html`.
- Do **not** modify `dashboard.html` unless the user explicitly asks. - 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 ```html
<!-- checkbox bound to a device field, auto-saves on change --> <!-- checkbox bound to a device field, auto-saves on change -->
@@ -322,35 +330,20 @@ static struct mg_field fields_leds[] = {
}; };
``` ```
In order to simulate array of elements, use an integer field as an index. Array field sets are recognised by the "index" pointer in the
Reader first writes an index of the element to read, and then it reads field set descriptor:
an element. // 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 ```c
struct event { static struct event {
int index; int index;
char message[100]; char message[100];
}; } s_event;
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 = { 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). 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 ## Key rules for AI code generation
- **Two files only**: always use `mongoose.h` + `mongoose.c`. Never add a - **Two files only**: always use `mongoose.h` + `mongoose.c`. Never add a
+644 -355
View File
File diff suppressed because it is too large. Load diff
+151 -49
View File
@@ -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 (*fn)(struct mg_connection *, const char *));
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *)); 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); 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); 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 *); 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 { enum mg_val_type {
MG_VAL_INT, MG_VAL_INT, // int
MG_VAL_BOOL, MG_VAL_UINT64, // uint64_t
MG_VAL_DBL, MG_VAL_BOOL, // bool
MG_VAL_STR, MG_VAL_DBL, // double
MG_VAL_RAW, 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 { struct mg_field {
const char *name; const char *name; // JSON key
enum mg_val_type type; enum mg_val_type type; // C type of the value, see enum mg_val_type
void *value; void *value; // Pointer to the bound variable
size_t value_size; 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 { struct mg_field_set {
const char *name; const char *name; // Set name, used as a JSON key
struct mg_field *fields; struct mg_field *fields; // Zero-terminated array of fields
void (*reader)(void); bool (*fn)(enum mg_dash_op op,
void (*writer)(void); struct mg_dash_user *u); // Read/write/delete/add callback
int read_level; int *index; // Non-NULL: array set. *index is set before fn(READ) call;
int write_level; // fn sets *index = -1 to signal end of iteration.
struct mg_field_set *next; // 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 {
struct mg_dash_custom_handler *next; struct mg_dash_custom_handler *next; // Next handler in the list
struct mg_str uri_pattern; struct mg_str uri_pattern; // URI to match, see mg_match()
mg_event_handler_t handler; mg_event_handler_t handler; // Event handler function
void *handler_data; void *handler_data; // Opaque data for the handler
};
struct mg_dash_file {
struct mg_dash_file *next;
char *name;
size_t size;
}; };
// Dashboard instance: fields, users and handlers served over HTTP and WS
struct mg_dash { struct mg_dash {
struct mg_field_set *sets; struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
struct mg_dash_custom_handler *custom_handlers; 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 (*authenticate)(char *user, size_t userlen, const char *pass);
int session_auto_expiration_seconds; int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
//struct mg_dash_file *files; 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_) \ #define MG_DASH_ADD_FIELD_SET(dash_, set_) \
@@ -3339,28 +3385,50 @@ struct mg_dash {
(dash_)->custom_handlers = &ch_; \ (dash_)->custom_handlers = &ch_; \
} while (0) } while (0)
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 // Dashboard event handler. Pass to mg_http_listen() as the fn argument
#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);
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); 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 *); 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 // Helper forward declarations for Mongoose CMSIS pack modules
extern struct mg_mgr g_mgr; extern struct mg_mgr g_mgr;
extern void mg_dash_init(struct mg_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, void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
const char *metadata_url, void (*fn)(const char *status)); 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:<MG_OTA_FIRMWARE_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 *);
+201 -82
View File
@@ -3,19 +3,13 @@
(function (global) { (function (global) {
"use strict"; "use strict";
const Rpc = (function() { const Watch = (function() {
function Rpc(url, onev) { function Watch(url, onev, onmsg) {
let ws, id = 1, oncall, q = [], reconnect = true; let ws, 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 connect = () => { const connect = () => {
ws = new WebSocket(url); ws = new WebSocket(url);
ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); }; ws.onopen = () => onev && onev('open');
ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); }; ws.onclose = () => { onev && onev('close'); reconnect && setTimeout(connect, 1000); };
ws.onmessage = e => { ws.onmessage = e => {
onev && onev('message', e.data); onev && onev('message', e.data);
let m; let m;
@@ -25,69 +19,92 @@
console.log('INVALID JSON:', e.data); console.log('INVALID JSON:', e.data);
return; return;
} }
onmsg && onmsg(m);
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) }
}));
}
}; };
}; };
connect(); connect();
return { return {
close: () => { reconnect = false; ws?.close(); }, 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; } get ws() { return ws; }
}; };
} }
return Rpc; return Watch;
})(); })();
function rpc_connect() { function fetch_and_ready() {
if (isMock) { if (isMock) {
setTimeout(() => userhandlers['ready']?.(), 500); return delay(500).then(rescan).then(mark_ready);
return;
} }
if (rpc) return rpc; return fetch('api/get')
//if (settings.auth && !status.authed) return null; .then(r => r.ok ? r.json() : Promise.reject(r.status))
rpc = Rpc('api/websocket', function (evname, args) { .then(data => {
if (evname == 'open') status.online = true, rescan(); for (const k in data) {
if (evname == 'close') status.online = false, rescan(); if (typeof data[k] === 'number') {
if (settings.debug) console.log('WS', evname, args); const prev = settings.data[k];
}); const arr = prev && typeof prev === 'object' ? prev : {};
rpc.handle((method, args) => { data[k] = {
if (method == 'change' && args) apply_and_rescan(settings.data, args); ...arr,
if (method == 'logout') rpc_disconnect(); size: data[k],
if (userhandlers[method]) userhandlers[method](args); start: typeof arr.start === 'number' ? arr.start : 0,
}); data: Array.isArray(arr.data) ? arr.data : [],
return rpc; };
}
}
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() { function ws_connect() {
if (!rpc) return; if (isMock || ws) return;
rpc.close(); ws = Watch('api/websocket', function (evname, args) {
rpc = null; 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; status.online = false;
} }
function login(params) { function login(params) {
if (isMock) { rpc_connect(); return Promise.resolve({}); } if (isMock) return fetch_and_ready().then(() => ({}));
const opts = {}; const opts = {};
if (params) { if (params) {
const username = (params?.username ?? '').trim(); const username = (params?.username ?? '').trim();
@@ -100,19 +117,18 @@
.then(resp => { .then(resp => {
status.username = resp?.user || ''; status.username = resp?.user || '';
status.userlevel = resp?.level || 0; status.userlevel = resp?.level || 0;
rpc_connect(); return fetch_and_ready().then(() => resp);
return resp;
}); });
}; };
function logout() { function logout() {
if (isMock) { rpc_disconnect(); return Promise.resolve({}); } if (isMock) { ws_disconnect(); return Promise.resolve({}); }
return fetch('api/logout') return fetch('api/logout')
.catch(() => true) .catch(() => true)
.then(() => { .then(() => {
status.username = ''; status.username = '';
status.userlevel = 0; status.userlevel = 0;
rpc_disconnect(); ws_disconnect();
return true; return true;
}); });
} }
@@ -122,7 +138,27 @@
const settings = {data: {}, edits: {}, debug: true}; const settings = {data: {}, edits: {}, debug: true};
const userhandlers = {}; // User event handlers const userhandlers = {}; // User event handlers
const status = { online: false, username: '', userlevel: 0, ready: false }; 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 fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value);
const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj); const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj);
@@ -222,7 +258,11 @@
function save(key) { function save(key) {
const changes = {}; const changes = {};
set(changes, key, get(settings.edits, key)); 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 // Cancel the edits for keys that match pattern
@@ -231,6 +271,14 @@
rescan(); 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) { function handle_save(el, key) {
el.disabled = !edited(key); el.disabled = !edited(key);
if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true; if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true;
@@ -244,18 +292,30 @@
function handle_repeat(el, key, context) { function handle_repeat(el, key, context) {
if (!has(context, key)) { console.error('EVAL', key, context); return; } if (!has(context, key)) { console.error('EVAL', key, context); return; }
if (!el.orig) el.orig = el.children[0].cloneNode(true); if (!el.orig) el.orig = el.children[0].cloneNode(true);
const v = get(context, key), v2 = JSON.stringify(v); const v = get(context, key);
if (el.v2 === v2) return;
const frag = document.createDocumentFragment();
const it = el.dataset.iterator ?? '__obj'; const it = el.dataset.iterator ?? '__obj';
v.forEach(function (item, index) { const json = v.map(item => JSON.stringify(item));
const child = el.orig.cloneNode(true); const prev = el.itemsJson || [];
const ctx = { ...context, [it]: item }; if (json.length === prev.length && el.children.length === json.length) {
process(child, ctx, true); // Same length: re-template each changed row's existing DOM node in
frag.appendChild(child); // place instead of replacing it. Untouched rows are left completely
}); // alone, and changed rows keep their live elements too - so bound
el.replaceChildren(frag); // properties (e.g. a checkbox's `checked`) change on an
el.v2 = v2; // 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) { function upload(el, is_ota) {
@@ -267,7 +327,7 @@
const reader = new FileReader(); const reader = new FileReader();
reader.readAsArrayBuffer(f); reader.readAsArrayBuffer(f);
reader.onload = function () { 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; let begin = Date.now(), body = reader.result, ok = false;
// console.log(2, url, body); // console.log(2, url, body);
fetch(url, { method: 'POST', body }) fetch(url, { method: 'POST', body })
@@ -295,7 +355,8 @@
} }
function process(root, context, x) { 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]; const els = root.nodeType === Node.DOCUMENT_NODE ? all : [root, ...all];
els.forEach(function (el) { els.forEach(function (el) {
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return; if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
@@ -309,14 +370,16 @@
} }
for (const attr of el.attributes) { for (const attr of el.attributes) {
const cv = attr.value || ''; const cv = attr.value || '';
if (attr.originalValue || cv.includes('${')) { if (!el.originalAttrs) el.originalAttrs = {};
if (!attr.originalValue) attr.originalValue = cv; if (el.originalAttrs[attr.name] || cv.includes('${')) {
const v = substituteExpressions(attr.originalValue, context); 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; if (v !== attr.value) attr.value = v;
} }
} }
const handlers = { const handlers = {
bind: handle_bind, bind: handle_bind,
checked: handle_checked,
save: handle_save, save: handle_save,
cancel: handle_cancel, cancel: handle_cancel,
repeat: handle_repeat, repeat: handle_repeat,
@@ -341,21 +404,75 @@
function rescan() { function rescan() {
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", realrescan, { once: true }); return new Promise(resolve => {
document.addEventListener("DOMContentLoaded", () => {
realrescan();
resolve();
}, { once: true });
});
} else { } else {
realrescan(); realrescan();
return Promise.resolve();
} }
}; };
const delay = (ms, value) => new Promise(r => setTimeout(() => r(value), ms)); 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) { function call(name, args) {
if (isMock) { if (isMock) {
return delay(750, true).then(val => console.log('response', name)); return delay(750, true).then(() => console.log('response', name));
} else {
return rpc.call(name, args)
.catch(err => console.log('CALL FAILED', name, args, err));
} }
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) { function apply(target, patch) {
@@ -373,16 +490,18 @@
function apply_and_rescan(target, patch) { function apply_and_rescan(target, patch) {
apply(target, patch); apply(target, patch);
prune(settings.data, settings.edits); prune(settings.data, settings.edits);
rescan(); return rescan();
}; };
function on(name, fn) { function on(name, fn) {
userhandlers[name] = fn; userhandlers[name] = fn;
if (name === 'ready' && status.ready) setTimeout(fn, 0);
}; };
function init(conf) { function init(conf) {
install_ready_gate();
apply_and_rescan(settings, conf); apply_and_rescan(settings, conf);
if (!settings.auth) rpc_connect(); if (!settings.auth) fetch_and_ready();
}; };
global.Dashboard = { init, call, on, status, login, logout }; global.Dashboard = { init, call, on, status, login, logout };
+1 -1
View File
@@ -62,7 +62,7 @@ $(foreach T,$(RP_TARGETS), $(foreach P,$(PROJECTS), $(eval $(call RP_DASHBOARD_R
%.zip: % %.zip: %
zip -qr $@ $< zip -qr $@ $<
upload: $(ZIPS) deploy: $(ZIPS)
scp $(ZIPS) h1:/data/downloads/ scp $(ZIPS) h1:/data/downloads/
clean: clean:
+52 -35
View File
@@ -5,7 +5,7 @@
// inlining referenced CSS, JS, images, and icons. // inlining referenced CSS, JS, images, and icons.
// //
// Usage: // Usage:
// node html2c.js [-o OUTPUT.c] HTML_FILE // node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@@ -15,24 +15,25 @@ const zlib = require('zlib');
const args = process.argv.slice(2); const args = process.argv.slice(2);
let outputFile = null; let outputFile = null;
let inputFile = null; const inputFiles = [];
const useMap = new Map();
for (let i = 0; i < args.length; i++) { for (let i = 0; i < args.length; i++) {
if (args[i] === '-o' && i + 1 < args.length) { if (args[i] === '-o' && i + 1 < args.length) {
outputFile = args[++i]; outputFile = args[++i];
} else if (!args[i].startsWith('-') && inputFile === null) { } else if (args[i] === '--use' && i + 1 < args.length) {
inputFile = args[i]; 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) { if (inputFiles.length === 0) {
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE'); console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]');
process.exit(1); process.exit(1);
} }
const baseDir = path.dirname(path.resolve(inputFile));
const html = fs.readFileSync(inputFile, 'utf8');
function fetchUrl(url) { function fetchUrl(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const client = url.startsWith('https') ? https : http; 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://')) { if (name.startsWith('http://') || name.startsWith('https://')) {
const data = await fetchUrl(name); const data = await fetchUrl(name);
return encoding ? data.toString(encoding) : data; 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) { function attr(match, name) {
@@ -66,21 +70,21 @@ function replaceAttr(match, name, value) {
return match.replace(re, `${name}="${value}"`); return match.replace(re, `${name}="${value}"`);
} }
async function inlineCss(match) { async function inlineCss(match, baseDir) {
const href = attr(match, 'href'); const href = attr(match, 'href');
if (!href) return match; if (!href) return match;
const css = await readAsset(href, 'utf8'); const css = await readAsset(href, 'utf8', baseDir);
return `<style>${css}</style>`; return `<style>${css}</style>`;
} }
async function inlineJs(match) { async function inlineJs(match, baseDir) {
const src = attr(match, 'src'); const src = attr(match, 'src');
if (!src) return match; if (!src) return match;
const js = await readAsset(src, 'utf8'); const js = await readAsset(src, 'utf8', baseDir);
return `<script>${js}</script>`; return `<script>${js}</script>`;
} }
async function inlineImg(match) { async function inlineImg(match, baseDir) {
const src = attr(match, 'src'); const src = attr(match, 'src');
if (!src) return match; if (!src) return match;
const ext = path.extname(src).toLowerCase(); const ext = path.extname(src).toLowerCase();
@@ -91,7 +95,7 @@ async function inlineImg(match) {
else if (ext === '.webp') mime = 'image/webp'; else if (ext === '.webp') mime = 'image/webp';
else if (ext === '.ico') mime = 'image/x-icon'; else if (ext === '.ico') mime = 'image/x-icon';
const data = await readAsset(src); const data = await readAsset(src, null, baseDir);
if (ext === '.svg') { if (ext === '.svg') {
const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data; const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data;
return replaceAttr( return replaceAttr(
@@ -107,10 +111,10 @@ async function inlineImg(match) {
); );
} }
async function inlineSvg(match) { async function inlineSvg(match, baseDir) {
const href = attr(match, 'href'); const href = attr(match, 'href');
if (!href || !href.toLowerCase().endsWith('.svg')) return match; if (!href || !href.toLowerCase().endsWith('.svg')) return match;
const data = await readAsset(href); const data = await readAsset(href, null, baseDir);
return replaceAttr( return replaceAttr(
match, match,
'href', 'href',
@@ -118,7 +122,7 @@ async function inlineSvg(match) {
); );
} }
async function processHtml(content) { async function processHtml(content, baseDir) {
let result = content; let result = content;
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g; const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g; const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
@@ -126,16 +130,16 @@ async function processHtml(content) {
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g; const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
for (const match of content.match(cssRegex) || []) { 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) || []) { 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) || []) { 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) || []) { 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; return result;
@@ -145,30 +149,43 @@ function cArray(data) {
return Array.from(Buffer.from(data)).concat(0).join(','); return Array.from(Buffer.from(data)).concat(0).join(',');
} }
function generateC(data) { function generateC(files) {
const destination = `${path.basename(inputFile)}.gz`; const vars = files.map(({inputFile, data}, i) => {
const stat = fs.statSync(inputFile); const zipped = zlib.gzipSync(data);
const mtime = parseInt(stat.mtimeMs / 1000); return {inputFile, zipped};
const zipped = zlib.gzipSync(data); });
const bytes = cArray(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: return `// DO NOT EDIT. This file is generated using this command:
// ${process.argv.join(' ')} // ${process.argv.join(' ')}
#include "mongoose.h" #include "mongoose.h"
static const unsigned char v0[] = {${bytes}}; ${decls}
const struct mg_mem_file mg_packed_files[] = { const struct mg_mem_file mg_packed_files[] = {
{"/${destination}", v0, sizeof(v0) - 1, ${mtime}}, // size: ${zipped.length} ${entries}
{NULL, NULL, 0, 0} {NULL, NULL, 0, 0}
}; };
`; `;
} }
processHtml(html) Promise.all(inputFiles.map(inputFile => {
.then(result => { const baseDir = path.dirname(path.resolve(inputFile));
const c = generateC(result); const html = fs.readFileSync(inputFile, 'utf8');
return processHtml(html, baseDir).then(data => ({inputFile, data}));
}))
.then(files => {
const c = generateC(files);
if (outputFile) { if (outputFile) {
fs.writeFileSync(outputFile, c); fs.writeFileSync(outputFile, c);
} else { } else {
-157
View File
@@ -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 `<style>${css}</style>`;
}
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 `<script>${js}</script>`;
}
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 = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
const imgRegex = /<img[^>]+src=["'][^"']+["'][^>]*>/g;
const svgRegex = /<link[^>]+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);
});
-43
View File
@@ -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}
};
`);
+2 -2
View File
@@ -70,8 +70,8 @@ function sign(firmware, privateKeyFile = 'private.pem') {
? firmware.slice(0, dot) + '.signed' + firmware.slice(dot) ? firmware.slice(0, dot) + '.signed' + firmware.slice(dot)
: firmware + '.signed'; : firmware + '.signed';
fs.writeFileSync(out, Buffer.concat([fw, raw])); fs.writeFileSync(out, Buffer.concat([fw, raw, Buffer.from('MGSG')]));
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature)`); console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature + 4 bytes magic)`);
} }
const [,, cmd, arg1, arg2] = process.argv; const [,, cmd, arg1, arg2] = process.argv;
+487 -245
View File
@@ -5,67 +5,21 @@
#define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" #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 #define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS
struct mg_dash_user { #define CONN_HANDLED 'Z'
struct mg_dash_user *next;
char name[32]; // User name struct mg_dash_cdata {
char token[21]; // Login token char marker;
int level; // Access level struct mg_dash_user *u;
uint64_t expire; // Expiration timestamp 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 static struct mg_str trimq(struct mg_str s) { // Trim double quotes
if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++; if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++;
return s; 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, static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash,
struct mg_str name) { struct mg_str name) {
struct mg_field_set *fs; 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"); n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
} else if (f->type == MG_VAL_INT) { } else if (f->type == MG_VAL_INT) {
n += mg_xprintf(fn, arg, "%d", *(int *) f->value); 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) { } else if (f->type == MG_VAL_DBL) {
n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value); n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value);
} else if (f->type == MG_VAL_STR) { } 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; 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 *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 *); 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); struct mg_field_set *set = mg_dash_find_field_set(dash, *name);
size_t n = 0; size_t n = 0;
if (name->len == 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 = ""; const char *comma = "";
n += mg_xprintf(fn, arg, "{"); n += mg_xprintf(fn, arg, "{");
for (fs = dash->sets; fs != NULL; fs = fs->next) { for (fs = dash->sets; fs != NULL; fs = fs->next) {
if (fs->read_level > 0 && level < fs->read_level) continue; if (fs->index != NULL) {
if (fs->reader) fs->reader(); int sz = mg_dash_array_size(fs, u);
n += mg_xprintf(fn, arg, comma); if (sz < 0) continue;
n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name)); n += mg_xprintf(fn, arg, comma);
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs); 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 = ","; comma = ",";
} }
n += mg_xprintf(fn, arg, "}"); n += mg_xprintf(fn, arg, "}");
} else if (set != NULL && } else if (set != NULL && (set->fn == NULL || set->fn(MG_DASH_READ, u))) {
(set->read_level <= 0 || level >= set->read_level)) { if (set->index != NULL && from_str != NULL && from_str->len > 0) {
if (set->reader) set->reader(); int from = 0, to = 0;
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set); 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 { } else {
n += mg_xprintf(fn, arg, "null"); n += mg_xprintf(fn, arg, "null");
} }
return n; 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) { void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) {
if (set->reader) set->reader(); struct mg_connection *c;
mg_dash_broadcast(mgr, set->read_level, "{%m:%m,%m:{%m:%M}}", for (c = mgr->conns; c != NULL; c = c->next) {
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"), struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
MG_ESC(set->name), mg_print_field_set, set); 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) { 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; *(int *) f->value = (int) d;
ok = true; 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) { } else if (f->type == MG_VAL_DBL) {
ok = f->value_size == sizeof(double) && ok = f->value_size == sizeof(double) &&
mg_json_get_num(json, json_path, (double *) f->value); 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, 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; struct mg_str key, val;
size_t ofs = 0; size_t ofs = 0;
int total_count = 0; int total_count = 0;
while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) { while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) {
struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key)); struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key));
int count = 0; 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; size_t i;
for (i = 0; set->fields[i].name != NULL; i++) { for (i = 0; set->fields[i].name != NULL; i++) {
if (mg_dash_parse_field(val, &set->fields[i])) count++; if (mg_dash_parse_field(val, &set->fields[i])) count++;
} }
if (count) { }
if (set->writer) set->writer(); if (count) {
mg_dash_send_change(c->mgr, set); if (set->fn) set->fn(MG_DASH_WRITE, u); // apply side effects
total_count += count; mg_dash_send_change(c->mgr, set);
} total_count += count;
} else if (set == NULL) {
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
} }
} }
return total_count; return total_count;
} }
static void mg_dash_process_msg(struct mg_connection *c, bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) {
struct mg_ws_message *wm, char dir[256], fname[128] = "";
struct mg_dash *dash) { struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix;
struct mg_str req = wm->data; struct mg_field *name_field = NULL, *size_field = NULL;
struct mg_str method = trimq(mg_json_get_tok(req, "$.method")); size_t i;
int level = *(int *) c->data;
if (mg_match(method, mg_str("get"), NULL)) { if (!set->get_dir(u, dir, sizeof(dir))) return false;
mg_dash_success(c, req, "%M", mg_dash_print, &req, dash, level);
} else if (mg_match(method, mg_str("set"), NULL)) { for (i = 0; set->fields[i].name != NULL; i++) {
struct mg_str params = trimq(mg_json_get_tok(req, "$.params")); if (name_field == NULL && set->fields[i].type == MG_VAL_STR &&
int count = mg_dash_apply(c, dash, params, level); strcmp(set->fields[i].name, "name") == 0)
mg_dash_success(c, req, "%d", count); name_field = &set->fields[i];
} else { if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0)
mg_dash_error(c, req, "%s", "unknown method"); 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) { static bool mg_dash_set_file_name(struct mg_field_set *set,
struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f)); struct mg_str name) {
f->name = mg_strdup(name).buf; size_t i;
f->size = size; for (i = 0; set->fields[i].name != NULL; i++) {
f->next = mg_dash_files; struct mg_field *f = &set->fields[i];
mg_dash_files = f; 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);
void mg_dash_file_del(struct mg_str name) { return true;
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;
} }
} }
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, static inline void mg_log_http_req(struct mg_connection *c,
struct mg_http_message *hm) { struct mg_http_message *hm) {
int len = 0; int len = 0;
size_t n, spaces = 0; size_t n, spaces = 0, body_n = hm->body.len;
struct mg_http_message tmp; struct mg_http_message tmp;
memset(&tmp, 0, sizeof(tmp)); memset(&tmp, 0, sizeof(tmp));
len = mg_http_parse((char *) c->send.buf, c->send.len, &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] == '\r' ||
c->send.buf[c->send.len - spaces - 1] == '\n')) c->send.buf[c->send.len - spaces - 1] == '\n'))
spaces++; spaces++;
MG_DEBUG(("%lu %.*s %.*s %.*s: %lu %.*s -> %lu %.*s", c->id, hm->method.len, // hm->body.len comes from Content-Length and can be larger than the bytes
hm->method.buf, hm->uri.len, hm->uri.buf, c->send.len > 15 ? 3 : 0, // actually buffered so far (e.g. mid-stream uploads); cap the preview to
&c->send.buf[9], hm->body.len, hm->body.len, hm->body.buf, // what's actually present in c->recv, or we'd read past its end
c->send.len - n, c->send.len - n - spaces, c->send.buf + n)); {
} char *recv_end = (char *) c->recv.buf + c->recv.len;
if (hm->body.buf >= (char *) c->recv.buf && hm->body.buf <= recv_end) {
static struct mg_str mg_dash_file_name(struct mg_http_message *hm) { size_t avail = (size_t) (recv_end - hm->body.buf);
struct mg_str name = mg_str_n(hm->uri.buf + 4, hm->uri.len - 4); // - /fs/ if (body_n > avail) body_n = avail;
// Decode file name in-place - directly into the request buffer } else {
int len = mg_url_decode(name.buf, name.len, name.buf, name.len + 1, 0); body_n = 0;
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len; }
return name; }
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) { 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) { if (errmsg) {
mg_http_reply(c, 500, NULL, "%s\n", errmsg); mg_http_reply(c, 500, NULL, "%s\n", errmsg);
} else { } else {
char path[128]; // mg_http_start_upload() repurposes c->data for its own bookkeeping,
size_t size = 0; // so the field set can't be cached there. Re-derive the dashboard from
mg_snprintf(path, sizeof(path), "/tmp/%s", c->data); // c->fn_data instead, and notify every file-backed array: the upload
mg_fs_posix.st(path, &size, NULL); // could belong to any of them, and re-querying get_dir() per recipient
mg_dash_file_add(mg_str(c->data), size); // 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_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; 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; 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 // 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) { struct mg_dash *dash) {
static struct mg_dash_user *s_users; // List of authenticated users static struct mg_dash_user *s_users; // List of authenticated users
char user[100], pass[100]; char user[100], pass[100];
static struct mg_dash_user admin = {NULL, "admin", "admin", 9, (uint64_t) -1}; struct mg_dash_user *u, *tmp;
struct mg_dash_user *u, *tmp, *result = NULL; 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)); 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, // MG_DEBUG(("user [%s], pass: [%s], h: %.*s", user, pass, hm->head.len,
// hm->head.buf)); // 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) { for (u = s_users; u != NULL; u = tmp) {
tmp = u->next; tmp = u->next;
if (u->expire < mg_millis()) { if (u->expire < mg_millis()) {
struct mg_connection *conn;
MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level, MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level,
u->expire, mg_millis() - u->expire)); 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); LIST_DELETE(struct mg_dash_user, &s_users, u);
mg_free(u); mg_free(u);
} }
} }
if (pass[0] != '\0') { if (pass[0] == '\0') return NULL;
struct mg_str *ah = mg_http_get_header(hm, "Authorization");
if (ah != NULL) { for (u = s_users; u != NULL; u = u->next) num_users++;
// Auth header and password are set, auth by user/password via glue API if (ah == NULL) {
int num_users = 0, level = dash->authenticate(user, sizeof(user), pass); u = mg_dash_find_token(s_users, pass);
MG_DEBUG(("user %s, level: %d", user, level)); if (u != NULL) {
if (level > 0) { // Proceed only if the firmware authenticated us mg_dash_refresh_user(dash, u);
for (u = s_users; u != NULL && result == NULL; return u;
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;
}
}
} }
} }
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")); // 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) { 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"); 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/<set>/<index>": modify one array element. Loads the
// element at <index> 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/<set>": 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) { 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 *dash = (struct mg_dash *) c->fn_data;
struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
if (ev == MG_EV_OPEN) { if (ev == MG_EV_OPEN) {
if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) { d->dash = dash;
MG_DASH_ADD_FIELD_SET(dash, &set_files);
}
// c->is_hexdumping = 1; // 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 // Received headers - check authentication and possibly start uploads/ota
struct mg_http_message *hm = (struct mg_http_message *) ev_data; 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) || if (mg_match(hm->uri, mg_str("/api/hi"), NULL) ||
mg_match(hm->uri, mg_str("/api/logout"), NULL)) { mg_match(hm->uri, mg_str("/api/logout"), NULL)) {
// Do nothing, handle them MG_EV_HTTP_MSG. We bypass auth for those // 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)) { } else if (u == NULL && mg_match(hm->uri, mg_str("/api/#"), NULL)) {
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n"); 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) { } else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) {
mg_handle_login(c, u); 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)) { } else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) {
mg_http_start_ota(c, hm, mg_dash_ota_cb); 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("POST")) == 0 ||
mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) {
struct mg_str name = mg_dash_file_name(hm); struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
mg_snprintf(c->data, sizeof(c->data), "%.*s", (int) name.len, name.buf); struct mg_str name = parts[1];
mg_http_start_upload(c, hm, name, mg_str("/tmp"), &mg_fs_posix, int len =
mg_dash_upload_cb); 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); if (d->marker != '\0') mg_log_http_req(c, hm);
} else if (ev == MG_EV_HTTP_MSG && c->data[0] != '\0') { } else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') {
// The response has been send in EV_HDRS path, so we're not reponding // 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. // anything but clearing the marker for the next request.
c->data[0] = 0; d->marker = 0;
c->is_resp = 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_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);
int level = u == NULL ? 0 : u->level;
struct mg_str parts[5]; struct mg_str parts[5];
memset(parts, 0, sizeof(parts)); 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_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"),
MG_ESC("logout")); MG_ESC("logout"));
} else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) { } else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) {
*(int *) c->data = level; d->u = u;
mg_ws_upgrade(c, hm, NULL); mg_ws_upgrade(c, hm, NULL);
} else if (mg_match(hm->uri, mg_str("/fs/#"), NULL)) { } else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) {
struct mg_str name = mg_dash_file_name(hm); struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
char path[128]; if (set == NULL || set->get_dir == NULL) {
mg_snprintf(path, sizeof(path), "/tmp/%.*s", name.len, name.buf); mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { } else if (u == NULL) {
// Delete file mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
mg_dash_file_del(name);
mg_dash_send_change(c->mgr, &set_files);
mg_http_reply(c, 200, NULL, "true");
} else { } else {
// Serve file char dir[256], path[512];
mg_http_serve_file(c, hm, path, NULL); 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_match(hm->uri, mg_str("/api/get"), NULL)) {
mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash, mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_endpoint,
&parts[0], level); dash, u, &parts[0], &parts[1], &parts[2]);
} else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) { } 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); mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
} else if (mg_match(hm->uri, mg_str("/"), NULL)) { } else if (mg_match(hm->uri, mg_str("/"), NULL)) {
struct mg_http_serve_opts opts; 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"); if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
mg_log_http_req(c, hm); 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);
}
} }
} }
+116 -49
View File
@@ -7,50 +7,95 @@
#include "str.h" #include "str.h"
#include "ws.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 { enum mg_val_type {
MG_VAL_INT, MG_VAL_INT, // int
MG_VAL_BOOL, MG_VAL_UINT64, // uint64_t
MG_VAL_DBL, MG_VAL_BOOL, // bool
MG_VAL_STR, MG_VAL_DBL, // double
MG_VAL_RAW, 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 { struct mg_field {
const char *name; const char *name; // JSON key
enum mg_val_type type; enum mg_val_type type; // C type of the value, see enum mg_val_type
void *value; void *value; // Pointer to the bound variable
size_t value_size; 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 { struct mg_field_set {
const char *name; const char *name; // Set name, used as a JSON key
struct mg_field *fields; struct mg_field *fields; // Zero-terminated array of fields
void (*reader)(void); bool (*fn)(enum mg_dash_op op,
void (*writer)(void); struct mg_dash_user *u); // Read/write/delete/add callback
int read_level; int *index; // Non-NULL: array set. *index is set before fn(READ) call;
int write_level; // fn sets *index = -1 to signal end of iteration.
struct mg_field_set *next; // 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 {
struct mg_dash_custom_handler *next; struct mg_dash_custom_handler *next; // Next handler in the list
struct mg_str uri_pattern; struct mg_str uri_pattern; // URI to match, see mg_match()
mg_event_handler_t handler; mg_event_handler_t handler; // Event handler function
void *handler_data; void *handler_data; // Opaque data for the handler
};
struct mg_dash_file {
struct mg_dash_file *next;
char *name;
size_t size;
}; };
// Dashboard instance: fields, users and handlers served over HTTP and WS
struct mg_dash { struct mg_dash {
struct mg_field_set *sets; struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
struct mg_dash_custom_handler *custom_handlers; 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 (*authenticate)(char *user, size_t userlen, const char *pass);
int session_auto_expiration_seconds; int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
//struct mg_dash_file *files; 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_) \ #define MG_DASH_ADD_FIELD_SET(dash_, set_) \
@@ -69,28 +114,50 @@ struct mg_dash {
(dash_)->custom_handlers = &ch_; \ (dash_)->custom_handlers = &ch_; \
} while (0) } while (0)
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32 // Dashboard event handler. Pass to mg_http_listen() as the fn argument
#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);
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data); 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 *); 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 // Helper forward declarations for Mongoose CMSIS pack modules
extern struct mg_mgr g_mgr; extern struct mg_mgr g_mgr;
extern void mg_dash_init(struct mg_mgr *); extern void mg_dash_init(struct mg_mgr *);
+16 -9
View File
@@ -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, MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size,
size, ok ? "ok" : "fail")); size, ok ? "ok" : "fail"));
#ifdef MG_OTA_PUBLIC_KEY #ifdef MG_OTA_PUBLIC_KEY
if (ok && s_size > 64) { if (ok) {
static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY; bool signed_fw = s_size > 68 &&
uint8_t hash[32]; memcmp((uint8_t *) base + s_size - 4, "MGSG", 4) == 0;
size_t fw_size = s_size - 64; if (signed_fw) {
mg_sha256(hash, (uint8_t *) base, fw_size); static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY;
ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash), uint8_t hash[32];
(uint8_t *) base + fw_size, size_t fw_size = s_size - 68; // strip 64-byte sig + 4-byte magic
mg_uecc_secp256r1()) == 1; mg_sha256(hash, (uint8_t *) base, fw_size);
MG_INFO(("Signature: %s", ok ? "ok" : "fail")); 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 #endif
s_size = 0; s_size = 0;
+8 -3
View File
@@ -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; } if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else mg_ota_end(); else mg_ota_end();
p->fn(c, "write error"); 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) { } else if (p->received >= p->expected) {
const char *errmsg = NULL; const char *errmsg = NULL;
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else if (!mg_ota_end()) errmsg = "OTA finalize failed"; else if (!mg_ota_end()) errmsg = "OTA finalize failed";
p->fn(c, errmsg); 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) { } else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
else mg_ota_end(); else mg_ota_end();
p->fn(c, ev == MG_EV_ERROR ? (const char *) ev_data : "connection closed"); 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; (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"); 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, void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
struct mg_str name, struct mg_str dir, struct mg_str name, struct mg_str dir,
struct mg_fs *fs, 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->fd = fd;
p->fn = fn; p->fn = fn;
c->fn = mg_upload_handler; c->fn = mg_upload_handler;
c->pfn_data = strdup(path);
c->pfn = NULL; c->pfn = NULL;
mg_iobuf_del(&c->recv, 0, hm->head.len); mg_iobuf_del(&c->recv, 0, hm->head.len);
mg_call(c, MG_EV_READ, &c->recv.len); mg_call(c, MG_EV_READ, &c->recv.len);
+1
View File
@@ -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 (*fn)(struct mg_connection *, const char *));
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
void (*fn)(struct mg_connection *, const char *)); 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); 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); 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 *); size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *);
+6
View File
@@ -2,6 +2,7 @@
#include "dns.h" #include "dns.h"
#include "fmt.h" #include "fmt.h"
#include "log.h" #include "log.h"
#include "ota.h"
#include "printf.h" #include "printf.h"
#include "profile.h" #include "profile.h"
#include "timer.h" #include "timer.h"
@@ -278,6 +279,11 @@ void mg_mgr_free(struct mg_mgr *mgr) {
void mg_mgr_init(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *mgr) {
memset(mgr, 0, sizeof(*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 MG_ENABLE_EPOLL
if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0)
MG_ERROR(("epoll_create1 errno %d", errno)); MG_ERROR(("epoll_create1 errno %d", errno));
+1
View File
@@ -2218,6 +2218,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
struct mg_connection *c, *tmp; struct mg_connection *c, *tmp;
uint64_t now = mg_millis(); uint64_t now = mg_millis();
mg_timer_poll(&mgr->timers, now); mg_timer_poll(&mgr->timers, now);
mg_ota_poll(mgr);
if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return; if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return;
mg_tcpip_poll(mgr->ifp, now); mg_tcpip_poll(mgr->ifp, now);
for (c = mgr->conns; c != NULL; c = tmp) { for (c = mgr->conns; c != NULL; c = tmp) {
+126 -100
View File
@@ -2,146 +2,172 @@
#include "http.h" #include "http.h"
#include "json.h" #include "json.h"
#include "log.h" #include "log.h"
#include "timer.h" #include "printf.h"
#include "url.h" #include "url.h"
#include "util.h" #include "util.h"
enum { MG_OTA_STATUS_WAITING, MG_OTA_STATUS_SUCCESS, MG_OTA_STATUS_FAIL }; #ifndef MG_OTA_MAX_URL_LEN
static int s_version_status; #define MG_OTA_MAX_URL_LEN 256
static int s_ota_status; #endif
static uint64_t s_start_time;
static struct mg_ota_metadata { // Scannable version tag embedded in every firmware binary, for server-side
char *version; // version extraction. Non-static so mg_mgr_init() can stash its address in
char *url; // 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; size_t size;
uint8_t sha256[32]; uint8_t sha256[32];
} s_ota_metadata; void (*fn)(const char *status);
} *s_ota;
static void free_ota_metadata(void) { static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data);
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 int fetch_ota_metadata(struct mg_http_message *response) { #if MG_ENABLE_CUSTOM_DEVICE_ID
double result; #else
if (mg_http_status(response) != 200) goto fetch_failed; void mg_ota_device_id(char *buf, size_t len) {
if (mg_json_get(response->body, "$", NULL) != 0) goto fetch_failed; #if MG_ARCH == MG_ARCH_CUBE && defined(UID_BASE)
free_ota_metadata(); mg_snprintf(buf, len, "%M", mg_print_hex, 12, (uint8_t *) UID_BASE);
s_ota_metadata.version = mg_json_get_str(response->body, "$.version"); #else
if (s_ota_metadata.version == NULL) goto fetch_failed; mg_snprintf(buf, len, "%d", 0);
s_ota_metadata.url = mg_json_get_str(response->body, "$.url"); #endif
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;
} }
#endif
static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) { 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); uint64_t expiration = *(uint64_t *) c->data;
struct mg_http_message *hm;
static int fetch_status;
if (ev == MG_EV_POLL) { if (ev == MG_EV_POLL) {
if (s_start_time + 5 * 1000 < mg_millis()) { if (mg_millis() > expiration) mg_error(c, "Metadata timeout");
mg_error(c, "Connection timeout");
}
} else if (ev == MG_EV_CONNECT) { } 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, 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" "Host: %.*s\r\n"
"Connection: close\r\n\r\n", "Connection: close\r\n\r\n",
mg_url_uri((char *) c->fn_data), (int) host.len, host.buf); uri, sep, MG_ARCH, s_ota->my_version, id,
fetch_status = MG_OTA_STATUS_WAITING; MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf);
} else if (ev == MG_EV_HTTP_MSG) { } else if (ev == MG_EV_HTTP_MSG) {
hm = (struct mg_http_message *) ev_data; struct mg_http_message *hm = (struct mg_http_message *) ev_data;
fetch_status = fetch_ota_metadata(hm); 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) { } else if (ev == MG_EV_ERROR) {
MG_ERROR(("%lu Connection error", c->id)); s_ota->fn((char *) ev_data);
s_version_status = MG_OTA_STATUS_FAIL; mg_free(s_ota);
} else if (ev == MG_EV_CLOSE) { s_ota = NULL;
MG_DEBUG(("%lu Connection closed %lu", c->id, mg_millis() - s_start_time));
s_version_status = fetch_status;
} }
} }
static void s_ota_done(struct mg_connection *c, const char *errmsg) { static void status_fn(const char *errmsg) {
(void) c;
s_ota_status = errmsg ? MG_OTA_STATUS_FAIL : MG_OTA_STATUS_SUCCESS;
if (errmsg) MG_ERROR(("OTA failed: %s", 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) { 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 (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) { } else if (ev == MG_EV_CONNECT) {
struct mg_str host = mg_url_host(s_ota->url);
mg_printf(c, mg_printf(c,
"GET %s HTTP/1.1\r\n" "GET %s HTTP/1.1\r\n"
"Host: %.*s\r\n" "Host: %.*s\r\n"
"Connection: close\r\n\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) { } else if (ev == MG_EV_HTTP_HDRS) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_http_message *hm = (struct mg_http_message *) ev_data;
int status = mg_http_status(hm); 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, mg_error(c, "Bad HTTP response: status %d, size %lu vs %lu", status,
(unsigned long) hm->body.len, (unsigned long) hm->body.len, (unsigned long) s_ota->size);
(unsigned long) s_ota_metadata.size); } else {
s_ota_status = MG_OTA_STATUS_FAIL; MG_DEBUG(("Beginning OTA (%lu bytes)", (unsigned long) s_ota->size));
return; 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) { } else if (ev == MG_EV_ERROR) {
MG_ERROR(("%lu Connection error", c->id)); s_ota->fn((char *) ev_data);
s_ota_status = MG_OTA_STATUS_FAIL; mg_free(s_ota);
s_ota = NULL;
} }
(void) ev_data;
} }
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
const char *metadata_url, const char *json_url, void (*fn)(const char *status)) {
void (*fn)(const char *status)) { if (fn == NULL) fn = status_fn;
s_version_status = MG_OTA_STATUS_WAITING; if (s_ota != NULL) {
s_ota_status = MG_OTA_STATUS_WAITING; fn("OTA already in progress");
s_start_time = mg_millis(); } else if ((s_ota = (struct mg_ota_state *) mg_calloc(1, sizeof(*s_ota))) ==
MG_DEBUG(("Connecting to %s to retrieve metadata", metadata_url)); NULL) {
if (!mg_http_connect(mgr, metadata_url, s_version_fn, fn("Out of memory");
(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;
}
} else { } else {
if (fn) fn("Version retrieving error"); struct mg_connection *c;
free_ota_metadata(); mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version);
return; 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();
} }
+34
View File
@@ -100,3 +100,37 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
// Pull based OTA over HTTP // Pull based OTA over HTTP
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
const char *metadata_url, void (*fn)(const char *status)); 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:<MG_OTA_FIRMWARE_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 *);
+2
View File
@@ -8,6 +8,7 @@
#include "tls.h" #include "tls.h"
#include "url.h" #include "url.h"
#include "util.h" #include "util.h"
#include "ota.h"
#if MG_ENABLE_SOCKET #if MG_ENABLE_SOCKET
@@ -757,6 +758,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
mg_iotest(mgr, ms); mg_iotest(mgr, ms);
now = mg_millis(); now = mg_millis();
mg_timer_poll(&mgr->timers, now); mg_timer_poll(&mgr->timers, now);
mg_ota_poll(mgr);
for (c = mgr->conns; c != NULL; c = tmp) { for (c = mgr->conns; c != NULL; c = tmp) {
bool is_resp = c->is_resp; bool is_resp = c->is_resp;
+385 -59
View File
@@ -4202,32 +4202,65 @@ static int dash_authenticate(char *user, size_t userlen, const char *pass) {
} }
struct dash_ws_test { struct dash_ws_test {
const char *req; int opened;
int done; int done;
int result;
int saw_set3; 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) { 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; struct dash_ws_test *d = (struct dash_ws_test *) c->fn_data;
if (ev == MG_EV_WS_OPEN) { if (ev == MG_EV_WS_OPEN) {
if (d->req != NULL) { d->opened = 1;
mg_ws_send(c, d->req, strlen(d->req), WEBSOCKET_OP_TEXT);
}
} else if (ev == MG_EV_WS_MSG) { } else if (ev == MG_EV_WS_MSG) {
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; 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 method = mg_json_get_tok(wm->data, "$.method");
struct mg_str set3 = mg_json_get_tok(wm->data, "$.params.set3"); 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) { if (mg_strcmp(method, mg_str("\"change\"")) == 0 && set3.buf != NULL) {
d->saw_set3 = 1; 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; d->done = 1;
c->is_closing = 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"; char three[10] = "hi";
int one = 1; 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) { static void test_dash(void) {
@@ -4248,7 +4362,7 @@ static void test_dash(void) {
const char *ws_url = "ws://localhost:26352/api/websocket"; const char *ws_url = "ws://localhost:26352/api/websocket";
struct mg_mgr mgr; struct mg_mgr mgr;
struct mg_dash dash; 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; bool two = false;
int admin = 7; int admin = 7;
int i; int i;
@@ -4265,13 +4379,20 @@ static void test_dash(void) {
{"admin", MG_VAL_INT, &admin, sizeof(admin)}, {"admin", MG_VAL_INT, &admin, sizeof(admin)},
{NULL, MG_VAL_INT, NULL, 0}, {NULL, MG_VAL_INT, NULL, 0},
}; };
struct mg_field_set set1 = {"set1", fields1, rfields1, NULL, 3, 7, NULL}; struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL, NULL};
struct mg_field_set set2 = {"set2", fields2, NULL, NULL, 0, 0, NULL}; struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL, NULL};
struct mg_field_set set3 = {"set3", fields3, NULL, NULL, 7, 7, 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 = const char *get_all_expected =
"{\"files\":{\"data\":[]},\"set3\":{\"admin\":7}," "{\"arr\":17,\"set3\":{\"admin\":7},"
"\"set2\":{\"two\":false}," "\"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" const char *set1_req = "POST /api/set HTTP/1.0\nContent-Length: 18\n\n"
"{\"set1\":{\"one\":2}}"; "{\"set1\":{\"one\":2}}";
const char *set1_set3_req = "POST /api/set HTTP/1.0\n" 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" "Authorization: Basic YWRtaW46YWRtaW4=\n"
"Content-Length: 20\n\n" "Content-Length: 20\n\n"
"{\"set3\":{\"admin\":8}}"; "{\"set3\":{\"admin\":8}}";
const char *set1_ws_set4_req = const char *set3_admin_req9 = "POST /api/set HTTP/1.0\n"
"{\"id\":1,\"method\":\"set\",\"params\":{\"set1\":{\"one\":4}}}"; "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_expected = "{\"one\":2,\"three\":\"t: 2\"}\n";
const char *set1_expected3 = "{\"one\":3,\"three\":\"t: 3\"}\n"; const char *set1_expected3 = "{\"one\":3,\"three\":\"t: 3\"}\n";
const char *set1_expected4 = "{\"one\":4,\"three\":\"t: 4\"}\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" const char *set2_req = "POST /api/set HTTP/1.0\nContent-Length: 21\n\n"
"{\"set2\":{\"two\":true}}"; "{\"set2\":{\"two\":true}}";
const char *set2_expected = "{\"two\":false}\n"; 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; char marker = 0;
memset(&dash, 0, sizeof(dash)); memset(&dash, 0, sizeof(dash));
dash.session_auto_expiration_seconds = 1; 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); MG_DASH_ADD_FIELD_SET(&dash, &set1);
ASSERT(dash.sets == &set1); ASSERT(dash.sets == &set1);
ASSERT(set1.next == NULL); ASSERT(set1.next == &test_files_set);
MG_DASH_ADD_FIELD_SET(&dash, &set2); MG_DASH_ADD_FIELD_SET(&dash, &set2);
ASSERT(dash.sets == &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 == dash_custom);
ASSERT(dash.custom_handlers->handler_data == &marker); ASSERT(dash.custom_handlers->handler_data == &marker);
while (mg_dash_files != NULL) mg_dash_file_del(mg_str(mg_dash_files->name)); for (i = 0; i < TEST_ARR_SIZE; i++) s_arr_store[i] = i * i;
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);
MG_DASH_ADD_FIELD_SET(&dash, &arr_set);
mg_mgr_init(&mgr); mg_mgr_init(&mgr);
mg_mem_files = mg_packed_files; mg_mem_files = mg_packed_files;
ASSERT(mg_http_listen(&mgr, url, mg_dash_ev_handler, &dash) != NULL); 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); "Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
ASSERT(cmpbody(buf, set1_expected3) == 0); ASSERT(cmpbody(buf, set1_expected3) == 0);
ws_user_notify.req = NULL; ws_user_notify.opened = 0;
ws_user_notify.done = 0; ws_user_notify.done = 0;
ws_user_notify.result = -1;
ws_user_notify.saw_set3 = 0; ws_user_notify.saw_set3 = 0;
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user_notify, "%s", mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user_notify, "%s",
"Authorization: Basic dXNlcjp1c2Vy\r\n"); "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); 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); 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, ASSERT(fetch(&mgr, buf, url,
"GET /api/get/set1 HTTP/1.0\n" "GET /api/get/set1 HTTP/1.0\n"
"Authorization: Basic dXNlcjp1c2Vy\n\n") == 200); "Authorization: Basic dXNlcjp1c2Vy\n\n") == 200);
ASSERT(cmpbody(buf, set1_expected3) == 0); ASSERT(cmpbody(buf, set1_expected3) == 0);
ws_admin.req = set1_ws_set4_req; ws_admin.opened = 0;
ws_admin.done = 0; ws_admin.done = 0;
ws_admin.result = -1;
ws_admin.saw_set3 = 0; ws_admin.saw_set3 = 0;
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_admin, "%s", mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_admin, "%s",
"Authorization: Basic YWRtaW46YWRtaW4=\r\n"); "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); for (i = 0; i < 200 && ws_admin.done == 0; i++) mg_mgr_poll(&mgr, 1);
ASSERT(ws_admin.done == 1); ASSERT(ws_admin.done == 1);
ASSERT(ws_admin.result == 1);
ASSERT(ws_admin.saw_set3 == 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, ASSERT(fetch(&mgr, buf, url,
"GET /api/get/set1 HTTP/1.0\n" "GET /api/get/set1 HTTP/1.0\n"
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200); "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(fetch(&mgr, buf, url, "GET /api/hi HTTP/1.0\n\n") == 200);
ASSERT(cmpbody(buf, "hi\n") == 0); 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/<set>/<index> updates one element
// and broadcasts a "<set>/<index>" 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/<set> appends an element and broadcasts
// a plain "<set>":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); 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) \ #define DASHBOARD(x) \
@@ -5,7 +5,7 @@ TOP ?= ../../..
SOURCES = dashboard.c file_data.c $(TOP)/mongoose.c SOURCES = dashboard.c file_data.c $(TOP)/mongoose.c
CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -O0 $(CFLAGS_EXTRA) 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 AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
all: $(PROG) all: $(PROG)
@@ -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
@@ -0,0 +1,129 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Array Dashboard</title>
<script src="https://mongoose.ws/resources/dashboard.js"></script>
<script>
let pageStart = 0;
function getPageSize() { return parseInt(document.getElementById('page-size').value) || 5; }
function loadPage(start) {
pageStart = Math.max(0, start);
Dashboard.call('get', { name: 'event', start: pageStart, size: getPageSize() });
}
// PATCH one element, then reload the current page to show the result
function modifyEntry(index, patch) {
return fetch(`api/get/event/${index}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
}).then(() => loadPage(pageStart));
}
// Delete one element, then reload the current page
function removeEntry(index) {
return fetch(`api/del/event/${index}`, { method: 'DELETE' })
.then(() => loadPage(pageStart));
}
// Append a new element from the "add" form, then reload the current page
function addEntry() {
const text = document.getElementById('new-text').value;
const checked = document.getElementById('new-checked').checked;
return fetch('api/add/event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, checked }),
}).then(() => {
document.getElementById('new-text').value = '';
document.getElementById('new-checked').checked = false;
loadPage(pageStart);
});
}
Dashboard.init({
debug: true,
data: { event: { start: 0, end: 100, data: [] } },
});
Dashboard.on('ready', () => {
document.body.style.visibility = 'visible';
loadPage(0);
});
</script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background:#f8fafc; font:500 14px/1.35 Arial,sans-serif; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 2rem; padding: 0.3rem 1rem; font-size: 0.85rem; font-family: inherit; font-weight: 600; border-radius: 0.5rem; border: 1px solid transparent; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; }
.btn-secondary { color: #64748b; background: #ffffff; border-color: #e2e8f0; }
.btn-secondary:hover { background: #f1f5f9; border-color: #cbd5e1; color: #475569; }
.btn-primary { color: #ffffff; background: linear-gradient(160deg, #38bdf8, #0ea5e9); border-color: #0284c7; }
.btn-primary:hover { background: linear-gradient(160deg, #0ea5e9, #0284c7); }
.btn:disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
.btn-icon { width: 2rem; padding: 0.3rem; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin: 1.25rem 0; }
input[type=text] { width: 4rem; padding: 0.3rem 0.5rem; font-size: 0.85rem; font-family: inherit; border: 1px solid #e2e8f0; border-radius: 0.5rem; outline: none; }
input[type=text]:focus { border-color: #7dd3fc; }
.toggle { position: relative; width: 2.75rem; height: 1.6rem; margin: 0; vertical-align: middle; border: 1px solid transparent; background: #d1d5db; border-radius: 0.8rem; outline: none; cursor: pointer; -webkit-appearance: none; appearance: none; transition: background 0.2s ease; }
.toggle::after { content: ""; position: absolute; left: 2px; top: 50%; width: 1.3rem; height: 1.3rem; background: #fff; border-radius: 50%; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transform: translateY(-50%) translateX(0); transition: transform 0.2s ease; }
.toggle:checked { background: #10b981; }
.toggle:checked::after { transform: translateY(-50%) translateX(calc(100% - 4px)); }
.toggle:focus-visible { box-shadow: 0 0 0 3px rgba(16,185,129,0.25); }
.table-wrap { max-width: 44rem; overflow: auto; border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #fff; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.65rem 0.85rem; border-bottom: 1px solid #e2e8f0; text-align: left; }
th { font-size: 0.75rem; text-transform: uppercase; color: #64748b; background: #f1f5f9; }
td:first-child { width: 7rem; color: #334155; font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
</style>
</head>
<body style="visibility: hidden;">
<main style="margin: 2rem; color: #64748b;">
<h1>Array read/write/add/delete</h1>
<div class="toolbar">
<button class="btn btn-secondary" onclick="loadPage(pageStart - getPageSize())">Prev</button>
<button class="btn btn-secondary" onclick="loadPage(pageStart + getPageSize())">Next</button>
<button class="btn btn-primary" onclick="loadPage(pageStart)">Reload</button>
<label>Page size: <input id="page-size" type="text" value="5" /></label>
<span>${(event.start ?? 0) + 1}–${(event.start ?? 0) + event.data.length} of ${event.size ?? 0}</span>
</div>
<div class="table-wrap" id="entries">
<table>
<thead><tr><th>Index</th><th>Text</th><th>Done</th><th></th></tr></thead>
<tbody data-repeat="event.data" data-iterator="ev">
<tr>
<td>${ev.index}</td>
<td><input type="text" style="width: 100%;" value="${ev.text}" onchange="modifyEntry(${ev.index}, {text: this.value})" /></td>
<td><input type="checkbox" class="toggle" data-checked="ev.checked" onchange="modifyEntry(${ev.index}, {checked: this.checked})" /></td>
<td>
<button class="btn btn-secondary btn-icon" onclick="removeEntry(${ev.index})" title="Delete" aria-label="Delete">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18" />
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="toolbar">
<input id="new-text" type="text" style="width: 12rem;" placeholder="New entry text" />
<label><input id="new-checked" type="checkbox" class="toggle" /> Done</label>
<button class="btn btn-primary" onclick="addEntry()">Add</button>
</div>
</main>
</body>
</html>
@@ -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
@@ -1,135 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Array Dashboard</title>
<script src="https://mongoose.ws/resources/dashboard.js"></script>
<script>
Dashboard.init({
debug: true,
data: { event: { index: 0, message: 'hi' } },
});
let pageStart = 0;
let loading = false;
let eventReadQueue = Promise.resolve();
async function readEvent(index) {
eventReadQueue = eventReadQueue.then(async () => {
await Dashboard.call('set', { event: { index } });
const ev = await Dashboard.call('get', 'event');
return ev && typeof ev == 'object' ? ev : { index, message: `my ev ${index}` };
});
return eventReadQueue;
}
function setLoading(value) {
loading = value;
document.querySelectorAll('[data-page-button]').forEach(btn => btn.disabled = value);
}
async function loadRows(start) {
if (loading) return;
const count = Number(document.querySelector('#page-size').value) || 10;
const tbody = document.querySelector('#events tbody');
const status = document.querySelector('#table-status');
pageStart = Math.max(0, start);
tbody.replaceChildren();
status.textContent = `Loading ${pageStart}..${pageStart + count - 1}`;
setLoading(true);
try {
const rows = await Promise.all(Array.from({ length: count }, (_, i) =>
readEvent(pageStart + i)));
const frag = document.createDocumentFragment();
rows.forEach(ev => {
const tr = document.createElement('tr');
const tdIndex = document.createElement('td');
const tdMessage = document.createElement('td');
tdIndex.textContent = ev.index;
tdMessage.textContent = ev.message;
tr.append(tdIndex, tdMessage);
frag.appendChild(tr);
});
tbody.replaceChildren(frag);
status.textContent = `Showing ${pageStart}..${pageStart + count - 1}`;
} catch (err) {
status.textContent = `Error: ${err && err.message ? err.message : err}`;
} finally {
setLoading(false);
}
}
// Show dashboard only when all device data is reported
Dashboard.on('ready', () => {
document.body.style.visibility = 'visible';
loadRows(0);
});
</script>
<style>
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background:#f8fafc; font:500 14px/1.35 Arial,sans-serif; }
code {white-space: nowrap; background: #ddd; border-radius: 4px; padding: 1px 4px;}
.field { display: flex; flex-direction: row; justify-content: space-between; align-items: center; gap: 0.75rem; }
.field-label { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; color: #94a3b8; flex-shrink: 0; white-space: nowrap; min-width: 3rem; }
.field-input { flex: 1; min-width: 0; padding: 0.45rem 0.75rem; font-size: 0.88rem; font-family: inherit; color: #334155; border: 1px solid #e2e8f0; border-radius: 0.55rem; outline: none; transition: border-color 0.15s ease; }
.field-input.edited { border-color: #00500040 !important; background: #00ff0020; }
.field-input.error { border-color: #50000040; background: #ff000020; }
.field-input::placeholder { color: #cbd5e1; }
.field-input:hover { border-color: #cbd5e1; }
.field-input:focus { border-color: #7dd3fc; }
.field-input:disabled, .field-input.disabled { color: #cbd5e1; border-color: #e2e8f0; cursor: not-allowed; pointer-events: none; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; height: 2rem; padding: 0.3rem 1rem; font-size: 0.85rem; font-family: inherit; font-weight: 600; border-radius: 0.5rem; border: 1px solid transparent; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; }
.btn-secondary { color: #64748b; background: #ffffff; border-color: #e2e8f0; box-shadow: 0 1px 2px rgba(148, 163, 184, 0.1); }
.btn-secondary:hover { background: #f1f5f9; border-color: #cbd5e1; color: #475569; }
.btn-primary { color: #ffffff; background: linear-gradient(160deg, #38bdf8, #0ea5e9); border-color: #0284c7; box-shadow: 0 1px 3px rgba(14, 165, 233, 0.3), 0 4px 10px rgba(14, 165, 233, 0.15); }
.btn-primary:hover { background: linear-gradient(160deg, #0ea5e9, #0284c7); box-shadow: 0 1px 3px rgba(14, 165, 233, 0.4), 0 6px 14px rgba(14, 165, 233, 0.2); }
.btn-primary:active, .btn-secondary:active { transform: translateY(0); }
.btn:disabled, .btn.disabled { opacity: 0.45; cursor: not-allowed; pointer-events: none; box-shadow: none; transform: none; }
.toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem; margin: 1.25rem 0; }
.table-wrap { max-width: 44rem; overflow: auto; border: 1px solid #e2e8f0; border-radius: 0.5rem; background: #fff; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.65rem 0.85rem; border-bottom: 1px solid #e2e8f0; text-align: left; }
th { font-size: 0.75rem; text-transform: uppercase; color: #64748b; background: #f1f5f9; }
td:first-child { width: 7rem; color: #334155; font-variant-numeric: tabular-nums; }
tr:last-child td { border-bottom: 0; }
#table-status { min-width: 12rem; color: #64748b; }
</style>
</head>
<body style="visibility: hidden;">
<main id="main" style="margin: 2rem; color: #64748b;">
<h1>Array read/write</h1>
<div class="toolbar">
<button class="btn btn-secondary" data-page-button onclick="loadRows(pageStart - Number(document.querySelector('#page-size').value || 10))">Prev</button>
<button class="btn btn-secondary" data-page-button onclick="loadRows(pageStart + Number(document.querySelector('#page-size').value || 10))">Next</button>
<div class="field" style="width: 9rem;">
Rows
<input id="page-size" class="field-input" type="text" value="10" />
</div>
<button class="btn btn-primary" data-page-button onclick="loadRows(pageStart)">Reload</button>
<span id="table-status"></span>
</div>
<div class="table-wrap">
<table id="events">
<thead>
<tr>
<th>Index</th>
<th>Message</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</main>
</body>
</html>
+96 -43
View File
@@ -9,33 +9,32 @@ static int authenticate(char *user, size_t userlen, const char *pass);
// Action buttons // Action buttons
static bool s_action1, s_action2; static bool s_action1, s_action2;
static uint64_t s_action2_timeout; static uint64_t s_action2_timeout;
static void write_actions(void) { static bool actions_fn(enum mg_dash_op op, struct mg_dash_user *u) {
if (s_action2 == true) s_action2_timeout = mg_now() + 750; (void) u;
} if (op == MG_DASH_READ) {
static void read_actions(void) { if (s_action2_timeout > mg_now()) s_action2 = true;
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[] = { static struct mg_field fields_actions[] = {
{"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)}, {"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)},
{"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)}, {"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)},
{NULL, MG_VAL_INT, NULL, 0}, {NULL, MG_VAL_INT, NULL, 0},
}; };
static struct mg_field_set set_actions = { static struct mg_field_set set_actions = {"actions", fields_actions, actions_fn,
"actions", fields_actions, read_actions, write_actions, 0, 0, NULL, NULL, NULL, NULL};
};
// Control panel // Control panel
// s_led1, s_led2, s_led3 are used to communicate LED status // s_led1, s_led2, s_led3 are used to communicate LED status
static bool s_led1, s_led2, s_led3; static bool s_led1, s_led2, s_led3;
static void write_leds(void) { static bool leds_fn(enum mg_dash_op op, struct mg_dash_user *u) {
// gpio_write(LED1_PIN, s_led1); (void) u;
// gpio_write(LED2_PIN, s_led2); return op == MG_DASH_READ || op == MG_DASH_WRITE;
// 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 struct mg_field fields_leds[] = { static struct mg_field fields_leds[] = {
{"led1", MG_VAL_BOOL, &s_led1, sizeof(s_led1)}, {"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)}, {"led3", MG_VAL_BOOL, &s_led3, sizeof(s_led3)},
{NULL, MG_VAL_INT, NULL, 0}, {NULL, MG_VAL_INT, NULL, 0},
}; };
static struct mg_field_set set_leds = { static struct mg_field_set set_leds = {"leds", fields_leds, leds_fn,
"leds", fields_leds, read_leds, write_leds, 0, 0, NULL, NULL, NULL, NULL};
};
// Read-only device Metrics // Read-only device Metrics
static int s_ram = 32, s_cpu = 7; static int s_ram = 32, s_cpu = 7;
static double s_temperature = 24.8; static double s_temperature = 24.8;
static void read_metrics(void) { static bool metrics_fn(enum mg_dash_op op, struct mg_dash_user *u) {
s_ram = 25 + (rand() % 16); // Sumulate metric change (void) u;
if (op != MG_DASH_READ) return false;
s_ram = 25 + (rand() % 16);
s_cpu = 7 + (rand() % 21); s_cpu = 7 + (rand() % 21);
s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0; s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0;
return true;
} }
static struct mg_field fields_metrics[] = { static struct mg_field fields_metrics[] = {
@@ -64,9 +65,8 @@ static struct mg_field fields_metrics[] = {
{NULL, MG_VAL_INT, NULL, 0}, {NULL, MG_VAL_INT, NULL, 0},
}; };
static struct mg_field_set set_metrics = { static struct mg_field_set set_metrics = {"metrics", fields_metrics, metrics_fn,
"metrics", fields_metrics, read_metrics, NULL, 0, 0, NULL, NULL, NULL, NULL};
};
// Read-write device settings // Read-write device settings
static bool s_enable_login = false; 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_status[40] = "No scans yet";
static char s_ota_url[100] = "https://my-product.com/ota.json"; static char s_ota_url[100] = "https://my-product.com/ota.json";
static void write_settings(void) { static bool settings_fn(enum mg_dash_op op, struct mg_dash_user *u) {
mg_log_level = s_log_level; if (u->level < 3) return false;
s_dash.authenticate = s_enable_login ? authenticate : NULL; if (op == MG_DASH_READ) {
} s_log_level = mg_log_level;
return true;
static void read_settings(void) { }
s_log_level = mg_log_level; 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[] = { static struct mg_field fields_settings[] = {
@@ -100,8 +105,7 @@ static struct mg_field fields_settings[] = {
}; };
static struct mg_field_set set_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 #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}, {NULL, MG_VAL_INT, NULL, 0},
}; };
static struct mg_field_set set_graph1 = { static bool graph1_fn(enum mg_dash_op op, struct mg_dash_user *u) {
"graph1", fields_graph1, read_graph1, NULL, 0, 0, NULL, (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 #define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send
static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1]; 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}, {NULL, MG_VAL_INT, NULL, 0},
}; };
static struct mg_field_set set_graph2 = { static bool graph2_fn(enum mg_dash_op op, struct mg_dash_user *u) {
"graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL, (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) { static int authenticate(char *user, size_t userlen, const char *pass) {
int level = 0; // Authentication failure int level = 0; // Authentication failure
@@ -188,7 +201,42 @@ static int authenticate(char *user, size_t userlen, const char *pass) {
return level; 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) { 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_leds);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics); MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_settings); 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_graph2);
MG_DASH_ADD_FIELD_SET(&s_dash, &set_actions); MG_DASH_ADD_FIELD_SET(&s_dash, &set_actions);
// Add two fake files - for demonstration #if MG_ARCH == MG_ARCH_UNIX
mg_dash_file_add(mg_str("device-config.json"), 1234); // Create demo files in the upload directory
mg_dash_file_add(mg_str("device-log-2026-04-25.txt"), 1327854); 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_mem_files = mg_packed_files;
mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dash); mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dash);
@@ -16,7 +16,7 @@
leds: { led1: true, led2: false, led3: false }, 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 }, 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' }, 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}]},
}, },
}); });
</script> </script>
@@ -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 => { Dashboard.on('change', args => {
args?.graph1?.data && updateGraph1(args?.graph1?.data); args?.graph1?.data && updateGraph1(args?.graph1?.data);
args?.graph2?.data && updateGraph2(args?.graph2?.data); args?.graph2?.data && updateGraph2(args?.graph2?.data);
typeof args?.files === 'number' && Dashboard.call('get', { name: 'files', start: 0, end: 100 });
}); });
}); });
</script> </script>
@@ -689,7 +691,7 @@ a, a:visited, a:link { text-decoration: none; }
<div class="panel-title"> <div class="panel-title">
<span> <span>
<span>File Manager</span> <span>File Manager</span>
<span class="metric-status">${files.data.length} Files</span> <span class="metric-status">${files.size ?? files.data.length} Files</span>
</span> </span>
<div class="header-actions"> <div class="header-actions">
<label class="btn btn-primary"> <label class="btn btn-primary">
@@ -699,7 +701,7 @@ a, a:visited, a:link { text-decoration: none; }
<path d="M5 21h14" /> <path d="M5 21h14" />
</svg> </svg>
Upload Upload
<input type="file" hidden data-upload="1" /> <input type="file" hidden data-upload="files" />
</label> </label>
</div> </div>
</div> </div>
@@ -717,14 +719,14 @@ a, a:visited, a:link { text-decoration: none; }
</div> </div>
<div class="file-size">${f.size}</div> <div class="file-size">${f.size}</div>
<div class="file-actions"> <div class="file-actions">
<a href="fs/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary"> <a href="fs/files/${encodeURIComponent(f.name)}" target="_blank" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 3v12" /> <path d="M12 3v12" />
<path d="m7 10 5 5 5-5" /> <path d="m7 10 5 5 5-5" />
<path d="M5 21h14" /> <path d="M5 21h14" />
</svg> </svg>
</a> </a>
<button onclick="fetch('fs/${encodeURIComponent(f.name)}', {method: 'DELETE'}); return false;" class="btn btn-secondary"> <button onclick="fetch('fs/files/${encodeURIComponent(f.name)}', {method: 'DELETE'}); return false;" class="btn btn-secondary">
<svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2"> <svg viewBox="0 0 24 24" class="nav-icon" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18" /> <path d="M3 6h18" />
<path d="M8 6V4h8v2" /> <path d="M8 6V4h8v2" />
+16 -14
View File
@@ -6,23 +6,25 @@
static bool s_led1; static bool s_led1;
static char s_version[10]; static char s_version[10];
static void read_status(void) { static bool status_fn(enum mg_dash_op op, struct mg_dash_user *u) {
MG_INFO(("READING STATUS")); (void) u;
mg_snprintf(s_version, sizeof(s_version), "1.2.3"); if (op == MG_DASH_READ) {
MG_INFO(("READING STATUS"));
// These conditionals make this file work on both microcontroller and desktop mg_snprintf(s_version, sizeof(s_version), "1.2.3");
// Useful for developing the UI on desktop, and copying to an MCU project. // These conditionals make this file work on both microcontroller and desktop
#if MG_ARCH == MG_ARCH_CUBE #if MG_ARCH == MG_ARCH_CUBE
s_led1 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); // Read LED state into field s_led1 = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
#endif #endif
} return true;
}
static void write_status(void) { if (op == MG_DASH_WRITE) {
MG_INFO(("WRITING STATUS")); MG_INFO(("WRITING STATUS"));
#if MG_ARCH == MG_ARCH_CUBE #if MG_ARCH == MG_ARCH_CUBE
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, s_led1); // Write LED state from field HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, s_led1);
#endif #endif
return true;
}
return false;
} }
static struct mg_field fields_status[] = { static struct mg_field fields_status[] = {
@@ -32,7 +34,7 @@ static struct mg_field fields_status[] = {
}; };
static struct mg_field_set field_set_status = { static struct mg_field_set field_set_status = {
"status", fields_status, read_status, write_status, 0, 0, NULL, "status", fields_status, status_fn, NULL, NULL, NULL,
}; };
void mg_dash_init(struct mg_mgr *mgr) { void mg_dash_init(struct mg_mgr *mgr) {
@@ -44,7 +44,7 @@
<div> <div>
This toggle button is a stylised checkbox. This toggle button is a stylised checkbox.
<code>data-bind="status.led1"</code> binds it to the device variable. <code>data-bind="status.led1"</code> binds it to the device variable.
<code>data-autosave="1"</code> makes it send <code>{"led1": VALUE}</code> to the device every time the user toggles the <code>data-autosave="1"</code> makes it send <code>{"status":{"led1": VALUE}}</code> to the device every time the user toggles the
button. On the device side, <code>read_status()</code> or <code>write_status()</code> are called every time button. On the device side, <code>read_status()</code> or <code>write_status()</code> are called every time
the LED state is read or written. the LED state is read or written.
</div> </div>