From 1755ee663cd67cd8d9adf9f06dd82b0477adab76 Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Date: Fri, 17 Nov 2023 12:53:59 +0100 Subject: http: Add support for PUT Notes: - Since curl would use the "Expect: 100-continue" header field for PUT operations, this was a good operation to fix the existing issues in its implementation. Breaking changes: - expect_continue is no longer exclusive to struct http_post. Now, it has been moved into struct http_payload and it is up to users to check it. --- http.c | 262 ++++++++++++++++++++++++++++++++++++++++++-------- include/libweb/http.h | 10 +- 2 files changed, 232 insertions(+), 40 deletions(-) diff --git a/http.c b/http.c index 80086e1..38bb025 100644 --- a/http.c +++ b/http.c @@ -38,11 +38,11 @@ struct http_ctx char *resource, *field, *value, *boundary; size_t len; - struct post + struct payload { char *path; unsigned long long len, read; - } post; + } payload; union { @@ -85,11 +85,19 @@ struct http_ctx char *name, *filename, *tmpname, *value; } *forms; } mf; + + struct put + { + char *tmpname; + int fd; + off_t written; + } put; } u; struct http_arg *args; size_t n_args, n_headers; struct http_header *headers; + bool has_length, expect_continue; } ctx; struct write_ctx @@ -340,6 +348,27 @@ end: return ret; } +static int get_op(const char *const line, const size_t n, + enum http_op *const out) +{ + static const char *const ops[] = + { + [HTTP_OP_GET] = "GET", + [HTTP_OP_POST] = "POST", + [HTTP_OP_HEAD] = "HEAD", + [HTTP_OP_PUT] = "PUT" + }; + + for (enum http_op op = 0; op < sizeof ops / sizeof *ops; op++) + if (!strncmp(line, ops[op], n)) + { + *out = op; + return 0; + } + + return -1; +} + static int start_line(struct http_ctx *const h) { const char *const line = h->line; @@ -361,13 +390,7 @@ static int start_line(struct http_ctx *const h) struct ctx *const c = &h->ctx; const size_t n = op - line; - if (!strncmp(line, "GET", n)) - c->op = HTTP_OP_GET; - else if (!strncmp(line, "POST", n)) - c->op = HTTP_OP_POST; - else if (!strncmp(line, "HEAD", n)) - c->op = HTTP_OP_HEAD; - else + if (get_op(line, n, &c->op)) { fprintf(stderr, "%s: unsupported HTTP op %.*s\n", __func__, (int)n, line); @@ -467,6 +490,16 @@ static void ctx_free(struct ctx *const c) free(m->forms); } + else if (c->op == HTTP_OP_PUT) + { + struct put *const p = &c->u.put; + + if (p->fd >= 0 && close(p->fd)) + fprintf(stderr, "%s: close(2) p->fd: %s\n", + __func__, strerror(errno)); + + free(c->u.put.tmpname); + } free(c->field); free(c->value); @@ -859,7 +892,7 @@ static int set_length(struct http_ctx *const h, const char *const len) char *end; errno = 0; - h->ctx.post.len = strtoull(len, &end, 10); + const unsigned long long value = strtoull(len, &end, 10); if (errno || *end != '\0') { @@ -868,6 +901,26 @@ static int set_length(struct http_ctx *const h, const char *const len) return 1; } + struct ctx *const c = &h->ctx; + + switch (c->op) + { + case HTTP_OP_POST: + /* Fall through. */ + case HTTP_OP_PUT: + c->payload.len = value; + c->u.put = (const struct put){.fd = -1}; + break; + + case HTTP_OP_GET: + /* Fall through. */ + case HTTP_OP_HEAD: + fprintf(stderr, "%s: unexpected header for HTTP op %d\n", + __func__, c->op); + return 1; + } + + c->has_length = true; return 0; } @@ -962,7 +1015,7 @@ static struct http_payload ctx_to_payload(const struct ctx *const c) }; } -static int process_payload(struct http_ctx *const h, const char *const line) +static int process_payload(struct http_ctx *const h) { struct ctx *const c = &h->ctx; const struct http_payload p = ctx_to_payload(c); @@ -1008,25 +1061,28 @@ static int expect(struct http_ctx *const h, const char *const value) if (!strcmp(value, "100-continue")) { struct ctx *const c = &h->ctx; - const struct http_payload p = - { - .u.post.expect_continue = true, - .cookie = - { - .field = c->field, - .value = c->value - }, - .op = c->op, - .resource = c->resource - }; - - const int ret = h->cfg.payload(&p, &h->wctx.r, h->cfg.user); + if (!c->has_length) + { + fprintf(stderr, "%s: 100-continue without expected content\n", + __func__); + return 1; + } - if (ret) - return ret; + switch (c->op) + { + case HTTP_OP_POST: + /* Fall through. */ + case HTTP_OP_PUT: + c->expect_continue = true; + break; - return start_response(h); + case HTTP_OP_GET: + /* Fall through. */ + case HTTP_OP_HEAD: + fprintf(stderr, "%s: unexpected op %d\n", __func__, c->op); + return 1; + } } return 0; @@ -1133,7 +1189,39 @@ static int check_length(struct http_ctx *const h) .value = c->value }; - return h->cfg.length(c->post.len, &cookie, &h->wctx.r, h->cfg.user); + return h->cfg.length(c->payload.len, &cookie, &h->wctx.r, h->cfg.user); +} + +static int process_expect(struct http_ctx *const h) +{ + struct ctx *const c = &h->ctx; + const int res = check_length(h); + + if (res) + { + if (res < 0) + { + fprintf(stderr, "%s: check_length failed\n", __func__); + return res; + } + + h->wctx.close = true; + return start_response(h); + } + + struct http_payload p = ctx_to_payload(c); + + p.expect_continue = true; + + const int ret = h->cfg.payload(&p, &h->wctx.r, h->cfg.user); + + h->wctx.op = c->op; + + if (ret) + return ret; + + c->state = BODY_LINE; + return start_response(h); } static int header_cr_line(struct http_ctx *const h) @@ -1148,12 +1236,14 @@ static int header_cr_line(struct http_ctx *const h) case HTTP_OP_GET: /* Fall through. */ case HTTP_OP_HEAD: - return process_payload(h, line); + return process_payload(h); case HTTP_OP_POST: { - if (!c->post.len) - return process_payload(h, line); + if (!c->payload.len) + return process_payload(h); + else if (c->expect_continue) + return process_expect(h); else if (c->boundary) { const int res = check_length(h); @@ -1175,6 +1265,21 @@ static int header_cr_line(struct http_ctx *const h) c->state = BODY_LINE; return 0; } + + case HTTP_OP_PUT: + if (!c->has_length) + { + fprintf(stderr, "%s: expected Content-Length header\n", + __func__); + return 1; + } + else if (!c->payload.len) + return process_payload(h); + else if (c->expect_continue) + return process_expect(h); + + c->state = BODY_LINE; + return 0; } } @@ -1472,7 +1577,7 @@ static int process_mf_line(struct http_ctx *const h) [MF_END_BOUNDARY_CR_LINE] = end_boundary_line }; - h->ctx.post.read += strlen(h->line) + strlen("\r\n"); + h->ctx.payload.read += strlen(h->line) + strlen("\r\n"); return state[h->ctx.u.mf.state](h); } @@ -1525,7 +1630,7 @@ static int read_mf_body_to_mem(struct http_ctx *const h, const void *const buf, memcpy(&h->line[m->written], buf, n); m->written += n; m->len += n; - c->post.read += n; + c->payload.read += n; return 0; } @@ -1549,7 +1654,7 @@ static int read_mf_body_to_file(struct http_ctx *const h, const void *const buf, m->written += res; m->len += res; - c->post.read += res; + c->payload.read += res; return 0; } @@ -1826,7 +1931,7 @@ static int read_multiform(struct http_ctx *const h, bool *const close) { /* Note: the larger the buffer below, the less CPU load. */ char buf[sizeof h->line]; - struct post *const p = &h->ctx.post; + struct payload *const p = &h->ctx.payload; const unsigned long long left = p->len - p->read; const size_t rem = left > sizeof buf ? sizeof buf : left; const int r = h->cfg.read(buf, rem, h->cfg.user); @@ -1846,7 +1951,7 @@ static int read_body_to_mem(struct http_ctx *const h, bool *const close) return rw_error(r, close); struct ctx *const c = &h->ctx; - struct post *const p = &c->post; + struct payload *const p = &c->payload; if (p->read >= sizeof h->line - 1) { @@ -1881,10 +1986,91 @@ static int read_body_to_mem(struct http_ctx *const h, bool *const close) return 0; } +static int read_put_body_to_file(struct http_ctx *const h, + const void *const buf, const size_t n) +{ + struct ctx *const c = &h->ctx; + struct put *const put = &c->u.put; + ssize_t res; + + if (!put->tmpname && !(put->tmpname = get_tmp(h->cfg.tmpdir))) + { + fprintf(stderr, "%s: get_tmp failed\n", __func__); + return -1; + } + else if (put->fd < 0 && (put->fd = mkstemp(put->tmpname)) < 0) + { + fprintf(stderr, "%s: mkstemp(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if ((res = pwrite(put->fd, buf, n, c->payload.read)) < 0) + { + fprintf(stderr, "%s: pwrite(2): %s\n", __func__, strerror(errno)); + return -1; + } + + c->payload.read += res; + return 0; +} + +static int read_to_file(struct http_ctx *const h, bool *const close) +{ + char buf[BUFSIZ]; + struct ctx *const c = &h->ctx; + struct payload *const p = &c->payload; + const unsigned long long left = p->len - p->read; + const size_t rem = left > sizeof buf ? sizeof buf : left; + const int r = h->cfg.read(buf, rem, h->cfg.user); + + if (r <= 0) + return rw_error(r, close); + else if (read_put_body_to_file(h, buf, r)) + return -1; + else if (p->read >= p->len) + { + const struct http_payload pl = + { + .cookie = + { + .field = c->field, + .value = c->value + }, + + .op = c->op, + .resource = c->resource, + .u.put = + { + .tmpname = c->u.put.tmpname + } + }; + + return send_payload(h, &pl); + } + + return 0; +} + static int read_body(struct http_ctx *const h, bool *const close) { - return h->ctx.boundary ? read_multiform(h, close) - : read_body_to_mem(h, close); + const struct ctx *const c = &h->ctx; + + switch (c->op) + { + case HTTP_OP_POST: + return c->boundary ? read_multiform(h, close) + : read_body_to_mem(h, close); + + case HTTP_OP_PUT: + return read_to_file(h, close); + + case HTTP_OP_GET: + /* Fall through. */ + case HTTP_OP_HEAD: + break; + } + + fprintf(stderr, "%s: unexpected op %d\n", __func__, c->op); + return -1; } static int process_line(struct http_ctx *const h) diff --git a/include/libweb/http.h b/include/libweb/http.h index ddb6a89..7af66b5 100644 --- a/include/libweb/http.h +++ b/include/libweb/http.h @@ -16,7 +16,8 @@ struct http_payload { HTTP_OP_GET, HTTP_OP_POST, - HTTP_OP_HEAD + HTTP_OP_HEAD, + HTTP_OP_PUT } op; const char *resource; @@ -30,7 +31,6 @@ struct http_payload { struct http_post { - bool expect_continue; const char *data; size_t nfiles, npairs; @@ -44,6 +44,11 @@ struct http_payload const char *name, *tmpname, *filename; } *files; } post; + + struct http_put + { + const char *tmpname; + } put; } u; const struct http_arg @@ -53,6 +58,7 @@ struct http_payload size_t n_args, n_headers; const struct http_header *headers; + bool expect_continue; }; #define HTTP_STATUSES \ -- cgit v1.2.3 From 1750bbd7ec7277375c0ccd815cd2aed541e7e862 Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Date: Fri, 17 Nov 2023 14:56:07 +0100 Subject: http.c. Limit multipart/form-data to POST --- http.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/http.c b/http.c index 38bb025..2026a8e 100644 --- a/http.c +++ b/http.c @@ -940,6 +940,12 @@ static int set_content_type(struct http_ctx *const h, const char *const type) __func__, (int)n, type); return 1; } + else if (h->ctx.op != HTTP_OP_POST) + { + fprintf(stderr, "%s: multipart/form-data only expected for POST\n", + __func__); + return 1; + } const char *boundary = sep + 1; -- cgit v1.2.3 From dc8b14d99028b9235aa7d7633906a979aa08e4f9 Mon Sep 17 00:00:00 2001 From: Xavier Del Campo Date: Fri, 17 Nov 2023 16:34:45 +0100 Subject: Add PUT server example --- .gitignore | 1 + examples/CMakeLists.txt | 1 + examples/Makefile | 3 +- examples/put/CMakeLists.txt | 4 ++ examples/put/Makefile | 26 +++++++++++++ examples/put/README.md | 18 +++++++++ examples/put/main.c | 90 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 examples/put/CMakeLists.txt create mode 100644 examples/put/Makefile create mode 100644 examples/put/README.md create mode 100644 examples/put/main.c diff --git a/.gitignore b/.gitignore index e39347a..58d8edc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ *.so.* examples/hello/hello examples/html/html +examples/put/put diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7353458..4c2a6a2 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,3 +2,4 @@ cmake_minimum_required(VERSION 3.13) add_subdirectory(headers) add_subdirectory(hello) add_subdirectory(html) +add_subdirectory(put) diff --git a/examples/Makefile b/examples/Makefile index 672f209..6d99b4f 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -3,7 +3,8 @@ all: \ headers \ hello \ - html + html \ + put clean: +cd hello && $(MAKE) clean diff --git a/examples/put/CMakeLists.txt b/examples/put/CMakeLists.txt new file mode 100644 index 0000000..200ae28 --- /dev/null +++ b/examples/put/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.13) +project(put C) +add_executable(${PROJECT_NAME} main.c) +target_link_libraries(${PROJECT_NAME} PRIVATE web dynstr) diff --git a/examples/put/Makefile b/examples/put/Makefile new file mode 100644 index 0000000..972208b --- /dev/null +++ b/examples/put/Makefile @@ -0,0 +1,26 @@ +.POSIX: + +PROJECT = put +DEPS = \ + main.o +LIBWEB = ../../libweb.a +DYNSTR = ../../dynstr/libdynstr.a +CFLAGS = -I ../../include -I ../../dynstr/include -g +LIBWEB_FLAGS = -L ../../ -l web +DYNSTR_FLAGS = -L ../../dynstr -l dynstr + +all: $(PROJECT) + +clean: + rm -f $(DEPS) + +FORCE: + +$(PROJECT): $(DEPS) $(LIBWEB) $(DYNSTR) + $(CC) $(LDFLAGS) $(DEPS) $(LIBWEB_FLAGS) $(DYNSTR_FLAGS) -o $@ + +$(LIBWEB): FORCE + +cd ../../ && $(MAKE) + +$(DYNSTR): FORCE + +cd ../../dynstr && $(MAKE) diff --git a/examples/put/README.md b/examples/put/README.md new file mode 100644 index 0000000..afa9191 --- /dev/null +++ b/examples/put/README.md @@ -0,0 +1,18 @@ +# HTTP `PUT` example + +This example shows a minimal setup for an application using `libweb` that only +accepts `PUT` requests. When executed, it starts a HTTP/1.1 server on a random +port that shall be printed to standard output. When a `PUT` request is received, +`libweb` shall store the body into a file in the `/tmp` directory, and print the +path of the temporary file to the standard output. + +## How to build + +If using `make(1)`, just run `make` from this directory. + +If using CMake, examples are built by default when configuring the project +from [the top-level `CMakeLists.txt`](../../CMakeLists.txt). + +## How to run + +Run the executable without any command line arguments. diff --git a/examples/put/main.c b/examples/put/main.c new file mode 100644 index 0000000..cf38248 --- /dev/null +++ b/examples/put/main.c @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include +#include + +static int on_put(const struct http_payload *const pl, + struct http_response *const r, void *const user) +{ + if (pl->expect_continue) + { + *r = (const struct http_response) + { + .status = HTTP_STATUS_CONTINUE + }; + + return 0; + } + + printf("File uploaded to %s\n", pl->u.put.tmpname); + + *r = (const struct http_response) + { + .status = HTTP_STATUS_OK + }; + + return 0; +} + +static int on_length(const unsigned long long len, + const struct http_cookie *const c, struct http_response *const r, + void *const user) +{ + *r = (const struct http_response) + { + .status = HTTP_STATUS_OK + }; + + return 0; +} + +int main(int argc, char *argv[]) +{ + int ret = EXIT_FAILURE; + const struct handler_cfg cfg = + { + .tmpdir = "/tmp", + .length = on_length + }; + + struct handler *const h = handler_alloc(&cfg); + static const char *const urls[] = {"/*"}; + + if (!h) + { + fprintf(stderr, "%s: handler_alloc failed\n", __func__); + goto end; + } + + for (size_t i = 0; i < sizeof urls / sizeof *urls; i++) + if (handler_add(h, urls[i], HTTP_OP_PUT, on_put, NULL)) + { + fprintf(stderr, "%s: handler_add failed\n", __func__); + goto end; + } + + unsigned short outport; + + if (handler_listen(h, 0, &outport)) + { + fprintf(stderr, "%s: handler_listen failed\n", __func__); + goto end; + } + + printf("Listening on port %hu\n", outport); + + if (handler_loop(h)) + { + fprintf(stderr, "%s: handler_loop failed\n", __func__); + goto end; + } + + ret = EXIT_SUCCESS; + +end: + handler_free(h); + return ret; +} -- cgit v1.2.3