From d1060d247b65dc3d3f5078992780759ba23af5e6 Mon Sep 17 00:00:00 2001 From: cpq Date: Wed, 23 Sep 2026 06:28:08 +0100 Subject: [PATCH] Directory listing as JSON --- mongoose.c | 250 ++++++++++++++++++++++------------------------- mongoose.h | 15 ++- src/http.c | 250 ++++++++++++++++++++++------------------------- src/http.h | 15 ++- test/unit_test.c | 42 +++++++- 5 files changed, 302 insertions(+), 270 deletions(-) diff --git a/mongoose.c b/mongoose.c index 3f3f4f51..8b00f474 100644 --- a/mongoose.c +++ b/mongoose.c @@ -4352,127 +4352,55 @@ struct printdirentrydata { struct mg_http_message *hm; const struct mg_http_serve_opts *opts; const char *dir; + size_t count; }; #if MG_ENABLE_DIRLIST -// Print file name, escaping HTML chars -static size_t html_esc(void (*fn)(char, void *), void *arg, va_list *ap) { - const char *s = va_arg(*ap, const char *); - size_t i, len = 0; - for (i = 0; s[i] != '\0'; i++) { - if (s[i] == '<') { - len += mg_xprintf(fn, arg, "%s", "<"); - } else if (s[i] == '>') { - len += mg_xprintf(fn, arg, "%s", ">"); - } else if (s[i] == '&') { - len += mg_xprintf(fn, arg, "%s", "&"); - } else { - len += mg_xprintf(fn, arg, "%c", s[i]); - } - } - return len; -} - static void printdirentry(const char *name, void *userdata) { struct printdirentrydata *d = (struct printdirentrydata *) userdata; struct mg_fs *fs = d->opts->fs == NULL ? &mg_fs_posix : d->opts->fs; size_t size = 0; time_t t = 0; - char path[MG_PATH_MAX], sz[40], mod[40]; - int flags, n = 0; + char path[MG_PATH_MAX]; + int flags; - // MG_DEBUG(("[%s] [%s]", d->dir, name)); - if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) > + if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) >= sizeof(path)) { MG_ERROR(("%s truncated", name)); } else if ((flags = fs->st(path, &size, &t)) == 0) { MG_ERROR(("%lu stat(%s)", d->c->id, path)); } else { - const char *slash = flags & MG_FS_DIR ? "/" : ""; if (flags & MG_FS_DIR) { - mg_snprintf(sz, sizeof(sz), "%s", "[DIR]"); + mg_printf(d->c, "%s\n {%m: %m, %m: true}", // + d->count == 0 ? "" : ",", // + MG_ESC("name"), MG_ESC(name), MG_ESC("dir")); } else { - mg_snprintf(sz, sizeof(sz), "%lld", (uint64_t) size); + mg_printf(d->c, "%s\n {%m: %m, %m: %zu, %m: %llu, %m: false}", // + d->count == 0 ? "" : ",", // + MG_ESC("name"), MG_ESC(name), // + MG_ESC("size"), size, // + MG_ESC("modified"), (uint64_t) t, MG_ESC("dir")); } -#if defined(MG_HTTP_DIRLIST_TIME_FMT) - { - char time_str[40]; - struct tm *time_info = localtime(&t); - strftime(time_str, sizeof time_str, "%Y/%m/%d %H:%M:%S", time_info); - mg_snprintf(mod, sizeof(mod), "%s", time_str); - } -#else - mg_snprintf(mod, sizeof(mod), "%lu", (unsigned long) t); -#endif - n = (int) mg_url_encode(name, strlen(name), path, sizeof(path)); - mg_printf(d->c, - " %M%s" - "%s%s\n", - n, path, slash, html_esc, name, slash, (unsigned long) t, mod, - flags & MG_FS_DIR ? (int64_t) -1 : (int64_t) size, sz); + d->count++; } } static void listdir(struct mg_connection *c, struct mg_http_message *hm, const struct mg_http_serve_opts *opts, char *dir) { - const char *sort_js_code = - ""; struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct printdirentrydata d = {c, hm, opts, dir}; - char tmp[10], buf[MG_PATH_MAX]; + struct printdirentrydata d = {c, hm, opts, dir, 0}; + char tmp[10]; size_t off, n; - int len = mg_url_decode(hm->uri.buf, hm->uri.len, buf, sizeof(buf), 0); - struct mg_str uri = len > 0 ? mg_str_n(buf, (size_t) len) : hm->uri; - mg_printf(c, - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html; charset=utf-8\r\n" - "%s" - "Content-Length: \r\n\r\n", - opts->extra_headers == NULL ? "" : opts->extra_headers); + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "%s" + "Content-Length: \r\n\r\n", + opts->extra_headers == NULL ? "" : opts->extra_headers); off = c->send.len; // Start of body - mg_printf(c, - "Index of %M%s%s" - "" - "

Index of %M

" - "" - "" - "" - "" - "\n", - mg_print_html_esc, (int) uri.len, uri.buf, sort_js_code, sort_js_code2, - mg_print_html_esc, (int) uri.len, uri.buf); - mg_printf(c, "%s", - " " - "\n"); + mg_printf(c, "["); fs->ls(dir, printdirentry, &d); - mg_printf(c, - "" - "
Name" - "ModifiedSize

..[DIR]

Mongoose v.%s
\n", - MG_VERSION); + mg_printf(c, "\n]\n"); n = mg_snprintf(tmp, sizeof(tmp), "%lu", (unsigned long) (c->send.len - off)); if (n > sizeof(tmp)) n = 0; memcpy(c->send.buf + off - 12, tmp, n); // Set content length @@ -4480,33 +4408,43 @@ static void listdir(struct mg_connection *c, struct mg_http_message *hm, } #endif +// Map requested URI to the file path (buf,len). Use root directory r. +// r could be a path optionally followed by map: PATH,/PREFIX1=PATH1,... +static bool uri2path(const char *r, struct mg_str uri, char *buf, size_t len) { + struct mg_str k, v, part, s = mg_str(r), u = {NULL, 0}, d = u; + size_t n; + while (mg_span(s, &part, &s, ',')) { + if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); + if (v.len == 0) v = k, k = mg_str("/"), u = k, d = v; + if (uri.len < k.len) continue; + if (mg_strcmp(k, mg_str_n(uri.buf, k.len)) != 0) continue; + u = k, d = v; + } + n = mg_snprintf(buf, len, "%.*s", (int) d.len, d.buf); + if (len == 0 || n + 2 >= len) return false; // Path overflow + if (n > 0 && buf[n - 1] != '/') buf[n++] = '/', buf[n] = '\0'; // Add slash + if (mg_url_decode(uri.buf + u.len, uri.len - u.len, buf + n, len - n, 0) < 0) { + return false; + } + buf[len - 1] = '\0'; // Double-check + n = strlen(buf); + if (!mg_path_is_sane(mg_str_n(buf, n))) return false; + while (n > 1 && buf[n - 1] == '/') buf[--n] = 0; // Trim trailing slashes + return true; +} + // Resolve requested file into `path` and return its fs->st() result -static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, - struct mg_fs *fs, struct mg_str url, struct mg_str dir, - char *path, size_t path_size) { +static int uri_to_file_status(struct mg_connection *c, + struct mg_http_message *hm, + struct mg_fs *fs, const char *root_dir, + char *path, size_t path_size) { int flags, tmp; - // Append URI to the root_dir, and sanitize it - size_t n = mg_snprintf(path, path_size, "%.*s", (int) dir.len, dir.buf); - if (n + 2 >= path_size) { - mg_http_reply(c, 400, "", "Exceeded path size"); - return -1; - } - path[path_size - 1] = '\0'; - // Terminate root dir with slash - if (n > 0 && path[n - 1] != '/') path[n++] = '/', path[n] = '\0'; - if (url.len < hm->uri.len && - mg_url_decode(hm->uri.buf + url.len, hm->uri.len - url.len, path + n, - path_size - n, 0) < 0) { + size_t n; + if (!uri2path(root_dir, hm->uri, path, path_size)) { mg_http_reply(c, 400, "", "Invalid path"); return -1; } - path[path_size - 1] = '\0'; // Double-check n = strlen(path); - if (!mg_path_is_sane(mg_str_n(path, n))) { - mg_http_reply(c, 400, "", "Invalid path"); - return -1; - } - while (n > 1 && path[n - 1] == '/') path[--n] = 0; // Trim trailing slashes flags = mg_strcmp(hm->uri, mg_str("/")) == 0 ? MG_FS_DIR : fs->st(path, NULL, NULL); MG_VERBOSE(("%lu %.*s -> %s %d", c->id, (int) hm->uri.len, hm->uri.buf, path, @@ -4543,34 +4481,36 @@ static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, return flags; } -static int uri_to_path(struct mg_connection *c, struct mg_http_message *hm, - const struct mg_http_serve_opts *opts, char *path, - size_t path_size) { - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct mg_str k, v, part, s = mg_str(opts->root_dir), u = {NULL, 0}, p = u; - while (mg_span(s, &part, &s, ',')) { - if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); - if (v.len == 0) v = k, k = mg_str("/"), u = k, p = v; - if (hm->uri.len < k.len) continue; - if (mg_strcmp(k, mg_str_n(hm->uri.buf, k.len)) != 0) continue; - u = k, p = v; - } - return uri_to_path2(c, hm, fs, u, p, path, path_size); -} - void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, const struct mg_http_serve_opts *opts) { char path[MG_PATH_MAX]; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; const char *sp = opts->ssi_pattern; - int flags = uri_to_path(c, hm, opts, path, sizeof(path)); + int flags = uri_to_file_status(c, hm, fs, opts->root_dir, path, sizeof(path)); if (flags < 0) { - // Do nothing: the response has already been sent by uri_to_path() + // Do nothing: the response has already been sent by uri_to_file_status() } else if (flags & MG_FS_DIR) { #if MG_ENABLE_DIRLIST listdir(c, hm, opts, path); #else mg_http_reply(c, 403, "", "Forbidden\n"); #endif + } else if (opts->allow_delete && + mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { + mg_http_reply(c, flags && fs->rm(path) ? 200 : 404, "", ""); + } else if (opts->allow_upload && + (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || + mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { + // Small or already fully buffered body. mg_http_serve_upload() handles + // the same thing without buffering, for big uploads, from MG_EV_HTTP_HDRS + void *fd; + bool ok = false; + fs->rm(path); // MG_FS_WRITE appends, not truncates: drop any old file + if ((fd = fs->op(path, MG_FS_WRITE)) != NULL) { + ok = fs->wr(fd, hm->body.buf, hm->body.len) == hm->body.len; + fs->cl(fd); + } + mg_http_reply(c, ok ? 200 : 500, "", ""); } else if (flags && sp != NULL && mg_match(mg_str(path), mg_str(sp), NULL)) { mg_http_serve_ssi(c, opts->root_dir, path); } else { @@ -4578,6 +4518,52 @@ void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, } } +// mg_http_stream_body() callback for mg_http_serve_upload(). The file is +// opened by the caller before streaming starts, so this only writes and closes +static bool serve_upload_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + struct mg_fd *fd = (struct mg_fd *) *p; + if (hm != NULL) return fd != NULL; // Start + if (data != NULL) { // Next chunk + return fd != NULL && fd->fs->wr(fd->fd, data->buf, data->len) == data->len; + } + if (fd != NULL) mg_fs_close(fd); // End + *p = NULL; + return true; +} + +// Starts a streaming upload for a big POST/PUT body, so it never sits fully +// buffered in memory. Companion to mg_http_serve_dir(): call this from +// MG_EV_HTTP_HDRS, and keep calling mg_http_serve_dir() from MG_EV_HTTP_MSG +// as before - it handles uploads too, for bodies this function skips. +// Does nothing unless opts->allow_upload is set, the method is POST or PUT, +// and Content-Length is known and >= 2 * MG_IO_SIZE. +void mg_http_serve_upload(struct mg_connection *c, struct mg_http_message *hm, + const struct mg_http_serve_opts *opts) { + char path[MG_PATH_MAX]; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + if (!opts->allow_upload) { + // Not enabled, nothing to do + } else if (mg_strcasecmp(hm->method, mg_str("POST")) != 0 && + mg_strcasecmp(hm->method, mg_str("PUT")) != 0) { + // Not an upload request, nothing to do + } else if (hm->body.len == (size_t) ~0 || hm->body.len < 2 * MG_IO_SIZE) { + // Unknown length, or small enough for mg_http_serve_dir() to buffer it + } else if (!uri2path(opts->root_dir, hm->uri, path, sizeof(path))) { + MG_ERROR(("Invalid upload path: %.*s", (int) hm->uri.len, hm->uri.buf)); + } else if (fs->st(path, NULL, NULL) & MG_FS_DIR) { + // Target is a directory, let mg_http_serve_dir() list it + } else { + struct mg_fd *fd; + fs->rm(path); // MG_FS_WRITE appends, not truncates: drop any old file + if ((fd = mg_fs_open(fs, path, MG_FS_WRITE)) == NULL) { + MG_ERROR(("Cannot open %s for upload", path)); + } else if (!mg_http_stream_body(c, hm, serve_upload_cb, fd)) { + mg_fs_close(fd); + } + } +} + static bool mg_is_url_safe(int c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == '_' || c == '-' || c == '~'; diff --git a/mongoose.h b/mongoose.h index 5c31b9e6..8500c569 100644 --- a/mongoose.h +++ b/mongoose.h @@ -2665,6 +2665,8 @@ struct mg_http_serve_opts { const char *mime_types; // Additional MIME types: "ext1=type1,ext2=type2". NULL for defaults only const char *page404; // Path to a custom 404 page, e.g. "/404.html". NULL for built-in struct mg_fs *fs; // Filesystem to use. NULL defaults to POSIX + bool allow_delete; // Allow to DELETE files + bool allow_upload; // Allow to POST/PUT files }; // A single part of a multipart/form-data body, filled by mg_http_next_multipart(). @@ -2739,7 +2741,9 @@ struct mg_connection *mg_http_connect(struct mg_mgr *, const char *url, // Notes: // Call from an MG_EV_HTTP_MSG handler. The uri in hm is mapped under // opts->root_dir. Directory listing depends on MG_ENABLE_DIRLIST; SSI uses -// opts->ssi_pattern when configured. +// opts->ssi_pattern when configured. opts->allow_delete enables DELETE; +// opts->allow_upload enables POST/PUT, writing the whole body to a file - +// see mg_http_serve_upload() to also stream big uploads without buffering. void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm, const struct mg_http_serve_opts *); @@ -2747,6 +2751,15 @@ void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm, void mg_http_serve_file(struct mg_connection *, struct mg_http_message *hm, const char *path, const struct mg_http_serve_opts *); +// Streams a big POST/PUT upload straight to a file, instead of buffering the +// whole body. Optional companion to mg_http_serve_dir(): call this from +// MG_EV_HTTP_HDRS with the same opts, and keep calling mg_http_serve_dir() +// from MG_EV_HTTP_MSG as usual - it still handles uploads this function skips. +// Does nothing unless opts->allow_upload is set, the method is POST or PUT, +// and Content-Length is known and big enough. +void mg_http_serve_upload(struct mg_connection *, struct mg_http_message *hm, + const struct mg_http_serve_opts *); + // Sends a complete HTTP response with Content-Length. // // Example: diff --git a/src/http.c b/src/http.c index a91cea2f..848c449a 100644 --- a/src/http.c +++ b/src/http.c @@ -676,127 +676,55 @@ struct printdirentrydata { struct mg_http_message *hm; const struct mg_http_serve_opts *opts; const char *dir; + size_t count; }; #if MG_ENABLE_DIRLIST -// Print file name, escaping HTML chars -static size_t html_esc(void (*fn)(char, void *), void *arg, va_list *ap) { - const char *s = va_arg(*ap, const char *); - size_t i, len = 0; - for (i = 0; s[i] != '\0'; i++) { - if (s[i] == '<') { - len += mg_xprintf(fn, arg, "%s", "<"); - } else if (s[i] == '>') { - len += mg_xprintf(fn, arg, "%s", ">"); - } else if (s[i] == '&') { - len += mg_xprintf(fn, arg, "%s", "&"); - } else { - len += mg_xprintf(fn, arg, "%c", s[i]); - } - } - return len; -} - static void printdirentry(const char *name, void *userdata) { struct printdirentrydata *d = (struct printdirentrydata *) userdata; struct mg_fs *fs = d->opts->fs == NULL ? &mg_fs_posix : d->opts->fs; size_t size = 0; time_t t = 0; - char path[MG_PATH_MAX], sz[40], mod[40]; - int flags, n = 0; + char path[MG_PATH_MAX]; + int flags; - // MG_DEBUG(("[%s] [%s]", d->dir, name)); - if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) > + if (mg_snprintf(path, sizeof(path), "%s%c%s", d->dir, '/', name) >= sizeof(path)) { MG_ERROR(("%s truncated", name)); } else if ((flags = fs->st(path, &size, &t)) == 0) { MG_ERROR(("%lu stat(%s)", d->c->id, path)); } else { - const char *slash = flags & MG_FS_DIR ? "/" : ""; if (flags & MG_FS_DIR) { - mg_snprintf(sz, sizeof(sz), "%s", "[DIR]"); + mg_printf(d->c, "%s\n {%m: %m, %m: true}", // + d->count == 0 ? "" : ",", // + MG_ESC("name"), MG_ESC(name), MG_ESC("dir")); } else { - mg_snprintf(sz, sizeof(sz), "%lld", (uint64_t) size); + mg_printf(d->c, "%s\n {%m: %m, %m: %zu, %m: %llu, %m: false}", // + d->count == 0 ? "" : ",", // + MG_ESC("name"), MG_ESC(name), // + MG_ESC("size"), size, // + MG_ESC("modified"), (uint64_t) t, MG_ESC("dir")); } -#if defined(MG_HTTP_DIRLIST_TIME_FMT) - { - char time_str[40]; - struct tm *time_info = localtime(&t); - strftime(time_str, sizeof time_str, "%Y/%m/%d %H:%M:%S", time_info); - mg_snprintf(mod, sizeof(mod), "%s", time_str); - } -#else - mg_snprintf(mod, sizeof(mod), "%lu", (unsigned long) t); -#endif - n = (int) mg_url_encode(name, strlen(name), path, sizeof(path)); - mg_printf(d->c, - " %M%s" - "%s%s\n", - n, path, slash, html_esc, name, slash, (unsigned long) t, mod, - flags & MG_FS_DIR ? (int64_t) -1 : (int64_t) size, sz); + d->count++; } } static void listdir(struct mg_connection *c, struct mg_http_message *hm, const struct mg_http_serve_opts *opts, char *dir) { - const char *sort_js_code = - ""; struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct printdirentrydata d = {c, hm, opts, dir}; - char tmp[10], buf[MG_PATH_MAX]; + struct printdirentrydata d = {c, hm, opts, dir, 0}; + char tmp[10]; size_t off, n; - int len = mg_url_decode(hm->uri.buf, hm->uri.len, buf, sizeof(buf), 0); - struct mg_str uri = len > 0 ? mg_str_n(buf, (size_t) len) : hm->uri; - mg_printf(c, - "HTTP/1.1 200 OK\r\n" - "Content-Type: text/html; charset=utf-8\r\n" - "%s" - "Content-Length: \r\n\r\n", - opts->extra_headers == NULL ? "" : opts->extra_headers); + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "%s" + "Content-Length: \r\n\r\n", + opts->extra_headers == NULL ? "" : opts->extra_headers); off = c->send.len; // Start of body - mg_printf(c, - "Index of %M%s%s" - "" - "

Index of %M

" - "" - "" - "" - "" - "\n", - mg_print_html_esc, (int) uri.len, uri.buf, sort_js_code, sort_js_code2, - mg_print_html_esc, (int) uri.len, uri.buf); - mg_printf(c, "%s", - " " - "\n"); + mg_printf(c, "["); fs->ls(dir, printdirentry, &d); - mg_printf(c, - "" - "
Name" - "ModifiedSize

..[DIR]

Mongoose v.%s
\n", - MG_VERSION); + mg_printf(c, "\n]\n"); n = mg_snprintf(tmp, sizeof(tmp), "%lu", (unsigned long) (c->send.len - off)); if (n > sizeof(tmp)) n = 0; memcpy(c->send.buf + off - 12, tmp, n); // Set content length @@ -804,33 +732,43 @@ static void listdir(struct mg_connection *c, struct mg_http_message *hm, } #endif +// Map requested URI to the file path (buf,len). Use root directory r. +// r could be a path optionally followed by map: PATH,/PREFIX1=PATH1,... +static bool uri2path(const char *r, struct mg_str uri, char *buf, size_t len) { + struct mg_str k, v, part, s = mg_str(r), u = {NULL, 0}, d = u; + size_t n; + while (mg_span(s, &part, &s, ',')) { + if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); + if (v.len == 0) v = k, k = mg_str("/"), u = k, d = v; + if (uri.len < k.len) continue; + if (mg_strcmp(k, mg_str_n(uri.buf, k.len)) != 0) continue; + u = k, d = v; + } + n = mg_snprintf(buf, len, "%.*s", (int) d.len, d.buf); + if (len == 0 || n + 2 >= len) return false; // Path overflow + if (n > 0 && buf[n - 1] != '/') buf[n++] = '/', buf[n] = '\0'; // Add slash + if (mg_url_decode(uri.buf + u.len, uri.len - u.len, buf + n, len - n, 0) < 0) { + return false; + } + buf[len - 1] = '\0'; // Double-check + n = strlen(buf); + if (!mg_path_is_sane(mg_str_n(buf, n))) return false; + while (n > 1 && buf[n - 1] == '/') buf[--n] = 0; // Trim trailing slashes + return true; +} + // Resolve requested file into `path` and return its fs->st() result -static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, - struct mg_fs *fs, struct mg_str url, struct mg_str dir, - char *path, size_t path_size) { +static int uri_to_file_status(struct mg_connection *c, + struct mg_http_message *hm, + struct mg_fs *fs, const char *root_dir, + char *path, size_t path_size) { int flags, tmp; - // Append URI to the root_dir, and sanitize it - size_t n = mg_snprintf(path, path_size, "%.*s", (int) dir.len, dir.buf); - if (n + 2 >= path_size) { - mg_http_reply(c, 400, "", "Exceeded path size"); - return -1; - } - path[path_size - 1] = '\0'; - // Terminate root dir with slash - if (n > 0 && path[n - 1] != '/') path[n++] = '/', path[n] = '\0'; - if (url.len < hm->uri.len && - mg_url_decode(hm->uri.buf + url.len, hm->uri.len - url.len, path + n, - path_size - n, 0) < 0) { + size_t n; + if (!uri2path(root_dir, hm->uri, path, path_size)) { mg_http_reply(c, 400, "", "Invalid path"); return -1; } - path[path_size - 1] = '\0'; // Double-check n = strlen(path); - if (!mg_path_is_sane(mg_str_n(path, n))) { - mg_http_reply(c, 400, "", "Invalid path"); - return -1; - } - while (n > 1 && path[n - 1] == '/') path[--n] = 0; // Trim trailing slashes flags = mg_strcmp(hm->uri, mg_str("/")) == 0 ? MG_FS_DIR : fs->st(path, NULL, NULL); MG_VERBOSE(("%lu %.*s -> %s %d", c->id, (int) hm->uri.len, hm->uri.buf, path, @@ -867,34 +805,36 @@ static int uri_to_path2(struct mg_connection *c, struct mg_http_message *hm, return flags; } -static int uri_to_path(struct mg_connection *c, struct mg_http_message *hm, - const struct mg_http_serve_opts *opts, char *path, - size_t path_size) { - struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; - struct mg_str k, v, part, s = mg_str(opts->root_dir), u = {NULL, 0}, p = u; - while (mg_span(s, &part, &s, ',')) { - if (!mg_span(part, &k, &v, '=')) k = part, v = mg_str_n(NULL, 0); - if (v.len == 0) v = k, k = mg_str("/"), u = k, p = v; - if (hm->uri.len < k.len) continue; - if (mg_strcmp(k, mg_str_n(hm->uri.buf, k.len)) != 0) continue; - u = k, p = v; - } - return uri_to_path2(c, hm, fs, u, p, path, path_size); -} - void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, const struct mg_http_serve_opts *opts) { char path[MG_PATH_MAX]; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; const char *sp = opts->ssi_pattern; - int flags = uri_to_path(c, hm, opts, path, sizeof(path)); + int flags = uri_to_file_status(c, hm, fs, opts->root_dir, path, sizeof(path)); if (flags < 0) { - // Do nothing: the response has already been sent by uri_to_path() + // Do nothing: the response has already been sent by uri_to_file_status() } else if (flags & MG_FS_DIR) { #if MG_ENABLE_DIRLIST listdir(c, hm, opts, path); #else mg_http_reply(c, 403, "", "Forbidden\n"); #endif + } else if (opts->allow_delete && + mg_strcasecmp(hm->method, mg_str("DELETE")) == 0) { + mg_http_reply(c, flags && fs->rm(path) ? 200 : 404, "", ""); + } else if (opts->allow_upload && + (mg_strcasecmp(hm->method, mg_str("POST")) == 0 || + mg_strcasecmp(hm->method, mg_str("PUT")) == 0)) { + // Small or already fully buffered body. mg_http_serve_upload() handles + // the same thing without buffering, for big uploads, from MG_EV_HTTP_HDRS + void *fd; + bool ok = false; + fs->rm(path); // MG_FS_WRITE appends, not truncates: drop any old file + if ((fd = fs->op(path, MG_FS_WRITE)) != NULL) { + ok = fs->wr(fd, hm->body.buf, hm->body.len) == hm->body.len; + fs->cl(fd); + } + mg_http_reply(c, ok ? 200 : 500, "", ""); } else if (flags && sp != NULL && mg_match(mg_str(path), mg_str(sp), NULL)) { mg_http_serve_ssi(c, opts->root_dir, path); } else { @@ -902,6 +842,52 @@ void mg_http_serve_dir(struct mg_connection *c, struct mg_http_message *hm, } } +// mg_http_stream_body() callback for mg_http_serve_upload(). The file is +// opened by the caller before streaming starts, so this only writes and closes +static bool serve_upload_cb(struct mg_http_message *hm, struct mg_str *data, + void **p) { + struct mg_fd *fd = (struct mg_fd *) *p; + if (hm != NULL) return fd != NULL; // Start + if (data != NULL) { // Next chunk + return fd != NULL && fd->fs->wr(fd->fd, data->buf, data->len) == data->len; + } + if (fd != NULL) mg_fs_close(fd); // End + *p = NULL; + return true; +} + +// Starts a streaming upload for a big POST/PUT body, so it never sits fully +// buffered in memory. Companion to mg_http_serve_dir(): call this from +// MG_EV_HTTP_HDRS, and keep calling mg_http_serve_dir() from MG_EV_HTTP_MSG +// as before - it handles uploads too, for bodies this function skips. +// Does nothing unless opts->allow_upload is set, the method is POST or PUT, +// and Content-Length is known and >= 2 * MG_IO_SIZE. +void mg_http_serve_upload(struct mg_connection *c, struct mg_http_message *hm, + const struct mg_http_serve_opts *opts) { + char path[MG_PATH_MAX]; + struct mg_fs *fs = opts->fs == NULL ? &mg_fs_posix : opts->fs; + if (!opts->allow_upload) { + // Not enabled, nothing to do + } else if (mg_strcasecmp(hm->method, mg_str("POST")) != 0 && + mg_strcasecmp(hm->method, mg_str("PUT")) != 0) { + // Not an upload request, nothing to do + } else if (hm->body.len == (size_t) ~0 || hm->body.len < 2 * MG_IO_SIZE) { + // Unknown length, or small enough for mg_http_serve_dir() to buffer it + } else if (!uri2path(opts->root_dir, hm->uri, path, sizeof(path))) { + MG_ERROR(("Invalid upload path: %.*s", (int) hm->uri.len, hm->uri.buf)); + } else if (fs->st(path, NULL, NULL) & MG_FS_DIR) { + // Target is a directory, let mg_http_serve_dir() list it + } else { + struct mg_fd *fd; + fs->rm(path); // MG_FS_WRITE appends, not truncates: drop any old file + if ((fd = mg_fs_open(fs, path, MG_FS_WRITE)) == NULL) { + MG_ERROR(("Cannot open %s for upload", path)); + } else if (!mg_http_stream_body(c, hm, serve_upload_cb, fd)) { + mg_fs_close(fd); + } + } +} + static bool mg_is_url_safe(int c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '.' || c == '_' || c == '-' || c == '~'; diff --git a/src/http.h b/src/http.h index 7fa1543e..aac3e99b 100644 --- a/src/http.h +++ b/src/http.h @@ -32,6 +32,8 @@ struct mg_http_serve_opts { const char *mime_types; // Additional MIME types: "ext1=type1,ext2=type2". NULL for defaults only const char *page404; // Path to a custom 404 page, e.g. "/404.html". NULL for built-in struct mg_fs *fs; // Filesystem to use. NULL defaults to POSIX + bool allow_delete; // Allow to DELETE files + bool allow_upload; // Allow to POST/PUT files }; // A single part of a multipart/form-data body, filled by mg_http_next_multipart(). @@ -106,7 +108,9 @@ struct mg_connection *mg_http_connect(struct mg_mgr *, const char *url, // Notes: // Call from an MG_EV_HTTP_MSG handler. The uri in hm is mapped under // opts->root_dir. Directory listing depends on MG_ENABLE_DIRLIST; SSI uses -// opts->ssi_pattern when configured. +// opts->ssi_pattern when configured. opts->allow_delete enables DELETE; +// opts->allow_upload enables POST/PUT, writing the whole body to a file - +// see mg_http_serve_upload() to also stream big uploads without buffering. void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm, const struct mg_http_serve_opts *); @@ -114,6 +118,15 @@ void mg_http_serve_dir(struct mg_connection *, struct mg_http_message *hm, void mg_http_serve_file(struct mg_connection *, struct mg_http_message *hm, const char *path, const struct mg_http_serve_opts *); +// Streams a big POST/PUT upload straight to a file, instead of buffering the +// whole body. Optional companion to mg_http_serve_dir(): call this from +// MG_EV_HTTP_HDRS with the same opts, and keep calling mg_http_serve_dir() +// from MG_EV_HTTP_MSG as usual - it still handles uploads this function skips. +// Does nothing unless opts->allow_upload is set, the method is POST or PUT, +// and Content-Length is known and big enough. +void mg_http_serve_upload(struct mg_connection *, struct mg_http_message *hm, + const struct mg_http_serve_opts *); + // Sends a complete HTTP response with Content-Length. // // Example: diff --git a/test/unit_test.c b/test/unit_test.c index c2db01b3..9e129a93 100644 --- a/test/unit_test.c +++ b/test/unit_test.c @@ -826,7 +826,16 @@ static void test_mqtt(void) { static void eh1(struct mg_connection *c, int ev, void *ev_data) { struct mg_tls_opts *topts = (struct mg_tls_opts *) c->fn_data; if (ev == MG_EV_ACCEPT && topts != NULL) mg_tls_init(c, topts); - if (ev == MG_EV_HTTP_MSG) { + if (ev == MG_EV_HTTP_HDRS && + mg_match(((struct mg_http_message *) ev_data)->uri, mg_str("/big.bin"), + NULL)) { + // mg_http_serve_upload: streams big uploads, called from MG_EV_HTTP_HDRS + struct mg_http_serve_opts sopts; + memset(&sopts, 0, sizeof(sopts)); + sopts.root_dir = "./data"; + sopts.allow_upload = true; + mg_http_serve_upload(c, (struct mg_http_message *) ev_data, &sopts); + } else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; MG_INFO(("[%.*s %.*s] message len %d", (int) hm->method.len, hm->method.buf, (int) hm->uri.len, hm->uri.buf, (int) hm->message.len)); @@ -874,6 +883,8 @@ static void eh1(struct mg_connection *c, int ev, void *ev_data) { sopts.root_dir = "./data"; sopts.ssi_pattern = "#.shtml"; sopts.extra_headers = "C: D\r\n"; + sopts.allow_delete = true; + sopts.allow_upload = true; mg_http_serve_dir(c, hm, &sopts); } } else if (ev == MG_EV_WS_OPEN) { @@ -1297,16 +1308,39 @@ ASSERT(system("touch 'dirtest/a.txt'") == 0); #endif ASSERT(fetch(&mgr, buf, url, "GET /dirtest/ HTTP/1.0\n\n") == 200); MG_DEBUG(("%s", buf)); - ASSERT(mgstrstr(mg_str(buf), mg_str(">Index of /dirtest/<")) != NULL); - ASSERT(mgstrstr(mg_str(buf), mg_str(">fuzz.c<")) != NULL); + ASSERT(mgstrstr(mg_str(buf), mg_str("fuzz.c")) != NULL); #if MG_ARCH == MG_ARCH_UNIX - ASSERT(mgstrstr(mg_str(buf), mg_str(">a<b&c>.txt<")) != NULL); + ASSERT(mgstrstr(mg_str(buf), mg_str("a.txt")) != NULL); if (system("rm 'dirtest/a.txt'") == 0) (void) 0; #endif ASSERT(cmpheader(buf, "A", "B")); ASSERT(!cmpheader(buf, "C", "D")); ASSERT(cmpheader(buf, "E", "F")); + { + // mg_http_serve_dir: allow_upload, small file first (buffered), then a + // big one (mg_http_serve_upload, streamed - see the MG_EV_HTTP_HDRS + // branch in eh1), then allow_delete + struct mg_str big = mg_file_read(&mg_fs_posix, "mongoose.c"); + struct mg_str got; + ASSERT(big.len > MG_IO_SIZE); + ASSERT(fetch(&mgr, buf, url, + "POST /del_me.txt HTTP/1.0\r\nContent-Length: 1\r\n\r\nx") == + 200); + ASSERT(fetch(&mgr, buf, url, "GET /del_me.txt HTTP/1.0\n\n") == 200); + ASSERT(fetch(&mgr, buf, url, + "POST /big.bin HTTP/1.0\r\nContent-Length: %d\r\n\r\n%.*s", + (int) big.len, (int) big.len, big.buf) == 200); + got = mg_file_read(&mg_fs_posix, "data/big.bin"); + ASSERT(got.len == big.len && memcmp(got.buf, big.buf, big.len) == 0); + mg_free((void *) got.buf); + mg_free((void *) big.buf); + remove("data/big.bin"); + ASSERT(fetch(&mgr, buf, url, "DELETE /del_me.txt HTTP/1.0\n\n") == 200); + ASSERT(fetch(&mgr, buf, url, "GET /del_me.txt HTTP/1.0\n\n") == 404); + ASSERT(fetch(&mgr, buf, url, "DELETE /del_me.txt HTTP/1.0\n\n") == 404); + } + { // Credentials struct mg_http_message hm;