diff options
| author | Xavier Del Campo Romero <xavi.dcr@tutanota.com> | 2023-01-09 01:22:54 +0100 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi.dcr@tutanota.com> | 2023-07-20 23:52:47 +0200 |
| commit | 2968c5f67daa1c571f5f9cf9445de907f9490636 (patch) | |
| tree | d6919b2446976f4818c62ad419065e3d92061e25 /http.c | |
| download | libweb-2968c5f67daa1c571f5f9cf9445de907f9490636.tar.gz | |
Initial commit
Diffstat (limited to 'http.c')
| -rw-r--r-- | http.c | 1612 |
1 files changed, 1612 insertions, 0 deletions
@@ -0,0 +1,1612 @@ +#include "http.h" +#include <dynstr.h> +#include <sys/types.h> +#include <unistd.h> +#include <ctype.h> +#include <errno.h> +#include <inttypes.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + +struct http_ctx +{ + struct ctx + { + enum state + { + START_LINE, + HEADER_CR_LINE, + BODY_LINE + } state; + + enum + { + LINE_CR, + LINE_LF + } lstate; + + enum http_op op; + char *resource, *field, *value, *boundary; + size_t len; + + struct post + { + char *path; + unsigned long long len, read; + } post; + + union + { + struct start_line + { + enum + { + START_LINE_OP, + START_LINE_RESOURCE, + START_LINE_PROTOCOL + } state; + } sl; + + struct multiform + { + enum mf_state + { + MF_START_BOUNDARY, + MF_HEADER_CR_LINE, + MF_BODY_BOUNDARY_LINE, + MF_END_BOUNDARY_CR_LINE + } state; + + enum + { + BODY_CR, + BODY_LF, + BODY_DATA + } bstate; + + off_t len, written; + char *boundary; + const char *dir; + size_t blen, nforms, nfiles; + int fd; + struct http_post_file *files; + + struct form + { + char *name, *filename, *tmpname, *value; + } *forms; + } mf; + } u; + } ctx; + + struct write_ctx + { + bool pending; + enum state state; + struct http_response r; + off_t n; + struct dynstr d; + } wctx; + + enum version + { + HTTP_1_0, + HTTP_1_1 + } version; + + /* From RFC9112, section 3 (Request line): + * It is RECOMMENDED that all HTTP senders and recipients support, + * at a minimum, request-line lengths of 8000 octets. */ + char line[8000]; + struct http_cfg cfg; +}; + +static const char *const versions[] = +{ + [HTTP_1_0] = "HTTP/1.0", + [HTTP_1_1] = "HTTP/1.1" +}; + +static int get_version(struct http_ctx *const h, const char *const v) +{ + for (enum version i = 0; i < sizeof versions / sizeof *versions; i++) + if (!strcmp(versions[i], v)) + { + h->version = i; + return 0; + } + + return -1; +} + +static int start_line(struct http_ctx *const h) +{ + const char *const line = (const char *)h->line; + + if (!*line) + { + fprintf(stderr, "%s: expected non-empty line\n", __func__); + return 1; + } + + const char *const op = strchr(line, ' '); + + if (!op || op == line) + { + fprintf(stderr, "%s: expected resource\n", __func__); + return 1; + } + + 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 + { + fprintf(stderr, "%s: unsupported HTTP op %.*s\n", + __func__, (int)n, line); + return 1; + } + + const char *const resource = op + 1, + *const res_end = strchr(resource, ' '); + + if (!res_end) + { + fprintf(stderr, "%s: expected protocol version\n", __func__); + return 1; + } + + const size_t res_n = res_end - resource; + + if (memchr(resource, '*', res_n)) + { + fprintf(stderr, "%s: illegal character * in resource %.*s\n", + __func__, (int)res_n, resource); + return 1; + } + + const char *const protocol = res_end + 1; + + if (!*protocol) + { + fprintf(stderr, "%s: expected protocol version\n", __func__); + return 1; + } + else if (strchr(protocol, ' ')) + { + fprintf(stderr, "%s: unexpected field after protocol version\n", + __func__); + return 1; + } + + int ret = 1; + char *enc_res = NULL; + + if (get_version(h, protocol)) + { + fprintf(stderr, "%s: unsupported protocol %s\n", __func__, protocol); + goto end; + } + else if (!(enc_res = strndup(resource, res_n))) + { + fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); + ret = -1; + goto end; + } + else if (!(c->resource = http_decode_url(enc_res))) + { + fprintf(stderr, "%s: http_decode_url failed\n", __func__); + goto end; + } + + printf("%.*s %s %s\n", (int)n, line, c->resource, protocol); + ret = 0; + c->state = HEADER_CR_LINE; + +end: + free(enc_res); + return ret; +} + +static void ctx_free(struct ctx *const c) +{ + if (c->boundary) + { + struct multiform *const m = &c->u.mf; + + free(m->files); + free(m->boundary); + + for (size_t i = 0; i < m->nforms; i++) + { + struct form *const f = &m->forms[i]; + + free(f->name); + free(f->filename); + free(f->tmpname); + free(f->value); + } + + free(m->forms); + + if (m->fd >= 0 && close(m->fd)) + fprintf(stderr, "%s: close(2) m->fd: %s\n", + __func__, strerror(errno)); + } + + free(c->field); + free(c->value); + free(c->resource); + free(c->boundary); + + *c = (const struct ctx){0}; +} + +static int prepare_headers(struct http_ctx *const h) +{ + struct write_ctx *const w = &h->wctx; + struct dynstr *const d = &w->d; + + dynstr_init(d); + + for (size_t i = 0; i < w->r.n_headers; i++) + { + const struct http_header *const hdr = &w->r.headers[i]; + + dynstr_append_or_ret_nonzero(d, "%s: %s\r\n", hdr->header, hdr->value); + free(hdr->header); + free(hdr->value); + } + + free(w->r.headers); + dynstr_append_or_ret_nonzero(d, "\r\n"); + return 0; +} + +static int rw_error(const int r, bool *const close) +{ + if (r < 0) + { + switch (errno) + { + case ECONNRESET: + *close = true; + return 1; + + case EAGAIN: + return 0; + + default: + break; + } + + fprintf(stderr, "%s: %s\n", __func__, strerror(errno)); + return -1; + } + else if (!r) + { + *close = true; + return 0; + } + + fprintf(stderr, "%s: unexpected value %d\n", __func__, r); + return -1; +} + +static int write_start_line(struct http_ctx *const h, bool *const close) +{ + struct write_ctx *const w = &h->wctx; + struct dynstr *const d = &w->d; + const size_t rem = d->len - w->n; + const int res = h->cfg.write(d->str + w->n, rem, + h->cfg.user); + + if (res <= 0) + return rw_error(res, close); + else if ((w->n += res) >= d->len) + { + char len[sizeof "18446744073709551615"]; + const int res = snprintf(len, sizeof len, "%llu", w->r.n); + + dynstr_free(d); + + if (res < 0 || res >= sizeof len) + { + fprintf(stderr, "%s: snprintf(3) failed\n", __func__); + return -1; + } + else if (http_response_add_header(&w->r, "Content-Length", len)) + { + fprintf(stderr, "%s: http_response_add_header failed\n", __func__); + return -1; + } + else if (prepare_headers(h)) + { + fprintf(stderr, "%s: prepare_headers failed\n", __func__); + return -1; + } + + w->state = HEADER_CR_LINE; + w->n = 0; + } + + return 0; +} + +static int write_ctx_free(struct write_ctx *const w) +{ + int ret = 0; + const struct http_response *const r = &w->r; + + if (r->free) + r->free(r->buf.rw); + + if (r->f && (ret = fclose(r->f))) + fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); + + dynstr_free(&w->d); + *w = (const struct write_ctx){0}; + return ret; +} + +static int write_header_cr_line(struct http_ctx *const h, bool *const close) +{ + struct write_ctx *const w = &h->wctx; + struct dynstr *const d = &w->d; + const size_t rem = d->len - w->n; + const int res = h->cfg.write(d->str + w->n, rem, h->cfg.user); + + if (res <= 0) + return rw_error(res, close); + else if ((w->n += res) >= d->len) + { + dynstr_free(d); + + if (w->r.n) + { + w->state = BODY_LINE; + w->n = 0; + } + else if (write_ctx_free(w)) + { + fprintf(stderr, "%s: write_ctx_free failed\n", __func__); + return -1; + } + } + + return 0; +} + +static int write_body_mem(struct http_ctx *const h, bool *const close) +{ + struct write_ctx *const w = &h->wctx; + const struct http_response *const r = &w->r; + const size_t rem = r->n - w->n; + const int res = h->cfg.write((const char *)r->buf.ro + w->n, rem, + h->cfg.user); + + if (res <= 0) + return rw_error(res, close); + else if ((w->n += res) >= r->n) + { + if (h->version == HTTP_1_0) + *close = true; + + return write_ctx_free(w); + } + + return 0; +} + +static int write_body_file(struct http_ctx *const h, bool *const close) +{ + struct write_ctx *const w = &h->wctx; + const struct http_response *const r = &w->r; + const unsigned long long left = r->n - w->n; + char buf[1024]; + const size_t rem = left > sizeof buf ? sizeof buf : left; + + if (!fread(buf, 1, rem, r->f)) + { + fprintf(stderr, "%s: fread(3) failed, ferror=%d, feof=%d\n", + __func__, ferror(r->f), feof(r->f)); + return -1; + } + + const int res = h->cfg.write(buf, rem, h->cfg.user); + + if (res <= 0) + return rw_error(res, close); + else if ((w->n += res) >= r->n) + { + if (h->version == HTTP_1_0) + *close = true; + + return write_ctx_free(w); + } + + return 0; +} + +static int write_body_line(struct http_ctx *const h, bool *const close) +{ + const struct http_response *const r = &h->wctx.r; + + if (r->buf.ro) + return write_body_mem(h, close); + else if (r->f) + return write_body_file(h, close); + + fprintf(stderr, "%s: expected either buffer or file path\n", __func__); + return -1; +} + +static int http_write(struct http_ctx *const h, bool *const close) +{ + static int (*const fn[])(struct http_ctx *, bool *) = + { + [START_LINE] = write_start_line, + [HEADER_CR_LINE] = write_header_cr_line, + [BODY_LINE] = write_body_line, + }; + + struct write_ctx *const w = &h->wctx; + + const int ret = fn[w->state](h, close); + + if (ret) + write_ctx_free(w); + + return ret; +} + +int http_response_add_header(struct http_response *const r, + const char *const header, const char *const value) +{ + if (!(r->headers = realloc(r->headers, + sizeof (r->n_headers + 1) *sizeof *r->headers))) + { + fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); + return -1; + } + + struct http_header *const h = &r->headers[r->n_headers++]; + + *h = (const struct http_header) + { + .header = strdup(header), + .value = strdup(value) + }; + + if (!h->header || !h->value) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + free(h->header); + free(h->value); + free(h); + return -1; + } + + return 0; +} + +static int start_response(struct http_ctx *const h) +{ + static const struct code + { + const char *descr; + int code; + } codes[] = + { +#define X(x, y, z) [HTTP_STATUS_##x] = {.descr = y, .code = z}, + HTTP_STATUSES +#undef X + }; + + struct write_ctx *const w = &h->wctx; + const struct code *const c = &codes[w->r.status]; + + w->pending = true; + dynstr_init(&w->d); + dynstr_append_or_ret_nonzero(&w->d, "%s %d %s\r\n", + versions[h->version], c->code, c->descr); + return 0; +} + +static int set_cookie(struct http_ctx *const h, const char *const cookie) +{ + struct ctx *const c = &h->ctx; + const char *const value = strchr(cookie, '='); + + if (!value) + { + fprintf(stderr, "%s: expected field=value for cookie %s\n", + __func__, cookie); + goto failure; + } + else if (!*(value + 1)) + { + fprintf(stderr, "%s: expected non-empty value for cookie %s\n", + __func__, cookie); + goto failure; + } + else if (!(c->field = strndup(cookie, value - cookie))) + { + fprintf(stderr, "%s: malloc(3) field: %s\n", __func__, strerror(errno)); + goto failure; + } + else if (!(c->value = strdup(value + 1))) + { + fprintf(stderr, "%s: malloc(3) value: %s\n", __func__, strerror(errno)); + goto failure; + } + + return 0; + +failure: + free(c->field); + free(c->value); + return -1; +} + +static int set_length(struct http_ctx *const h, const char *const len) +{ + h->ctx.post.len = strtoull(len, NULL, 10); + return 0; +} + +static int set_content_type(struct http_ctx *const h, const char *const type) +{ + const char *const sep = strchr(type, ';'); + + if (!sep) + /* No multipart/form-data expected. */ + return 0; + + const size_t n = sep - type; + + if (strncmp(type, "multipart/form-data", n)) + { + fprintf(stderr, "%s: unsupported Content-Type %.*s\n", + __func__, (int)n, type); + return 1; + } + + const char *boundary = sep + 1; + + while (*boundary == ' ') + boundary++; + + if (!*boundary) + { + fprintf(stderr, "%s: expected boundary\n", __func__); + return 1; + } + + const char *const eq = strchr(boundary, '='); + + if (!eq) + { + fprintf(stderr, "%s: expected = after boundary\n", __func__); + return 1; + } + + const size_t bn = eq - boundary; + + if (strncmp(boundary, "boundary", bn)) + { + fprintf(stderr, "%s: expected boundary, got %.*s\n", + __func__, (int)bn, boundary); + return 1; + } + + const char *val = eq + 1; + + while (*val == ' ') + val++; + + if (!*val) + { + fprintf(stderr, "%s: expected value after boundary\n", __func__); + return 1; + } + + struct ctx *const c = &h->ctx; + struct dynstr b; + + dynstr_init(&b); + + if (dynstr_append(&b, "\r\n--%s", val)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + return -1; + } + + c->boundary = b.str; + c->u.mf = (const struct multiform){.fd = -1}; + return 0; +} + +static int payload_get(struct http_ctx *const h, const char *const line) +{ + struct ctx *const c = &h->ctx; + const struct http_payload p = + { + .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); + + ctx_free(c); + + if (ret) + return ret; + + return start_response(h); +} + +static int payload_post(struct http_ctx *const h, const char *const line) +{ + struct ctx *const c = &h->ctx; + const struct http_payload pl = + { + .cookie = + { + .field = c->field, + .value = c->value + }, + + .op = c->op, + .resource = c->resource + }; + + const int ret = h->cfg.payload(&pl, &h->wctx.r, h->cfg.user); + + ctx_free(c); + + if (ret) + return ret; + + return start_response(h); +} + +static int get_field_value(const char *const line, size_t *const n, + const char **const value) +{ + const char *const field = strchr(line, ':'); + + if (!field || line == field) + { + fprintf(stderr, "%s: expected field:value\n", __func__); + return 1; + } + + *n = field - line; + *value = field + 1; + + if (!**value) + { + fprintf(stderr, "%s: expected value\n", __func__); + return 1; + } + + while (**value == ' ') + (*value)++; + + return 0; +} + +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 (ret) + return ret; + + return start_response(h); + } + + return 0; +} + +static int process_header(struct http_ctx *const h, const char *const line, + const size_t n, const char *const value) +{ + static const struct header + { + const char *header; + int (*f)(struct http_ctx *, const char *); + } headers[] = + { + { + .header = "Cookie", + .f = set_cookie + }, + + { + .header = "Content-Length", + .f = set_length + }, + + { + .header = "Expect", + .f = expect + }, + + { + .header = "Content-Type", + .f = set_content_type + } + }; + + for (size_t i = 0; i < sizeof headers / sizeof *headers; i++) + { + const struct header *const hdr = &headers[i]; + int ret; + + if (!strncmp(line, hdr->header, n) && (ret = hdr->f(h, value))) + return ret; + } + + return 0; +} + +static int header_cr_line(struct http_ctx *const h) +{ + const char *const line = (const char *)h->line; + struct ctx *const c = &h->ctx; + + if (!*line) + { + switch (c->op) + { + case HTTP_OP_GET: + return payload_get(h, line); + + case HTTP_OP_POST: + if (!c->post.len) + return payload_post(h, line); + + c->state = BODY_LINE; + return 0; + } + } + + const char *value; + size_t n; + const int ret = get_field_value(line, &n, &value); + + if (ret) + return ret; + + return process_header(h, line, n, value); +} + +static int send_payload(struct http_ctx *const h, + const struct http_payload *const p) +{ + struct ctx *const c = &h->ctx; + const int ret = h->cfg.payload(p, &h->wctx.r, h->cfg.user); + + ctx_free(c); + + if (ret) + return ret; + + return start_response(h); +} + +static int update_lstate(struct http_ctx *const h, bool *const close, + int (*const f)(struct http_ctx *), const char b) +{ + int ret = 1; + struct ctx *const c = &h->ctx; + + switch (c->lstate) + { + case LINE_CR: + if (b == '\r') + c->lstate = LINE_LF; + else if (c->len < sizeof h->line - 2) + h->line[c->len++] = b; + else + { + fprintf(stderr, "%s: line too long\n", __func__); + goto failure; + } + + break; + + case LINE_LF: + if (b == '\n') + { + h->line[c->len] = '\0'; + + if ((ret = f(h))) + goto failure; + + c->len = 0; + } + else if (c->len < sizeof h->line - 3) + { + h->line[c->len++] = '\r'; + h->line[c->len++] = b; + } + else + { + fprintf(stderr, "%s: line too long\n", __func__); + goto failure; + } + + c->lstate = LINE_CR; + break; + } + + return 0; + +failure: + ctx_free(c); + return ret; +} + +static int start_boundary_line(struct http_ctx *const h) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + const char *const line = h->line; + + if (strcmp(line, c->boundary + strlen("\r\n"))) + { + fprintf(stderr, "%s: expected boundary %s, got %s\n", + __func__, c->boundary, line); + return 1; + } + + m->state = MF_HEADER_CR_LINE; + m->len = strlen(line) + strlen("\r\n"); + return 0; +} + +static int cd_fields(struct http_ctx *const h, struct form *const f, + const char *sep) +{ + do + { + while (*++sep == ' ') + ; + + if (!*sep) + break; + + const char *const op = strchr(sep, '='); + + if (!op) + { + fprintf(stderr, "%s: expected attr=value\n", __func__); + return 1; + } + + const char *const value = op + 1, *const end = strchr(value, ';'); + const size_t vlen = end ? end - value : strlen(value); + + if (*value != '\"' || value[vlen - 1] != '\"') + { + fprintf(stderr, "%s: expected \"-enclosed value, got %.*s\n", + __func__, (int)vlen, value); + return 1; + } + + const char *const evalue = value + 1; + const size_t evlen = vlen - 2; + + if (!strncmp(sep, "name", op - sep)) + { + if (!evlen) + { + fprintf(stderr, "%s: expected non-empty name\n", __func__); + return 1; + } + else if (!(f->name = strndup(evalue, evlen))) + { + fprintf(stderr, "%s: strndup(3): %s\n", + __func__, strerror(errno)); + return -1; + } + } + else if (!strncmp(sep, "filename", op - sep)) + { + if (!evlen) + { + fprintf(stderr, "%s: expected non-empty filename\n", __func__); + return 1; + } + else if (!(f->filename = strndup(evalue, evlen))) + { + fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); + return -1; + } + } + } while ((sep = strchr(sep, ';'))); + + if (!f->name) + { + fprintf(stderr, "%s: expected name\n", __func__); + return 1; + } + + return 0; +} + +static int set_content_disposition(struct http_ctx *const h, + const char *const c) +{ + const char *const sep = strchr(c, ';'); + struct multiform *const m = &h->ctx.u.mf; + + if (!sep) + { + fprintf(stderr, "%s: no fields found\n", __func__); + return 1; + } + else if (strncmp(c, "form-data", sep - c)) + { + fprintf(stderr, "%s: expected form-data, got %.*s\n", + __func__, (int)(sep - c), c); + return 1; + } + else if (!(m->forms = realloc(m->forms, + (m->nforms + 1) * sizeof *m->forms))) + { + fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); + return -1; + } + + struct form *const f = &m->forms[m->nforms++]; + + *f = (const struct form){0}; + return cd_fields(h, f, sep); +} + +static int mf_header_cr_line(struct http_ctx *const h) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + const char *const line = h->line; + + m->len += strlen(line) + strlen("\r\n"); + + if (!*line) + { + const size_t n = strlen("\r\n") + strlen(c->boundary) + 1; + + if (!m->boundary && !(m->boundary = calloc(1, n))) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + return -1; + } + + m->state = MF_BODY_BOUNDARY_LINE; + return 0; + } + + const char *value; + size_t n; + int ret = get_field_value(line, &n, &value); + + if (ret) + return ret; + else if (!strncmp(line, "Content-Disposition", n) + && (ret = set_content_disposition(h, value))) + return ret; + + return 0; +} + +static int end_boundary_line(struct http_ctx *const h) +{ + const char *const line = h->line; + + if (!*line) + { + h->ctx.u.mf.state = MF_HEADER_CR_LINE; + return 0; + } + else if (!strcmp(line, "--")) + { + /* Found end boundary. */ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + + const struct http_payload p = + { + .cookie = + { + .field = c->field, + .value = c->value + }, + + .op = c->op, + .resource = c->resource, + .u.post = + { + .dir = m->dir, + .files = m->files, + .n = m->nfiles + } + }; + + return send_payload(h, &p); + } + + fprintf(stderr, "%s: unexpected line after boundary: %s\n", + __func__, line); + return 1; +} + +static int process_mf_line(struct http_ctx *const h) +{ + static int (*const state[])(struct http_ctx *) = + { + [MF_START_BOUNDARY] = start_boundary_line, + [MF_HEADER_CR_LINE] = mf_header_cr_line, + [MF_END_BOUNDARY_CR_LINE] = end_boundary_line + }; + + h->ctx.post.read += strlen(h->line) + strlen("\r\n"); + return state[h->ctx.u.mf.state](h); +} + +static char *get_tmp(const char *const tmpdir) +{ + struct dynstr d; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/tmp.XXXXXX", tmpdir)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + return NULL; + } + + return d.str; +} + +static int generate_mf_file(struct http_ctx *const h) +{ + struct multiform *const m = &h->ctx.u.mf; + struct form *const f = &m->forms[m->nforms - 1]; + + if (!(f->tmpname = get_tmp(h->cfg.tmpdir))) + { + fprintf(stderr, "%s: get_tmp failed\n", __func__); + return -1; + } + else if ((m->fd = mkstemp(f->tmpname)) < 0) + { + fprintf(stderr, "%s: mkstemp(3): %s\n", __func__, strerror(errno)); + return -1; + } + + return 0; +} + +static int read_mf_body_to_mem(struct http_ctx *const h, const void *const buf, + const size_t n) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + + if (m->written + n > sizeof h->line) + { + fprintf(stderr, "%s: maximum length exceeded\n", __func__); + return 1; + } + + memcpy(&h->line[m->written], buf, n); + m->written += n; + m->len += n; + c->post.read += n; + return 0; +} + +static int read_mf_body_to_file(struct http_ctx *const h, const void *const buf, + const size_t n) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + ssize_t res; + + if (m->fd < 0 && generate_mf_file(h)) + { + fprintf(stderr, "%s: generate_mf_file failed\n", __func__); + return -1; + } + else if ((res = pwrite(m->fd, buf, n, m->written)) < 0) + { + fprintf(stderr, "%s: pwrite(2): %s\n", __func__, strerror(errno)); + return -1; + } + + m->written += res; + m->len += res; + c->post.read += res; + return 0; +} + +static int reset_boundary(struct http_ctx *const h, const void *const buf, + const size_t n) +{ + struct multiform *const m = &h->ctx.u.mf; + struct form *const f = &m->forms[m->nforms - 1]; + const size_t len = strlen(m->boundary); + int (*const read_mf)(struct http_ctx *, const void *, size_t) = + f->filename ? read_mf_body_to_file : read_mf_body_to_mem; + const int res = read_mf(h, m->boundary, len); + + if (res) + return res; + + memset(m->boundary, '\0', len); + m->blen = 0; + return read_mf(h, buf, n); +} + +static int apply_from_file(struct http_ctx *const h, struct form *const f) +{ + struct multiform *const m = &h->ctx.u.mf; + + if (close(m->fd)) + { + fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); + return -1; + } + + m->fd = -1; + + if (!(m->files = realloc(m->files, (m->nfiles + 1) * sizeof *m->files))) + { + fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); + return -1; + } + + struct http_post_file *const pf = &m->files[m->nfiles++]; + + *pf = (const struct http_post_file) + { + .tmpname = f->tmpname, + .filename = f->filename + }; + + return 0; +} + +static int apply_from_mem(struct http_ctx *const h, struct form *const f) +{ + struct multiform *const m = &h->ctx.u.mf; + + if (!(f->value = strndup(h->line, m->written))) + { + fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); + return -1; + } + else if (!strcmp(f->name, "dir")) + { + if (m->dir) + { + fprintf(stderr, "%s: \"dir\" defined more than once\n", __func__); + return 1; + } + + m->dir = f->value; + } + + return 0; +} + +static int read_mf_body_boundary_byte(struct http_ctx *const h, const char b, + const size_t len) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + + if (b == c->boundary[m->blen]) + { + m->boundary[len] = b; + + if (++m->blen >= strlen(c->boundary)) + { + /* Found intermediate boundary. */ + struct form *const f = &m->forms[m->nforms - 1]; + const int ret = f->filename ? apply_from_file(h, f) + : apply_from_mem(h, f); + + memset(m->boundary, '\0', len + 1); + m->blen = 0; + m->state = MF_END_BOUNDARY_CR_LINE; + m->written = 0; + return ret; + } + } + + return 0; +} + +/* Similar to memmem(3), provided here to avoid the use of GNU extensions. */ +static const char *http_memmem(const char *const a, const void *const b, + const size_t n) +{ + const size_t len = strlen(a); + + if (len > n) + return NULL; + + const char *s = a, *st = NULL; + + for (size_t i = 0; i < n; i++) + { + const char *bc = b; + const char c = bc[i]; + + if (*s == c) + { + if (!st) + st = &bc[i]; + + if (!*++s) + return st; + } + else if (*(s = a) != c) + st = NULL; + else + { + st = &bc[i]; + + if (!*++s) + return st; + } + } + + return NULL; +} + +static int read_mf_body_boundary(struct http_ctx *const h, + const char **const buf, size_t *const n) +{ + struct ctx *const c = &h->ctx; + struct multiform *const m = &c->u.mf; + const char *const boundary = http_memmem(&c->boundary[m->blen], *buf, *n); + int res; + + if (!boundary) + { + if ((res = reset_boundary(h, *buf, *n))) + return res; + + *n = 0; + return 0; + } + + const size_t prev = boundary - *buf; + + if ((res = reset_boundary(h, *buf, prev))) + return res; + + *buf += prev; + *n -= prev; + + const size_t len = strlen(m->boundary), + rem = strlen(c->boundary) - len, + r = rem > *n ? *n : rem; + + for (size_t i = 0; i < r; i++) + { + const char *const b = *buf; + + if ((res = read_mf_body_boundary_byte(h, b[i], len))) + return res; + } + + *buf += r; + *n -= r; + return 0; +} + +static int read_multiform_n(struct http_ctx *const h, bool *const close, + const char *buf, size_t n) +{ + struct multiform *const m = &h->ctx.u.mf; + + while (n) + { + int res; + + switch (m->state) + { + case MF_START_BOUNDARY: + /* Fall through. */ + case MF_HEADER_CR_LINE: + /* Fall through. */ + case MF_END_BOUNDARY_CR_LINE: + { + if ((res = update_lstate(h, close, process_mf_line, *buf))) + return res; + + buf++; + n--; + } + + break; + + case MF_BODY_BOUNDARY_LINE: + if ((res = read_mf_body_boundary(h, &buf, &n))) + return res; + } + } + + return 0; +} + +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; + 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); + + return read_multiform_n(h, close, buf, r); +} + +static int read_body_to_mem(struct http_ctx *const h, bool *const close) +{ + char b; + const int r = h->cfg.read(&b, sizeof b, h->cfg.user); + + if (r <= 0) + return rw_error(r, close); + + struct ctx *const c = &h->ctx; + struct post *const p = &c->post; + + if (p->read >= sizeof h->line) + { + fprintf(stderr, "%s: exceeded maximum length\n", __func__); + return 1; + } + + h->line[p->read++] = b; + + if (p->read >= p->len) + { + const struct http_payload pl = + { + .cookie = + { + .field = c->field, + .value = c->value + }, + + .op = c->op, + .resource = c->resource, + .u.post = + { + .data = h->line, + .n = p->len + } + }; + + 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); +} + +static int process_line(struct http_ctx *const h) +{ + static int (*const state[])(struct http_ctx *) = + { + [START_LINE] = start_line, + [HEADER_CR_LINE] = header_cr_line + }; + + return state[h->ctx.state](h); +} + +static int http_read(struct http_ctx *const h, bool *const close) +{ + switch (h->ctx.state) + { + case START_LINE: + /* Fall through. */ + case HEADER_CR_LINE: + { + char b; + const int r = h->cfg.read(&b, sizeof b, h->cfg.user); + + if (r <= 0) + return rw_error(r, close); + + return update_lstate(h, close, process_line, b); + } + + case BODY_LINE: + return read_body(h, close); + } + + fprintf(stderr, "%s: unexpected state %d\n", __func__, h->ctx.state); + return -1; +} + +char *http_cookie_create(const char *const key, const char *const value) +{ + struct dynstr d; + + dynstr_init(&d); + dynstr_append_or_ret_null(&d, "%s=%s; HttpOnly", key, value); + return d.str; +} + +int http_update(struct http_ctx *const h, bool *const write, bool *const close) +{ + *close = false; + + struct write_ctx *const w = &h->wctx; + const int ret = w->pending ? http_write(h, close) : http_read(h, close); + + *write = w->pending; + return ret; +} + +void http_free(struct http_ctx *const h) +{ + if (h) + { + ctx_free(&h->ctx); + write_ctx_free(&h->wctx); + } + + free(h); +} + +struct http_ctx *http_alloc(const struct http_cfg *const cfg) +{ + struct http_ctx *const h = malloc(sizeof *h); + + if (!h) + { + fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); + goto failure; + } + + *h = (const struct http_ctx) + { + .cfg = *cfg + }; + + return h; + +failure: + http_free(h); + return NULL; +} + +char *http_encode_url(const char *url) +{ + struct dynstr d; + char c; + + dynstr_init(&d); + + while ((c = *url++)) + { + /* Unreserved characters must not be percent-encoded. */ + if ((c >= 'A' && c <= 'Z') + || (c >= 'a' && c <= 'z') + || (c == '-') + || (c == '_') + || (c == '.') + || (c == '/') + || (c == '~')) + { + if (dynstr_append(&d, "%c", c)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto failure; + } + } + else if (dynstr_append(&d, "%%%hhx", c)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto failure; + } + } + + return d.str; + +failure: + dynstr_free(&d); + return NULL; +} + +char *http_decode_url(const char *url) +{ + char *ret = NULL; + size_t n = 0; + + while (*url) + { + if (!(ret = realloc(ret, n + 1))) + { + fprintf(stderr, "%s: realloc(3) loop: %s\n", + __func__, strerror(errno)); + goto failure; + } + else if (*url != '%') + ret[n++] = *url++; + else if (*(url + 1) && *(url + 2)) + { + const char buf[sizeof "00"] = {*(url + 1), *(url + 2)}; + + ret[n++] = strtoul(buf, NULL, 16); + url += 3; + } + else + { + fprintf(stderr, "%s: unterminated %%\n", __func__); + goto failure; + } + } + + if (!(ret = realloc(ret, n + 1))) + { + fprintf(stderr, "%s: realloc(3) end: %s\n", __func__, strerror(errno)); + goto failure; + } + + ret[n] = '\0'; + return ret; + +failure: + free(ret); + return NULL; +} |
