diff --git a/mongoose.c b/mongoose.c index 51b54f57..3f3f4f51 100644 --- a/mongoose.c +++ b/mongoose.c @@ -977,6 +977,7 @@ bool mg_wakeup(struct mg_mgr *mgr, unsigned long conn_id, const void *buf, + #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 @@ -1340,31 +1341,75 @@ static inline void mg_log_http_req(struct mg_connection *c, c->send.buf + n)); } -static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) { - mg_http_reply(c, errmsg ? 500 : 200, NULL, errmsg ? errmsg : "ok\n"); - c->is_draining = 1; -} +// State of one file upload to a dashboard file array. Owned by the upload +// callback, which frees it in the end call +struct mg_dash_upload { + struct mg_mgr *mgr; // Manager, to notify WebSocket clients + struct mg_dash *dash; // Dashboard whose file arrays get notified + struct mg_fd *fd; // Destination file, opened by the request handler + size_t expected; // Number of body bytes to receive + size_t received; // Number of body bytes written +}; -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 { - // 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"); - 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); +// Upload callback for mg_http_stream_body(). Notifies clients only if the whole +// body was written. A dropped or failed upload sends no notification +static bool mg_dash_upload_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + struct mg_dash_upload *up = (struct mg_dash_upload *) *p; + bool ok; + if (hm != NULL) return true; // Start, the file is already open + if (data != NULL) { // Next chunk + ok = up->fd->fs->wr(up->fd->fd, data->buf, data->len) == data->len; + if (ok) up->received += data->len; + return ok; + } + ok = up->received == up->expected; // End + mg_fs_close(up->fd); + if (ok) { + // 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_field_set *set; + for (set = up->dash->sets; set != NULL; set = set->next) { + if (set->get_dir == NULL) continue; + *set->index = -1; // Signal mg_dash_send_change() to broadcast new size + mg_dash_send_change(up->mgr, set); } } - c->is_draining = 1; + mg_free(up); + *p = NULL; + return ok; +} + +// Opens dir/name and starts streaming the request body into it. Returns true +// if streaming started, otherwise the error reply has been sent already +static bool mg_dash_start_upload(struct mg_connection *c, + struct mg_http_message *hm, + struct mg_dash *dash, struct mg_fs *fs, + const char *dir, struct mg_str name) { + char path[MG_PATH_MAX]; + struct mg_dash_upload *up = NULL; + struct mg_fd *fd = NULL; + size_t n = mg_snprintf(path, sizeof(path), "%s%c%.*s", dir, MG_DIRSEP, + (int) name.len, name.buf); + if (n < sizeof(path)) fd = mg_fs_open(fs, path, MG_FS_WRITE); + if (fd != NULL) up = (struct mg_dash_upload *) mg_calloc(1, sizeof(*up)); + if (up == NULL) { + if (fd != NULL) mg_fs_close(fd); + mg_http_reply(c, 500, MG_JSON_HEADERS, "Open failed\n"); + return false; + } + up->mgr = c->mgr; + up->dash = dash; + up->fd = fd; + up->expected = hm->body.len; + if (mg_http_stream_body(c, hm, mg_dash_upload_cb, up)) { + return true; // The callback owns up and fd now + } + mg_fs_close(fd); // Chunked body or unexpected method: not streamed + mg_free(up); + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad request\n"); + return false; } static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { @@ -1606,6 +1651,21 @@ static void mg_dash_handle_add(struct mg_connection *c, struct mg_dash *dash, } } +// Firmware upload callback for mg_http_stream_body(). Uses *p as a flag: +// non-NULL means an OTA session is open and must be closed by mg_ota_end() +static bool otacb(struct mg_http_message *hm, struct mg_str *data, void **p) { + if (hm != NULL) { // Start + if (!mg_ota_begin(hm->body.len)) return false; + *p = (void *) (ptrdiff_t) 1; + return true; + } else if (data != NULL) { // Next chunk + return mg_ota_write(data->buf, data->len); + } + if (*p == NULL) return false; // End. Begin failed, nothing to finish + *p = NULL; + return mg_ota_end(); +} + 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; @@ -1632,7 +1692,9 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_handle_login(c, u); 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); + // Authenticated above. If streaming started, the stream handler owns + // the connection and c->data, so skip the marker check below + if (mg_http_stream_body(c, hm, otacb, NULL)) return; } 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)) { @@ -1661,9 +1723,10 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { 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 if (mg_dash_start_upload(c, hm, dash, fs, dir, name)) { + return; // The stream handler owns the connection and c->data } else { - mg_http_start_upload(c, hm, name, mg_str(dir), fs, - mg_dash_upload_cb); + d->marker = CONN_HANDLED; // Error reply has been sent } } } @@ -3621,7 +3684,6 @@ struct mg_health mg_health_record MG_HEALTH_RAM; - static int mg_ncasecmp(const char *s1, const char *s2, size_t len) { int diff = 0; if (len > 0) do { @@ -4638,137 +4700,96 @@ int mg_http_status(const struct mg_http_message *hm) { return atoi(hm->uri.buf); } -struct mg_upload_priv { - size_t expected; - size_t received; - struct mg_fd *fd; // non-NULL: file upload; NULL: OTA - void (*fn)(struct mg_connection *, const char *); +// State of a streamed request or response body, kept in c->data. The callback +// is kept in c->fn_data, which is free because we own the connection +struct mg_upload_state { + size_t expected; // Number of body bytes to receive + size_t received; // Number of body bytes received so far + void *user_data; // Passed to cb, free for cb to use + bool failed; // Set when cb returned false }; -static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - if (p->fn == NULL) return; - if (ev == MG_EV_READ && c->recv.len > 0) { - size_t alignment = 512; - size_t left = p->expected > p->received ? p->expected - p->received : 0; - size_t aligned = c->recv.len < left ? MG_ROUND_DOWN(c->recv.len, alignment) - : left; - bool ok = true; - if (aligned > 0) { - if (p->fd != NULL) { - ok = p->fd->fs->wr(p->fd->fd, c->recv.buf, aligned) == aligned; - } else { - ok = mg_ota_write(c->recv.buf, aligned); - } - } - p->received += aligned; - mg_iobuf_del(&c->recv, 0, aligned); - if (!ok) { - if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } - else mg_ota_end(); - p->fn(c, "write error"); - 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); - 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"); - mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; - } +// Event handler installed by mg_http_stream_body(). Feeds the body to the +// callback, then replies (servers only). It does not restore the previous +// handler: the connection is closed after the reply. +static void mg_stream_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + bool (*fn)(struct mg_http_message *, struct mg_str *, void **) = + (bool (*)(struct mg_http_message *, struct mg_str *, void **)) + (ptrdiff_t) c->fn_data; (void) ev_data; + if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) { + if (us->received < us->expected) { + // Connection dropped mid-upload. Let cb clean up, e.g. call mg_ota_end() + us->received = us->expected; // MG_EV_ERROR is followed by MG_EV_CLOSE + fn(NULL, NULL, &us->user_data); + } + } else if (ev == MG_EV_READ && !c->is_draining) { + if (c->recv.len > 0 && us->received < us->expected) { + // Got some uploaded data. Feed by 512-byte aligned chunks, for OTA + size_t left = us->expected - us->received, r = c->recv.len; + size_t aligned = r < left ? MG_ROUND_DOWN(r, 512) : left; + struct mg_str data = mg_str_n((char *) c->recv.buf, aligned); + if (data.len > 0 && !us->failed) { + us->failed = !fn(NULL, &data, &us->user_data); + } + us->received += aligned; + mg_iobuf_del(&c->recv, 0, aligned); // Shift the unprocessed tail + } + if (us->received >= us->expected) { + // Finished upload. Always call cb, even after error, so it can clean up + us->failed |= !fn(NULL, NULL, &us->user_data); + if (!c->is_client) { + mg_http_reply(c, us->failed ? 500 : 200, NULL, "%zu %s\n", + us->received, us->failed ? "fail" : "ok"); + } + c->is_draining = 1; + } + } } -static void mg_upload_default_cb(struct mg_connection *c, const char *status) { - MG_INFO(("%lu %s", c->id, status ? status : "ok")); - mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok"); -} +bool mg_http_stream_body(struct mg_connection *c, struct mg_http_message *hm, + bool (*cb)(struct mg_http_message *, struct mg_str *, + void **user_data), + void *user_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + struct mg_str *te = mg_http_get_header(hm, "Transfer-Encoding"); + bool handled = false; -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, - void (*fn)(struct mg_connection *, const char *)) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - char path[MG_PATH_MAX]; - struct mg_fd *fd; - if (fn == NULL) fn = mg_upload_default_cb; - if (sizeof(*p) > sizeof(c->data)) { fn(c, "data too small"); return; } - if (!mg_path_is_sane(name)) { fn(c, "bad name"); return; } - mg_snprintf(path, sizeof(path), "%.*s%c%.*s", (int) dir.len, dir.buf, - MG_DIRSEP, (int) name.len, name.buf); - fd = mg_fs_open(fs, path, MG_FS_WRITE); - if (fd == NULL) { fn(c, "open failed"); return; } - p->expected = hm->body.len; - p->received = 0; - 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); -} - -void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, - void (*fn)(struct mg_connection *, const char *)) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - if (fn == NULL) fn = mg_upload_default_cb; - if (sizeof(*p) > sizeof(c->data)) { fn(c, "data too small"); return; } - if (!mg_ota_begin(hm->body.len)) { fn(c, "ota begin failed"); return; } - p->expected = hm->body.len; - p->received = 0; - p->fd = NULL; - p->fn = fn; - c->fn = mg_upload_handler; - c->pfn = NULL; - mg_iobuf_del(&c->recv, 0, hm->head.len); - mg_call(c, MG_EV_READ, &c->recv.len); -} - -void mg_http_stream_body(struct mg_connection *c, int ev, void *ev_data, - struct mg_str uri_pattern, - void (*cb)(struct mg_http_message *, struct mg_str *, - void **user_data)) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - - // Catch upload requests early, without buffering whole body - // When we receive MG_EV_HTTP_HDRS event, that means we've received all - // HTTP headers but not necessarily full HTTP body - if (ev == MG_EV_HTTP_HDRS && - (mg_strcmp(hm->method, mg_str("POST")) == 0 || - mg_strcmp(hm->method, mg_str("PUT")) == 0) && - hm->body.len != (size_t) ~0 && mg_match(hm->uri, uri_pattern, NULL)) { - c->pfn = NULL; // Silence HTTP protocol handler, we'll use MG_EV_READ - c->pfn_data = (void *) (ptrdiff_t) hm->body.len; // Record expected len - cb(hm, NULL, &c->fn_data); // Call handler before deleting headers + // Catch upload requests early, without buffering whole body. Called on the + // MG_EV_HTTP_HDRS event: we've received all HTTP headers but not necessarily + // the full HTTP body + if (hm->body.len == (size_t) ~0 || + (te != NULL && mg_strcasecmp(*te, mg_str("chunked")) == 0)) { + // Unknown length or chunked, cannot stream. Leave it to the caller + } else if (!c->is_client && + mg_strcmp(hm->method, mg_str("POST")) != 0 && + mg_strcmp(hm->method, mg_str("PUT")) != 0) { + // Not an upload request. Leave it to the caller + } else if (sizeof(*us) > sizeof(c->data)) { + if (!c->is_client) { // Nobody to reply to on a client connection + c->pfn = NULL; + mg_http_reply(c, 500, NULL, "data too small\n"); + c->is_draining = 1; + cb(NULL, NULL, &user_data); // Let cb release user_data, start skipped + handled = true; + } + } else { + c->pfn = NULL; // Silence HTTP protocol handler + memset(us, 0, sizeof(*us)); + us->expected = hm->body.len; // Store number of bytes we expect + us->user_data = user_data; + us->failed = !cb(hm, NULL, &us->user_data); // Call before deleting headers if (mg_http_get_header(hm, "Expect") != NULL) { mg_http_reply(c, 100, NULL, ""); // If curl expects, we continue } mg_iobuf_del(&c->recv, 0, hm->head.len); // Delete HTTP headers + c->fn = mg_stream_handler; // We own the connection from now + c->fn_data = (void *) (ptrdiff_t) cb; + mg_call(c, MG_EV_READ, &c->recv.len); // Process the body bytes we have + handled = true; } - - if (c->pfn == NULL && ev != MG_EV_OPEN) { - if (c->pfn_data != NULL && c->recv.len > 0) { - // Got some uploaded data. Feed by 512-byte aligned chunks, for OTA - size_t left = (size_t) (ptrdiff_t) c->pfn_data, r = c->recv.len; - size_t aligned = r < left ? MG_ROUND_DOWN(r, 512) : left; - struct mg_str data = mg_str_n((char *) c->recv.buf, aligned); - if (data.len) cb(NULL, &data, &c->fn_data); - c->pfn_data = (void *) (ptrdiff_t) (left - aligned); - c->recv.len -= aligned; - } - if (c->pfn_data == NULL && c->is_draining == 0) { - // Finished upload - mg_http_reply(c, 200, NULL, "ok\n"); - c->is_draining = 1; - cb(NULL, NULL, &c->fn_data); - } - } + return handled; } static bool is_hex_digit(int c) { @@ -11007,6 +11028,9 @@ static struct mg_ota_state { size_t size; uint8_t sha256[32]; void (*fn)(const char *error_message); + size_t received; // Firmware bytes written so far + bool open; // mg_ota_begin() succeeded, mg_ota_end() is pending + const char *err; // First error during download, NULL if none } *s_ota; static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); @@ -11127,11 +11151,38 @@ 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); +// Firmware download callback for mg_http_stream_body(). Writes chunks to the +// OTA target and reports the result via s_ota->fn(). The end call also happens +// when the connection drops before the whole body arrived. +static bool s_firmware_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + bool finalized = false, ok; + (void) p; + if (s_ota == NULL) return false; + if (hm != NULL) { // Start + s_ota->received = 0; + s_ota->open = mg_ota_begin(hm->body.len); + if (!s_ota->open) s_ota->err = "OTA begin failed"; + return s_ota->open; + } else if (data != NULL) { // Next chunk + if (!mg_ota_write(data->buf, data->len)) { + s_ota->err = "OTA write failed"; + return false; + } + s_ota->received += data->len; + return true; + } + // End + if (s_ota->open) finalized = mg_ota_end(); + if (s_ota->err == NULL && s_ota->received != s_ota->size) { + s_ota->err = "Connection closed"; + } + if (s_ota->err == NULL && !finalized) s_ota->err = "OTA finalize failed"; + ok = s_ota->err == NULL; + s_ota->fn(s_ota->err); mg_free(s_ota); s_ota = NULL; - mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok"); + return ok; } static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { @@ -11162,7 +11213,9 @@ static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { (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); + if (!mg_http_stream_body(c, hm, s_firmware_cb, NULL)) { + mg_error(c, "Cannot stream firmware"); + } } } else if (ev == MG_EV_ERROR) { s_ota->fn((char *) ev_data); diff --git a/mongoose.h b/mongoose.h index 13bd8fec..5c31b9e6 100644 --- a/mongoose.h +++ b/mongoose.h @@ -2793,20 +2793,6 @@ void mg_http_creds(struct mg_http_message *, char *, size_t, char *, size_t); long mg_http_upload(struct mg_connection *c, struct mg_http_message *hm, struct mg_fs *fs, const char *dir, size_t max_size); -// Streams the raw request body into dir/name via fs. Non-blocking: installs an -// internal handler and returns immediately. fn is called with NULL on success -// or an error string on failure. -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, - void (*fn)(struct mg_connection *, const char *)); - -// Starts an OTA firmware update from an HTTP POST upload. -// Calls mg_ota_begin/write/end internally; fn is called with NULL on success, -// or an error string on failure. -void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, - void (*fn)(struct mg_connection *, const char *)); - // Sends a 401 Unauthorized response with a Basic Auth WWW-Authenticate challenge. void mg_http_bauth(struct mg_connection *, const char *user, const char *pass); @@ -2825,7 +2811,7 @@ struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v); // Full examples: // tutorials/http/file-upload-html-form, tutorials/http/http-server // Related APIs: -// mg_http_listen(), mg_http_start_upload() +// mg_http_listen(), mg_http_stream_body() // Notes: // Call from an MG_EV_HTTP_MSG handler after the full request body is // received. part.name, part.filename, and part.body are zero-copy slices into @@ -2836,27 +2822,33 @@ size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *); int mg_http_status(const struct mg_http_message *hm); // Streams a POST/PUT body to a callback as it arrives, without buffering it. -// Call it from your connection handler on every event. +// Call it from an MG_EV_HTTP_HDRS handler, after matching the request, e.g. +// with mg_match(). Returns true if it took over the connection, false if the +// request is not POST/PUT, is chunked, or has no Content-Length. // // Callback upload_fn(hm, data, user_data) is called: -// - at start: hm is set, data is NULL. Open the file, set user_data. -// - per chunk: hm is NULL, data is set. Chunks are 512 bytes aligned -// except the last one. -// - at end: both NULL. Close the file. -// After the last chunk, "200 ok" is sent and the connection is drained. +// - at start: hm is set. Open the file, set *user_data. +// - per chunk: data is set, 512 byte aligned except the last. Not called +// again after a failure. +// - at end: both NULL. Always called once, also when the connection drops +// early, so release resources here. A truncated body looks like a complete +// one: validate before committing. +// Return true on success, false on error. // Example: -// mg_http_stream_body(c, ev, ev_data, mg_str("/fs/*"), upload); +// if (ev == MG_EV_HTTP_HDRS && mg_match(hm->uri, mg_str("/fs/*"), NULL) { +// mg_http_stream_body(c, hm, upload, NULL)); +// } // Full example: // tutorials/http/file-upload-single-post // Related APIs: -// mg_http_start_upload(), mg_http_start_ota() +// mg_http_listen(), mg_http_next_multipart() // Notes: -// Requires Content-Length, chunked bodies are not streamed. user_data is -// &c->fn_data. While streaming, c->pfn is NULL -void mg_http_stream_body(struct mg_connection *c, int ev, void *ev_data, - struct mg_str uri_pattern, - void (*upload_fn)(struct mg_http_message *, - struct mg_str *, void **user_data)); +// After it returns true, this function hijacks the connection. Existing event +// handler not called, hm is invalid, c->data and c->fn_data are modified +bool mg_http_stream_body(struct mg_connection *c, struct mg_http_message *hm, + bool (*upload_fn)(struct mg_http_message *, + struct mg_str *, void **user_data), + void *user_data); void mg_http_serve_ssi(struct mg_connection *c, const char *root, @@ -4738,7 +4730,7 @@ extern void mg_mqtt_poll(struct mg_mgr *); // This is a low-level OTA API, not intended to be called directly by users. // Users are provided with a higher-level API: // - defining MG_OTA_URL enables HTTP or HTTPS pull-based updates -// - mg_http_start_ota() enables push-based updates +// - mg_http_stream_body() enables push-based updates // // However, it is possible to use the mg_ota_* API directly. Below is the // example code of a function that is called repeatedly. It expects the diff --git a/src/dash.c b/src/dash.c index e6d445e0..00bdcab3 100644 --- a/src/dash.c +++ b/src/dash.c @@ -1,5 +1,6 @@ #include "dash.h" #include "http.h" +#include "ota.h" #include "util.h" #define MG_NO_CACHE_HEADERS "Cache-Control: no-cache\r\n" @@ -365,31 +366,75 @@ static inline void mg_log_http_req(struct mg_connection *c, c->send.buf + n)); } -static void mg_dash_ota_cb(struct mg_connection *c, const char *errmsg) { - mg_http_reply(c, errmsg ? 500 : 200, NULL, errmsg ? errmsg : "ok\n"); - c->is_draining = 1; -} +// State of one file upload to a dashboard file array. Owned by the upload +// callback, which frees it in the end call +struct mg_dash_upload { + struct mg_mgr *mgr; // Manager, to notify WebSocket clients + struct mg_dash *dash; // Dashboard whose file arrays get notified + struct mg_fd *fd; // Destination file, opened by the request handler + size_t expected; // Number of body bytes to receive + size_t received; // Number of body bytes written +}; -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 { - // 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"); - 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); +// Upload callback for mg_http_stream_body(). Notifies clients only if the whole +// body was written. A dropped or failed upload sends no notification +static bool mg_dash_upload_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + struct mg_dash_upload *up = (struct mg_dash_upload *) *p; + bool ok; + if (hm != NULL) return true; // Start, the file is already open + if (data != NULL) { // Next chunk + ok = up->fd->fs->wr(up->fd->fd, data->buf, data->len) == data->len; + if (ok) up->received += data->len; + return ok; + } + ok = up->received == up->expected; // End + mg_fs_close(up->fd); + if (ok) { + // 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_field_set *set; + for (set = up->dash->sets; set != NULL; set = set->next) { + if (set->get_dir == NULL) continue; + *set->index = -1; // Signal mg_dash_send_change() to broadcast new size + mg_dash_send_change(up->mgr, set); } } - c->is_draining = 1; + mg_free(up); + *p = NULL; + return ok; +} + +// Opens dir/name and starts streaming the request body into it. Returns true +// if streaming started, otherwise the error reply has been sent already +static bool mg_dash_start_upload(struct mg_connection *c, + struct mg_http_message *hm, + struct mg_dash *dash, struct mg_fs *fs, + const char *dir, struct mg_str name) { + char path[MG_PATH_MAX]; + struct mg_dash_upload *up = NULL; + struct mg_fd *fd = NULL; + size_t n = mg_snprintf(path, sizeof(path), "%s%c%.*s", dir, MG_DIRSEP, + (int) name.len, name.buf); + if (n < sizeof(path)) fd = mg_fs_open(fs, path, MG_FS_WRITE); + if (fd != NULL) up = (struct mg_dash_upload *) mg_calloc(1, sizeof(*up)); + if (up == NULL) { + if (fd != NULL) mg_fs_close(fd); + mg_http_reply(c, 500, MG_JSON_HEADERS, "Open failed\n"); + return false; + } + up->mgr = c->mgr; + up->dash = dash; + up->fd = fd; + up->expected = hm->body.len; + if (mg_http_stream_body(c, hm, mg_dash_upload_cb, up)) { + return true; // The callback owns up and fd now + } + mg_fs_close(fd); // Chunked body or unexpected method: not streamed + mg_free(up); + mg_http_reply(c, 400, MG_JSON_HEADERS, "Bad request\n"); + return false; } static uint64_t mg_dash_make_expiration_time(struct mg_dash *dash) { @@ -631,6 +676,21 @@ static void mg_dash_handle_add(struct mg_connection *c, struct mg_dash *dash, } } +// Firmware upload callback for mg_http_stream_body(). Uses *p as a flag: +// non-NULL means an OTA session is open and must be closed by mg_ota_end() +static bool otacb(struct mg_http_message *hm, struct mg_str *data, void **p) { + if (hm != NULL) { // Start + if (!mg_ota_begin(hm->body.len)) return false; + *p = (void *) (ptrdiff_t) 1; + return true; + } else if (data != NULL) { // Next chunk + return mg_ota_write(data->buf, data->len); + } + if (*p == NULL) return false; // End. Begin failed, nothing to finish + *p = NULL; + return mg_ota_end(); +} + 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; @@ -657,7 +717,9 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { mg_handle_login(c, u); 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); + // Authenticated above. If streaming started, the stream handler owns + // the connection and c->data, so skip the marker check below + if (mg_http_stream_body(c, hm, otacb, NULL)) return; } 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)) { @@ -686,9 +748,10 @@ void mg_dash_ev_handler(struct mg_connection *c, int ev, void *ev_data) { 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 if (mg_dash_start_upload(c, hm, dash, fs, dir, name)) { + return; // The stream handler owns the connection and c->data } else { - mg_http_start_upload(c, hm, name, mg_str(dir), fs, - mg_dash_upload_cb); + d->marker = CONN_HANDLED; // Error reply has been sent } } } diff --git a/src/http.c b/src/http.c index 6340d726..a91cea2f 100644 --- a/src/http.c +++ b/src/http.c @@ -3,7 +3,6 @@ #include "fmt.h" #include "log.h" #include "net.h" -#include "ota.h" #include "printf.h" #include "ssi.h" #include "util.h" @@ -1025,137 +1024,96 @@ int mg_http_status(const struct mg_http_message *hm) { return atoi(hm->uri.buf); } -struct mg_upload_priv { - size_t expected; - size_t received; - struct mg_fd *fd; // non-NULL: file upload; NULL: OTA - void (*fn)(struct mg_connection *, const char *); +// State of a streamed request or response body, kept in c->data. The callback +// is kept in c->fn_data, which is free because we own the connection +struct mg_upload_state { + size_t expected; // Number of body bytes to receive + size_t received; // Number of body bytes received so far + void *user_data; // Passed to cb, free for cb to use + bool failed; // Set when cb returned false }; -static void mg_upload_handler(struct mg_connection *c, int ev, void *ev_data) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - if (p->fn == NULL) return; - if (ev == MG_EV_READ && c->recv.len > 0) { - size_t alignment = 512; - size_t left = p->expected > p->received ? p->expected - p->received : 0; - size_t aligned = c->recv.len < left ? MG_ROUND_DOWN(c->recv.len, alignment) - : left; - bool ok = true; - if (aligned > 0) { - if (p->fd != NULL) { - ok = p->fd->fs->wr(p->fd->fd, c->recv.buf, aligned) == aligned; - } else { - ok = mg_ota_write(c->recv.buf, aligned); - } - } - p->received += aligned; - mg_iobuf_del(&c->recv, 0, aligned); - if (!ok) { - if (p->fd != NULL) { mg_fs_close(p->fd); p->fd = NULL; } - else mg_ota_end(); - p->fn(c, "write error"); - 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); - 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"); - mg_free(c->pfn_data); c->pfn_data = NULL; p->fn = NULL; - } +// Event handler installed by mg_http_stream_body(). Feeds the body to the +// callback, then replies (servers only). It does not restore the previous +// handler: the connection is closed after the reply. +static void mg_stream_handler(struct mg_connection *c, int ev, void *ev_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + bool (*fn)(struct mg_http_message *, struct mg_str *, void **) = + (bool (*)(struct mg_http_message *, struct mg_str *, void **)) + (ptrdiff_t) c->fn_data; (void) ev_data; + if (ev == MG_EV_CLOSE || ev == MG_EV_ERROR) { + if (us->received < us->expected) { + // Connection dropped mid-upload. Let cb clean up, e.g. call mg_ota_end() + us->received = us->expected; // MG_EV_ERROR is followed by MG_EV_CLOSE + fn(NULL, NULL, &us->user_data); + } + } else if (ev == MG_EV_READ && !c->is_draining) { + if (c->recv.len > 0 && us->received < us->expected) { + // Got some uploaded data. Feed by 512-byte aligned chunks, for OTA + size_t left = us->expected - us->received, r = c->recv.len; + size_t aligned = r < left ? MG_ROUND_DOWN(r, 512) : left; + struct mg_str data = mg_str_n((char *) c->recv.buf, aligned); + if (data.len > 0 && !us->failed) { + us->failed = !fn(NULL, &data, &us->user_data); + } + us->received += aligned; + mg_iobuf_del(&c->recv, 0, aligned); // Shift the unprocessed tail + } + if (us->received >= us->expected) { + // Finished upload. Always call cb, even after error, so it can clean up + us->failed |= !fn(NULL, NULL, &us->user_data); + if (!c->is_client) { + mg_http_reply(c, us->failed ? 500 : 200, NULL, "%zu %s\n", + us->received, us->failed ? "fail" : "ok"); + } + c->is_draining = 1; + } + } } -static void mg_upload_default_cb(struct mg_connection *c, const char *status) { - MG_INFO(("%lu %s", c->id, status ? status : "ok")); - mg_http_reply(c, status ? 500 : 200, "", "%s\n", status ? status : "ok"); -} +bool mg_http_stream_body(struct mg_connection *c, struct mg_http_message *hm, + bool (*cb)(struct mg_http_message *, struct mg_str *, + void **user_data), + void *user_data) { + struct mg_upload_state *us = (struct mg_upload_state *) c->data; + struct mg_str *te = mg_http_get_header(hm, "Transfer-Encoding"); + bool handled = false; -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, - void (*fn)(struct mg_connection *, const char *)) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - char path[MG_PATH_MAX]; - struct mg_fd *fd; - if (fn == NULL) fn = mg_upload_default_cb; - if (sizeof(*p) > sizeof(c->data)) { fn(c, "data too small"); return; } - if (!mg_path_is_sane(name)) { fn(c, "bad name"); return; } - mg_snprintf(path, sizeof(path), "%.*s%c%.*s", (int) dir.len, dir.buf, - MG_DIRSEP, (int) name.len, name.buf); - fd = mg_fs_open(fs, path, MG_FS_WRITE); - if (fd == NULL) { fn(c, "open failed"); return; } - p->expected = hm->body.len; - p->received = 0; - 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); -} - -void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, - void (*fn)(struct mg_connection *, const char *)) { - struct mg_upload_priv *p = (struct mg_upload_priv *) c->data; - if (fn == NULL) fn = mg_upload_default_cb; - if (sizeof(*p) > sizeof(c->data)) { fn(c, "data too small"); return; } - if (!mg_ota_begin(hm->body.len)) { fn(c, "ota begin failed"); return; } - p->expected = hm->body.len; - p->received = 0; - p->fd = NULL; - p->fn = fn; - c->fn = mg_upload_handler; - c->pfn = NULL; - mg_iobuf_del(&c->recv, 0, hm->head.len); - mg_call(c, MG_EV_READ, &c->recv.len); -} - -void mg_http_stream_body(struct mg_connection *c, int ev, void *ev_data, - struct mg_str uri_pattern, - void (*cb)(struct mg_http_message *, struct mg_str *, - void **user_data)) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - - // Catch upload requests early, without buffering whole body - // When we receive MG_EV_HTTP_HDRS event, that means we've received all - // HTTP headers but not necessarily full HTTP body - if (ev == MG_EV_HTTP_HDRS && - (mg_strcmp(hm->method, mg_str("POST")) == 0 || - mg_strcmp(hm->method, mg_str("PUT")) == 0) && - hm->body.len != (size_t) ~0 && mg_match(hm->uri, uri_pattern, NULL)) { - c->pfn = NULL; // Silence HTTP protocol handler, we'll use MG_EV_READ - c->pfn_data = (void *) (ptrdiff_t) hm->body.len; // Record expected len - cb(hm, NULL, &c->fn_data); // Call handler before deleting headers + // Catch upload requests early, without buffering whole body. Called on the + // MG_EV_HTTP_HDRS event: we've received all HTTP headers but not necessarily + // the full HTTP body + if (hm->body.len == (size_t) ~0 || + (te != NULL && mg_strcasecmp(*te, mg_str("chunked")) == 0)) { + // Unknown length or chunked, cannot stream. Leave it to the caller + } else if (!c->is_client && + mg_strcmp(hm->method, mg_str("POST")) != 0 && + mg_strcmp(hm->method, mg_str("PUT")) != 0) { + // Not an upload request. Leave it to the caller + } else if (sizeof(*us) > sizeof(c->data)) { + if (!c->is_client) { // Nobody to reply to on a client connection + c->pfn = NULL; + mg_http_reply(c, 500, NULL, "data too small\n"); + c->is_draining = 1; + cb(NULL, NULL, &user_data); // Let cb release user_data, start skipped + handled = true; + } + } else { + c->pfn = NULL; // Silence HTTP protocol handler + memset(us, 0, sizeof(*us)); + us->expected = hm->body.len; // Store number of bytes we expect + us->user_data = user_data; + us->failed = !cb(hm, NULL, &us->user_data); // Call before deleting headers if (mg_http_get_header(hm, "Expect") != NULL) { mg_http_reply(c, 100, NULL, ""); // If curl expects, we continue } mg_iobuf_del(&c->recv, 0, hm->head.len); // Delete HTTP headers + c->fn = mg_stream_handler; // We own the connection from now + c->fn_data = (void *) (ptrdiff_t) cb; + mg_call(c, MG_EV_READ, &c->recv.len); // Process the body bytes we have + handled = true; } - - if (c->pfn == NULL && ev != MG_EV_OPEN) { - if (c->pfn_data != NULL && c->recv.len > 0) { - // Got some uploaded data. Feed by 512-byte aligned chunks, for OTA - size_t left = (size_t) (ptrdiff_t) c->pfn_data, r = c->recv.len; - size_t aligned = r < left ? MG_ROUND_DOWN(r, 512) : left; - struct mg_str data = mg_str_n((char *) c->recv.buf, aligned); - if (data.len) cb(NULL, &data, &c->fn_data); - c->pfn_data = (void *) (ptrdiff_t) (left - aligned); - c->recv.len -= aligned; - } - if (c->pfn_data == NULL && c->is_draining == 0) { - // Finished upload - mg_http_reply(c, 200, NULL, "ok\n"); - c->is_draining = 1; - cb(NULL, NULL, &c->fn_data); - } - } + return handled; } static bool is_hex_digit(int c) { diff --git a/src/http.h b/src/http.h index 04d646c9..7fa1543e 100644 --- a/src/http.h +++ b/src/http.h @@ -160,20 +160,6 @@ void mg_http_creds(struct mg_http_message *, char *, size_t, char *, size_t); long mg_http_upload(struct mg_connection *c, struct mg_http_message *hm, struct mg_fs *fs, const char *dir, size_t max_size); -// Streams the raw request body into dir/name via fs. Non-blocking: installs an -// internal handler and returns immediately. fn is called with NULL on success -// or an error string on failure. -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, - void (*fn)(struct mg_connection *, const char *)); - -// Starts an OTA firmware update from an HTTP POST upload. -// Calls mg_ota_begin/write/end internally; fn is called with NULL on success, -// or an error string on failure. -void mg_http_start_ota(struct mg_connection *c, struct mg_http_message *hm, - void (*fn)(struct mg_connection *, const char *)); - // Sends a 401 Unauthorized response with a Basic Auth WWW-Authenticate challenge. void mg_http_bauth(struct mg_connection *, const char *user, const char *pass); @@ -192,7 +178,7 @@ struct mg_str mg_http_get_header_var(struct mg_str s, struct mg_str v); // Full examples: // tutorials/http/file-upload-html-form, tutorials/http/http-server // Related APIs: -// mg_http_listen(), mg_http_start_upload() +// mg_http_listen(), mg_http_stream_body() // Notes: // Call from an MG_EV_HTTP_MSG handler after the full request body is // received. part.name, part.filename, and part.body are zero-copy slices into @@ -203,24 +189,30 @@ size_t mg_http_next_multipart(struct mg_str, size_t, struct mg_http_part *); int mg_http_status(const struct mg_http_message *hm); // Streams a POST/PUT body to a callback as it arrives, without buffering it. -// Call it from your connection handler on every event. +// Call it from an MG_EV_HTTP_HDRS handler, after matching the request, e.g. +// with mg_match(). Returns true if it took over the connection, false if the +// request is not POST/PUT, is chunked, or has no Content-Length. // // Callback upload_fn(hm, data, user_data) is called: -// - at start: hm is set, data is NULL. Open the file, set user_data. -// - per chunk: hm is NULL, data is set. Chunks are 512 bytes aligned -// except the last one. -// - at end: both NULL. Close the file. -// After the last chunk, "200 ok" is sent and the connection is drained. +// - at start: hm is set. Open the file, set *user_data. +// - per chunk: data is set, 512 byte aligned except the last. Not called +// again after a failure. +// - at end: both NULL. Always called once, also when the connection drops +// early, so release resources here. A truncated body looks like a complete +// one: validate before committing. +// Return true on success, false on error. // Example: -// mg_http_stream_body(c, ev, ev_data, mg_str("/fs/*"), upload); +// if (ev == MG_EV_HTTP_HDRS && mg_match(hm->uri, mg_str("/fs/*"), NULL) { +// mg_http_stream_body(c, hm, upload, NULL)); +// } // Full example: // tutorials/http/file-upload-single-post // Related APIs: -// mg_http_start_upload(), mg_http_start_ota() +// mg_http_listen(), mg_http_next_multipart() // Notes: -// Requires Content-Length, chunked bodies are not streamed. user_data is -// &c->fn_data. While streaming, c->pfn is NULL -void mg_http_stream_body(struct mg_connection *c, int ev, void *ev_data, - struct mg_str uri_pattern, - void (*upload_fn)(struct mg_http_message *, - struct mg_str *, void **user_data)); +// After it returns true, this function hijacks the connection. Existing event +// handler not called, hm is invalid, c->data and c->fn_data are modified +bool mg_http_stream_body(struct mg_connection *c, struct mg_http_message *hm, + bool (*upload_fn)(struct mg_http_message *, + struct mg_str *, void **user_data), + void *user_data); diff --git a/src/ota.c b/src/ota.c index 9879af1b..15e8178d 100644 --- a/src/ota.c +++ b/src/ota.c @@ -23,6 +23,9 @@ static struct mg_ota_state { size_t size; uint8_t sha256[32]; void (*fn)(const char *error_message); + size_t received; // Firmware bytes written so far + bool open; // mg_ota_begin() succeeded, mg_ota_end() is pending + const char *err; // First error during download, NULL if none } *s_ota; static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); @@ -143,11 +146,38 @@ 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); +// Firmware download callback for mg_http_stream_body(). Writes chunks to the +// OTA target and reports the result via s_ota->fn(). The end call also happens +// when the connection drops before the whole body arrived. +static bool s_firmware_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + bool finalized = false, ok; + (void) p; + if (s_ota == NULL) return false; + if (hm != NULL) { // Start + s_ota->received = 0; + s_ota->open = mg_ota_begin(hm->body.len); + if (!s_ota->open) s_ota->err = "OTA begin failed"; + return s_ota->open; + } else if (data != NULL) { // Next chunk + if (!mg_ota_write(data->buf, data->len)) { + s_ota->err = "OTA write failed"; + return false; + } + s_ota->received += data->len; + return true; + } + // End + if (s_ota->open) finalized = mg_ota_end(); + if (s_ota->err == NULL && s_ota->received != s_ota->size) { + s_ota->err = "Connection closed"; + } + if (s_ota->err == NULL && !finalized) s_ota->err = "OTA finalize failed"; + ok = s_ota->err == NULL; + s_ota->fn(s_ota->err); mg_free(s_ota); s_ota = NULL; - mg_http_reply(c, errmsg ? 500 : 200, "", "%s\n", errmsg ? errmsg : "ok"); + return ok; } static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { @@ -178,7 +208,9 @@ static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) { (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); + if (!mg_http_stream_body(c, hm, s_firmware_cb, NULL)) { + mg_error(c, "Cannot stream firmware"); + } } } else if (ev == MG_EV_ERROR) { s_ota->fn((char *) ev_data); diff --git a/src/ota.h b/src/ota.h index 4c2761d7..0de5a4b3 100644 --- a/src/ota.h +++ b/src/ota.h @@ -3,7 +3,7 @@ // This is a low-level OTA API, not intended to be called directly by users. // Users are provided with a higher-level API: // - defining MG_OTA_URL enables HTTP or HTTPS pull-based updates -// - mg_http_start_ota() enables push-based updates +// - mg_http_stream_body() enables push-based updates // // However, it is possible to use the mg_ota_* API directly. Below is the // example code of a function that is called repeatedly. It expects the diff --git a/test/unit_test.c b/test/unit_test.c index 9202073d..c2db01b3 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -3104,26 +3104,83 @@ static void uc(struct mg_connection *c, int ev, void *ev_data) { } } -static int s_su_done; +static int s_su_done; // 1: upload ended, -1: upload rejected +static int s_su_end; // Number of end calls -static void su_cb(struct mg_connection *c, const char *errmsg) { - s_su_done = errmsg ? -1 : 1; - mg_http_reply(c, errmsg ? 400 : 200, "", errmsg ? errmsg : "ok\n"); - c->is_draining = 1; +// mg_http_stream_body() callback, saves the body to a file named as the URI +static bool su_cb(struct mg_http_message *hm, struct mg_str *data, void **p) { + if (hm != NULL) { // Start + char path[100]; + struct mg_str name = mg_str_n(hm->uri.buf + 1, hm->uri.len - 1); + *p = NULL; + if (!mg_path_is_sane(name)) { + s_su_done = -1; + return false; + } + mg_snprintf(path, sizeof(path), "./%.*s", (int) name.len, name.buf); + *p = mg_fs_open(&mg_fs_posix, path, MG_FS_WRITE); + return *p != NULL; + } else if (data != NULL) { // Next chunk + struct mg_fd *fd = (struct mg_fd *) *p; + return fd != NULL && fd->fs->wr(fd->fd, data->buf, data->len) == data->len; + } + s_su_end++; // End, also called when the connection drops + if (*p != NULL) mg_fs_close((struct mg_fd *) *p); + *p = NULL; + if (s_su_done == 0) s_su_done = 1; + return true; } static void su(struct mg_connection *c, int ev, void *ev_data) { if (ev == MG_EV_HTTP_HDRS) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_str name = mg_str_n(hm->uri.buf + 1, hm->uri.len - 1); - mg_http_start_upload(c, hm, name, mg_str("."), &mg_fs_posix, su_cb); + mg_http_stream_body(c, (struct mg_http_message *) ev_data, su_cb, NULL); + } else if (ev == MG_EV_HTTP_MSG) { // Only for requests that were not streamed + mg_http_reply(c, 400, "", "not streamed\n"); } } -static void cu(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_CONNECT) - mg_printf(c, "POST %s HTTP/1.0\r\nContent-Length: 8\r\n\r\nfoo\nbar\n", - (char *) c->fn_data); +// Client that announces a large body, sends a few bytes and stays silent +static void cd(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_CONNECT) { + mg_printf(c, "POST /su_drop.txt HTTP/1.0\r\nContent-Length: 100000\r\n\r\nfoo\n"); + } + (void) ev_data; +} + +static char s_cr_buf[4096]; // Response body streamed by the client +static size_t s_cr_len; +static int s_cr_end; + +static bool cr_cb(struct mg_http_message *hm, struct mg_str *data, void **p) { + (void) p; + if (hm != NULL) { + s_cr_len = 0; + } else if (data != NULL) { + ASSERT(s_cr_len + data->len <= sizeof(s_cr_buf)); + memcpy(s_cr_buf + s_cr_len, data->buf, data->len); + s_cr_len += data->len; + } else { + s_cr_end++; + } + return true; +} + +// Client that streams the response body with mg_http_stream_body() +static void cr(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_CONNECT) { + mg_printf(c, "GET / HTTP/1.0\r\n\r\n"); + } else if (ev == MG_EV_HTTP_HDRS) { + ASSERT(mg_http_stream_body(c, (struct mg_http_message *) ev_data, cr_cb, + NULL)); + } +} + +static void cs(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + char body[3000]; + memset(body, 'x', sizeof(body)); + mg_http_reply(c, 200, "", "%.*s", (int) sizeof(body), body); + } (void) ev_data; } @@ -3141,16 +3198,18 @@ static void test_http_upload(void) { mg_mgr_free(&mgr); ASSERT(mgr.conns == NULL); - // mg_http_start_upload: successful upload, file content verified + // mg_http_stream_body: successful upload, file content verified { char buf[FETCH_BUF_SIZE]; struct mg_str fc; remove("su_ok.txt"); + s_su_done = s_su_end = 0; mg_mgr_init(&mgr); mg_http_listen(&mgr, "http://127.0.0.1:12355", su, NULL); ASSERT(fetch(&mgr, buf, "http://127.0.0.1:12355", "POST /su_ok.txt HTTP/1.0\r\nContent-Length: 8\r\n\r\nfoo\nbar\n") == 200); + ASSERT(s_su_done == 1 && s_su_end == 1); fc = mg_file_read(&mg_fs_posix, "su_ok.txt"); ASSERT(mg_strcmp(fc, mg_str("foo\nbar\n")) == 0); mg_free((void *) fc.buf); @@ -3159,14 +3218,68 @@ static void test_http_upload(void) { ASSERT(mgr.conns == NULL); } - // mg_http_start_upload: path traversal rejected, callback gets error + // mg_http_stream_body: path traversal rejected by the callback, "500" sent { - s_su_done = 0; + char buf[FETCH_BUF_SIZE]; + s_su_done = s_su_end = 0; mg_mgr_init(&mgr); mg_http_listen(&mgr, "http://127.0.0.1:12356", su, NULL); - mg_http_connect(&mgr, "http://127.0.0.1:12356", cu, (void *) "/../evil.txt"); - for (i = 0; i < 50 && !s_su_done; i++) mg_mgr_poll(&mgr, 5); - ASSERT(s_su_done == -1); + ASSERT(fetch(&mgr, buf, "http://127.0.0.1:12356", + "POST /../evil.txt HTTP/1.0\r\nContent-Length: 8\r\n\r\nfoo\nbar\n") == + 500); + ASSERT(s_su_done == -1 && s_su_end == 1); + ASSERT(fopen("../evil.txt", "r") == NULL); + mg_mgr_free(&mgr); + ASSERT(mgr.conns == NULL); + } + + // mg_http_stream_body: chunked request is not streamed, caller replies "400" + { + char buf[FETCH_BUF_SIZE]; + s_su_done = s_su_end = 0; + remove("su_chunked.txt"); + mg_mgr_init(&mgr); + mg_http_listen(&mgr, "http://127.0.0.1:12393", su, NULL); + ASSERT(fetch(&mgr, buf, "http://127.0.0.1:12393", + "POST /su_chunked.txt HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n\r\n" + "3\r\nfoo\r\n0\r\n\r\n") == 400); + ASSERT(s_su_done == 0 && s_su_end == 0); // The callback was never called + ASSERT(fopen("su_chunked.txt", "r") == NULL); + mg_mgr_free(&mgr); + ASSERT(mgr.conns == NULL); + } + + // mg_http_stream_body: connection drops mid-body, the end call still happens + { + struct mg_connection *cc; + s_su_done = s_su_end = 0; + remove("su_drop.txt"); + mg_mgr_init(&mgr); + mg_http_listen(&mgr, "http://127.0.0.1:12391", su, NULL); + cc = mg_http_connect(&mgr, "http://127.0.0.1:12391", cd, NULL); + ASSERT(cc != NULL); + for (i = 0; i < 20; i++) mg_mgr_poll(&mgr, 5); + ASSERT(s_su_end == 0); // Still waiting for the rest of the body + cc->is_closing = 1; + for (i = 0; i < 20; i++) mg_mgr_poll(&mgr, 5); + ASSERT(s_su_end == 1); + remove("su_drop.txt"); + mg_mgr_free(&mgr); + ASSERT(mgr.conns == NULL); + } + + // mg_http_stream_body on a client connection: response body is streamed + { + s_cr_end = 0; + s_cr_len = 0; + mg_mgr_init(&mgr); + mg_http_listen(&mgr, "http://127.0.0.1:12392", cs, NULL); + mg_http_connect(&mgr, "http://127.0.0.1:12392", cr, NULL); + for (i = 0; i < 50 && s_cr_end == 0; i++) mg_mgr_poll(&mgr, 5); + ASSERT(s_cr_end == 1); + ASSERT(s_cr_len == 3000); + ASSERT(s_cr_buf[0] == 'x' && s_cr_buf[2999] == 'x'); mg_mgr_free(&mgr); ASSERT(mgr.conns == NULL); } diff --git a/tutorials/http/file-upload-single-post/main.c b/tutorials/http/file-upload-single-post/main.c index f11448bd..a94c7290 100644 --- a/tutorials/http/file-upload-single-post/main.c +++ b/tutorials/http/file-upload-single-post/main.c @@ -29,7 +29,7 @@ static const char *s_tls_key = "4hf5Gx17YJkq5/z3k6ogPDPpoAYWIw1/sw==\n" "-----END EC PRIVATE KEY-----\n"; -static void upload(struct mg_http_message *hm, struct mg_str *data, void **p) { +static bool upload(struct mg_http_message *hm, struct mg_str *data, void **p) { struct mg_fs *fs = &mg_fs_posix; if (hm != NULL) { char path[MG_PATH_MAX]; @@ -48,6 +48,7 @@ static void upload(struct mg_http_message *hm, struct mg_str *data, void **p) { MG_INFO(("UPLOAD END %p", p)); if (*p) fs->cl(*p), *p = NULL; } + return true; // signal success } static void fn(struct mg_connection *c, int ev, void *ev_data) { @@ -55,13 +56,16 @@ static void fn(struct mg_connection *c, int ev, void *ev_data) { struct mg_tls_opts opts = {.cert = mg_str(s_tls_cert), .key = mg_str(s_tls_key)}; mg_tls_init(c, &opts); + } else if (ev == MG_EV_HTTP_HDRS) { + // Handle upload requests to /fs/* + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_match(hm->uri, mg_str("/fs/*"), NULL)) { + mg_http_stream_body(c, hm, upload, NULL); + } } else if (ev == MG_EV_HTTP_MSG && c->pfn != NULL) { struct mg_http_serve_opts opts = {.root_dir = "web_root"}; mg_http_serve_dir(c, ev_data, &opts); } - - // Handle upload requests to /fs/* - mg_http_stream_body(c, ev, ev_data, mg_str("/fs/*"), upload); } int main(void) { diff --git a/tutorials/stm32/nucleo-h563zi/minimal/Makefile b/tutorials/stm32/nucleo-h563zi/minimal/Makefile index 5b0e4910..9045ed37 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/Makefile +++ b/tutorials/stm32/nucleo-h563zi/minimal/Makefile @@ -12,7 +12,7 @@ SOURCES = main.c hal.c SOURCES += cmsis_h5/Source/Templates/gcc/startup_stm32h563xx.s SOURCES += mongoose/mongoose.c -all build example: firmware.signed.bin +all build example: firmware.bin mongoose/mongoose.c mongoose/mongoose.h: cp ../../../../mongoose.[ch] mongoose/ @@ -25,9 +25,6 @@ firmware.bin: firmware.elf @echo @echo "To flash, run 'make flash', or use STM32CubeProgrammer" -sign firmware.signed.bin: firmware.bin - node ../../../../resources/sign.js sign firmware.bin private.pem - flash: firmware.bin STM32_Programmer_CLI -c port=swd mode=UR -w firmware.elf -hardRst diff --git a/tutorials/stm32/nucleo-h563zi/minimal/hal.h b/tutorials/stm32/nucleo-h563zi/minimal/hal.h index 378e241e..d4299107 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/hal.h +++ b/tutorials/stm32/nucleo-h563zi/minimal/hal.h @@ -181,23 +181,7 @@ static inline void hal_system_init(void) { __ISB(); } -// Enable TAMP/RTC APB clock, backup domain write access, and RTC/TAMP kernel -// clock (LSI). Idempotent: skips RTC init if already enabled after a reset. -// Must be called before accessing TAMP backup registers or IWDG. -static inline void hal_backup_domain_init(void) { - RCC->APB3ENR |= RCC_APB3ENR_RTCAPBEN; // enable TAMP/RTC APB clock - PWR->DBPCR |= PWR_DBPCR_DBP; // enable backup domain write access - if ((RCC->BDCR & RCC_BDCR_RTCEN) == 0) { // init only once (not warm reset) - RCC->BDCR |= RCC_BDCR_LSION; // enable LSI - while ((RCC->BDCR & RCC_BDCR_LSIRDY) == 0) (void) 0; // wait ready - RCC->BDCR |= (2U << RCC_BDCR_RTCSEL_Pos); // LSI as clock source - RCC->BDCR |= RCC_BDCR_RTCEN; // enable RTC/TAMP clock - } -} - static inline void hal_clock_init(void) { - hal_backup_domain_init(); - // Set flash latency. RM0481, section 7.11.1, section 7.3.4 table 37 CLRSET(FLASH->ACR, (FLASH_ACR_WRHIGHFREQ_Msk | FLASH_ACR_LATENCY_Msk), FLASH_ACR_LATENCY_5WS | FLASH_ACR_WRHIGHFREQ_1); diff --git a/tutorials/stm32/nucleo-h563zi/minimal/main.c b/tutorials/stm32/nucleo-h563zi/minimal/main.c index 473589ba..cea2a7a6 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/main.c +++ b/tutorials/stm32/nucleo-h563zi/minimal/main.c @@ -41,31 +41,13 @@ bool mg_random(void *buf, size_t len) { } static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_HTTP_HDRS) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - // Start OTA on headers so body chunks stream directly to flash (no RAM buffer) - if (mg_match(hm->uri, mg_str("/api/ota/update"), NULL)) { - mg_http_start_ota(c, hm, NULL); - } - } else if (ev == MG_EV_HTTP_MSG) { + if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; if (mg_match(hm->uri, mg_str("/api/tick"), NULL)) { mg_http_reply(c, 200, "", "{%m:%llu}\n", MG_ESC("tick"), hal_get_tick()); - } else if (mg_match(hm->uri, mg_str("/api/ota/commit"), NULL)) { - c->data[0] = 1; - mg_http_reply(c, 200, "", "ok\n"); - } else if (mg_match(hm->uri, mg_str("/api/ota/rollback"), NULL)) { - c->data[0] = 2; - mg_http_reply(c, 200, "", "ok\n"); } else { mg_http_reply(c, 200, "", "Hi from Mongoose, tick %llu\n", hal_get_tick()); } - } else if (ev == MG_EV_CLOSE && c->data[0] == 1) { - MG_OTA_STATE_SET(MG_OTA_CONFIRMED); - NVIC_SystemReset(); - } else if (ev == MG_EV_CLOSE && c->data[0] == 2) { - MG_OTA_STATE_SET(MG_OTA_CONFIRMED); - MG_OTA_ROLLBACK(); } } @@ -74,8 +56,6 @@ int main(void) { hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); mg_log_set_fn(log_fn, UART_DEBUG); - MG_OTA_BOOT_CHECK(); // Must be called after clock init - hal_rng_init(); hal_gpio_output(LED_1); hal_gpio_output(LED_2); diff --git a/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h b/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h index 061fce03..cdc861c4 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h +++ b/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h @@ -3,7 +3,6 @@ // See https://mongoose.ws/documentation/#build-options #define MG_ARCH MG_ARCH_ARMGCC -#define MG_OTA MG_OTA_STM32H5 #define MG_TLS MG_TLS_BUILTIN #define MG_IO_SIZE 2048 @@ -13,39 +12,6 @@ #define MG_ENABLE_PACKED_FS 1 #define MG_ENABLE_DRIVER_STM32H 1 -#define MG_OTA_PUBLIC_KEY \ - { \ - 0x19, 0x7f, 0xed, 0x81, 0x8b, 0xb7, 0xb2, 0xa5, 0xde, 0xe0, 0xde, \ - 0x04, 0x83, 0x60, 0xc8, 0x69, 0x61, 0x39, 0x30, 0x1f, 0xc1, 0x52, \ - 0x11, 0x4b, 0xb7, 0xa4, 0xd7, 0x52, 0x03, 0x36, 0xc6, 0xc1, 0xbf, \ - 0x22, 0x75, 0x04, 0x49, 0xf8, 0x46, 0x47, 0xec, 0x20, 0xd6, 0x8d, \ - 0x48, 0x29, 0x7d, 0x63, 0xb3, 0xf7, 0x1f, 0x58, 0x44, 0x82, 0xcc, \ - 0x7a, 0x4b, 0x5e, 0x68, 0x98, 0x24, 0x1f, 0x86, 0xf4, \ - } - -// OTA rollback timer via IWDG. PR=8 → /1024 on H563 (RM0481 §52.4.2). -// Fixed 10 s hang-detection window: RLR = 10 * 32000 / 1024 = 312. -// PVU/RVU must clear before writing RLR/starting (RM0481 §52.4.3). -// mg_ota_poll() feeds the watchdog every 500ms while in MG_OTA_TESTING state. -#define MG_OTA_ROLLBACK_TIMER_START() \ - do { \ - IWDG->KR = 0x5555U; /* unlock */ \ - IWDG->PR = 8U; /* /1024 */ \ - while (IWDG->SR & IWDG_SR_PVU) (void) 0; /* wait PVU */ \ - IWDG->RLR = 312U; /* 10 s */ \ - while (IWDG->SR & IWDG_SR_RVU) (void) 0; /* wait RVU */ \ - IWDG->KR = 0xCCCCU; /* start */ \ - IWDG->KR = 0xAAAAU; /* reload RLR */ \ - } while (0) -#define MG_OTA_ROLLBACK_TIMER_FEED() (IWDG->KR = 0xAAAAU) - -// OTA state in TAMP backup register 0 (survives all resets, clears on POR). -// Requires hal_backup_domain_init() (called from hal_clock_init) to have run. -#define MG_OTA_STATE_GET() (RCC->APB3ENR |= RCC_APB3ENR_RTCAPBEN, TAMP->BKP0R) -#define MG_OTA_STATE_SET(v) \ - (RCC->APB3ENR |= RCC_APB3ENR_RTCAPBEN, PWR->DBPCR |= PWR_DBPCR_DBP, \ - (TAMP->BKP0R = (uint32_t) (v)), TAMP->BKP0R) - // #define MG_DRIVER_MDC_CR 4 // RMII MDC clock divider, from 0 to 4 // #define MG_TCPIP_PHY_ADDR 0 // PHY address diff --git a/tutorials/stm32/nucleo-h563zi/minimal/private.pem b/tutorials/stm32/nucleo-h563zi/minimal/private.pem deleted file mode 100644 index b8c6698d..00000000 --- a/tutorials/stm32/nucleo-h563zi/minimal/private.pem +++ /dev/null @@ -1,5 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgto6awcVrVzHfgSaD -WFiy6JrXahab3F5lfGvb7A1XzjShRANCAAQZf+2Bi7eypd7g3gSDYMhpYTkwH8FS -EUu3pNdSAzbGwb8idQRJ+EZH7CDWjUgpfWOz9x9YRILMekteaJgkH4b0 ------END PRIVATE KEY-----