mirror of
https://github.com/cesanta/mongoose.git
synced 2026-10-21 11:30:01 +00:00
Dash OTA dogfooding nits
This commit is contained in:
1 parent
73037880e6
commit
27459efc61
29 files changed
+2635
-1478
No files matched your search
@@ -4,6 +4,14 @@ Mongoose is a two-file C/C++ networking library for embedded and desktop systems
|
||||
It is developed by Cesanta (https://cesanta.com) and available at https://mongoose.ws.
|
||||
License: GPLv2 or commercial.
|
||||
|
||||
## General rules
|
||||
|
||||
- Always re-read files before patching, so you do not overwrite existing changes
|
||||
- Never guess. If you don't know, say you don't know and stop
|
||||
- Read mongoose.h for API definition and docstrings
|
||||
- Do not use a separate HTTP library, MQTT library, or WebSocket library,
|
||||
or Modbus-TCP library alongside Mongoose. Mongoose provides all of these.
|
||||
|
||||
## Integration
|
||||
|
||||
Copy `mongoose.h` and `mongoose.c` into a `mongoose/` subdirectory of the
|
||||
@@ -38,16 +46,12 @@ your_project/
|
||||
└── mongoose_config.h # required for embedded: set MG_ARCH and options
|
||||
```
|
||||
|
||||
Minimal `mongoose_config.h` for STM32:
|
||||
Minimal `mongoose_config.h` should set `MG_ARCH`. For exmaple, for STM32:
|
||||
|
||||
```c
|
||||
#define MG_ARCH MG_ARCH_CUBE
|
||||
// add driver, TLS, and other options here
|
||||
```
|
||||
|
||||
Do not use a separate HTTP library, MQTT library, or WebSocket library alongside
|
||||
Mongoose. Mongoose provides all of these.
|
||||
|
||||
## Core API
|
||||
|
||||
```c
|
||||
@@ -80,7 +84,7 @@ void handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
|
||||
## TCP/IP stack — set exactly one
|
||||
|
||||
Configure in the build system (`-D` flag) or in `mongoose_config.h`:
|
||||
Configure in `mongoose_config.h`:
|
||||
|
||||
| Define | Use when |
|
||||
|--------|----------|
|
||||
@@ -216,7 +220,11 @@ for (;;) {
|
||||
- Do **not** add vanilla JS event listeners, `fetch()` calls, or custom
|
||||
reactive logic to `dashboard.html`.
|
||||
- Do **not** modify `dashboard.html` unless the user explicitly asks.
|
||||
- Bind controls to device state using `data-bind` attributes:
|
||||
- Bind controls to device state using `data-bind` attributes
|
||||
- The `__status` object in evaluations is read-only, do not alter it
|
||||
- If you need to pass data between the UI and backend.c, add extra
|
||||
fieldsets/fields - see next section about it
|
||||
- If you want to display device data in HTML, use `${}` evaluations
|
||||
|
||||
```html
|
||||
<!-- 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.
|
||||
Reader first writes an index of the element to read, and then it reads
|
||||
an element.
|
||||
Array field sets are recognised by the "index" pointer in the
|
||||
field set descriptor:
|
||||
// Non-NULL: array set. *index is set before fn(READ) call;
|
||||
// fn sets *index = -1 to signal end of iteration.
|
||||
// Size query: framework sets *index = -1 before fn(READ); fn sets *index = total size
|
||||
|
||||
```c
|
||||
struct event {
|
||||
static struct event {
|
||||
int index;
|
||||
char message[100];
|
||||
};
|
||||
|
||||
static struct event s_event;
|
||||
|
||||
static void read_event(void) {
|
||||
mg_snprintf(s_event.message, sizeof(s_event.message), "my ev %d",
|
||||
s_event.index);
|
||||
}
|
||||
|
||||
static void write_event(void) {
|
||||
// Do nothing. Dashboard sets the s_event.index
|
||||
}
|
||||
|
||||
static struct mg_field fields_event[] = {
|
||||
{"index", MG_VAL_INT, &s_event.index, sizeof(s_event.index)},
|
||||
{"message", MG_VAL_STR, &s_event.message, sizeof(s_event.message)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
} s_event;
|
||||
|
||||
static struct mg_field_set field_set_event = {
|
||||
"event", fields_event, read_event, write_event, 0, 0, NULL,
|
||||
"event", fields_event, event_fn, &s_event.index, 0, 0, 0
|
||||
};
|
||||
```
|
||||
|
||||
@@ -398,6 +391,18 @@ MG_ERROR(("errno=%d", err));
|
||||
|
||||
Set `MG_ENABLE_LOG=0` to disable all logging (production / size-sensitive builds).
|
||||
|
||||
## Firmware OTA
|
||||
|
||||
Mongoose implements OTA for a variety of targets, see MG_OTA_ defines. To
|
||||
enable, set the respective define in `mongoose_config.h`, for example:
|
||||
|
||||
```c
|
||||
#define MG_OTA MG_OTA_STM32H5
|
||||
```
|
||||
|
||||
Once this is done, you do OTA via HTTP upload, or HTTP periodic pull, or over
|
||||
MQTT. Read https://mongoose.ws/docs/guides/firmware-ota-updates/ for details.
|
||||
|
||||
## Key rules for AI code generation
|
||||
|
||||
- **Two files only**: always use `mongoose.h` + `mongoose.c`. Never add a
|
||||
|
||||
+644
-355
File diff suppressed because it is too large.
Load diff
+151
-49
@@ -1897,6 +1897,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
|
||||
void (*fn)(struct mg_connection *, const char *));
|
||||
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
|
||||
void (*fn)(struct mg_connection *, const char *));
|
||||
const char *mg_upload_path(struct mg_connection *c);
|
||||
void mg_http_bauth(struct mg_connection *, const char *user, const char *pass);
|
||||
struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v);
|
||||
size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *);
|
||||
@@ -3277,50 +3278,95 @@ void mg_rpc_list(struct mg_rpc_req *r);
|
||||
|
||||
|
||||
|
||||
#ifndef MG_DASH_MAX_USER_NAME
|
||||
#define MG_DASH_MAX_USER_NAME 32
|
||||
#endif
|
||||
|
||||
#ifndef MG_HTTP_ADDR
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
|
||||
#else
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:80"
|
||||
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
|
||||
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
|
||||
#endif
|
||||
|
||||
// C type of a value bound to a dashboard field, see struct mg_field
|
||||
enum mg_val_type {
|
||||
MG_VAL_INT,
|
||||
MG_VAL_BOOL,
|
||||
MG_VAL_DBL,
|
||||
MG_VAL_STR,
|
||||
MG_VAL_RAW,
|
||||
MG_VAL_INT, // int
|
||||
MG_VAL_UINT64, // uint64_t
|
||||
MG_VAL_BOOL, // bool
|
||||
MG_VAL_DBL, // double
|
||||
MG_VAL_STR, // Zero-terminated string
|
||||
MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim
|
||||
};
|
||||
|
||||
// Dashboard field: binds a JSON key to a C variable for read and/or write
|
||||
struct mg_field {
|
||||
const char *name;
|
||||
enum mg_val_type type;
|
||||
void *value;
|
||||
size_t value_size;
|
||||
const char *name; // JSON key
|
||||
enum mg_val_type type; // C type of the value, see enum mg_val_type
|
||||
void *value; // Pointer to the bound variable
|
||||
size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only
|
||||
};
|
||||
|
||||
// Operation passed to a struct mg_field_set read/write/delete/add callback.
|
||||
// MG_DASH_ADD: fields hold the values of a new element to append; *index is
|
||||
// not set. Return true to accept and persist it, false to reject (e.g. a cap
|
||||
// was reached)
|
||||
enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD };
|
||||
|
||||
struct mg_dash;
|
||||
|
||||
// Authenticated (or guest) dashboard user
|
||||
struct mg_dash_user {
|
||||
struct mg_dash_user *next; // Next user in the list
|
||||
char name[MG_DASH_MAX_USER_NAME]; // Username
|
||||
char token[21]; // Session token, sent to the client as a cookie
|
||||
int level; // Access level, see struct mg_dash::authenticate
|
||||
uint64_t expire; // Session expiration time, see mg_millis()
|
||||
struct mg_dash *dash; // Dashboard this user belongs to
|
||||
};
|
||||
|
||||
// Named group of related dashboard fields, e.g. "settings" or "metrics"
|
||||
struct mg_field_set {
|
||||
const char *name;
|
||||
struct mg_field *fields;
|
||||
void (*reader)(void);
|
||||
void (*writer)(void);
|
||||
int read_level;
|
||||
int write_level;
|
||||
struct mg_field_set *next;
|
||||
const char *name; // Set name, used as a JSON key
|
||||
struct mg_field *fields; // Zero-terminated array of fields
|
||||
bool (*fn)(enum mg_dash_op op,
|
||||
struct mg_dash_user *u); // Read/write/delete/add callback
|
||||
int *index; // Non-NULL: array set. *index is set before fn(READ) call;
|
||||
// fn sets *index = -1 to signal end of iteration.
|
||||
// Size query: framework sets *index = -1 before fn(READ); fn
|
||||
// sets *index = total size
|
||||
bool (*get_dir)(const struct mg_dash_user *u, char *buf,
|
||||
size_t len); // Non-NULL: file array
|
||||
struct mg_field_set *next; // Next set in the list
|
||||
};
|
||||
|
||||
// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER()
|
||||
struct mg_dash_custom_handler {
|
||||
struct mg_dash_custom_handler *next;
|
||||
struct mg_str uri_pattern;
|
||||
mg_event_handler_t handler;
|
||||
void *handler_data;
|
||||
};
|
||||
|
||||
struct mg_dash_file {
|
||||
struct mg_dash_file *next;
|
||||
char *name;
|
||||
size_t size;
|
||||
struct mg_dash_custom_handler *next; // Next handler in the list
|
||||
struct mg_str uri_pattern; // URI to match, see mg_match()
|
||||
mg_event_handler_t handler; // Event handler function
|
||||
void *handler_data; // Opaque data for the handler
|
||||
};
|
||||
|
||||
// Dashboard instance: fields, users and handlers served over HTTP and WS
|
||||
struct mg_dash {
|
||||
struct mg_field_set *sets;
|
||||
struct mg_dash_custom_handler *custom_handlers;
|
||||
struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
|
||||
struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers
|
||||
// Validates pass, fills in user, returns access level (<= 0: failure).
|
||||
// NULL: no login required, all clients get guest access
|
||||
int (*authenticate)(char *user, size_t userlen, const char *pass);
|
||||
int session_auto_expiration_seconds;
|
||||
//struct mg_dash_file *files;
|
||||
int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
|
||||
struct mg_dash_user *guest; // Used when authenticate == NULL
|
||||
struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix
|
||||
};
|
||||
|
||||
#define MG_DASH_ADD_FIELD_SET(dash_, set_) \
|
||||
@@ -3339,28 +3385,50 @@ struct mg_dash {
|
||||
(dash_)->custom_handlers = &ch_; \
|
||||
} while (0)
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
|
||||
#else
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:80"
|
||||
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
|
||||
#endif
|
||||
|
||||
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
|
||||
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
|
||||
#endif
|
||||
|
||||
// File manager
|
||||
extern struct mg_dash_file *mg_dash_files;
|
||||
void mg_dash_file_add(struct mg_str name, size_t size);
|
||||
void mg_dash_file_del(struct mg_str name);
|
||||
|
||||
// Dashboard event handler. Pass to mg_http_listen() as the fn argument
|
||||
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data);
|
||||
|
||||
// Notify WebSocket clients that a field set changed: broadcasts the new
|
||||
// array size if *set->index < 0, or a single element update otherwise
|
||||
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *);
|
||||
|
||||
// Default reader for file-backed array sets (set->get_dir != NULL)
|
||||
// Usage example - this is a complete implemenation of the file manager:
|
||||
// ```c
|
||||
// static struct file {
|
||||
// int index;
|
||||
// char name[64];
|
||||
// size_t size;
|
||||
// uint64_t checksum;
|
||||
// } s_file;
|
||||
// static struct mg_field_set set_files;
|
||||
//
|
||||
// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
|
||||
// (void) u;
|
||||
// mkdir("/tmp/dashboard", 0755);
|
||||
// mg_snprintf(buf, len, "%s", "/tmp/dashboard");
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// static struct mg_field fields_files[] = {
|
||||
// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
|
||||
// {"size", MG_VAL_UINT64, &s_file.size, 0},
|
||||
// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
|
||||
// {NULL, MG_VAL_INT, NULL, 0},
|
||||
// };
|
||||
//
|
||||
// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7;
|
||||
// if (op != MG_DASH_READ) return false;
|
||||
// if (!mg_dash_dir_read(&set_files, u)) return false;
|
||||
// s_file.checksum = 0; // Mock to show how to set custom fields
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL};
|
||||
// ```
|
||||
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u);
|
||||
|
||||
// Helper forward declarations for Mongoose CMSIS pack modules
|
||||
extern struct mg_mgr g_mgr;
|
||||
extern void mg_dash_init(struct mg_mgr *);
|
||||
@@ -3470,6 +3538,40 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
|
||||
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
|
||||
const char *metadata_url, void (*fn)(const char *status));
|
||||
|
||||
#ifndef MG_OTA_URL
|
||||
#define MG_OTA_URL NULL
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_STATUS_FN
|
||||
#define MG_OTA_STATUS_FN NULL
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_FIRMWARE_VERSION
|
||||
#define MG_OTA_FIRMWARE_VERSION "1.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_MAX_VERSION_LEN
|
||||
#define MG_OTA_MAX_VERSION_LEN 64
|
||||
#endif
|
||||
|
||||
// Scannable "MG_VERSION:<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
@@ -3,19 +3,13 @@
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
const Rpc = (function() {
|
||||
function Rpc(url, onev) {
|
||||
let ws, id = 1, oncall, q = [], reconnect = true;
|
||||
const pending = new Map();
|
||||
const realsend = s => { ws.send(s); onev && onev('send', s); };
|
||||
const fail = e => { for (const p of pending.values()) p[1](e || 'disconnected'); pending.clear(); };
|
||||
const send = x => ws && ws.readyState === 1
|
||||
? realsend(JSON.stringify(x))
|
||||
: q.push(x);
|
||||
const Watch = (function() {
|
||||
function Watch(url, onev, onmsg) {
|
||||
let ws, reconnect = true;
|
||||
const connect = () => {
|
||||
ws = new WebSocket(url);
|
||||
ws.onopen = () => { onev && onev('open'); while (q.length) realsend(JSON.stringify(q.shift())); };
|
||||
ws.onclose = () => { onev && onev('close'); fail(); reconnect && setTimeout(connect, 1000); };
|
||||
ws.onopen = () => onev && onev('open');
|
||||
ws.onclose = () => { onev && onev('close'); reconnect && setTimeout(connect, 1000); };
|
||||
ws.onmessage = e => {
|
||||
onev && onev('message', e.data);
|
||||
let m;
|
||||
@@ -25,69 +19,92 @@
|
||||
console.log('INVALID JSON:', e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m.id && pending.has(m.id)) {
|
||||
const [ok, bad] = pending.get(m.id);
|
||||
pending.delete(m.id);
|
||||
m.error ? bad(m.error) : ok(m.result);
|
||||
} else if (oncall && m.method) {
|
||||
Promise.resolve(oncall(m.method, m.params))
|
||||
.then(r => m.id && send({ id: m.id, result: r }))
|
||||
.catch(err => m.id && send({
|
||||
id: m.id,
|
||||
error: { code: -32000, message: String(err?.message || err) }
|
||||
}));
|
||||
}
|
||||
onmsg && onmsg(m);
|
||||
};
|
||||
};
|
||||
|
||||
connect();
|
||||
return {
|
||||
close: () => { reconnect = false; ws?.close(); },
|
||||
call: (method, params) => new Promise((ok, bad) => {
|
||||
const i = id++;
|
||||
pending.set(i, [ok, bad]);
|
||||
send({ id: i, method, params });
|
||||
}),
|
||||
notify: (method, params) => send({ method, params }),
|
||||
handle: fn => (oncall = fn, this),
|
||||
get ws() { return ws; }
|
||||
};
|
||||
}
|
||||
|
||||
return Rpc;
|
||||
return Watch;
|
||||
})();
|
||||
|
||||
function rpc_connect() {
|
||||
function fetch_and_ready() {
|
||||
if (isMock) {
|
||||
setTimeout(() => userhandlers['ready']?.(), 500);
|
||||
return;
|
||||
return delay(500).then(rescan).then(mark_ready);
|
||||
}
|
||||
if (rpc) return rpc;
|
||||
//if (settings.auth && !status.authed) return null;
|
||||
rpc = Rpc('api/websocket', function (evname, args) {
|
||||
if (evname == 'open') status.online = true, rescan();
|
||||
if (evname == 'close') status.online = false, rescan();
|
||||
if (settings.debug) console.log('WS', evname, args);
|
||||
});
|
||||
rpc.handle((method, args) => {
|
||||
if (method == 'change' && args) apply_and_rescan(settings.data, args);
|
||||
if (method == 'logout') rpc_disconnect();
|
||||
if (userhandlers[method]) userhandlers[method](args);
|
||||
});
|
||||
return rpc;
|
||||
return fetch('api/get')
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.status))
|
||||
.then(data => {
|
||||
for (const k in data) {
|
||||
if (typeof data[k] === 'number') {
|
||||
const prev = settings.data[k];
|
||||
const arr = prev && typeof prev === 'object' ? prev : {};
|
||||
data[k] = {
|
||||
...arr,
|
||||
size: data[k],
|
||||
start: typeof arr.start === 'number' ? arr.start : 0,
|
||||
data: Array.isArray(arr.data) ? arr.data : [],
|
||||
};
|
||||
}
|
||||
}
|
||||
return apply_and_rescan(settings.data, data);
|
||||
})
|
||||
.then(expand_arrays)
|
||||
.then(() => {
|
||||
ws_connect();
|
||||
return mark_ready();
|
||||
})
|
||||
.catch(err => console.log('GET failed', err));
|
||||
}
|
||||
|
||||
function rpc_disconnect() {
|
||||
if (!rpc) return;
|
||||
rpc.close();
|
||||
rpc = null;
|
||||
function ws_connect() {
|
||||
if (isMock || ws) return;
|
||||
ws = Watch('api/websocket', function (evname, args) {
|
||||
if (evname == 'open') { status.online = true; rescan(); }
|
||||
if (evname == 'close') { status.online = false; rescan(); }
|
||||
if (settings.debug) console.log('WS', evname, args);
|
||||
}, function (m) {
|
||||
if (m.method == 'change' && m.params) {
|
||||
const rest = {};
|
||||
const arrays = [];
|
||||
for (const k in m.params) {
|
||||
const ma = k.match(/^(.+)\/(\d+)$/);
|
||||
if (ma) {
|
||||
const arr = settings.data[ma[1]];
|
||||
if (arr?.data) { const i = +ma[2] - (arr.start || 0); if (i >= 0 && i < arr.data.length) arr.data[i] = Object.assign(arr.data[i] || {}, m.params[k]); }
|
||||
} else if (typeof m.params[k] === 'number' && settings.data[k]?.data !== undefined) {
|
||||
const arr = settings.data[k];
|
||||
arr.size = m.params[k];
|
||||
if (is_expandable(arr)) arrays.push(expand_array(k, arr.start, arr.end));
|
||||
} else {
|
||||
rest[k] = m.params[k];
|
||||
}
|
||||
}
|
||||
apply(settings.data, rest);
|
||||
prune(settings.data, settings.edits);
|
||||
Promise.all(arrays)
|
||||
.then(rescan)
|
||||
.then(() => userhandlers['change']?.(m.params));
|
||||
}
|
||||
if (m.method == 'logout') ws_disconnect();
|
||||
});
|
||||
}
|
||||
|
||||
function ws_disconnect() {
|
||||
if (!ws) return;
|
||||
ws.close();
|
||||
ws = null;
|
||||
status.online = false;
|
||||
}
|
||||
|
||||
|
||||
function login(params) {
|
||||
if (isMock) { rpc_connect(); return Promise.resolve({}); }
|
||||
if (isMock) return fetch_and_ready().then(() => ({}));
|
||||
const opts = {};
|
||||
if (params) {
|
||||
const username = (params?.username ?? '').trim();
|
||||
@@ -100,19 +117,18 @@
|
||||
.then(resp => {
|
||||
status.username = resp?.user || '';
|
||||
status.userlevel = resp?.level || 0;
|
||||
rpc_connect();
|
||||
return resp;
|
||||
return fetch_and_ready().then(() => resp);
|
||||
});
|
||||
};
|
||||
|
||||
function logout() {
|
||||
if (isMock) { rpc_disconnect(); return Promise.resolve({}); }
|
||||
if (isMock) { ws_disconnect(); return Promise.resolve({}); }
|
||||
return fetch('api/logout')
|
||||
.catch(() => true)
|
||||
.then(() => {
|
||||
status.username = '';
|
||||
status.userlevel = 0;
|
||||
rpc_disconnect();
|
||||
ws_disconnect();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -122,7 +138,27 @@
|
||||
const settings = {data: {}, edits: {}, debug: true};
|
||||
const userhandlers = {}; // User event handlers
|
||||
const status = { online: false, username: '', userlevel: 0, ready: false };
|
||||
let rpc = null;
|
||||
let ws = null;
|
||||
let ready_started = false;
|
||||
|
||||
function install_ready_gate() {
|
||||
document.documentElement.classList.remove('dashboard-ready');
|
||||
if (document.getElementById('dashboard-ready-gate')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'dashboard-ready-gate';
|
||||
style.textContent = `html:not(.dashboard-ready) *,html:not(.dashboard-ready) *::before,html:not(.dashboard-ready) *::after{transition:none!important;animation:none!important}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function mark_ready() {
|
||||
if (ready_started) return Promise.resolve();
|
||||
ready_started = true;
|
||||
return next_frame().then(() => {
|
||||
status.ready = true;
|
||||
document.documentElement.classList.add('dashboard-ready');
|
||||
userhandlers['ready']?.();
|
||||
});
|
||||
}
|
||||
|
||||
const fromPath = (path, value) => path.split('.').reverse().reduce((acc, k) => ({ [k]: acc }), value);
|
||||
const get = (obj, path) => path.split('.').reduce((o, k) => o?.[k], obj);
|
||||
@@ -222,7 +258,11 @@
|
||||
function save(key) {
|
||||
const changes = {};
|
||||
set(changes, key, get(settings.edits, key));
|
||||
rpc.call('set', changes).then(r => apply_and_rescan(settings.data, r));
|
||||
fetch('api/set', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(changes),
|
||||
}).catch(err => console.log('SET failed', key, err));
|
||||
};
|
||||
|
||||
// Cancel the edits for keys that match pattern
|
||||
@@ -231,6 +271,14 @@
|
||||
rescan();
|
||||
};
|
||||
|
||||
// Set a checkbox's `checked` *property* from a boolean expression. The
|
||||
// `checked` content attribute is presence-based - templating its value
|
||||
// would not work, so this binds straight to the IDL property instead
|
||||
function handle_checked(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
el.checked = !!get(context, key);
|
||||
};
|
||||
|
||||
function handle_save(el, key) {
|
||||
el.disabled = !edited(key);
|
||||
if (!el.bound) el.addEventListener("click", ev => save(key)), el.bound = true;
|
||||
@@ -244,18 +292,30 @@
|
||||
function handle_repeat(el, key, context) {
|
||||
if (!has(context, key)) { console.error('EVAL', key, context); return; }
|
||||
if (!el.orig) el.orig = el.children[0].cloneNode(true);
|
||||
const v = get(context, key), v2 = JSON.stringify(v);
|
||||
if (el.v2 === v2) return;
|
||||
const frag = document.createDocumentFragment();
|
||||
const v = get(context, key);
|
||||
const it = el.dataset.iterator ?? '__obj';
|
||||
v.forEach(function (item, index) {
|
||||
const child = el.orig.cloneNode(true);
|
||||
const ctx = { ...context, [it]: item };
|
||||
process(child, ctx, true);
|
||||
frag.appendChild(child);
|
||||
});
|
||||
el.replaceChildren(frag);
|
||||
el.v2 = v2;
|
||||
const json = v.map(item => JSON.stringify(item));
|
||||
const prev = el.itemsJson || [];
|
||||
if (json.length === prev.length && el.children.length === json.length) {
|
||||
// Same length: re-template each changed row's existing DOM node in
|
||||
// place instead of replacing it. Untouched rows are left completely
|
||||
// alone, and changed rows keep their live elements too - so bound
|
||||
// properties (e.g. a checkbox's `checked`) change on an
|
||||
// already-painted node, which is what lets CSS transitions play, and
|
||||
// in-progress edits in unrelated fields of that row survive
|
||||
json.forEach((j, i) => {
|
||||
if (j !== prev[i]) process(el.children[i], { ...context, [it]: v[i] }, true);
|
||||
});
|
||||
} else {
|
||||
const frag = document.createDocumentFragment();
|
||||
v.forEach(item => {
|
||||
const node = el.orig.cloneNode(true);
|
||||
process(node, { ...context, [it]: item }, true);
|
||||
frag.appendChild(node);
|
||||
});
|
||||
el.replaceChildren(frag);
|
||||
}
|
||||
el.itemsJson = json;
|
||||
};
|
||||
|
||||
function upload(el, is_ota) {
|
||||
@@ -267,7 +327,7 @@
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(f);
|
||||
reader.onload = function () {
|
||||
const url = is_ota ? 'api/ota' : `fs/${encodeURIComponent(f.name)}`;
|
||||
const url = is_ota ? 'api/ota' : `fs/${el.dataset.upload}/${encodeURIComponent(f.name)}`;
|
||||
let begin = Date.now(), body = reader.result, ok = false;
|
||||
// console.log(2, url, body);
|
||||
fetch(url, { method: 'POST', body })
|
||||
@@ -295,7 +355,8 @@
|
||||
}
|
||||
|
||||
function process(root, context, x) {
|
||||
const all = root.querySelectorAll("*:not([data-repeat] *)");
|
||||
const selector = x ? "*" : "*:not([data-repeat] *)";
|
||||
const all = root.querySelectorAll(selector);
|
||||
const els = root.nodeType === Node.DOCUMENT_NODE ? all : [root, ...all];
|
||||
els.forEach(function (el) {
|
||||
if (el.tagName === "SCRIPT" || el.tagName === "STYLE") return;
|
||||
@@ -309,14 +370,16 @@
|
||||
}
|
||||
for (const attr of el.attributes) {
|
||||
const cv = attr.value || '';
|
||||
if (attr.originalValue || cv.includes('${')) {
|
||||
if (!attr.originalValue) attr.originalValue = cv;
|
||||
const v = substituteExpressions(attr.originalValue, context);
|
||||
if (!el.originalAttrs) el.originalAttrs = {};
|
||||
if (el.originalAttrs[attr.name] || cv.includes('${')) {
|
||||
if (!el.originalAttrs[attr.name]) el.originalAttrs[attr.name] = cv;
|
||||
const v = substituteExpressions(el.originalAttrs[attr.name], context);
|
||||
if (v !== attr.value) attr.value = v;
|
||||
}
|
||||
}
|
||||
const handlers = {
|
||||
bind: handle_bind,
|
||||
checked: handle_checked,
|
||||
save: handle_save,
|
||||
cancel: handle_cancel,
|
||||
repeat: handle_repeat,
|
||||
@@ -341,21 +404,75 @@
|
||||
|
||||
function rescan() {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", realrescan, { once: true });
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
realrescan();
|
||||
resolve();
|
||||
}, { once: true });
|
||||
});
|
||||
} else {
|
||||
realrescan();
|
||||
return Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
const delay = (ms, value) => new Promise(r => setTimeout(() => r(value), ms));
|
||||
const next_frame = () => new Promise(r => requestAnimationFrame(() => r()));
|
||||
const is_expandable = arr => arr && Array.isArray(arr.data) &&
|
||||
typeof arr.start === 'number' && typeof arr.end === 'number';
|
||||
|
||||
function expand_array(name, start, end) {
|
||||
const arr = settings.data[name];
|
||||
const page = 100, all = [];
|
||||
const stop = Math.min(end, typeof arr?.size === 'number' ? arr.size - 1 : end);
|
||||
const done = () => {
|
||||
set(settings.data, `${name}.data`, all);
|
||||
set(settings.data, `${name}.start`, start);
|
||||
return rescan().then(() => all);
|
||||
};
|
||||
const next = from => {
|
||||
if (from > stop) return Promise.resolve(done());
|
||||
const to = Math.min(from + page - 1, stop);
|
||||
return fetch(`api/get/${name}/${from}/${to}`)
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
|
||||
.then(data => {
|
||||
if (!data || !data.length) return done();
|
||||
all.push(...data);
|
||||
return next(from + data.length);
|
||||
});
|
||||
};
|
||||
return next(start);
|
||||
}
|
||||
|
||||
function expand_arrays() {
|
||||
const all = [];
|
||||
for (const name in settings.data) {
|
||||
const arr = settings.data[name];
|
||||
if (is_expandable(arr)) all.push(expand_array(name, arr.start, arr.end));
|
||||
}
|
||||
return Promise.all(all);
|
||||
}
|
||||
|
||||
function call(name, args) {
|
||||
if (isMock) {
|
||||
return delay(750, true).then(val => console.log('response', name));
|
||||
} else {
|
||||
return rpc.call(name, args)
|
||||
.catch(err => console.log('CALL FAILED', name, args, err));
|
||||
return delay(750, true).then(() => console.log('response', name));
|
||||
}
|
||||
if (name === 'set') {
|
||||
return fetch('api/set', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(args),
|
||||
}).catch(err => console.log('CALL FAILED', name, args, err));
|
||||
}
|
||||
if (args && typeof args === 'object') {
|
||||
const {name: aname, start: s = 0, size: total = 100, end} = args;
|
||||
const stop = typeof end === 'number' ? end : s + total - 1;
|
||||
return expand_array(aname, s, stop).catch(err => console.log('CALL FAILED', name, args, err));
|
||||
}
|
||||
const path = typeof args === 'string' && args ? '/' + args : '';
|
||||
return fetch('api/get' + path)
|
||||
.then(r => r.ok ? r.json() : Promise.reject(r.statusText))
|
||||
.catch(err => console.log('CALL FAILED', name, args, err));
|
||||
};
|
||||
|
||||
function apply(target, patch) {
|
||||
@@ -373,16 +490,18 @@
|
||||
function apply_and_rescan(target, patch) {
|
||||
apply(target, patch);
|
||||
prune(settings.data, settings.edits);
|
||||
rescan();
|
||||
return rescan();
|
||||
};
|
||||
|
||||
function on(name, fn) {
|
||||
userhandlers[name] = fn;
|
||||
if (name === 'ready' && status.ready) setTimeout(fn, 0);
|
||||
};
|
||||
|
||||
function init(conf) {
|
||||
install_ready_gate();
|
||||
apply_and_rescan(settings, conf);
|
||||
if (!settings.auth) rpc_connect();
|
||||
if (!settings.auth) fetch_and_ready();
|
||||
};
|
||||
|
||||
global.Dashboard = { init, call, on, status, login, logout };
|
||||
|
||||
@@ -62,7 +62,7 @@ $(foreach T,$(RP_TARGETS), $(foreach P,$(PROJECTS), $(eval $(call RP_DASHBOARD_R
|
||||
%.zip: %
|
||||
zip -qr $@ $<
|
||||
|
||||
upload: $(ZIPS)
|
||||
deploy: $(ZIPS)
|
||||
scp $(ZIPS) h1:/data/downloads/
|
||||
|
||||
clean:
|
||||
|
||||
+52
-35
@@ -5,7 +5,7 @@
|
||||
// inlining referenced CSS, JS, images, and icons.
|
||||
//
|
||||
// Usage:
|
||||
// node html2c.js [-o OUTPUT.c] HTML_FILE
|
||||
// node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -15,24 +15,25 @@ const zlib = require('zlib');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let outputFile = null;
|
||||
let inputFile = null;
|
||||
const inputFiles = [];
|
||||
const useMap = new Map();
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '-o' && i + 1 < args.length) {
|
||||
outputFile = args[++i];
|
||||
} else if (!args[i].startsWith('-') && inputFile === null) {
|
||||
inputFile = args[i];
|
||||
} else if (args[i] === '--use' && i + 1 < args.length) {
|
||||
const eq = args[++i].indexOf('=');
|
||||
if (eq > 0) useMap.set(args[i].slice(0, eq), args[i].slice(eq + 1));
|
||||
} else if (!args[i].startsWith('-')) {
|
||||
inputFiles.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE');
|
||||
if (inputFiles.length === 0) {
|
||||
console.error('Usage: node html2c.js [-o OUTPUT.c] HTML_FILE [HTML_FILE ...]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseDir = path.dirname(path.resolve(inputFile));
|
||||
const html = fs.readFileSync(inputFile, 'utf8');
|
||||
|
||||
function fetchUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https') ? https : http;
|
||||
@@ -47,12 +48,15 @@ function fetchUrl(url) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readAsset(name, encoding) {
|
||||
async function readAsset(name, encoding, baseDir) {
|
||||
const override = useMap.get(name);
|
||||
if (override != null) name = override;
|
||||
if (name.startsWith('http://') || name.startsWith('https://')) {
|
||||
const data = await fetchUrl(name);
|
||||
return encoding ? data.toString(encoding) : data;
|
||||
}
|
||||
return fs.readFileSync(path.join(baseDir, name), encoding || null);
|
||||
const fullPath = path.isAbsolute(name) ? name : path.join(baseDir, name);
|
||||
return fs.readFileSync(fullPath, encoding || null);
|
||||
}
|
||||
|
||||
function attr(match, name) {
|
||||
@@ -66,21 +70,21 @@ function replaceAttr(match, name, value) {
|
||||
return match.replace(re, `${name}="${value}"`);
|
||||
}
|
||||
|
||||
async function inlineCss(match) {
|
||||
async function inlineCss(match, baseDir) {
|
||||
const href = attr(match, 'href');
|
||||
if (!href) return match;
|
||||
const css = await readAsset(href, 'utf8');
|
||||
const css = await readAsset(href, 'utf8', baseDir);
|
||||
return `<style>${css}</style>`;
|
||||
}
|
||||
|
||||
async function inlineJs(match) {
|
||||
async function inlineJs(match, baseDir) {
|
||||
const src = attr(match, 'src');
|
||||
if (!src) return match;
|
||||
const js = await readAsset(src, 'utf8');
|
||||
const js = await readAsset(src, 'utf8', baseDir);
|
||||
return `<script>${js}</script>`;
|
||||
}
|
||||
|
||||
async function inlineImg(match) {
|
||||
async function inlineImg(match, baseDir) {
|
||||
const src = attr(match, 'src');
|
||||
if (!src) return match;
|
||||
const ext = path.extname(src).toLowerCase();
|
||||
@@ -91,7 +95,7 @@ async function inlineImg(match) {
|
||||
else if (ext === '.webp') mime = 'image/webp';
|
||||
else if (ext === '.ico') mime = 'image/x-icon';
|
||||
|
||||
const data = await readAsset(src);
|
||||
const data = await readAsset(src, null, baseDir);
|
||||
if (ext === '.svg') {
|
||||
const svg = Buffer.isBuffer(data) ? data.toString('utf8') : data;
|
||||
return replaceAttr(
|
||||
@@ -107,10 +111,10 @@ async function inlineImg(match) {
|
||||
);
|
||||
}
|
||||
|
||||
async function inlineSvg(match) {
|
||||
async function inlineSvg(match, baseDir) {
|
||||
const href = attr(match, 'href');
|
||||
if (!href || !href.toLowerCase().endsWith('.svg')) return match;
|
||||
const data = await readAsset(href);
|
||||
const data = await readAsset(href, null, baseDir);
|
||||
return replaceAttr(
|
||||
match,
|
||||
'href',
|
||||
@@ -118,7 +122,7 @@ async function inlineSvg(match) {
|
||||
);
|
||||
}
|
||||
|
||||
async function processHtml(content) {
|
||||
async function processHtml(content, baseDir) {
|
||||
let result = content;
|
||||
const cssRegex = /<link[^>]+rel=["']stylesheet["'][^>]*>/g;
|
||||
const jsRegex = /<script[^>]+src=["'][^"']+["'][^>]*><\/script>/g;
|
||||
@@ -126,16 +130,16 @@ async function processHtml(content) {
|
||||
const svgRegex = /<link[^>]+rel=["']icon["'][^>]*>/g;
|
||||
|
||||
for (const match of content.match(cssRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineCss(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(jsRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineJs(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(imgRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineImg(match, baseDir));
|
||||
}
|
||||
for (const match of content.match(svgRegex) || []) {
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match));
|
||||
if (!match.includes('data:')) result = result.replace(match, await inlineSvg(match, baseDir));
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -145,30 +149,43 @@ function cArray(data) {
|
||||
return Array.from(Buffer.from(data)).concat(0).join(',');
|
||||
}
|
||||
|
||||
function generateC(data) {
|
||||
const destination = `${path.basename(inputFile)}.gz`;
|
||||
const stat = fs.statSync(inputFile);
|
||||
const mtime = parseInt(stat.mtimeMs / 1000);
|
||||
const zipped = zlib.gzipSync(data);
|
||||
const bytes = cArray(zipped);
|
||||
function generateC(files) {
|
||||
const vars = files.map(({inputFile, data}, i) => {
|
||||
const zipped = zlib.gzipSync(data);
|
||||
return {inputFile, zipped};
|
||||
});
|
||||
|
||||
const decls = vars.map(({zipped}, i) =>
|
||||
`static const unsigned char v${i}[] = {${cArray(zipped)}};`
|
||||
).join('\n');
|
||||
|
||||
const entries = vars.map(({inputFile, zipped}, i) => {
|
||||
const destination = `${path.basename(inputFile)}.gz`;
|
||||
const mtime = parseInt(fs.statSync(inputFile).mtimeMs / 1000);
|
||||
return ` {"/${destination}", v${i}, sizeof(v${i}) - 1, ${mtime}}, // size: ${zipped.length}`;
|
||||
}).join('\n');
|
||||
|
||||
return `// DO NOT EDIT. This file is generated using this command:
|
||||
// ${process.argv.join(' ')}
|
||||
|
||||
#include "mongoose.h"
|
||||
|
||||
static const unsigned char v0[] = {${bytes}};
|
||||
${decls}
|
||||
|
||||
const struct mg_mem_file mg_packed_files[] = {
|
||||
{"/${destination}", v0, sizeof(v0) - 1, ${mtime}}, // size: ${zipped.length}
|
||||
${entries}
|
||||
{NULL, NULL, 0, 0}
|
||||
};
|
||||
`;
|
||||
}
|
||||
|
||||
processHtml(html)
|
||||
.then(result => {
|
||||
const c = generateC(result);
|
||||
Promise.all(inputFiles.map(inputFile => {
|
||||
const baseDir = path.dirname(path.resolve(inputFile));
|
||||
const html = fs.readFileSync(inputFile, 'utf8');
|
||||
return processHtml(html, baseDir).then(data => ({inputFile, data}));
|
||||
}))
|
||||
.then(files => {
|
||||
const c = generateC(files);
|
||||
if (outputFile) {
|
||||
fs.writeFileSync(outputFile, c);
|
||||
} else {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
@@ -70,8 +70,8 @@ function sign(firmware, privateKeyFile = 'private.pem') {
|
||||
? firmware.slice(0, dot) + '.signed' + firmware.slice(dot)
|
||||
: firmware + '.signed';
|
||||
|
||||
fs.writeFileSync(out, Buffer.concat([fw, raw]));
|
||||
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature)`);
|
||||
fs.writeFileSync(out, Buffer.concat([fw, raw, Buffer.from('MGSG')]));
|
||||
console.log(`Signed: ${out} (${fw.length} bytes firmware + 64 bytes signature + 4 bytes magic)`);
|
||||
}
|
||||
|
||||
const [,, cmd, arg1, arg2] = process.argv;
|
||||
|
||||
+487
-245
@@ -5,67 +5,21 @@
|
||||
#define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n"
|
||||
#define MG_JSON_HEADERS "Content-Type: application/json\r\n" MG_NO_CACHE_HEADERS
|
||||
|
||||
struct mg_dash_user {
|
||||
struct mg_dash_user *next;
|
||||
char name[32]; // User name
|
||||
char token[21]; // Login token
|
||||
int level; // Access level
|
||||
uint64_t expire; // Expiration timestamp
|
||||
#define CONN_HANDLED 'Z'
|
||||
|
||||
struct mg_dash_cdata {
|
||||
char marker;
|
||||
struct mg_dash_user *u;
|
||||
struct mg_dash *dash;
|
||||
};
|
||||
|
||||
#define CONN_HANDLED 'Z'
|
||||
static struct mg_dash_user s_guest;
|
||||
|
||||
static struct mg_str trimq(struct mg_str s) { // Trim double quotes
|
||||
if (s.len > 1 && s.buf[0] == '"') s.len -= 2, s.buf++;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void mg_dash_broadcast(struct mg_mgr *mgr, int level, const char *fmt,
|
||||
...) {
|
||||
struct mg_connection *c;
|
||||
va_list ap;
|
||||
for (c = mgr->conns; c != NULL; c = c->next) {
|
||||
int user_level = *(int *) c->data;
|
||||
if (!c->is_websocket) continue;
|
||||
if (level > 0 && user_level < level) continue;
|
||||
if (c->send.len > MG_DASH_MAX_SEND_BUF_SIZE) {
|
||||
mg_error(c, "%lu buffered data %lu > MG_DASH_MAX_SEND_BUF_SIZE", c->id,
|
||||
c->send.len);
|
||||
} else {
|
||||
va_start(ap, fmt);
|
||||
mg_ws_vprintf(c, WEBSOCKET_OP_TEXT, fmt, &ap);
|
||||
va_end(ap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static size_t mg_print_fmt(mg_pfn_t out, void *param, va_list *ap) {
|
||||
const char *fmt = va_arg(*ap, const char *);
|
||||
ap = va_arg(*ap, va_list *);
|
||||
return mg_vxprintf(out, param, fmt, ap);
|
||||
}
|
||||
|
||||
static void mg_dash_success(struct mg_connection *c, struct mg_str req,
|
||||
const char *fmt, ...) {
|
||||
struct mg_str id = mg_json_get_tok(req, "$.id");
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:%M}", MG_ESC("id"), id.len,
|
||||
id.buf, MG_ESC("result"), mg_print_fmt, fmt, &ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static void mg_dash_error(struct mg_connection *c, struct mg_str req,
|
||||
const char *fmt, ...) {
|
||||
struct mg_str id = mg_json_get_tok(req, "$.id");
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%.*s,%m:{%m:%d,%m:%m}}", MG_ESC("id"),
|
||||
id.len, id.buf, MG_ESC("error"), MG_ESC("code"), -1,
|
||||
MG_ESC("message"), mg_print_fmt, fmt, &ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
static struct mg_field_set *mg_dash_find_field_set(struct mg_dash *dash,
|
||||
struct mg_str name) {
|
||||
struct mg_field_set *fs;
|
||||
@@ -92,6 +46,8 @@ static size_t mg_print_field(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
n += mg_xprintf(fn, arg, "%s", *(bool *) f->value ? "true" : "false");
|
||||
} else if (f->type == MG_VAL_INT) {
|
||||
n += mg_xprintf(fn, arg, "%d", *(int *) f->value);
|
||||
} else if (f->type == MG_VAL_UINT64) {
|
||||
n += mg_xprintf(fn, arg, "%llu", (uint64_t) *(uint64_t *) f->value);
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
n += mg_xprintf(fn, arg, "%.2f", *(double *) f->value);
|
||||
} else if (f->type == MG_VAL_STR) {
|
||||
@@ -116,10 +72,56 @@ static size_t mg_print_field_set(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
return n;
|
||||
}
|
||||
|
||||
static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
static int mg_dash_array_size(struct mg_field_set *set,
|
||||
struct mg_dash_user *u) {
|
||||
int saved = *set->index, sz = -1;
|
||||
*set->index = -1;
|
||||
if (set->fn) {
|
||||
if (set->fn(MG_DASH_READ, u)) sz = *set->index;
|
||||
} else if (set->get_dir) {
|
||||
mg_dash_dir_read(set, u);
|
||||
sz = *set->index;
|
||||
}
|
||||
*set->index = saved;
|
||||
return sz;
|
||||
}
|
||||
|
||||
static size_t mg_dash_print_array(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
struct mg_field_set *set = va_arg(*ap, struct mg_field_set *);
|
||||
int from = va_arg(*ap, int);
|
||||
int to = va_arg(*ap, int);
|
||||
struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *);
|
||||
bool started = false;
|
||||
int saved = *set->index;
|
||||
size_t n = 0;
|
||||
*set->index = from;
|
||||
n += mg_xprintf(fn, arg, "[");
|
||||
for (;;) {
|
||||
bool done = to >= 0 && *set->index > to;
|
||||
if (!done) {
|
||||
if (set->fn)
|
||||
set->fn(MG_DASH_READ, u);
|
||||
else if (set->get_dir)
|
||||
mg_dash_dir_read(set, u);
|
||||
done = *set->index < 0;
|
||||
}
|
||||
if (done) break;
|
||||
n += mg_xprintf(fn, arg, "%s%M", started ? "," : "", mg_print_field_set,
|
||||
set);
|
||||
started = true;
|
||||
(*set->index)++;
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "]");
|
||||
*set->index = saved;
|
||||
return n;
|
||||
}
|
||||
|
||||
static size_t mg_dash_print_endpoint(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
|
||||
struct mg_dash_user *u = va_arg(*ap, struct mg_dash_user *);
|
||||
struct mg_str *name = va_arg(*ap, struct mg_str *);
|
||||
int level = va_arg(*ap, int);
|
||||
struct mg_str *from_str = va_arg(*ap, struct mg_str *);
|
||||
struct mg_str *to_str = va_arg(*ap, struct mg_str *);
|
||||
struct mg_field_set *set = mg_dash_find_field_set(dash, *name);
|
||||
size_t n = 0;
|
||||
if (name->len == 0) {
|
||||
@@ -127,37 +129,74 @@ static size_t mg_dash_print_name(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
const char *comma = "";
|
||||
n += mg_xprintf(fn, arg, "{");
|
||||
for (fs = dash->sets; fs != NULL; fs = fs->next) {
|
||||
if (fs->read_level > 0 && level < fs->read_level) continue;
|
||||
if (fs->reader) fs->reader();
|
||||
n += mg_xprintf(fn, arg, comma);
|
||||
n += mg_xprintf(fn, arg, "%m:", MG_ESC(fs->name));
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, fs);
|
||||
if (fs->index != NULL) {
|
||||
int sz = mg_dash_array_size(fs, u);
|
||||
if (sz < 0) continue;
|
||||
n += mg_xprintf(fn, arg, comma);
|
||||
n += mg_xprintf(fn, arg, "%m:%d", MG_ESC(fs->name), sz);
|
||||
} else {
|
||||
if (fs->fn && !fs->fn(MG_DASH_READ, u)) continue;
|
||||
n += mg_xprintf(fn, arg, comma);
|
||||
n += mg_xprintf(fn, arg, "%m:%M", MG_ESC(fs->name), mg_print_field_set,
|
||||
fs);
|
||||
}
|
||||
comma = ",";
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "}");
|
||||
} else if (set != NULL &&
|
||||
(set->read_level <= 0 || level >= set->read_level)) {
|
||||
if (set->reader) set->reader();
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set);
|
||||
} else if (set != NULL && (set->fn == NULL || set->fn(MG_DASH_READ, u))) {
|
||||
if (set->index != NULL && from_str != NULL && from_str->len > 0) {
|
||||
int from = 0, to = 0;
|
||||
mg_str_to_num(*from_str, 10, &from, sizeof(from));
|
||||
to = from;
|
||||
if (to_str != NULL && to_str->len > 0) {
|
||||
mg_str_to_num(*to_str, 10, &to, sizeof(to));
|
||||
}
|
||||
n += mg_xprintf(fn, arg, "%M", mg_dash_print_array, set, from, to, u);
|
||||
} else if (set->index != NULL) {
|
||||
n += mg_xprintf(fn, arg, "%d", mg_dash_array_size(set, u));
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "%M", mg_print_field_set, set);
|
||||
}
|
||||
} else {
|
||||
n += mg_xprintf(fn, arg, "null");
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
static size_t mg_dash_print(mg_pfn_t fn, void *arg, va_list *ap) {
|
||||
struct mg_str *req = va_arg(*ap, struct mg_str *);
|
||||
struct mg_dash *dash = va_arg(*ap, struct mg_dash *);
|
||||
int level = va_arg(*ap, int);
|
||||
struct mg_str name = trimq(mg_json_get_tok(*req, "$.params"));
|
||||
return mg_xprintf(fn, arg, "%M", mg_dash_print_name, dash, &name, level);
|
||||
}
|
||||
|
||||
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *set) {
|
||||
if (set->reader) set->reader();
|
||||
mg_dash_broadcast(mgr, set->read_level, "{%m:%m,%m:{%m:%M}}",
|
||||
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
|
||||
MG_ESC(set->name), mg_print_field_set, set);
|
||||
struct mg_connection *c;
|
||||
for (c = mgr->conns; c != NULL; c = c->next) {
|
||||
struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
|
||||
struct mg_dash_user *u = d->u;
|
||||
if (!c->is_websocket) continue;
|
||||
if (u == NULL) continue;
|
||||
if (set->index != NULL && *set->index < 0) {
|
||||
int sz = mg_dash_array_size(set, u);
|
||||
if (sz < 0) continue;
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%d}}", MG_ESC("method"),
|
||||
MG_ESC("change"), MG_ESC("params"), MG_ESC(set->name), sz);
|
||||
} else {
|
||||
int saved_idx = set->index != NULL ? *set->index : 0;
|
||||
bool ok = set->fn ? set->fn(MG_DASH_READ, u)
|
||||
: (set->get_dir ? mg_dash_dir_read(set, u) : true);
|
||||
if (!ok) {
|
||||
if (set->index != NULL) *set->index = saved_idx;
|
||||
continue;
|
||||
}
|
||||
if (set->index != NULL) {
|
||||
char key[64];
|
||||
mg_snprintf(key, sizeof(key), "%s/%d", set->name, *set->index);
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}",
|
||||
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
|
||||
MG_ESC(key), mg_print_field_set, set);
|
||||
*set->index = saved_idx;
|
||||
} else {
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m,%m:{%m:%M}}",
|
||||
MG_ESC("method"), MG_ESC("change"), MG_ESC("params"),
|
||||
MG_ESC(set->name), mg_print_field_set, set);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
|
||||
@@ -174,6 +213,13 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
|
||||
*(int *) f->value = (int) d;
|
||||
ok = true;
|
||||
}
|
||||
} else if (f->type == MG_VAL_UINT64) {
|
||||
double d;
|
||||
if (f->value_size == sizeof(uint64_t) &&
|
||||
mg_json_get_num(json, json_path, &d) && d == (double) (int64_t) d) {
|
||||
*(uint64_t *) f->value = (uint64_t) d;
|
||||
ok = true;
|
||||
}
|
||||
} else if (f->type == MG_VAL_DBL) {
|
||||
ok = f->value_size == sizeof(double) &&
|
||||
mg_json_get_num(json, json_path, (double *) f->value);
|
||||
@@ -187,97 +233,99 @@ static int mg_dash_parse_field(struct mg_str json, struct mg_field *f) {
|
||||
}
|
||||
|
||||
static int mg_dash_apply(struct mg_connection *c, struct mg_dash *dash,
|
||||
struct mg_str json, int level) {
|
||||
struct mg_str json, struct mg_dash_user *u) {
|
||||
struct mg_str key, val;
|
||||
size_t ofs = 0;
|
||||
int total_count = 0;
|
||||
while ((ofs = mg_json_next(json, ofs, &key, &val)) > 0) {
|
||||
struct mg_field_set *set = mg_dash_find_field_set(dash, trimq(key));
|
||||
int count = 0;
|
||||
if (set != NULL && (set->write_level <= 0 || level >= set->write_level)) {
|
||||
if (set == NULL) {
|
||||
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
|
||||
continue;
|
||||
}
|
||||
if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) continue; // auth check
|
||||
{
|
||||
size_t i;
|
||||
for (i = 0; set->fields[i].name != NULL; i++) {
|
||||
if (mg_dash_parse_field(val, &set->fields[i])) count++;
|
||||
}
|
||||
if (count) {
|
||||
if (set->writer) set->writer();
|
||||
mg_dash_send_change(c->mgr, set);
|
||||
total_count += count;
|
||||
}
|
||||
} else if (set == NULL) {
|
||||
MG_ERROR(("UNKNOWN SET: [%.*s]", key.len, key.buf));
|
||||
}
|
||||
if (count) {
|
||||
if (set->fn) set->fn(MG_DASH_WRITE, u); // apply side effects
|
||||
mg_dash_send_change(c->mgr, set);
|
||||
total_count += count;
|
||||
}
|
||||
}
|
||||
return total_count;
|
||||
}
|
||||
|
||||
static void mg_dash_process_msg(struct mg_connection *c,
|
||||
struct mg_ws_message *wm,
|
||||
struct mg_dash *dash) {
|
||||
struct mg_str req = wm->data;
|
||||
struct mg_str method = trimq(mg_json_get_tok(req, "$.method"));
|
||||
int level = *(int *) c->data;
|
||||
if (mg_match(method, mg_str("get"), NULL)) {
|
||||
mg_dash_success(c, req, "%M", mg_dash_print, &req, dash, level);
|
||||
} else if (mg_match(method, mg_str("set"), NULL)) {
|
||||
struct mg_str params = trimq(mg_json_get_tok(req, "$.params"));
|
||||
int count = mg_dash_apply(c, dash, params, level);
|
||||
mg_dash_success(c, req, "%d", count);
|
||||
} else {
|
||||
mg_dash_error(c, req, "%s", "unknown method");
|
||||
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u) {
|
||||
char dir[256], fname[128] = "";
|
||||
struct mg_fs *fs = u->dash->upload_fs ? u->dash->upload_fs : &mg_fs_posix;
|
||||
struct mg_field *name_field = NULL, *size_field = NULL;
|
||||
size_t i;
|
||||
|
||||
if (!set->get_dir(u, dir, sizeof(dir))) return false;
|
||||
|
||||
for (i = 0; set->fields[i].name != NULL; i++) {
|
||||
if (name_field == NULL && set->fields[i].type == MG_VAL_STR &&
|
||||
strcmp(set->fields[i].name, "name") == 0)
|
||||
name_field = &set->fields[i];
|
||||
if (size_field == NULL && strcmp(set->fields[i].name, "size") == 0)
|
||||
size_field = &set->fields[i];
|
||||
}
|
||||
if (name_field == NULL) return false;
|
||||
|
||||
if (*set->index == -1) { // Size query: count all files
|
||||
int count = 0;
|
||||
while (mg_fs_ls(fs, dir, fname, sizeof(fname))) count++;
|
||||
*set->index = count;
|
||||
return true;
|
||||
}
|
||||
|
||||
{ // Regular read: scan to *set->index
|
||||
int target = *set->index, cur = 0;
|
||||
while (mg_fs_ls(fs, dir, fname, sizeof(fname))) {
|
||||
if (cur++ == target) {
|
||||
mg_snprintf((char *) name_field->value, name_field->value_size, "%s",
|
||||
fname);
|
||||
if (size_field != NULL) {
|
||||
char path[512];
|
||||
size_t sz = 0;
|
||||
mg_snprintf(path, sizeof(path), "%s/%s", dir, fname);
|
||||
fs->st(path, &sz, NULL);
|
||||
if (size_field->type == MG_VAL_UINT64)
|
||||
*(uint64_t *) size_field->value = (uint64_t) sz;
|
||||
else if (size_field->type == MG_VAL_INT)
|
||||
*(int *) size_field->value = (int) sz;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
*set->index = -1; // No more entries
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void mg_dash_file_add(struct mg_str name, size_t size) {
|
||||
struct mg_dash_file *f = (struct mg_dash_file *) mg_calloc(1, sizeof(*f));
|
||||
f->name = mg_strdup(name).buf;
|
||||
f->size = size;
|
||||
f->next = mg_dash_files;
|
||||
mg_dash_files = f;
|
||||
}
|
||||
|
||||
void mg_dash_file_del(struct mg_str name) {
|
||||
struct mg_dash_file **head, *f;
|
||||
for (head = &mg_dash_files, f = *head; f; head = &(*head)->next, f = *head) {
|
||||
if (mg_strcmp(mg_str(f->name), name) == 0) {
|
||||
MG_INFO(("Deleting %s", f->name));
|
||||
*head = f->next;
|
||||
mg_free(f->name);
|
||||
mg_free(f);
|
||||
return;
|
||||
static bool mg_dash_set_file_name(struct mg_field_set *set,
|
||||
struct mg_str name) {
|
||||
size_t i;
|
||||
for (i = 0; set->fields[i].name != NULL; i++) {
|
||||
struct mg_field *f = &set->fields[i];
|
||||
if (f->type == MG_VAL_STR && strcmp(f->name, "name") == 0) {
|
||||
mg_snprintf((char *) f->value, f->value_size, "%.*s", (int) name.len,
|
||||
name.buf);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Files array: [{"name": "foo.txt", "size": 1234}]
|
||||
struct mg_dash_file *mg_dash_files;
|
||||
static char s_files[1024];
|
||||
|
||||
static void read_files(void) {
|
||||
size_t len = 0;
|
||||
struct mg_dash_file *f;
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "[");
|
||||
for (f = mg_dash_files; f; f = f->next) {
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "%s{%m:%m,%m:%u}",
|
||||
len > 1 ? "," : "", MG_ESC("name"), MG_ESC(f->name),
|
||||
MG_ESC("size"), f->size);
|
||||
}
|
||||
len += mg_snprintf(s_files + len, sizeof(s_files) - len, "]");
|
||||
}
|
||||
|
||||
static struct mg_field fields_files[] = {
|
||||
{"data", MG_VAL_RAW, s_files, sizeof(s_files)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
|
||||
static struct mg_field_set set_files = {
|
||||
"files", fields_files, read_files, NULL, 0, 0, NULL,
|
||||
};
|
||||
|
||||
static inline void mg_log_http_req(struct mg_connection *c,
|
||||
struct mg_http_message *hm) {
|
||||
int len = 0;
|
||||
size_t n, spaces = 0;
|
||||
size_t n, spaces = 0, body_n = hm->body.len;
|
||||
struct mg_http_message tmp;
|
||||
memset(&tmp, 0, sizeof(tmp));
|
||||
len = mg_http_parse((char *) c->send.buf, c->send.len, &tmp);
|
||||
@@ -286,18 +334,24 @@ static inline void mg_log_http_req(struct mg_connection *c,
|
||||
(c->send.buf[c->send.len - spaces - 1] == '\r' ||
|
||||
c->send.buf[c->send.len - spaces - 1] == '\n'))
|
||||
spaces++;
|
||||
MG_DEBUG(("%lu %.*s %.*s %.*s: %lu %.*s -> %lu %.*s", c->id, hm->method.len,
|
||||
hm->method.buf, hm->uri.len, hm->uri.buf, c->send.len > 15 ? 3 : 0,
|
||||
&c->send.buf[9], hm->body.len, hm->body.len, hm->body.buf,
|
||||
c->send.len - n, c->send.len - n - spaces, c->send.buf + n));
|
||||
}
|
||||
|
||||
static struct mg_str mg_dash_file_name(struct mg_http_message *hm) {
|
||||
struct mg_str name = mg_str_n(hm->uri.buf + 4, hm->uri.len - 4); // - /fs/
|
||||
// Decode file name in-place - directly into the request buffer
|
||||
int len = mg_url_decode(name.buf, name.len, name.buf, name.len + 1, 0);
|
||||
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
|
||||
return name;
|
||||
// hm->body.len comes from Content-Length and can be larger than the bytes
|
||||
// actually buffered so far (e.g. mid-stream uploads); cap the preview to
|
||||
// what's actually present in c->recv, or we'd read past its end
|
||||
{
|
||||
char *recv_end = (char *) c->recv.buf + c->recv.len;
|
||||
if (hm->body.buf >= (char *) c->recv.buf && hm->body.buf <= recv_end) {
|
||||
size_t avail = (size_t) (recv_end - hm->body.buf);
|
||||
if (body_n > avail) body_n = avail;
|
||||
} else {
|
||||
body_n = 0;
|
||||
}
|
||||
}
|
||||
MG_DEBUG(("%lu %.*s %.*s%s%.*s %.*s: %lu %.*s -> %lu %.*s", c->id,
|
||||
hm->method.len, hm->method.buf, hm->uri.len, hm->uri.buf,
|
||||
hm->query.len > 0 ? "?" : "", hm->query.len, hm->query.buf,
|
||||
c->send.len > 15 ? 3 : 0, &c->send.buf[9], hm->body.len, body_n,
|
||||
hm->body.buf, c->send.len - n, c->send.len - n - spaces,
|
||||
c->send.buf + n));
|
||||
}
|
||||
|
||||
static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) {
|
||||
@@ -309,13 +363,20 @@ static void mg_dash_upload_cb(struct mg_connection *c, const char *errmsg) {
|
||||
if (errmsg) {
|
||||
mg_http_reply(c, 500, NULL, "%s\n", errmsg);
|
||||
} else {
|
||||
char path[128];
|
||||
size_t size = 0;
|
||||
mg_snprintf(path, sizeof(path), "/tmp/%s", c->data);
|
||||
mg_fs_posix.st(path, &size, NULL);
|
||||
mg_dash_file_add(mg_str(c->data), size);
|
||||
// mg_http_start_upload() repurposes c->data for its own bookkeeping,
|
||||
// so the field set can't be cached there. Re-derive the dashboard from
|
||||
// c->fn_data instead, and notify every file-backed array: the upload
|
||||
// could belong to any of them, and re-querying get_dir() per recipient
|
||||
// is what mg_dash_send_change() does anyway (directories can be
|
||||
// user-specific)
|
||||
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
|
||||
struct mg_field_set *fs;
|
||||
mg_http_reply(c, 200, NULL, "ok\n");
|
||||
mg_dash_send_change(c->mgr, &set_files);
|
||||
for (fs = dash->sets; fs != NULL; fs = fs->next) {
|
||||
if (fs->get_dir == NULL) continue;
|
||||
*fs->index = -1; // Signal mg_dash_send_change() to broadcast new size
|
||||
mg_dash_send_change(c->mgr, fs);
|
||||
}
|
||||
}
|
||||
c->is_draining = 1;
|
||||
}
|
||||
@@ -326,16 +387,69 @@ static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) {
|
||||
return mg_millis() + t * 1000;
|
||||
}
|
||||
|
||||
static struct mg_dash_user *mg_dash_add_user(struct mg_dash_user **users,
|
||||
struct mg_dash *dash,
|
||||
const char *name,
|
||||
const char *token, int level) {
|
||||
struct mg_dash_user *u = (struct mg_dash_user *) mg_calloc(1, sizeof(*u));
|
||||
if (u != NULL) {
|
||||
mg_snprintf(u->name, sizeof(u->name), "%s", name);
|
||||
if (token == NULL) {
|
||||
mg_random_str(u->token, sizeof(u->token) - 1);
|
||||
} else {
|
||||
mg_snprintf(u->token, sizeof(u->token), "%s", token);
|
||||
}
|
||||
u->level = level;
|
||||
u->expire = mg_dash_make_expiration_time(dash);
|
||||
u->dash = dash;
|
||||
u->next = *users;
|
||||
*users = u;
|
||||
}
|
||||
return u;
|
||||
}
|
||||
|
||||
static struct mg_dash_user *mg_dash_find_user(struct mg_dash_user *users,
|
||||
const char *name) {
|
||||
struct mg_dash_user *u;
|
||||
for (u = users; u != NULL; u = u->next) {
|
||||
if (strcmp(u->name, name) == 0) return u;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct mg_dash_user *mg_dash_find_token(struct mg_dash_user *users,
|
||||
const char *token) {
|
||||
struct mg_dash_user *u;
|
||||
for (u = users; u != NULL; u = u->next) {
|
||||
if (strcmp(u->token, token) == 0) return u;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void mg_dash_refresh_user(struct mg_dash *dash,
|
||||
struct mg_dash_user *user) {
|
||||
user->expire = mg_dash_make_expiration_time(dash);
|
||||
}
|
||||
|
||||
// Parse HTTP requests, return authenticated user or NULL
|
||||
static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm,
|
||||
static struct mg_dash_user *mg_dash_authenticate(struct mg_connection *c,
|
||||
struct mg_http_message *hm,
|
||||
struct mg_dash *dash) {
|
||||
static struct mg_dash_user *s_users; // List of authenticated users
|
||||
char user[100], pass[100];
|
||||
static struct mg_dash_user admin = {NULL, "admin", "admin", 9, (uint64_t) -1};
|
||||
struct mg_dash_user *u, *tmp, *result = NULL;
|
||||
struct mg_dash_user *u, *tmp;
|
||||
struct mg_str *ah;
|
||||
int level = 0, num_users = 0;
|
||||
|
||||
if (dash->authenticate == NULL) return &admin;
|
||||
if (dash->authenticate == NULL) {
|
||||
mg_snprintf(s_guest.name, sizeof(s_guest.name), "%s", "guest");
|
||||
s_guest.level = 9;
|
||||
s_guest.dash = dash;
|
||||
dash->guest = &s_guest;
|
||||
return dash->guest;
|
||||
}
|
||||
mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass));
|
||||
ah = mg_http_get_header(hm, "Authorization");
|
||||
// MG_DEBUG(("user [%s], pass: [%s], h: %.*s", user, pass, hm->head.len,
|
||||
// hm->head.buf));
|
||||
|
||||
@@ -343,47 +457,48 @@ static struct mg_dash_user *mg_dash_authenticate(struct mg_http_message *hm,
|
||||
for (u = s_users; u != NULL; u = tmp) {
|
||||
tmp = u->next;
|
||||
if (u->expire < mg_millis()) {
|
||||
struct mg_connection *conn;
|
||||
MG_DEBUG(("Deleting expired auth %s/%d %llu %u", u->name, u->level,
|
||||
u->expire, mg_millis() - u->expire));
|
||||
for (conn = c->mgr->conns; conn != NULL; conn = conn->next) {
|
||||
struct mg_dash_cdata *d = (struct mg_dash_cdata *) conn->data;
|
||||
if (conn->is_websocket && d->u == u) {
|
||||
d->u = NULL;
|
||||
conn->is_closing = 1;
|
||||
}
|
||||
}
|
||||
LIST_DELETE(struct mg_dash_user, &s_users, u);
|
||||
mg_free(u);
|
||||
}
|
||||
}
|
||||
|
||||
if (pass[0] != '\0') {
|
||||
struct mg_str *ah = mg_http_get_header(hm, "Authorization");
|
||||
if (ah != NULL) {
|
||||
// Auth header and password are set, auth by user/password via glue API
|
||||
int num_users = 0, level = dash->authenticate(user, sizeof(user), pass);
|
||||
MG_DEBUG(("user %s, level: %d", user, level));
|
||||
if (level > 0) { // Proceed only if the firmware authenticated us
|
||||
for (u = s_users; u != NULL && result == NULL;
|
||||
u = u->next, num_users++) {
|
||||
if (strcmp(user, u->name) == 0) {
|
||||
u->expire = mg_dash_make_expiration_time(dash);
|
||||
result = u;
|
||||
}
|
||||
}
|
||||
// Not yet authenticated, add to the list
|
||||
if (result == NULL && num_users < 10) {
|
||||
result = (struct mg_dash_user *) mg_calloc(1, sizeof(*result));
|
||||
mg_snprintf(result->name, sizeof(result->name), "%s", user);
|
||||
mg_random_str(result->token, sizeof(result->token) - 1);
|
||||
result->level = level, result->next = s_users, s_users = result;
|
||||
result->expire = mg_dash_make_expiration_time(dash);
|
||||
}
|
||||
}
|
||||
} else if (ah == NULL) {
|
||||
for (u = s_users; u != NULL && result == NULL; u = u->next) {
|
||||
if (strcmp(u->token, pass) == 0) {
|
||||
u->expire = mg_dash_make_expiration_time(dash);
|
||||
result = u;
|
||||
}
|
||||
}
|
||||
if (pass[0] == '\0') return NULL;
|
||||
|
||||
for (u = s_users; u != NULL; u = u->next) num_users++;
|
||||
if (ah == NULL) {
|
||||
u = mg_dash_find_token(s_users, pass);
|
||||
if (u != NULL) {
|
||||
mg_dash_refresh_user(dash, u);
|
||||
return u;
|
||||
}
|
||||
}
|
||||
|
||||
level = dash->authenticate(user, sizeof(user), pass);
|
||||
MG_DEBUG(("user %s, level: %d", user, level));
|
||||
if (level <= 0) return NULL;
|
||||
|
||||
u = mg_dash_find_user(s_users, user);
|
||||
if (u != NULL) {
|
||||
if (ah == NULL) mg_snprintf(u->token, sizeof(u->token), "%s", pass);
|
||||
mg_dash_refresh_user(dash, u);
|
||||
return u;
|
||||
}
|
||||
|
||||
if (num_users < 10)
|
||||
return mg_dash_add_user(&s_users, dash, user, ah == NULL ? pass : NULL,
|
||||
level);
|
||||
// MG_DEBUG(("[%s/%s] -> %s", user, pass, result ? "OK" : "FAIL"));
|
||||
return result;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void mg_handle_login(struct mg_connection *c, struct mg_dash_user *u) {
|
||||
@@ -408,48 +523,157 @@ static void mg_handle_logout(struct mg_connection *c) {
|
||||
mg_http_reply(c, 401, cookie, "Unauthorized\n");
|
||||
}
|
||||
|
||||
static void mg_dash_handle_del(struct mg_connection *c, struct mg_dash *dash,
|
||||
struct mg_dash_user *u, struct mg_str *parts) {
|
||||
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
|
||||
if (set == NULL || set->index == NULL) {
|
||||
mg_http_reply(c, 404, MG_JSON_HEADERS, "null\n");
|
||||
} else {
|
||||
int from = 0, to = 0, count = 0;
|
||||
if (parts[1].len) mg_str_to_num(parts[1], 10, &from, sizeof(from));
|
||||
to = parts[2].len ? 0 : from;
|
||||
if (parts[2].len) mg_str_to_num(parts[2], 10, &to, sizeof(to));
|
||||
for (*set->index = from; *set->index <= to; (*set->index)++) {
|
||||
if (set->fn && set->fn(MG_DASH_DELETE, u))
|
||||
count++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (count) {
|
||||
*set->index = -1;
|
||||
mg_dash_send_change(c->mgr, set);
|
||||
mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
|
||||
} else {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "false\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "POST /api/get/<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) {
|
||||
struct mg_dash *dash = (struct mg_dash *) c->fn_data;
|
||||
struct mg_dash_cdata *d = (struct mg_dash_cdata *) c->data;
|
||||
|
||||
if (ev == MG_EV_OPEN) {
|
||||
if (mg_dash_find_field_set(dash, mg_str("files")) == NULL) {
|
||||
MG_DASH_ADD_FIELD_SET(dash, &set_files);
|
||||
}
|
||||
d->dash = dash;
|
||||
// c->is_hexdumping = 1;
|
||||
} else if (ev == MG_EV_HTTP_HDRS && c->data[0] == 0) {
|
||||
} else if (ev == MG_EV_HTTP_HDRS && d->marker == 0) {
|
||||
// Received headers - check authentication and possibly start uploads/ota
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
struct mg_dash_user *u = mg_dash_authenticate(hm, dash);
|
||||
struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash);
|
||||
struct mg_str parts[3];
|
||||
memset(parts, 0, sizeof(parts));
|
||||
|
||||
if (mg_match(hm->uri, mg_str("/api/hi"), NULL) ||
|
||||
mg_match(hm->uri, mg_str("/api/logout"), NULL)) {
|
||||
// Do nothing, handle them MG_EV_HTTP_MSG. We bypass auth for those
|
||||
} else if (u == NULL && mg_match(hm->uri, mg_str("/api/#"), NULL)) {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
|
||||
c->data[0] = CONN_HANDLED;
|
||||
d->marker = CONN_HANDLED;
|
||||
} else if (mg_match(hm->uri, mg_str("/api/login"), NULL) && u != NULL) {
|
||||
mg_handle_login(c, u);
|
||||
c->data[0] = CONN_HANDLED;
|
||||
d->marker = CONN_HANDLED;
|
||||
} else if (mg_match(hm->uri, mg_str("/api/ota"), NULL)) {
|
||||
mg_http_start_ota(c, hm, mg_dash_ota_cb);
|
||||
} else if (mg_match(hm->uri, mg_str("/fs/#"), NULL) &&
|
||||
} else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts) &&
|
||||
(mg_strcasecmp(hm->method, mg_str("POST")) == 0 ||
|
||||
mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) {
|
||||
struct mg_str name = mg_dash_file_name(hm);
|
||||
mg_snprintf(c->data, sizeof(c->data), "%.*s", (int) name.len, name.buf);
|
||||
mg_http_start_upload(c, hm, name, mg_str("/tmp"), &mg_fs_posix,
|
||||
mg_dash_upload_cb);
|
||||
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
|
||||
struct mg_str name = parts[1];
|
||||
int len =
|
||||
mg_url_decode(name.buf, name.len, (char *) name.buf, name.len + 1, 0);
|
||||
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
|
||||
if (set == NULL || set->get_dir == NULL) {
|
||||
mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found\n");
|
||||
d->marker = CONN_HANDLED;
|
||||
} else if (u == NULL) {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
|
||||
d->marker = CONN_HANDLED;
|
||||
} else if (!mg_path_is_sane(name)) {
|
||||
mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n");
|
||||
d->marker = CONN_HANDLED;
|
||||
} else {
|
||||
mg_dash_set_file_name(set, name);
|
||||
if (set->fn != NULL && !set->fn(MG_DASH_WRITE, u)) {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
|
||||
d->marker = CONN_HANDLED;
|
||||
} else {
|
||||
char dir[256];
|
||||
struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix;
|
||||
if (!set->get_dir(u, dir, sizeof(dir))) {
|
||||
mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n");
|
||||
d->marker = CONN_HANDLED;
|
||||
} else {
|
||||
mg_http_start_upload(c, hm, name, mg_str(dir), fs,
|
||||
mg_dash_upload_cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c->data[0] != '\0') mg_log_http_req(c, hm);
|
||||
} else if (ev == MG_EV_HTTP_MSG && c->data[0] != '\0') {
|
||||
if (d->marker != '\0') mg_log_http_req(c, hm);
|
||||
} else if (ev == MG_EV_HTTP_MSG && d->marker != '\0') {
|
||||
// The response has been send in EV_HDRS path, so we're not reponding
|
||||
// anything but clearing the c->data[0] flag for the next request.
|
||||
c->data[0] = 0;
|
||||
// anything but clearing the marker for the next request.
|
||||
d->marker = 0;
|
||||
c->is_resp = 0;
|
||||
} else if (ev == MG_EV_HTTP_MSG && c->data[0] == '\0') {
|
||||
} else if (ev == MG_EV_HTTP_MSG && d->marker == '\0') {
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
struct mg_dash_user *u = mg_dash_authenticate(hm, dash);
|
||||
int level = u == NULL ? 0 : u->level;
|
||||
struct mg_dash_user *u = mg_dash_authenticate(c, hm, dash);
|
||||
struct mg_str parts[5];
|
||||
memset(parts, 0, sizeof(parts));
|
||||
|
||||
@@ -460,27 +684,60 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{%m:%m}", MG_ESC("method"),
|
||||
MG_ESC("logout"));
|
||||
} else if (mg_match(hm->uri, mg_str("/api/websocket"), NULL)) {
|
||||
*(int *) c->data = level;
|
||||
d->u = u;
|
||||
mg_ws_upgrade(c, hm, NULL);
|
||||
} else if (mg_match(hm->uri, mg_str("/fs/#"), NULL)) {
|
||||
struct mg_str name = mg_dash_file_name(hm);
|
||||
char path[128];
|
||||
mg_snprintf(path, sizeof(path), "/tmp/%.*s", name.len, name.buf);
|
||||
if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) {
|
||||
// Delete file
|
||||
mg_dash_file_del(name);
|
||||
mg_dash_send_change(c->mgr, &set_files);
|
||||
mg_http_reply(c, 200, NULL, "true");
|
||||
} else if (mg_match(hm->uri, mg_str("/fs/*/*"), parts)) {
|
||||
struct mg_field_set *set = mg_dash_find_field_set(dash, parts[0]);
|
||||
if (set == NULL || set->get_dir == NULL) {
|
||||
mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
|
||||
} else if (u == NULL) {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
|
||||
} else {
|
||||
// Serve file
|
||||
mg_http_serve_file(c, hm, path, NULL);
|
||||
char dir[256], path[512];
|
||||
struct mg_fs *fs = dash->upload_fs ? dash->upload_fs : &mg_fs_posix;
|
||||
struct mg_str name = parts[1];
|
||||
int len = mg_url_decode(name.buf, name.len, (char *) name.buf,
|
||||
name.len + 1, 0);
|
||||
if (len > 0 && (size_t) len <= name.len) name.len = (size_t) len;
|
||||
if (!mg_path_is_sane(name)) {
|
||||
mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad file name\n");
|
||||
return;
|
||||
}
|
||||
if (!set->get_dir(u, dir, sizeof(dir))) {
|
||||
mg_http_reply(c, 500, MG_JSON_HEADERS, "Upload dir error\n");
|
||||
return;
|
||||
}
|
||||
mg_snprintf(path, sizeof(path), "%s/%.*s", dir, name.len, name.buf);
|
||||
if (mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) {
|
||||
mg_dash_set_file_name(set, name);
|
||||
if (set->fn != NULL && !set->fn(MG_DASH_DELETE, u)) {
|
||||
mg_http_reply(c, 403, MG_JSON_HEADERS, "Not Authorised\n");
|
||||
} else {
|
||||
fs->rm(path);
|
||||
*set->index = -1; // Signal mg_dash_send_change() to send new size
|
||||
mg_dash_send_change(c->mgr, set);
|
||||
mg_http_reply(c, 200, NULL, "true");
|
||||
}
|
||||
} else {
|
||||
mg_http_serve_file(c, hm, path, NULL);
|
||||
}
|
||||
}
|
||||
} else if (mg_match(hm->uri, mg_str("/api/get/#"), parts) ||
|
||||
} else if (mg_match(hm->uri, mg_str("/api/del/*/*/*"), parts) ||
|
||||
mg_match(hm->uri, mg_str("/api/del/*/*"), parts)) {
|
||||
mg_dash_handle_del(c, dash, u, parts);
|
||||
} else if (mg_match(hm->uri, mg_str("/api/add/*"), parts)) {
|
||||
mg_dash_handle_add(c, dash, u, parts, hm->body);
|
||||
} else if (mg_match(hm->uri, mg_str("/api/get/*/*"), parts) &&
|
||||
mg_strcasecmp(hm->method, mg_str("POST")) == 0) {
|
||||
mg_dash_handle_mod(c, dash, u, parts, hm->body);
|
||||
} else if (mg_match(hm->uri, mg_str("/api/get/*/*/*"), parts) ||
|
||||
mg_match(hm->uri, mg_str("/api/get/*/*"), parts) ||
|
||||
mg_match(hm->uri, mg_str("/api/get/*"), parts) ||
|
||||
mg_match(hm->uri, mg_str("/api/get"), NULL)) {
|
||||
mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_name, dash,
|
||||
&parts[0], level);
|
||||
mg_http_reply(c, 200, MG_JSON_HEADERS, "%M\n", mg_dash_print_endpoint,
|
||||
dash, u, &parts[0], &parts[1], &parts[2]);
|
||||
} else if (mg_match(hm->uri, mg_str("/api/set"), NULL)) {
|
||||
int count = mg_dash_apply(c, dash, hm->body, level);
|
||||
int count = mg_dash_apply(c, dash, hm->body, u);
|
||||
mg_http_reply(c, 200, MG_JSON_HEADERS, "%d\n", count);
|
||||
} else if (mg_match(hm->uri, mg_str("/"), NULL)) {
|
||||
struct mg_http_serve_opts opts;
|
||||
@@ -498,20 +755,5 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
if (ch == NULL) mg_http_reply(c, 404, MG_JSON_HEADERS, "Not Found");
|
||||
mg_log_http_req(c, hm);
|
||||
}
|
||||
} else if (ev == MG_EV_WS_OPEN) {
|
||||
// WS connection established, send change notifications for all data
|
||||
struct mg_field_set *fs;
|
||||
for (fs = dash->sets; fs != NULL; fs = fs->next) {
|
||||
mg_dash_send_change(c->mgr, fs);
|
||||
}
|
||||
mg_dash_broadcast(c->mgr, 0, "{%m:%m}", MG_ESC("method"), MG_ESC("ready"));
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
// Add this to automatically handle "get" and "set" JSON-RPC calls
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
if (dash == NULL) {
|
||||
mg_dash_error(c, wm->data, "%s", "no dash defined");
|
||||
} else {
|
||||
mg_dash_process_msg(c, wm, dash);
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
-49
@@ -7,50 +7,95 @@
|
||||
#include "str.h"
|
||||
#include "ws.h"
|
||||
|
||||
#ifndef MG_DASH_MAX_USER_NAME
|
||||
#define MG_DASH_MAX_USER_NAME 32
|
||||
#endif
|
||||
|
||||
#ifndef MG_HTTP_ADDR
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
|
||||
#else
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:80"
|
||||
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
|
||||
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
|
||||
#endif
|
||||
|
||||
// C type of a value bound to a dashboard field, see struct mg_field
|
||||
enum mg_val_type {
|
||||
MG_VAL_INT,
|
||||
MG_VAL_BOOL,
|
||||
MG_VAL_DBL,
|
||||
MG_VAL_STR,
|
||||
MG_VAL_RAW,
|
||||
MG_VAL_INT, // int
|
||||
MG_VAL_UINT64, // uint64_t
|
||||
MG_VAL_BOOL, // bool
|
||||
MG_VAL_DBL, // double
|
||||
MG_VAL_STR, // Zero-terminated string
|
||||
MG_VAL_RAW, // Pre-formatted JSON value, copied verbatim
|
||||
};
|
||||
|
||||
// Dashboard field: binds a JSON key to a C variable for read and/or write
|
||||
struct mg_field {
|
||||
const char *name;
|
||||
enum mg_val_type type;
|
||||
void *value;
|
||||
size_t value_size;
|
||||
const char *name; // JSON key
|
||||
enum mg_val_type type; // C type of the value, see enum mg_val_type
|
||||
void *value; // Pointer to the bound variable
|
||||
size_t value_size; // sizeof(*value) for type checking; 0 makes it read-only
|
||||
};
|
||||
|
||||
// Operation passed to a struct mg_field_set read/write/delete/add callback.
|
||||
// MG_DASH_ADD: fields hold the values of a new element to append; *index is
|
||||
// not set. Return true to accept and persist it, false to reject (e.g. a cap
|
||||
// was reached)
|
||||
enum mg_dash_op { MG_DASH_READ, MG_DASH_WRITE, MG_DASH_DELETE, MG_DASH_ADD };
|
||||
|
||||
struct mg_dash;
|
||||
|
||||
// Authenticated (or guest) dashboard user
|
||||
struct mg_dash_user {
|
||||
struct mg_dash_user *next; // Next user in the list
|
||||
char name[MG_DASH_MAX_USER_NAME]; // Username
|
||||
char token[21]; // Session token, sent to the client as a cookie
|
||||
int level; // Access level, see struct mg_dash::authenticate
|
||||
uint64_t expire; // Session expiration time, see mg_millis()
|
||||
struct mg_dash *dash; // Dashboard this user belongs to
|
||||
};
|
||||
|
||||
// Named group of related dashboard fields, e.g. "settings" or "metrics"
|
||||
struct mg_field_set {
|
||||
const char *name;
|
||||
struct mg_field *fields;
|
||||
void (*reader)(void);
|
||||
void (*writer)(void);
|
||||
int read_level;
|
||||
int write_level;
|
||||
struct mg_field_set *next;
|
||||
const char *name; // Set name, used as a JSON key
|
||||
struct mg_field *fields; // Zero-terminated array of fields
|
||||
bool (*fn)(enum mg_dash_op op,
|
||||
struct mg_dash_user *u); // Read/write/delete/add callback
|
||||
int *index; // Non-NULL: array set. *index is set before fn(READ) call;
|
||||
// fn sets *index = -1 to signal end of iteration.
|
||||
// Size query: framework sets *index = -1 before fn(READ); fn
|
||||
// sets *index = total size
|
||||
bool (*get_dir)(const struct mg_dash_user *u, char *buf,
|
||||
size_t len); // Non-NULL: file array
|
||||
struct mg_field_set *next; // Next set in the list
|
||||
};
|
||||
|
||||
// Custom URI handler, registered with MG_DASH_REGISTER_CUSTOM_HANDLER()
|
||||
struct mg_dash_custom_handler {
|
||||
struct mg_dash_custom_handler *next;
|
||||
struct mg_str uri_pattern;
|
||||
mg_event_handler_t handler;
|
||||
void *handler_data;
|
||||
};
|
||||
|
||||
struct mg_dash_file {
|
||||
struct mg_dash_file *next;
|
||||
char *name;
|
||||
size_t size;
|
||||
struct mg_dash_custom_handler *next; // Next handler in the list
|
||||
struct mg_str uri_pattern; // URI to match, see mg_match()
|
||||
mg_event_handler_t handler; // Event handler function
|
||||
void *handler_data; // Opaque data for the handler
|
||||
};
|
||||
|
||||
// Dashboard instance: fields, users and handlers served over HTTP and WS
|
||||
struct mg_dash {
|
||||
struct mg_field_set *sets;
|
||||
struct mg_dash_custom_handler *custom_handlers;
|
||||
struct mg_field_set *sets; // Field sets, see MG_DASH_ADD_FIELD_SET()
|
||||
struct mg_dash_custom_handler *custom_handlers; // Custom URI handlers
|
||||
// Validates pass, fills in user, returns access level (<= 0: failure).
|
||||
// NULL: no login required, all clients get guest access
|
||||
int (*authenticate)(char *user, size_t userlen, const char *pass);
|
||||
int session_auto_expiration_seconds;
|
||||
//struct mg_dash_file *files;
|
||||
int session_auto_expiration_seconds; // Session lifetime, 0: 1 hour default
|
||||
struct mg_dash_user *guest; // Used when authenticate == NULL
|
||||
struct mg_fs *upload_fs; // FS for uploads/listing, default &mg_fs_posix
|
||||
};
|
||||
|
||||
#define MG_DASH_ADD_FIELD_SET(dash_, set_) \
|
||||
@@ -69,28 +114,50 @@ struct mg_dash {
|
||||
(dash_)->custom_handlers = &ch_; \
|
||||
} while (0)
|
||||
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:8000"
|
||||
#define MG_HTTPS_ADDR "https://0.0.0.0:8443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:8502"
|
||||
#else
|
||||
#define MG_HTTP_ADDR "http://0.0.0.0:80"
|
||||
#define MG_HTTPS_ADDR "http://0.0.0.0:443"
|
||||
#define MG_MODBUS_ADDR "tcp://0.0.0.0:502"
|
||||
#endif
|
||||
|
||||
#ifndef MG_DASH_MAX_SEND_BUF_SIZE
|
||||
#define MG_DASH_MAX_SEND_BUF_SIZE (MG_IO_SIZE * 5)
|
||||
#endif
|
||||
|
||||
// File manager
|
||||
extern struct mg_dash_file *mg_dash_files;
|
||||
void mg_dash_file_add(struct mg_str name, size_t size);
|
||||
void mg_dash_file_del(struct mg_str name);
|
||||
|
||||
// Dashboard event handler. Pass to mg_http_listen() as the fn argument
|
||||
void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data);
|
||||
|
||||
// Notify WebSocket clients that a field set changed: broadcasts the new
|
||||
// array size if *set->index < 0, or a single element update otherwise
|
||||
void mg_dash_send_change(struct mg_mgr *mgr, struct mg_field_set *);
|
||||
|
||||
// Default reader for file-backed array sets (set->get_dir != NULL)
|
||||
// Usage example - this is a complete implemenation of the file manager:
|
||||
// ```c
|
||||
// static struct file {
|
||||
// int index;
|
||||
// char name[64];
|
||||
// size_t size;
|
||||
// uint64_t checksum;
|
||||
// } s_file;
|
||||
// static struct mg_field_set set_files;
|
||||
//
|
||||
// static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
|
||||
// (void) u;
|
||||
// mkdir("/tmp/dashboard", 0755);
|
||||
// mg_snprintf(buf, len, "%s", "/tmp/dashboard");
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// static struct mg_field fields_files[] = {
|
||||
// {"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
|
||||
// {"size", MG_VAL_UINT64, &s_file.size, 0},
|
||||
// {"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
|
||||
// {NULL, MG_VAL_INT, NULL, 0},
|
||||
// };
|
||||
//
|
||||
// static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
// if (op == MG_DASH_WRITE) return s_uploads_enabled && u->level >= 7;
|
||||
// if (op != MG_DASH_READ) return false;
|
||||
// if (!mg_dash_dir_read(&set_files, u)) return false;
|
||||
// s_file.checksum = 0; // Mock to show how to set custom fields
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// static struct mg_field_set set_files = {"files", fields_files, files_fn, &s_file.index, get_dir, NULL};
|
||||
// ```
|
||||
bool mg_dash_dir_read(struct mg_field_set *set, struct mg_dash_user *u);
|
||||
|
||||
// Helper forward declarations for Mongoose CMSIS pack modules
|
||||
extern struct mg_mgr g_mgr;
|
||||
extern void mg_dash_init(struct mg_mgr *);
|
||||
|
||||
+16
-9
@@ -64,15 +64,22 @@ bool mg_ota_flash_end(struct mg_flash *flash) {
|
||||
MG_DEBUG(("CRC: %x/%x, size: %lu/%lu, status: %s", s_crc32, crc32, s_size,
|
||||
size, ok ? "ok" : "fail"));
|
||||
#ifdef MG_OTA_PUBLIC_KEY
|
||||
if (ok && s_size > 64) {
|
||||
static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY;
|
||||
uint8_t hash[32];
|
||||
size_t fw_size = s_size - 64;
|
||||
mg_sha256(hash, (uint8_t *) base, fw_size);
|
||||
ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash),
|
||||
(uint8_t *) base + fw_size,
|
||||
mg_uecc_secp256r1()) == 1;
|
||||
MG_INFO(("Signature: %s", ok ? "ok" : "fail"));
|
||||
if (ok) {
|
||||
bool signed_fw = s_size > 68 &&
|
||||
memcmp((uint8_t *) base + s_size - 4, "MGSG", 4) == 0;
|
||||
if (signed_fw) {
|
||||
static const uint8_t s_pubkey[] = MG_OTA_PUBLIC_KEY;
|
||||
uint8_t hash[32];
|
||||
size_t fw_size = s_size - 68; // strip 64-byte sig + 4-byte magic
|
||||
mg_sha256(hash, (uint8_t *) base, fw_size);
|
||||
ok = mg_uecc_verify(s_pubkey, hash, sizeof(hash),
|
||||
(uint8_t *) base + fw_size,
|
||||
mg_uecc_secp256r1()) == 1;
|
||||
MG_INFO(("Signature: %s", ok ? "ok" : "fail"));
|
||||
} else {
|
||||
ok = false;
|
||||
MG_ERROR(("Unsigned firmware rejected"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
s_size = 0;
|
||||
|
||||
+8
-3
@@ -1024,19 +1024,19 @@ static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) {
|
||||
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
|
||||
else mg_ota_end();
|
||||
p->fn(c, "write error");
|
||||
p->fn = NULL;
|
||||
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
|
||||
} else if (p->received >= p->expected) {
|
||||
const char *errmsg = NULL;
|
||||
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
|
||||
else if (!mg_ota_end()) errmsg = "OTA finalize failed";
|
||||
p->fn(c, errmsg);
|
||||
p->fn = NULL;
|
||||
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
|
||||
}
|
||||
} else if (ev == MG_EV_ERROR || ev == MG_EV_CLOSE) {
|
||||
if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; }
|
||||
else mg_ota_end();
|
||||
p->fn(c, ev == MG_EV_ERROR ? (const char *) ev_data : "connection closed");
|
||||
p->fn = NULL;
|
||||
mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL;
|
||||
}
|
||||
(void) ev_data;
|
||||
}
|
||||
@@ -1046,6 +1046,10 @@ static void mg_upload_default_cb(struct mg_connection *c, const char *status) {
|
||||
mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok");
|
||||
}
|
||||
|
||||
const char *mg_upload_path(struct mg_connection *c) {
|
||||
return (const char *) c->pfn_data;
|
||||
}
|
||||
|
||||
void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
|
||||
struct mg_str name, struct mg_str dir,
|
||||
struct mg_fs *fs,
|
||||
@@ -1065,6 +1069,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
|
||||
p->fd = fd;
|
||||
p->fn = fn;
|
||||
c->fn = mg_upload_handler;
|
||||
c->pfn_data = strdup(path);
|
||||
c->pfn = NULL;
|
||||
mg_iobuf_del(&c->recv, 0, hm->head.len);
|
||||
mg_call(c, MG_EV_READ, &c->recv.len);
|
||||
|
||||
@@ -64,6 +64,7 @@ void mg_http_start_upload(struct mg_connection *c, struct mg_http_message *hm,
|
||||
void (*fn)(struct mg_connection *, const char *));
|
||||
void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm,
|
||||
void (*fn)(struct mg_connection *, const char *));
|
||||
const char *mg_upload_path(struct mg_connection *c);
|
||||
void mg_http_bauth(struct mg_connection *, const char *user, const char *pass);
|
||||
struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v);
|
||||
size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "dns.h"
|
||||
#include "fmt.h"
|
||||
#include "log.h"
|
||||
#include "ota.h"
|
||||
#include "printf.h"
|
||||
#include "profile.h"
|
||||
#include "timer.h"
|
||||
@@ -278,6 +279,11 @@ void mg_mgr_free(struct mg_mgr *mgr) {
|
||||
|
||||
void mg_mgr_init(struct mg_mgr *mgr) {
|
||||
memset(mgr, 0, sizeof(*mgr));
|
||||
// Anchor mg_fw_version: a real store into a struct field the optimiser
|
||||
// can't elide forces the linker to keep the section alive even when no
|
||||
// OTA polling code runs it. Callers that want mgr->userdata for their own
|
||||
// purposes simply overwrite it after this call returns
|
||||
mgr->userdata = (void *) mg_fw_version;
|
||||
#if MG_ENABLE_EPOLL
|
||||
if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0)
|
||||
MG_ERROR(("epoll_create1 errno %d", errno));
|
||||
|
||||
@@ -2180,6 +2180,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
|
||||
struct mg_connection *c, *tmp;
|
||||
uint64_t now = mg_millis();
|
||||
mg_timer_poll(&mgr->timers, now);
|
||||
mg_ota_poll(mgr);
|
||||
if (mgr->ifp == NULL || mgr->ifp->driver == NULL) return;
|
||||
mg_tcpip_poll(mgr->ifp, now);
|
||||
for (c = mgr->conns; c != NULL; c = tmp) {
|
||||
|
||||
@@ -2,146 +2,172 @@
|
||||
#include "http.h"
|
||||
#include "json.h"
|
||||
#include "log.h"
|
||||
#include "timer.h"
|
||||
#include "printf.h"
|
||||
#include "url.h"
|
||||
#include "util.h"
|
||||
|
||||
enum { MG_OTA_STATUS_WAITING, MG_OTA_STATUS_SUCCESS, MG_OTA_STATUS_FAIL };
|
||||
static int s_version_status;
|
||||
static int s_ota_status;
|
||||
static uint64_t s_start_time;
|
||||
#ifndef MG_OTA_MAX_URL_LEN
|
||||
#define MG_OTA_MAX_URL_LEN 256
|
||||
#endif
|
||||
|
||||
static struct mg_ota_metadata {
|
||||
char *version;
|
||||
char *url;
|
||||
// Scannable version tag embedded in every firmware binary, for server-side
|
||||
// version extraction. Non-static so mg_mgr_init() can stash its address in
|
||||
// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from
|
||||
// stripping it on builds that never poll for OTAs (an attribute alone does
|
||||
// not - the linker drops unreferenced sections regardless of "used"/"retain")
|
||||
const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
|
||||
|
||||
static struct mg_ota_state {
|
||||
char my_version[MG_OTA_MAX_VERSION_LEN];
|
||||
char json_url[MG_OTA_MAX_URL_LEN];
|
||||
char version[MG_OTA_MAX_VERSION_LEN];
|
||||
char url[MG_OTA_MAX_URL_LEN];
|
||||
size_t size;
|
||||
uint8_t sha256[32];
|
||||
} s_ota_metadata;
|
||||
void (*fn)(const char *status);
|
||||
} *s_ota;
|
||||
|
||||
static void free_ota_metadata(void) {
|
||||
if (s_ota_metadata.version) mg_free(s_ota_metadata.version);
|
||||
if (s_ota_metadata.url) mg_free(s_ota_metadata.url);
|
||||
memset(&s_ota_metadata, 0, sizeof(s_ota_metadata));
|
||||
}
|
||||
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data);
|
||||
|
||||
static int fetch_ota_metadata(struct mg_http_message *response) {
|
||||
double result;
|
||||
if (mg_http_status(response) != 200) goto fetch_failed;
|
||||
if (mg_json_get(response->body, "$", NULL) != 0) goto fetch_failed;
|
||||
free_ota_metadata();
|
||||
s_ota_metadata.version = mg_json_get_str(response->body, "$.version");
|
||||
if (s_ota_metadata.version == NULL) goto fetch_failed;
|
||||
s_ota_metadata.url = mg_json_get_str(response->body, "$.url");
|
||||
if (s_ota_metadata.url == NULL) goto fetch_failed;
|
||||
if (!mg_json_get_num(response->body, "$.size", &result) || result <= 0)
|
||||
goto fetch_failed;
|
||||
s_ota_metadata.size = (size_t) result;
|
||||
// TODO (robertc2000): parse and validate sha256
|
||||
MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota_metadata.version,
|
||||
s_ota_metadata.url, s_ota_metadata.size));
|
||||
return MG_OTA_STATUS_SUCCESS;
|
||||
fetch_failed:
|
||||
free_ota_metadata();
|
||||
return MG_OTA_STATUS_FAIL;
|
||||
#if MG_ENABLE_CUSTOM_DEVICE_ID
|
||||
#else
|
||||
void mg_ota_device_id(char *buf, size_t len) {
|
||||
#if MG_ARCH == MG_ARCH_CUBE && defined(UID_BASE)
|
||||
mg_snprintf(buf, len, "%M", mg_print_hex, 12, (uint8_t *) UID_BASE);
|
||||
#else
|
||||
mg_snprintf(buf, len, "%d", 0);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
struct mg_str host = mg_url_host((char *) c->fn_data);
|
||||
struct mg_http_message *hm;
|
||||
static int fetch_status;
|
||||
|
||||
uint64_t expiration = *(uint64_t *) c->data;
|
||||
if (ev == MG_EV_POLL) {
|
||||
if (s_start_time + 5 * 1000 < mg_millis()) {
|
||||
mg_error(c, "Connection timeout");
|
||||
}
|
||||
if (mg_millis() > expiration) mg_error(c, "Metadata timeout");
|
||||
} else if (ev == MG_EV_CONNECT) {
|
||||
char id[33];
|
||||
struct mg_str host = mg_url_host(s_ota->json_url);
|
||||
const char *uri = mg_url_uri(s_ota->json_url);
|
||||
const char *sep = strchr(uri, '?') == NULL ? "?" : "&";
|
||||
mg_ota_device_id(id, sizeof(id));
|
||||
id[sizeof(id) - 1] = '\0';
|
||||
mg_printf(c,
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n"
|
||||
"Host: %.*s\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
mg_url_uri((char *) c->fn_data), (int) host.len, host.buf);
|
||||
fetch_status = MG_OTA_STATUS_WAITING;
|
||||
uri, sep, MG_ARCH, s_ota->my_version, id,
|
||||
MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf);
|
||||
} else if (ev == MG_EV_HTTP_MSG) {
|
||||
hm = (struct mg_http_message *) ev_data;
|
||||
fetch_status = fetch_ota_metadata(hm);
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
double result;
|
||||
MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf));
|
||||
if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 ||
|
||||
!mg_json_unescape(hm->body, "$.version", s_ota->version,
|
||||
sizeof(s_ota->version)) ||
|
||||
!mg_json_unescape(hm->body, "$.url", s_ota->url, sizeof(s_ota->url)) ||
|
||||
!mg_json_get_num(hm->body, "$.size", &result) || result <= 0) {
|
||||
char buf[100];
|
||||
mg_snprintf(buf, sizeof(buf), "Bad metadata: %.*s", hm->body.len,
|
||||
hm->body.buf);
|
||||
s_ota->fn(buf);
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
} else if (strcmp(s_ota->version, s_ota->my_version) == 0) {
|
||||
s_ota->fn("Same version");
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
} else {
|
||||
struct mg_connection *fc;
|
||||
s_ota->size = (size_t) result;
|
||||
// TODO (robertc2000): parse and validate sha256
|
||||
MG_DEBUG(("Firmware version: %s, url: %s, size: %ld", s_ota->version,
|
||||
s_ota->url, s_ota->size));
|
||||
fc = mg_http_connect(c->mgr, s_ota->url, s_firmware_fn, NULL);
|
||||
if (fc == NULL) {
|
||||
s_ota->fn("Failed to connect");
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
} else {
|
||||
*(uint64_t *) fc->data = mg_millis() + 300 * 1000; // Set expiration
|
||||
}
|
||||
}
|
||||
c->is_closing = 1;
|
||||
} else if (ev == MG_EV_ERROR) {
|
||||
MG_ERROR(("%lu Connection error", c->id));
|
||||
s_version_status = MG_OTA_STATUS_FAIL;
|
||||
} else if (ev == MG_EV_CLOSE) {
|
||||
MG_DEBUG(("%lu Connection closed %lu", c->id, mg_millis() - s_start_time));
|
||||
s_version_status = fetch_status;
|
||||
s_ota->fn((char *) ev_data);
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void s_ota_done(struct mg_connection *c, const char *errmsg) {
|
||||
(void) c;
|
||||
s_ota_status = errmsg ? MG_OTA_STATUS_FAIL : MG_OTA_STATUS_SUCCESS;
|
||||
static void status_fn(const char *errmsg) {
|
||||
if (errmsg) MG_ERROR(("OTA failed: %s", errmsg));
|
||||
}
|
||||
|
||||
static void status_fn_2(struct mg_connection *c, const char *errmsg) {
|
||||
if (s_ota) s_ota->fn(errmsg);
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok");
|
||||
}
|
||||
|
||||
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||
struct mg_str host = mg_url_host(s_ota_metadata.url);
|
||||
uint64_t expiration = *(uint64_t *) c->data;
|
||||
if (ev == MG_EV_POLL) {
|
||||
if (s_start_time + 120 * 1000 < mg_millis()) mg_error(c, "Timeout");
|
||||
if (mg_millis() > expiration) mg_error(c, "OTA timeout");
|
||||
} else if (ev == MG_EV_CONNECT) {
|
||||
struct mg_str host = mg_url_host(s_ota->url);
|
||||
mg_printf(c,
|
||||
"GET %s HTTP/1.1\r\n"
|
||||
"Host: %.*s\r\n"
|
||||
"Connection: close\r\n\r\n",
|
||||
mg_url_uri(s_ota_metadata.url), (int) host.len, host.buf);
|
||||
mg_url_uri(s_ota->url), (int) host.len, host.buf);
|
||||
} else if (ev == MG_EV_HTTP_HDRS) {
|
||||
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||
int status = mg_http_status(hm);
|
||||
if (status != 200 || hm->body.len != s_ota_metadata.size) {
|
||||
if (status != 200 || hm->body.len != s_ota->size) {
|
||||
mg_error(c, "Bad HTTP response: status %d, size %lu vs %lu", status,
|
||||
(unsigned long) hm->body.len,
|
||||
(unsigned long) s_ota_metadata.size);
|
||||
s_ota_status = MG_OTA_STATUS_FAIL;
|
||||
return;
|
||||
(unsigned long) hm->body.len, (unsigned long) s_ota->size);
|
||||
} else {
|
||||
MG_DEBUG(("Beginning OTA (%lu bytes)", (unsigned long) s_ota->size));
|
||||
mg_http_start_ota(c, hm, status_fn_2);
|
||||
}
|
||||
MG_DEBUG(
|
||||
("Beginning OTA (%lu bytes)", (unsigned long) s_ota_metadata.size));
|
||||
mg_http_start_ota(c, hm, s_ota_done);
|
||||
} else if (ev == MG_EV_ERROR) {
|
||||
MG_ERROR(("%lu Connection error", c->id));
|
||||
s_ota_status = MG_OTA_STATUS_FAIL;
|
||||
s_ota->fn((char *) ev_data);
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
}
|
||||
(void) ev_data;
|
||||
}
|
||||
|
||||
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
|
||||
const char *metadata_url,
|
||||
void (*fn)(const char *status)) {
|
||||
s_version_status = MG_OTA_STATUS_WAITING;
|
||||
s_ota_status = MG_OTA_STATUS_WAITING;
|
||||
s_start_time = mg_millis();
|
||||
MG_DEBUG(("Connecting to %s to retrieve metadata", metadata_url));
|
||||
if (!mg_http_connect(mgr, metadata_url, s_version_fn,
|
||||
(void *) metadata_url)) {
|
||||
if (fn) fn("Failed to connect");
|
||||
return;
|
||||
}
|
||||
while (s_version_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10);
|
||||
if (s_version_status == MG_OTA_STATUS_SUCCESS) {
|
||||
if (strcmp(s_ota_metadata.version, current_version) == 0) {
|
||||
if (fn) fn("Same version");
|
||||
free_ota_metadata();
|
||||
return;
|
||||
}
|
||||
void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
|
||||
const char *json_url, void (*fn)(const char *status)) {
|
||||
if (fn == NULL) fn = status_fn;
|
||||
if (s_ota != NULL) {
|
||||
fn("OTA already in progress");
|
||||
} else if ((s_ota = (struct mg_ota_state *) mg_calloc(1, sizeof(*s_ota))) ==
|
||||
NULL) {
|
||||
fn("Out of memory");
|
||||
} else {
|
||||
if (fn) fn("Version retrieving error");
|
||||
free_ota_metadata();
|
||||
return;
|
||||
struct mg_connection *c;
|
||||
mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version);
|
||||
mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url);
|
||||
MG_DEBUG(("Connecting to %s", json_url));
|
||||
c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL);
|
||||
if (c == NULL) {
|
||||
mg_free(s_ota);
|
||||
s_ota = NULL;
|
||||
fn("Failed to connect");
|
||||
} else {
|
||||
s_ota->fn = fn;
|
||||
*(uint64_t *) c->data = mg_millis() + 5 * 1000; // Set expiration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mg_ota_poll(struct mg_mgr *mgr) {
|
||||
static uint64_t t = 1;
|
||||
if (MG_OTA_URL != NULL &&
|
||||
mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) {
|
||||
mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL,
|
||||
MG_OTA_STATUS_FN);
|
||||
}
|
||||
if (fn) fn("Pulling firmware");
|
||||
s_start_time = mg_millis();
|
||||
MG_DEBUG(("Connecting to %s to download firmware", s_ota_metadata.url));
|
||||
if (!mg_http_connect(mgr, s_ota_metadata.url, s_firmware_fn, NULL)) {
|
||||
if (fn) fn("Failed to connect");
|
||||
free_ota_metadata();
|
||||
return;
|
||||
}
|
||||
while (s_ota_status == MG_OTA_STATUS_WAITING) mg_mgr_poll(mgr, 10);
|
||||
if (s_ota_status == MG_OTA_STATUS_FAIL && fn) fn("OTA fail");
|
||||
free_ota_metadata();
|
||||
}
|
||||
@@ -100,3 +100,37 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
|
||||
// Pull based OTA over HTTP
|
||||
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
|
||||
const char *metadata_url, void (*fn)(const char *status));
|
||||
|
||||
#ifndef MG_OTA_URL
|
||||
#define MG_OTA_URL NULL
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_STATUS_FN
|
||||
#define MG_OTA_STATUS_FN NULL
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_FIRMWARE_VERSION
|
||||
#define MG_OTA_FIRMWARE_VERSION "1.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef MG_OTA_MAX_VERSION_LEN
|
||||
#define MG_OTA_MAX_VERSION_LEN 64
|
||||
#endif
|
||||
|
||||
// Scannable "MG_VERSION:<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 *);
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "tls.h"
|
||||
#include "url.h"
|
||||
#include "util.h"
|
||||
#include "ota.h"
|
||||
|
||||
#if MG_ENABLE_SOCKET
|
||||
|
||||
@@ -757,6 +758,7 @@ void mg_mgr_poll(struct mg_mgr *mgr, int ms) {
|
||||
mg_iotest(mgr, ms);
|
||||
now = mg_millis();
|
||||
mg_timer_poll(&mgr->timers, now);
|
||||
mg_ota_poll(mgr);
|
||||
|
||||
for (c = mgr->conns; c != NULL; c = tmp) {
|
||||
bool is_resp = c->is_resp;
|
||||
|
||||
+385
-59
@@ -4202,32 +4202,65 @@ static int dash_authenticate(char *user, size_t userlen, const char *pass) {
|
||||
}
|
||||
|
||||
struct dash_ws_test {
|
||||
const char *req;
|
||||
int opened;
|
||||
int done;
|
||||
int result;
|
||||
int saw_set3;
|
||||
};
|
||||
|
||||
struct dash_arr_ws_test {
|
||||
int opened;
|
||||
int done;
|
||||
int saw_arr_index; // index seen in "arr/INDEX" change key, -1 if not seen
|
||||
int saw_arr_val; // "val" seen in the change params, -1 if not seen
|
||||
int saw_arr_size; // size seen in a plain "arr":N change, -1 if not seen
|
||||
};
|
||||
|
||||
static void dash_arr_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
|
||||
struct dash_arr_ws_test *d = (struct dash_arr_ws_test *) c->fn_data;
|
||||
if (ev == MG_EV_WS_OPEN) {
|
||||
d->opened = 1;
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
struct mg_str method = mg_json_get_tok(wm->data, "$.method");
|
||||
if (mg_strcmp(method, mg_str("\"change\"")) == 0) {
|
||||
// Check for "arr/INDEX" (element change) or "arr":N (size change)
|
||||
const char *p = wm->data.buf;
|
||||
const char *end = p + wm->data.len;
|
||||
while (p < end - 6) {
|
||||
if (strncmp(p, "\"arr/", 5) == 0) {
|
||||
const char *v;
|
||||
p += 5;
|
||||
d->saw_arr_index = (int) strtol(p, NULL, 10);
|
||||
v = strstr(p, "\"val\":");
|
||||
if (v != NULL) d->saw_arr_val = (int) strtol(v + 6, NULL, 10);
|
||||
d->done = 1;
|
||||
c->is_closing = 1;
|
||||
break;
|
||||
}
|
||||
if (strncmp(p, "\"arr\":", 6) == 0) {
|
||||
d->saw_arr_size = (int) strtol(p + 6, NULL, 10);
|
||||
d->done = 1;
|
||||
c->is_closing = 1;
|
||||
break;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
}
|
||||
} else if (ev == MG_EV_CLOSE && d->done == 0) {
|
||||
d->done = -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
|
||||
struct dash_ws_test *d = (struct dash_ws_test *) c->fn_data;
|
||||
if (ev == MG_EV_WS_OPEN) {
|
||||
if (d->req != NULL) {
|
||||
mg_ws_send(c, d->req, strlen(d->req), WEBSOCKET_OP_TEXT);
|
||||
}
|
||||
d->opened = 1;
|
||||
} else if (ev == MG_EV_WS_MSG) {
|
||||
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||
struct mg_str method = mg_json_get_tok(wm->data, "$.method");
|
||||
struct mg_str set3 = mg_json_get_tok(wm->data, "$.params.set3");
|
||||
double result = 0;
|
||||
if (mg_strcmp(method, mg_str("\"change\"")) == 0 && set3.buf != NULL) {
|
||||
d->saw_set3 = 1;
|
||||
}
|
||||
if (mg_json_get_num(wm->data, "$.result", &result)) {
|
||||
d->result = (int) result;
|
||||
d->done = 1;
|
||||
c->is_closing = 1;
|
||||
} else if (d->req == NULL &&
|
||||
mg_strcmp(method, mg_str("\"ready\"")) == 0) {
|
||||
d->done = 1;
|
||||
c->is_closing = 1;
|
||||
}
|
||||
@@ -4238,8 +4271,89 @@ static void dash_ws_cb(struct mg_connection *c, int ev, void *ev_data) {
|
||||
|
||||
char three[10] = "hi";
|
||||
int one = 1;
|
||||
static void rfields1(void) {
|
||||
mg_snprintf(three, sizeof(three), "t: %d", one); // Simulate array
|
||||
|
||||
#define TEST_ARR_SIZE 17
|
||||
#define TEST_ARR_CAP (TEST_ARR_SIZE + 2) // Cap for MG_DASH_ADD tests
|
||||
#define TEST_ARR_RO_INDEX 3 // This element is read-only: WRITE is always denied
|
||||
static int s_arr_index;
|
||||
static int s_arr_val;
|
||||
static int s_arr_store[TEST_ARR_CAP]; // Backing store, init to squares
|
||||
static int s_arr_count = TEST_ARR_SIZE; // Logical size, grows via MG_DASH_ADD
|
||||
|
||||
static bool set1_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
if (u->level < 3) return false;
|
||||
if (op == MG_DASH_READ) {
|
||||
mg_snprintf(three, sizeof(three), "t: %d", one);
|
||||
return true;
|
||||
}
|
||||
return u->level >= 7;
|
||||
}
|
||||
static bool set2_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u; return op == MG_DASH_READ;
|
||||
}
|
||||
static bool set3_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
if (u->level < 7) return false;
|
||||
return op == MG_DASH_READ || op == MG_DASH_WRITE;
|
||||
}
|
||||
static bool test_upload_dir(const struct mg_dash_user *u, char *buf, size_t len) {
|
||||
(void) u;
|
||||
#if MG_ARCH == MG_ARCH_UNIX || MG_ARCH == MG_ARCH_WIN32
|
||||
mkdir("/tmp/mongoose_dash_test", 0755);
|
||||
#endif
|
||||
mg_snprintf(buf, len, "%s", "/tmp/mongoose_dash_test");
|
||||
return true;
|
||||
}
|
||||
|
||||
struct test_file_entry {
|
||||
char name[64];
|
||||
size_t size;
|
||||
};
|
||||
static struct test_file_entry s_test_file;
|
||||
static int s_test_file_index;
|
||||
static struct mg_field test_files_fields[] = {
|
||||
{"name", MG_VAL_STR, s_test_file.name, sizeof(s_test_file.name)},
|
||||
{"size", MG_VAL_UINT64, &s_test_file.size, 0},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
static struct mg_field_set test_files_set = {
|
||||
"files", test_files_fields, NULL, &s_test_file_index, test_upload_dir, NULL};
|
||||
static struct test_file_entry s_delete_file;
|
||||
static char s_deleted_file[sizeof(s_delete_file.name)];
|
||||
static char s_uploaded_file[sizeof(s_delete_file.name)];
|
||||
static struct mg_field_set *s_delete_file_set;
|
||||
static bool delete_file_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
if (op == MG_DASH_READ) return mg_dash_dir_read(s_delete_file_set, u);
|
||||
if (op == MG_DASH_WRITE) {
|
||||
mg_snprintf(s_uploaded_file, sizeof(s_uploaded_file), "%s",
|
||||
s_delete_file.name);
|
||||
}
|
||||
if (op == MG_DASH_DELETE) {
|
||||
mg_snprintf(s_deleted_file, sizeof(s_deleted_file), "%s",
|
||||
s_delete_file.name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool arr_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op == MG_DASH_READ) {
|
||||
if (s_arr_index < 0) { s_arr_index = s_arr_count; return true; }
|
||||
if (s_arr_index >= s_arr_count) { s_arr_index = -1; return true; }
|
||||
s_arr_val = s_arr_store[s_arr_index];
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_WRITE) {
|
||||
if (s_arr_index < 0 || s_arr_index >= s_arr_count) return false;
|
||||
if (s_arr_index == TEST_ARR_RO_INDEX) return false; // Deny: read-only
|
||||
s_arr_store[s_arr_index] = s_arr_val;
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_ADD) {
|
||||
if (s_arr_count >= TEST_ARR_CAP) return false; // Deny: cap reached
|
||||
s_arr_store[s_arr_count++] = s_arr_val;
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_DELETE) return s_arr_index < s_arr_count;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void test_dash(void) {
|
||||
@@ -4248,7 +4362,7 @@ static void test_dash(void) {
|
||||
const char *ws_url = "ws://localhost:26352/api/websocket";
|
||||
struct mg_mgr mgr;
|
||||
struct mg_dash dash;
|
||||
struct dash_ws_test ws_user, ws_admin, ws_user_notify;
|
||||
struct dash_ws_test ws_admin, ws_user_notify;
|
||||
bool two = false;
|
||||
int admin = 7;
|
||||
int i;
|
||||
@@ -4265,13 +4379,20 @@ static void test_dash(void) {
|
||||
{"admin", MG_VAL_INT, &admin, sizeof(admin)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
struct mg_field_set set1 = {"set1", fields1, rfields1, NULL, 3, 7, NULL};
|
||||
struct mg_field_set set2 = {"set2", fields2, NULL, NULL, 0, 0, NULL};
|
||||
struct mg_field_set set3 = {"set3", fields3, NULL, NULL, 7, 7, NULL};
|
||||
struct mg_field_set set1 = {"set1", fields1, set1_fn, NULL, NULL, NULL};
|
||||
struct mg_field_set set2 = {"set2", fields2, set2_fn, NULL, NULL, NULL};
|
||||
struct mg_field_set set3 = {"set3", fields3, set3_fn, NULL, NULL, NULL};
|
||||
struct mg_field arr_fields[] = {
|
||||
{"val", MG_VAL_INT, &s_arr_val, sizeof(s_arr_val)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
struct mg_field_set arr_set = {"arr", arr_fields, arr_fn, &s_arr_index, NULL,
|
||||
NULL};
|
||||
const char *get_all_expected =
|
||||
"{\"files\":{\"data\":[]},\"set3\":{\"admin\":7},"
|
||||
"{\"arr\":17,\"set3\":{\"admin\":7},"
|
||||
"\"set2\":{\"two\":false},"
|
||||
"\"set1\":{\"one\":1,\"three\":\"t: 1\"}}\n";
|
||||
"\"set1\":{\"one\":1,\"three\":\"t: 1\"},"
|
||||
"\"files\":0}\n";
|
||||
const char *set1_req = "POST /api/set HTTP/1.0\nContent-Length: 18\n\n"
|
||||
"{\"set1\":{\"one\":2}}";
|
||||
const char *set1_set3_req = "POST /api/set HTTP/1.0\n"
|
||||
@@ -4290,8 +4411,14 @@ static void test_dash(void) {
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 20\n\n"
|
||||
"{\"set3\":{\"admin\":8}}";
|
||||
const char *set1_ws_set4_req =
|
||||
"{\"id\":1,\"method\":\"set\",\"params\":{\"set1\":{\"one\":4}}}";
|
||||
const char *set3_admin_req9 = "POST /api/set HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 20\n\n"
|
||||
"{\"set3\":{\"admin\":9}}";
|
||||
const char *set1_admin_set4_req = "POST /api/set HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 18\n\n"
|
||||
"{\"set1\":{\"one\":4}}";
|
||||
const char *set1_expected = "{\"one\":2,\"three\":\"t: 2\"}\n";
|
||||
const char *set1_expected3 = "{\"one\":3,\"three\":\"t: 3\"}\n";
|
||||
const char *set1_expected4 = "{\"one\":4,\"three\":\"t: 4\"}\n";
|
||||
@@ -4302,14 +4429,47 @@ static void test_dash(void) {
|
||||
const char *set2_req = "POST /api/set HTTP/1.0\nContent-Length: 21\n\n"
|
||||
"{\"set2\":{\"two\":true}}";
|
||||
const char *set2_expected = "{\"two\":false}\n";
|
||||
const char *arr7_expected = "[{\"val\":49}]\n";
|
||||
const char *arr0_2_expected = "[{\"val\":0},{\"val\":1},{\"val\":4}]\n";
|
||||
const char *arr5_mod_req = "POST /api/get/arr/5 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 10\n\n"
|
||||
"{\"val\":99}";
|
||||
const char *arr100_mod_req = "POST /api/get/arr/100 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 10\n\n"
|
||||
"{\"val\":99}";
|
||||
const char *arr3_mod_req = "POST /api/get/arr/3 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 11\n\n"
|
||||
"{\"val\":777}";
|
||||
const char *arr_add555_req = "POST /api/add/arr HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 11\n\n"
|
||||
"{\"val\":555}";
|
||||
const char *arr_add556_req = "POST /api/add/arr HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 11\n\n"
|
||||
"{\"val\":556}";
|
||||
const char *arr_add557_req = "POST /api/add/arr HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n"
|
||||
"Content-Length: 11\n\n"
|
||||
"{\"val\":557}";
|
||||
const char *arr5_mod_expected = "[{\"val\":99}]\n";
|
||||
const char *arr3_expected = "[{\"val\":9}]\n";
|
||||
const char *arr17_expected = "[{\"val\":555}]\n";
|
||||
const char *files_probe = "\"files\"";
|
||||
const char *test_bin_probe = "\"test.bin\"";
|
||||
char marker = 0;
|
||||
|
||||
memset(&dash, 0, sizeof(dash));
|
||||
dash.session_auto_expiration_seconds = 1;
|
||||
remove("/tmp/mongoose_dash_test/test.bin");
|
||||
|
||||
MG_DASH_ADD_FIELD_SET(&dash, &test_files_set);
|
||||
MG_DASH_ADD_FIELD_SET(&dash, &set1);
|
||||
ASSERT(dash.sets == &set1);
|
||||
ASSERT(set1.next == NULL);
|
||||
ASSERT(set1.next == &test_files_set);
|
||||
|
||||
MG_DASH_ADD_FIELD_SET(&dash, &set2);
|
||||
ASSERT(dash.sets == &set2);
|
||||
@@ -4326,26 +4486,9 @@ static void test_dash(void) {
|
||||
ASSERT(dash.custom_handlers->handler == dash_custom);
|
||||
ASSERT(dash.custom_handlers->handler_data == &marker);
|
||||
|
||||
while (mg_dash_files != NULL) mg_dash_file_del(mg_str(mg_dash_files->name));
|
||||
mg_dash_file_add(mg_str("one.txt"), 123);
|
||||
ASSERT(mg_dash_files != NULL);
|
||||
ASSERT(strcmp(mg_dash_files->name, "one.txt") == 0);
|
||||
ASSERT(mg_dash_files->size == 123);
|
||||
mg_dash_file_add(mg_str("two.bin"), 456);
|
||||
ASSERT(mg_dash_files != NULL);
|
||||
ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0);
|
||||
ASSERT(mg_dash_files->size == 456);
|
||||
ASSERT(mg_dash_files->next != NULL);
|
||||
ASSERT(strcmp(mg_dash_files->next->name, "one.txt") == 0);
|
||||
mg_dash_file_del(mg_str("one.txt"));
|
||||
ASSERT(mg_dash_files != NULL);
|
||||
ASSERT(strcmp(mg_dash_files->name, "two.bin") == 0);
|
||||
ASSERT(mg_dash_files->next == NULL);
|
||||
mg_dash_file_del(mg_str("missing.txt"));
|
||||
ASSERT(mg_dash_files != NULL);
|
||||
mg_dash_file_del(mg_str("two.bin"));
|
||||
ASSERT(mg_dash_files == NULL);
|
||||
for (i = 0; i < TEST_ARR_SIZE; i++) s_arr_store[i] = i * i;
|
||||
|
||||
MG_DASH_ADD_FIELD_SET(&dash, &arr_set);
|
||||
mg_mgr_init(&mgr);
|
||||
mg_mem_files = mg_packed_files;
|
||||
ASSERT(mg_http_listen(&mgr, url, mg_dash_ev_handler, &dash) != NULL);
|
||||
@@ -4422,41 +4565,38 @@ static void test_dash(void) {
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, set1_expected3) == 0);
|
||||
|
||||
ws_user_notify.req = NULL;
|
||||
ws_user_notify.opened = 0;
|
||||
ws_user_notify.done = 0;
|
||||
ws_user_notify.result = -1;
|
||||
ws_user_notify.saw_set3 = 0;
|
||||
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user_notify, "%s",
|
||||
"Authorization: Basic dXNlcjp1c2Vy\r\n");
|
||||
for (i = 0; i < 200 && ws_user_notify.opened == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_user_notify.opened == 1);
|
||||
ASSERT(fetch(&mgr, buf, url, set3_admin_req9) == 200);
|
||||
ASSERT(cmpbody(buf, "1\n") == 0);
|
||||
for (i = 0; i < 200 && ws_user_notify.done == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_user_notify.done == 1);
|
||||
ASSERT(ws_user_notify.done == 0);
|
||||
ASSERT(ws_user_notify.saw_set3 == 0);
|
||||
|
||||
ws_user.req = set1_ws_set4_req;
|
||||
ws_user.done = 0;
|
||||
ws_user.result = -1;
|
||||
ws_user.saw_set3 = 0;
|
||||
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_user, "%s",
|
||||
"Authorization: Basic dXNlcjp1c2Vy\r\n");
|
||||
for (i = 0; i < 200 && ws_user.done == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_user.done == 1);
|
||||
ASSERT(ws_user.result == 0);
|
||||
ASSERT(ws_user.saw_set3 == 0);
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/get/set1 HTTP/1.0\n"
|
||||
"Authorization: Basic dXNlcjp1c2Vy\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, set1_expected3) == 0);
|
||||
|
||||
ws_admin.req = set1_ws_set4_req;
|
||||
ws_admin.opened = 0;
|
||||
ws_admin.done = 0;
|
||||
ws_admin.result = -1;
|
||||
ws_admin.saw_set3 = 0;
|
||||
mg_ws_connect(&mgr, ws_url, dash_ws_cb, &ws_admin, "%s",
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
|
||||
for (i = 0; i < 200 && ws_admin.opened == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_admin.opened == 1);
|
||||
ASSERT(fetch(&mgr, buf, url, set3_admin_req) == 200);
|
||||
ASSERT(cmpbody(buf, "1\n") == 0);
|
||||
for (i = 0; i < 200 && ws_admin.done == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_admin.done == 1);
|
||||
ASSERT(ws_admin.result == 1);
|
||||
ASSERT(ws_admin.saw_set3 == 1);
|
||||
ASSERT(fetch(&mgr, buf, url, set1_admin_set4_req) == 200);
|
||||
ASSERT(cmpbody(buf, "1\n") == 0);
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/get/set1 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
@@ -4466,7 +4606,193 @@ static void test_dash(void) {
|
||||
ASSERT(fetch(&mgr, buf, url, "GET /api/hi HTTP/1.0\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, "hi\n") == 0);
|
||||
|
||||
// Array tests with auth (17 squares)
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/get/arr HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, "17\n") == 0);
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/get/arr/7 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, arr7_expected) == 0);
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/get/arr/0/2 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, arr0_2_expected) == 0);
|
||||
|
||||
// Array deletion: single element
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/del/arr/3 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, "1\n") == 0);
|
||||
|
||||
// Array deletion: range
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/del/arr/1/3 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 200);
|
||||
ASSERT(cmpbody(buf, "3\n") == 0);
|
||||
|
||||
// Array deletion: out-of-range returns 403
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/del/arr/100 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 403);
|
||||
|
||||
// Non-array set returns 404
|
||||
ASSERT(fetch(&mgr, buf, url,
|
||||
"GET /api/del/set1/0 HTTP/1.0\n"
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\n\n") == 404);
|
||||
|
||||
// Test that array change notification uses "arr/INDEX" key format
|
||||
{
|
||||
struct dash_arr_ws_test ws_arr;
|
||||
memset(&ws_arr, 0, sizeof(ws_arr));
|
||||
ws_arr.saw_arr_index = -1;
|
||||
ws_arr.saw_arr_val = -1;
|
||||
ws_arr.saw_arr_size = -1;
|
||||
mg_ws_connect(&mgr, ws_url, dash_arr_ws_cb, &ws_arr, "%s",
|
||||
"Authorization: Basic YWRtaW46YWRtaW4=\r\n");
|
||||
for (i = 0; i < 200 && ws_arr.opened == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_arr.opened == 1);
|
||||
s_arr_index = 5;
|
||||
mg_dash_send_change(&mgr, &arr_set);
|
||||
for (i = 0; i < 200 && ws_arr.done == 0; i++) mg_mgr_poll(&mgr, 1);
|
||||
ASSERT(ws_arr.done == 1);
|
||||
ASSERT(ws_arr.saw_arr_index == 5);
|
||||
}
|
||||
|
||||
// Array modification: POST /api/get/<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);
|
||||
|
||||
// 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) \
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ TOP ?= ../../..
|
||||
SOURCES = dashboard.c file_data.c $(TOP)/mongoose.c
|
||||
CFLAGS_EXTRA ?= -DMG_TLS=MG_TLS_BUILTIN
|
||||
CFLAGS ?= -W -Wall -Wextra -Werror -Wundef -Wshadow -O0 $(CFLAGS_EXTRA)
|
||||
PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN
|
||||
PFLAGS = -I$(TOP) -DMG_DATA_SIZE=64 -DMG_ENABLE_LINES=1 -DMAIN -DMG_DASH_STREAM_BATCH=100
|
||||
AFLAGS ?= -fsanitize=signed-integer-overflow,address,undefined,alignment
|
||||
|
||||
all: $(PROG)
|
||||
@@ -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>
|
||||
@@ -9,33 +9,32 @@ static int authenticate(char *user, size_t userlen, const char *pass);
|
||||
// Action buttons
|
||||
static bool s_action1, s_action2;
|
||||
static uint64_t s_action2_timeout;
|
||||
static void write_actions(void) {
|
||||
if (s_action2 == true) s_action2_timeout = mg_now() + 750;
|
||||
}
|
||||
static void read_actions(void) {
|
||||
if (s_action2_timeout > mg_now()) s_action2 = true;
|
||||
static bool actions_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op == MG_DASH_READ) {
|
||||
if (s_action2_timeout > mg_now()) s_action2 = true;
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_WRITE) {
|
||||
if (s_action2 == true) s_action2_timeout = mg_now() + 750;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static struct mg_field fields_actions[] = {
|
||||
{"action1", MG_VAL_BOOL, &s_action1, sizeof(s_action1)},
|
||||
{"action2", MG_VAL_BOOL, &s_action2, sizeof(s_action2)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
static struct mg_field_set set_actions = {
|
||||
"actions", fields_actions, read_actions, write_actions, 0, 0, NULL,
|
||||
};
|
||||
static struct mg_field_set set_actions = {"actions", fields_actions, actions_fn,
|
||||
NULL, NULL, NULL};
|
||||
|
||||
// Control panel
|
||||
// s_led1, s_led2, s_led3 are used to communicate LED status
|
||||
static bool s_led1, s_led2, s_led3;
|
||||
static void write_leds(void) {
|
||||
// gpio_write(LED1_PIN, s_led1);
|
||||
// gpio_write(LED2_PIN, s_led2);
|
||||
// gpio_write(LED3_PIN, s_led3);
|
||||
}
|
||||
static void read_leds(void) {
|
||||
// s_led1 = gpio_read(LED1_PIN);
|
||||
// s_led2 = gpio_read(LED2_PIN);
|
||||
// s_led3 = gpio_read(LED3_PIN);
|
||||
static bool leds_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
return op == MG_DASH_READ || op == MG_DASH_WRITE;
|
||||
}
|
||||
static struct mg_field fields_leds[] = {
|
||||
{"led1", MG_VAL_BOOL, &s_led1, sizeof(s_led1)},
|
||||
@@ -43,18 +42,20 @@ static struct mg_field fields_leds[] = {
|
||||
{"led3", MG_VAL_BOOL, &s_led3, sizeof(s_led3)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
static struct mg_field_set set_leds = {
|
||||
"leds", fields_leds, read_leds, write_leds, 0, 0, NULL,
|
||||
};
|
||||
static struct mg_field_set set_leds = {"leds", fields_leds, leds_fn,
|
||||
NULL, NULL, NULL};
|
||||
|
||||
// Read-only device Metrics
|
||||
static int s_ram = 32, s_cpu = 7;
|
||||
static double s_temperature = 24.8;
|
||||
|
||||
static void read_metrics(void) {
|
||||
s_ram = 25 + (rand() % 16); // Sumulate metric change
|
||||
static bool metrics_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op != MG_DASH_READ) return false;
|
||||
s_ram = 25 + (rand() % 16);
|
||||
s_cpu = 7 + (rand() % 21);
|
||||
s_temperature = 14.8 + ((double) rand() / RAND_MAX) * 20.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct mg_field fields_metrics[] = {
|
||||
@@ -64,9 +65,8 @@ static struct mg_field fields_metrics[] = {
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
|
||||
static struct mg_field_set set_metrics = {
|
||||
"metrics", fields_metrics, read_metrics, NULL, 0, 0, NULL,
|
||||
};
|
||||
static struct mg_field_set set_metrics = {"metrics", fields_metrics, metrics_fn,
|
||||
NULL, NULL, NULL};
|
||||
|
||||
// Read-write device settings
|
||||
static bool s_enable_login = false;
|
||||
@@ -78,13 +78,18 @@ static char s_ota_version[] = "1.1.2";
|
||||
static char s_ota_status[40] = "No scans yet";
|
||||
static char s_ota_url[100] = "https://my-product.com/ota.json";
|
||||
|
||||
static void write_settings(void) {
|
||||
mg_log_level = s_log_level;
|
||||
s_dash.authenticate = s_enable_login ? authenticate : NULL;
|
||||
}
|
||||
|
||||
static void read_settings(void) {
|
||||
s_log_level = mg_log_level;
|
||||
static bool settings_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
if (u->level < 3) return false;
|
||||
if (op == MG_DASH_READ) {
|
||||
s_log_level = mg_log_level;
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_WRITE && u->level >= 7) {
|
||||
mg_log_level = s_log_level;
|
||||
s_dash.authenticate = s_enable_login ? authenticate : NULL;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct mg_field fields_settings[] = {
|
||||
@@ -100,8 +105,7 @@ static struct mg_field fields_settings[] = {
|
||||
};
|
||||
|
||||
static struct mg_field_set set_settings = {
|
||||
"settings", fields_settings, read_settings, write_settings, 3, 7, NULL,
|
||||
};
|
||||
"settings", fields_settings, settings_fn, NULL, NULL, NULL};
|
||||
|
||||
#define NUM_POINTS_GRAPH1 7 // How many graph1 data points to send
|
||||
|
||||
@@ -139,9 +143,14 @@ static struct mg_field fields_graph1[] = {
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
|
||||
static struct mg_field_set set_graph1 = {
|
||||
"graph1", fields_graph1, read_graph1, NULL, 0, 0, NULL,
|
||||
};
|
||||
static bool graph1_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op != MG_DASH_READ) return false;
|
||||
read_graph1();
|
||||
return true;
|
||||
}
|
||||
static struct mg_field_set set_graph1 = {"graph1", fields_graph1, graph1_fn,
|
||||
NULL, NULL, NULL};
|
||||
|
||||
#define NUM_POINTS_GRAPH2 100 // How many graph2 data points to send
|
||||
static char s_graph2_data[NUM_POINTS_GRAPH2 * 4 + 2 + 1];
|
||||
@@ -172,9 +181,13 @@ static struct mg_field fields_graph2[] = {
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
|
||||
static struct mg_field_set set_graph2 = {
|
||||
"graph2", fields_graph2, read_graph2, NULL, 0, 0, NULL,
|
||||
};
|
||||
static bool graph2_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op == MG_DASH_READ) read_graph2();
|
||||
return op == MG_DASH_READ || op == MG_DASH_WRITE;
|
||||
}
|
||||
static struct mg_field_set set_graph2 = {"graph2", fields_graph2, graph2_fn,
|
||||
NULL, NULL, NULL};
|
||||
|
||||
static int authenticate(char *user, size_t userlen, const char *pass) {
|
||||
int level = 0; // Authentication failure
|
||||
@@ -188,7 +201,42 @@ static int authenticate(char *user, size_t userlen, const char *pass) {
|
||||
return level;
|
||||
}
|
||||
|
||||
static struct file {
|
||||
int index;
|
||||
char name[64];
|
||||
size_t size;
|
||||
uint64_t checksum;
|
||||
} s_file;
|
||||
static struct mg_field_set set_files;
|
||||
|
||||
static bool get_dir(const struct mg_dash_user *u, char *buf, size_t len) {
|
||||
(void) u;
|
||||
mg_snprintf(buf, len, "%s", "/tmp/dashboard");
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct mg_field fields_files[] = {
|
||||
{"name", MG_VAL_STR, s_file.name, sizeof(s_file.name)},
|
||||
{"size", MG_VAL_UINT64, &s_file.size, 0},
|
||||
{"checksum", MG_VAL_UINT64, &s_file.checksum, sizeof(s_file.checksum)},
|
||||
{NULL, MG_VAL_INT, NULL, 0},
|
||||
};
|
||||
|
||||
// Custom reader: let the framework fill "name" and "size", then add our
|
||||
// own field. A real implementation would hash the file instead of
|
||||
// hardcoding zero
|
||||
static bool files_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
if (op != MG_DASH_READ) return false;
|
||||
if (!mg_dash_dir_read(&set_files, u)) return false;
|
||||
s_file.checksum = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static struct mg_field_set set_files = {"files", fields_files, files_fn,
|
||||
&s_file.index, get_dir, NULL};
|
||||
|
||||
void mg_dash_init(struct mg_mgr *mgr) {
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_files);
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_leds);
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_metrics);
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_settings);
|
||||
@@ -196,9 +244,14 @@ void mg_dash_init(struct mg_mgr *mgr) {
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_graph2);
|
||||
MG_DASH_ADD_FIELD_SET(&s_dash, &set_actions);
|
||||
|
||||
// Add two fake files - for demonstration
|
||||
mg_dash_file_add(mg_str("device-config.json"), 1234);
|
||||
mg_dash_file_add(mg_str("device-log-2026-04-25.txt"), 1327854);
|
||||
#if MG_ARCH == MG_ARCH_UNIX
|
||||
// Create demo files in the upload directory
|
||||
mkdir("/tmp/dashboard", 0755);
|
||||
mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-config.json",
|
||||
"{\"model\":\"example\",\"fw\":\"1.0\"}");
|
||||
mg_file_printf(&mg_fs_posix, "/tmp/dashboard/device-log-2026-04-25.txt",
|
||||
"2026-04-25 12:00:00 Device started\n");
|
||||
#endif
|
||||
|
||||
mg_mem_files = mg_packed_files;
|
||||
mg_http_listen(mgr, MG_HTTP_ADDR, mg_dash_ev_handler, &s_dash);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
leds: { led1: true, led2: false, led3: false },
|
||||
settings: { volume: 12, log_level: 3, name: 'Galway', enable_login: true, ota_version: '1.2.3', ota_status: 'No checks yet', ota_url: 'http://my-product.com/ota.json', ota_interval: 30 },
|
||||
metrics: { temperature: 1.2, cpu: 4, ram: 5, version: '1.2.3' },
|
||||
files: {data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
|
||||
files: {start: 0, end: 100, data: [{name: 'device-log-2026-04-25.txt', size: 8625563}]},
|
||||
},
|
||||
});
|
||||
</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 => {
|
||||
args?.graph1?.data && updateGraph1(args?.graph1?.data);
|
||||
args?.graph2?.data && updateGraph2(args?.graph2?.data);
|
||||
typeof args?.files === 'number' && Dashboard.call('get', { name: 'files', start: 0, end: 100 });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -689,7 +691,7 @@ a, a:visited, a:link { text-decoration: none; }
|
||||
<div class="panel-title">
|
||||
<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>
|
||||
<div class="header-actions">
|
||||
<label class="btn btn-primary">
|
||||
@@ -699,7 +701,7 @@ a, a:visited, a:link { text-decoration: none; }
|
||||
<path d="M5 21h14" />
|
||||
</svg>
|
||||
Upload
|
||||
<input type="file" hidden data-upload="1" />
|
||||
<input type="file" hidden data-upload="files" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -717,14 +719,14 @@ a, a:visited, a:link { text-decoration: none; }
|
||||
</div>
|
||||
<div class="file-size">${f.size}</div>
|
||||
<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">
|
||||
<path d="M12 3v12" />
|
||||
<path d="m7 10 5 5 5-5" />
|
||||
<path d="M5 21h14" />
|
||||
</svg>
|
||||
</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">
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4h8v2" />
|
||||
|
||||
@@ -6,23 +6,25 @@
|
||||
static bool s_led1;
|
||||
static char s_version[10];
|
||||
|
||||
static void read_status(void) {
|
||||
MG_INFO(("READING STATUS"));
|
||||
mg_snprintf(s_version, sizeof(s_version), "1.2.3");
|
||||
|
||||
// These conditionals make this file work on both microcontroller and desktop
|
||||
// Useful for developing the UI on desktop, and copying to an MCU project.
|
||||
static bool status_fn(enum mg_dash_op op, struct mg_dash_user *u) {
|
||||
(void) u;
|
||||
if (op == MG_DASH_READ) {
|
||||
MG_INFO(("READING STATUS"));
|
||||
mg_snprintf(s_version, sizeof(s_version), "1.2.3");
|
||||
// These conditionals make this file work on both microcontroller and desktop
|
||||
#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
|
||||
}
|
||||
|
||||
static void write_status(void) {
|
||||
MG_INFO(("WRITING STATUS"));
|
||||
|
||||
return true;
|
||||
}
|
||||
if (op == MG_DASH_WRITE) {
|
||||
MG_INFO(("WRITING STATUS"));
|
||||
#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
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static struct mg_field fields_status[] = {
|
||||
@@ -32,7 +34,7 @@ static struct mg_field fields_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) {
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div>
|
||||
This toggle button is a stylised checkbox.
|
||||
<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
|
||||
the LED state is read or written.
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user