#define _POSIX_C_SOURCE 200809L #include "libweb/http.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define HTTP_VERSION "HTTP/1.1" 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; union { struct http_get get; struct payload { char *path; unsigned long long len, read; } payload; } u2; 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, MF_END_BOUNDARY_CR_LINE } state; enum { BODY_CR, BODY_LF, BODY_DATA } bstate; off_t len, written; char *boundary; size_t blen, nforms, nfiles, npairs; FILE *f; struct http_post_file *files; struct http_post_pair *pairs; struct form { char *name, *filename, *tmpname, *value; } *forms; } mf; struct put { char *tmpname; FILE *f; } put; } u; struct http_arg *args; size_t n_args, n_headers; struct http_header *headers; bool has_length, expect_continue; int (*response)(struct http_ctx *); void *buf; size_t buflen; } ctx; struct write_ctx { bool pending, close, chunk_done; enum state state; struct http_response r; struct http_get_range gr; off_t n; struct dynstr d; enum http_op op; void *chunk; int (*next)(struct http_ctx *, bool *close); size_t written, chunklen; } wctx; /* 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 int read_chunk(struct http_ctx *, bool *); static int adjust_partial(struct write_ctx *const w) { int ret = -1; struct http_response *const r = &w->r; const struct http_get_range *const g = &w->gr; struct dynstr d; const unsigned long long total = r->n; unsigned long long start = 0, end = total - 1; dynstr_init(&d); if (!r->f) { fprintf(stderr, "%s: expected non-NULL file\n", __func__); goto end; } else if (g->state & HTTP_GET_RANGE_END) { if (g->end >= r->n) { fprintf(stderr, "%s: end offset larger than file size " "(%llu, %llu)\n", __func__, g->start, r->n); ret = 1; goto end; } else if (dynstr_append(&d, "%llu")) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } r->n = g->end; end = g->end; } if (g->state & HTTP_GET_RANGE_START) { if (g->start >= r->n) { fprintf(stderr, "%s: start offset larger than file size " "(%llu, %llu)\n", __func__, g->start, r->n); ret = 1; goto end; } else if (fseeko(r->f, g->start, SEEK_SET)) { fprintf(stderr, "%s: fseeko(3): %s\n", __func__, strerror(errno)); goto end; } r->n -= g->start; start = g->start; } char range[sizeof "18446744073709551615"]; const int res = snprintf(range, sizeof range, "%llu", r->n); if (res < 0 || res >= sizeof range) { fprintf(stderr, "%s: snprintf(3) failed: %d\n", __func__, res); goto end; } else if (dynstr_append(&d, "bytes %llu-%llu/%llu", start, end, total)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if (http_response_add_header(r, "Content-Range", d.str)) { fprintf(stderr, "%s: http_response_add_header range failed\n", __func__); goto end; } else if (http_response_add_header(r, "Content-Length", range)) { fprintf(stderr, "%s: http_response_add_header length failed\n", __func__); goto end; } ret = 0; end: dynstr_free(&d); return ret; } static int send_length(struct write_ctx *const w) { struct http_response *const r = &w->r; if (r->status == HTTP_STATUS_PARTIAL_CONTENT) { const int res = adjust_partial(w); if (res) { fprintf(stderr, "%s: adjust_file failed\n", __func__); return res; } } else if (r->chunk) { if (http_response_add_header(r, "Transfer-Encoding", "chunked")) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); return -1; } w->next = read_chunk; } else { char len[sizeof "18446744073709551615"]; const int res = snprintf(len, sizeof len, "%llu", r->n); if (res < 0 || res >= sizeof len) { fprintf(stderr, "%s: snprintf(3) failed\n", __func__); return -1; } else if (http_response_add_header(r, "Content-Length", len)) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); return -1; } } if (!r->chunk && http_response_add_header(r, "Accept-Ranges", "bytes")) { fprintf(stderr, "%s: http_response_add_header length failed\n", __func__); return -1; } return 0; } static void arg_free(struct http_arg *const a) { if (a) { free(a->key); free(a->value); } } static size_t chrcnt(const char *s, const int c) { size_t ret = 0; while (*s++ == c) ret++; return ret; } static int parse_arg(struct ctx *const c, const char *const arg, const size_t n) { int ret = -1; struct http_arg a = {0}, *args = NULL; const char *sep = memchr(arg, '=', n); char *enckey = NULL, *encvalue = NULL, *deckey = NULL, *decvalue = NULL; if (!sep) { fprintf(stderr, "%s: expected '='\n", __func__); ret = 1; goto end; } else if (sep == arg) { fprintf(stderr, "%s: expected key\n", __func__); ret = 1; goto end; } const char *const value = sep + 1; if (!*value) { fprintf(stderr, "%s: missing value: %.*s\n", __func__, (int)n, arg); ret = 1; goto end; } const size_t keylen = sep - arg, valuelen = n - keylen - 1; if (!(enckey = strndup(arg, keylen))) { fprintf(stderr, "%s: strndup(3) key: %s\n", __func__, strerror(errno)); goto end; } else if (!(encvalue = strndup(value, valuelen))) { fprintf(stderr, "%s: strndup(3) value: %s\n", __func__, strerror(errno)); goto end; } /* URL parameters use '+' for whitespace, rather than %20. */ else if ((ret = http_decode_url(enckey, true, &deckey))) { fprintf(stderr, "%s: http_decode_url enckey failed\n", __func__); goto end; } else if ((ret = http_decode_url(encvalue, true, &decvalue))) { fprintf(stderr, "%s: http_decode_url encvalue failed\n", __func__); goto end; } a = (const struct http_arg) { .key = deckey, .value = decvalue }; if (!(args = realloc(c->args, (c->n_args + 1) * sizeof *args))) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); goto end; } args[c->n_args++] = a; c->args = args; ret = 0; end: if (ret) { arg_free(&a); free(deckey); free(decvalue); } free(enckey); free(encvalue); return ret; } static int parse_first_arg(struct ctx *const c, const char *const arg, const char *const ad_arg, const char *const res) { int error; const char *const next = arg + 1; if (chrcnt(next, '?')) { fprintf(stderr, "%s: more than one argument indicator '?' found: %s\n", __func__, res); return 1; } const size_t n = ad_arg ? ad_arg - next : strlen(next); if (!n) { fprintf(stderr, "%s: unterminated argument: %s\n", __func__, res); return 1; } else if ((error = parse_arg(c, next, n))) { fprintf(stderr, "%s: parse_arg failed: %s\n", __func__, res); return error; } return 0; } static int parse_adargs(struct ctx *const c, const char *const start, const char *const res) { for (const char *arg = start, *next; arg; arg = next) { next = strchr(++arg, '&'); int error; const size_t n = next ? next - arg : strlen(arg); if ((error = parse_arg(c, arg, n))) { fprintf(stderr, "%s: parse_arg failed: %s\n", __func__, res); return error; } } return 0; } static int parse_args(struct ctx *const c, const char *const res, size_t *const reslen) { int error; const char *const tmp_arg_start = strrchr(res, '?'), *const arg_start = tmp_arg_start && *(tmp_arg_start + 1) ? tmp_arg_start : NULL, *const ad_arg = arg_start ? strchr(arg_start, '&') : NULL; if (!arg_start) { if (!ad_arg) { *reslen = strlen(res); return 0; } else { fprintf(stderr, "%s: expected argument indicator '?': %s\n", __func__, res); return 1; } } else if (arg_start == res) { fprintf(stderr, "%s: expected resource: %s\n", __func__, res); return 1; } else if (ad_arg && ad_arg <= arg_start) { fprintf(stderr, "%s: expected '?' before '&': %s\n", __func__, res); return 1; } else if ((error = parse_first_arg(c, arg_start, ad_arg, res))) { fprintf(stderr, "%s: parse_first_arg failed\n", __func__); return error; } else if ((error = parse_adargs(c, ad_arg, res))) { fprintf(stderr, "%s: parse_adargs failed\n", __func__); return error; } *reslen = arg_start - res; return 0; } static int parse_resource(struct ctx *const c, const char *const enc_res) { int ret = -1, error; size_t reslen; char *trimmed_encres = NULL, *resource = NULL; if ((error = parse_args(c, enc_res, &reslen))) { fprintf(stderr, "%s: parse_args failed\n", __func__); ret = error; goto end; } else if (!(trimmed_encres = strndup(enc_res, reslen))) { fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); goto end; } else if ((ret = http_decode_url(trimmed_encres, false, &resource))) { fprintf(stderr, "%s: http_decode_url failed\n", __func__); goto end; } c->resource = resource; ret = 0; end: free(trimmed_encres); 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; 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 (get_op(line, n, &c->op)) { 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, error; char *enc_res = NULL; if (strcmp(protocol, HTTP_VERSION)) { 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 ((error = parse_resource(c, enc_res))) { fprintf(stderr, "%s: parse_resource failed\n", __func__); ret = error; 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->pairs); free(m->boundary); if (m->f && fclose(m->f)) fprintf(stderr, "%s: fclose(3) m->f: %s\n", __func__, strerror(errno)); for (size_t i = 0; i < m->nforms; i++) { struct form *const f = &m->forms[i]; free(f->name); free(f->filename); free(f->value); if (f->tmpname && remove(f->tmpname) && errno != ENOENT) fprintf(stderr, "%s: remove(3) %s: %s\n", __func__, f->tmpname, strerror(errno)); free(f->tmpname); } free(m->forms); } else if (c->op == HTTP_OP_PUT) { struct put *const p = &c->u.put; if (p->f && fclose(p->f)) fprintf(stderr, "%s: fclose(3) p->f: %s\n", __func__, strerror(errno)); if (p->tmpname && remove(p->tmpname) && errno != ENOENT) fprintf(stderr, "%s: remove(3) %s: %s\n", __func__, p->tmpname, strerror(errno)); free(p->tmpname); } free(c->field); free(c->value); free(c->resource); free(c->boundary); for (size_t i = 0; i < c->n_args; i++) arg_free(&c->args[i]); for (size_t i = 0; i < c->n_headers; i++) { const struct http_header *const hdr = &c->headers[i]; free(hdr->header); free(hdr->value); } free(c->headers); free(c->args); free(c->buf); *c = (const struct ctx){0}; } static void free_response_headers(struct http_response *const r) { for (size_t i = 0; i < r->n_headers; i++) { const struct http_header *const hdr = &r->headers[i]; free(hdr->header); free(hdr->value); } free(r->headers); r->headers = NULL; r->n_headers = 0; } static int prepare_headers(struct http_ctx *const h) { int ret = -1; struct write_ctx *const w = &h->wctx; struct dynstr *const d = &w->d; struct http_response *const r = &w->r; dynstr_init(d); for (size_t i = 0; i < w->r.n_headers; i++) { const struct http_header *const hdr = &r->headers[i]; if (dynstr_append(d, "%s: %s\r\n", hdr->header, hdr->value)) { fprintf(stderr, "%s: dynstr_append hdr failed\n", __func__); goto end; } } if (dynstr_append(d, "\r\n")) { fprintf(stderr, "%s: dynstr_append crlf failed\n", __func__); goto end; } ret = 0; end: free_response_headers(r); return ret; } static int rw_error(const int r, bool *const close) { if (r < 0) { switch (errno) { case EPIPE: /* Fall through. */ 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) { int res; dynstr_free(&w->d); if ((res = send_length(w))) { fprintf(stderr, "%s: send_length failed\n", __func__); return res; } 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->args ? r->args : r->buf.rw); if (r->f && (ret = fclose(r->f))) fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); free(w->chunk); dynstr_free(&w->d); free_response_headers(&w->r); *w = (const struct write_ctx){0}; return ret; } static bool must_write_body(const struct write_ctx *const w) { return w->op != HTTP_OP_HEAD; } 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) { const bool close_pending = w->close; dynstr_free(d); if (w->r.chunk || (w->r.n && must_write_body(w))) { w->state = BODY_LINE; w->n = 0; } else if (write_ctx_free(w)) { fprintf(stderr, "%s: write_ctx_free failed\n", __func__); return -1; } else if (close_pending) *close = true; } return 0; } static int write_chunkend(struct http_ctx *const h, bool *const close) { static const char end[] = "\r\n"; struct write_ctx *const w = &h->wctx; const void *const src = end + w->written; const int r = h->cfg.write(src, strlen(end) - w->written, h->cfg.user); if (r < 0) return rw_error(r, close); else if ((w->written += r) >= strlen(end)) { if (w->chunk_done) { const bool close_pending = w->close; if (write_ctx_free(w)) { fprintf(stderr, "%s: write_ctx_free failed\n", __func__); return -1; } else if (close_pending) *close = true; } else w->next = read_chunk; } return 0; } static int write_chunkdata(struct http_ctx *const h, bool *const close) { struct write_ctx *const w = &h->wctx; const void *const src = (const char *)w->chunk + w->written; const int r = h->cfg.write(src, w->chunklen - w->written, h->cfg.user); if (r < 0) return rw_error(r, close); else if ((w->written += r) >= w->chunklen) { w->written = 0; w->next = write_chunkend; } return 0; } static int write_chunklen(struct http_ctx *const h, bool *const close) { struct write_ctx *const w = &h->wctx; struct dynstr *const d = &w->d; const void *const src = d->str + w->written; const int r = h->cfg.write(src, d->len - w->written, h->cfg.user); if (r < 0) return rw_error(r, close); else if ((w->written += r) >= d->len) { dynstr_free(d); w->written = 0; w->next = write_chunkdata; } return 0; } static int read_chunk(struct http_ctx *const h, bool *const close) { bool done = false; struct write_ctx *const w = &h->wctx; struct dynstr *const d = &w->d; const struct http_response *const r = &w->r; const int n = r->chunk(w->chunk, BUFSIZ, &done, h->cfg.user, r->args); if (n < 0) { fprintf(stderr, "%s: user callback failed\n", __func__); return -1; } else if (!n && !done) return 0; dynstr_init(d); if (dynstr_append(d, "%X\r\n", n)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); return -1; } w->written = 0; w->chunklen = n; w->chunk_done = done; w->next = write_chunklen; return 0; } static int write_chunk(struct http_ctx *const h, bool *const close) { struct write_ctx *const w = &h->wctx; if (!w->chunk && !(w->chunk = malloc(BUFSIZ))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); return -1; } return w->next(h, close); } 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) { const bool close_pending = w->close; if (write_ctx_free(w)) { fprintf(stderr, "%s: write_ctx_free failed\n", __func__); return -1; } else if (close_pending) *close = true; return res; } 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[BUFSIZ]; 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) { const bool close_pending = w->close; if (write_ctx_free(w)) { fprintf(stderr, "%s: write_ctx_free failed\n", __func__); return -1; } else if (close_pending) *close = true; } 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->chunk) return write_chunk(h, close); else if (r->buf.ro) return write_body_mem(h, close); else if (r->f) return write_body_file(h, close); fprintf(stderr, "%s: expected chunk, 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) { const size_t n = r->n_headers + 1; struct http_header *const headers = realloc(r->headers, n * sizeof *r->headers), *h = NULL; if (!headers) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } h = &headers[r->n_headers]; *h = (const struct http_header) { .header = strdup(header), .value = strdup(value) }; if (!h->header || !h->value) { fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); free(h->header); free(h->value); return -1; } r->headers = headers; r->n_headers = n; 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); if (dynstr_append(&w->d, HTTP_VERSION " %d %s\r\n", c->code, c->descr)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); return -1; } 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) { char *end; errno = 0; const unsigned long long value = strtoull(len, &end, 10); if (errno || *end != '\0') { fprintf(stderr, "%s: invalid length %s: %s\n", __func__, len, strerror(errno)); return 1; } struct ctx *const c = &h->ctx; switch (c->op) { case HTTP_OP_POST: /* Fall through. */ case HTTP_OP_PUT: c->u2.payload.len = value; c->u.put = (const struct put){0}; 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; } static int get_boundary(const char *bnd, char *const buf, const size_t n) { size_t len = strlen(bnd); bool quotes = false; if (*bnd == '"') { if (bnd[len - 1] != '"') { fprintf(stderr, "%s: boundary starts with, but does not end with, " "double quotes\n", __func__); return 1; } len -= strlen("\"\""); quotes = true; bnd++; } int res = snprintf(buf, n, "%.*s", (int)len, bnd); if (res < 0) { fprintf(stderr, "%s: snprintf(3) failed with %d\n", __func__, res); return -1; } else if (res >= n) { fprintf(stderr, "%s: boundary exceeds maximum length (%d/%zu)\n", __func__, res, n); return 1; } else if (buf[res - 1] == ' ') { fprintf(stderr, "%s: boundary ends with whitespace character: %s\n", __func__, buf); return 1; } #define BSET "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "0123456789'()+_,-./=?" static const char bset[] = BSET, extset[] = BSET ": "; #undef BSET const char *const set = quotes ? extset : bset; if (strspn(buf, set) != res) { fprintf(stderr, "%s: boundary contains invalid characters: %s\n", __func__, buf); return 1; } 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; } 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; 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; /* According to RFC 2046, section 5.1.1, null character included. */ enum {MAXLEN = 71}; char buf[MAXLEN]; const int ret = get_boundary(val, buf, sizeof buf); if (ret) { fprintf(stderr, "%s: get_boundary failed\n", __func__); return ret; } struct ctx *const c = &h->ctx; struct dynstr b; dynstr_init(&b); if (dynstr_append(&b, "\r\n--%s", buf)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); return -1; } c->boundary = b.str; c->u.mf = (const struct multiform){0}; return 0; } static struct http_payload ctx_to_payload(const struct ctx *const c) { return (const struct http_payload) { .cookie = { .field = c->field, .value = c->value }, .op = c->op, .resource = c->resource, .args = c->args, .n_args = c->n_args, .headers = c->headers, .n_headers = c->n_headers }; } static int process_payload(struct http_ctx *const h) { struct ctx *const c = &h->ctx; const struct http_payload p = ctx_to_payload(c); struct http_response *const r = &h->wctx.r; const int ret = h->cfg.payload(&p, &h->wctx.r, h->cfg.user); if (ret) { ctx_free(c); return ret; } else if (!r->step.payload) { h->wctx.op = c->op; ctx_free(c); return start_response(h); } return 0; } 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; if (!c->has_length) { fprintf(stderr, "%s: 100-continue without expected content\n", __func__); return 1; } switch (c->op) { case HTTP_OP_POST: /* Fall through. */ case HTTP_OP_PUT: c->expect_continue = true; break; case HTTP_OP_GET: /* Fall through. */ case HTTP_OP_HEAD: fprintf(stderr, "%s: unexpected op %d\n", __func__, c->op); return 1; } } return 0; } static int set_range(struct http_ctx *const h, const char *const range) { const char *const sep = strchr(range, '='); if (!sep) { fprintf(stderr, "%s: missing separator\n", __func__); return 1; } const size_t tlen = sep - range; if (strncmp(range, "bytes", tlen)) { fprintf(stderr, "%s: unsupported range type %.*s\n", __func__, (int)tlen, range); return 1; } const char *const ssep = strchr(sep, '-'); if (!ssep) { fprintf(stderr, "%s: missing range separator\n", __func__); return 1; } struct http_get_range *const r = &h->ctx.u2.get.range; *r = (const struct http_get_range){0}; if (ssep != sep + 1) { const char *const start = sep + 1; char *endptr; errno = 0; r->start = strtoull(start, &endptr, 10); if (errno || endptr != ssep) { fprintf(stderr, "%s: unexpected start range %.*s\n", __func__, (int)(ssep - start), start); return 1; } r->state |= HTTP_GET_RANGE_START; } const char *const end = ssep + 1; if (*end) { char *endptr; errno = 0; r->end = strtoull(end, &endptr, 10); if (errno || *endptr) { fprintf(stderr, "%s: unexpected end range %s\n", __func__, end); return 1; } r->state |= HTTP_GET_RANGE_END; if (r->state & HTTP_GET_RANGE_START && r->start > r->end) { fprintf(stderr, "%s: start offset larger than end (%llu, %llu)\n", __func__, r->start, r->end); return 1; } } return 0; } static int append_header(struct http_ctx *const h, const char *const line, const size_t n, const char *const value) { struct ctx *const c = &h->ctx; if (c->n_headers >= h->cfg.max_headers) return 0; struct http_header *const headers = realloc(c->headers, (c->n_headers + 1) * sizeof *headers); char *headerdup = NULL, *valuedup = NULL; int ret = -1; if (!headers) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } c->headers = headers; if (!(headerdup = strndup(line, n))) { fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(valuedup = strdup(value))) { fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); goto end; } c->headers[c->n_headers++] = (const struct http_header) { .header = headerdup, .value = valuedup }; ret = 0; end: if (ret) { free(headerdup); free(valuedup); } return ret; } 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 }, { .header = "Range", .f = set_range } }; for (size_t i = 0; i < sizeof headers / sizeof *headers; i++) { const struct header *const hdr = &headers[i]; int ret; if ((ret = http_strncasecmp(line, hdr->header, n))) { if (ret < 0) { fprintf(stderr, "%s: http_strncasecmp failed\n", __func__); return ret; } } else if ((ret = hdr->f(h, value))) return ret; else break; } return append_header(h, line, n, value); } static int check_length(struct http_ctx *const h) { struct ctx *const c = &h->ctx; const struct http_cookie cookie = { .field = c->field, .value = c->value }; return h->cfg.length(c->op, c->resource, c->u2.payload.len, &cookie, &h->wctx.r, h->cfg.user); } static int process_payload_expect(struct http_ctx *const h) { struct ctx *const c = &h->ctx; struct write_ctx *const w = &h->wctx; struct http_response *const r = &h->wctx.r; struct http_payload p = ctx_to_payload(c); p.expect_continue = true; const int ret = h->cfg.payload(&p, r, h->cfg.user); if (ret) { ctx_free(c); return ret; } else if (!r->step.payload) { w->op = c->op; c->state = BODY_LINE; return start_response(h); } return 0; } static int process_expect(struct http_ctx *const h) { struct ctx *const c = &h->ctx; struct write_ctx *const w = &h->wctx; struct http_response *const r = &h->wctx.r; const int res = check_length(h); if (res) { if (res < 0) { fprintf(stderr, "%s: check_length failed\n", __func__); return res; } w->close = true; return start_response(h); } else if (!r->step.length) c->response = process_payload_expect; return 0; } static int process_get(struct http_ctx *const h) { int ret; struct ctx *const c = &h->ctx; struct http_payload p = ctx_to_payload(c); struct write_ctx *const w = &h->wctx; struct http_response *const r = &w->r; p.u.get.range = w->gr = c->u2.get.range; if ((ret = h->cfg.payload(&p, &h->wctx.r, h->cfg.user))) { ctx_free(c); return ret; } else if (!r->step.payload) { w->op = c->op; ctx_free(c); if (w->gr.state) w->r.status = HTTP_STATUS_PARTIAL_CONTENT; return start_response(h); } return 0; } static int process_boundary(struct http_ctx *const h) { struct ctx *const c = &h->ctx; struct write_ctx *const w = &h->wctx; const int res = check_length(h); if (res) { if (res < 0) { fprintf(stderr, "%s: check_length failed\n", __func__); return res; } w->close = true; ctx_free(c); return start_response(h); } else if (!w->r.step.length) { c->response = NULL; c->state = BODY_LINE; } 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: c->response = process_get; return 0; case HTTP_OP_HEAD: c->response = process_payload; return 0; case HTTP_OP_POST: { if (!c->u2.payload.len) { c->response = process_payload; return 0; } else if (c->expect_continue) { c->response = process_expect; return 0; } else if (c->boundary) { c->response = process_boundary; return 0; } 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->u2.payload.len) { c->response = process_payload; return 0; } else if (c->expect_continue) { c->response = process_expect; return 0; } 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; struct http_response *const r = &h->wctx.r; const int ret = h->cfg.payload(p, r, h->cfg.user); if (ret) { ctx_free(c); return ret; } else if (!r->step.payload) { ctx_free(c); return start_response(h); } return 0; } static int update_lstate(struct http_ctx *const h, int (*const f)(struct http_ctx *), const char *const buf, size_t *const sz) { struct ctx *const c = &h->ctx; const char b = *buf; 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__); return 1; } break; case LINE_LF: if (b == '\n') { h->line[c->len] = '\0'; const int ret = f(h); if (ret) return ret; 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__); return 1; } c->lstate = LINE_CR; break; } (*sz)--; return 0; } 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; } else if (!strcmp(f->filename, ".") || !strcmp(f->filename, "..") || strpbrk(f->filename, "/*")) { fprintf(stderr, "%s: invalid filename %s\n", __func__, f->filename); 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; } const size_t n = m->nforms + 1; struct form *const forms = realloc(m->forms, n * sizeof *m->forms); if (!forms) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } struct form *const f = &forms[m->nforms]; *f = (const struct form){0}; m->nforms = n; m->forms = forms; 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 (!strncasecmp(line, "Content-Disposition", n) && (ret = set_content_disposition(h, value))) return ret; return 0; } static int end_boundary(struct http_ctx *const h) { /* Found end boundary. */ struct ctx *const c = &h->ctx; struct multiform *const m = &c->u.mf; struct http_payload p = ctx_to_payload(c); p.u.post = (const struct http_post) { .files = m->files, .pairs = m->pairs, .nfiles = m->nfiles, .npairs = m->npairs }; return send_payload(h, &p); } 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 }; h->ctx.u2.payload.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) { int ret = -1; struct multiform *const m = &h->ctx.u.mf; struct form *const f = &m->forms[m->nforms - 1]; int fd = -1; FILE *fp; char *const tmpname = get_tmp(h->cfg.tmpdir); if (!tmpname) { fprintf(stderr, "%s: get_tmp failed\n", __func__); goto end; } else if ((fd = mkstemp(tmpname)) < 0) { fprintf(stderr, "%s: mkstemp(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(fp = fdopen(fd, "wb"))) { fprintf(stderr, "%s: fdopen(3): %s\n", __func__, strerror(errno)); goto end; } f->tmpname = tmpname; m->f = fp; ret = 0; end: if (ret) { if (fd >= 0 && close(fd)) fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); free(tmpname); } return ret; } static int read_mf_body_to_mem(struct http_ctx *const h, const char *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 - 1) { fprintf(stderr, "%s: maximum length exceeded\n", __func__); return 1; } memcpy(h->line, buf, n); m->written += n; m->len += n; c->u2.payload.read += n; return 0; } static int read_mf_body_to_file(struct http_ctx *const h, const char *const buf, const size_t n) { struct ctx *const c = &h->ctx; struct multiform *const m = &c->u.mf; if (!m->f && generate_mf_file(h)) { fprintf(stderr, "%s: generate_mf_file failed\n", __func__); return -1; } else if (n && !fwrite(buf, n, 1, m->f)) { fprintf(stderr, "%s: fwrite(3) failed, feof=%d, ferror=%d\n", __func__, feof(m->f), ferror(m->f)); return -1; } m->written += n; m->len += n; c->u2.payload.read += n; return 0; } static int reset_boundary(struct http_ctx *const h) { struct multiform *const m = &h->ctx.u.mf; struct form *const f = &m->forms[m->nforms - 1]; int (*const read_mf)(struct http_ctx *, const char *, size_t) = f->filename ? read_mf_body_to_file : read_mf_body_to_mem; const size_t len = strlen(m->boundary); const int res = read_mf(h, m->boundary, len); if (res) return res; memset(m->boundary, '\0', len); m->blen = 0; return 0; } static int dump_body(struct http_ctx *const h, const char *const buf, const size_t n) { struct multiform *const m = &h->ctx.u.mf; struct form *const f = &m->forms[m->nforms - 1]; int (*const read_mf)(struct http_ctx *, const char *, size_t) = f->filename ? read_mf_body_to_file : read_mf_body_to_mem; 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 (fclose(m->f)) { fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); return -1; } m->f = NULL; const struct http_cfg_post *const cfg = &h->cfg.post; const size_t n = m->nfiles + 1; if (n > cfg->max_files) { fprintf(stderr, "%s: exceeded maximum number of files (%zu)\n", __func__, cfg->max_files); return 1; } struct http_post_file *const files = realloc(m->files, n * sizeof *m->files); if (!files) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } struct http_post_file *const pf = &files[m->nfiles]; *pf = (const struct http_post_file) { .name = f->name, .tmpname = f->tmpname, .filename = f->filename }; m->files = files; m->nfiles = n; return 0; } static bool name_exists(const struct multiform *const m, const struct form *const f) { for (size_t i = 0; i < m->npairs; i++) { const struct http_post_pair *const p = &m->pairs[i]; if (!strcmp(f->name, p->name)) { fprintf(stderr, "%s: \"%s\" defined more than once\n", __func__, f->name); return true; } } return false; } static int apply_from_mem(struct http_ctx *const h, struct form *const f) { struct multiform *const m = &h->ctx.u.mf; if (name_exists(m, f)) return 1; const struct http_cfg_post *const cfg = &h->cfg.post; struct http_post_pair *pairs; const size_t n = m->npairs + 1; if (n > cfg->max_pairs) { fprintf(stderr, "%s: exceeded maximum number of pairs (%zu)\n", __func__, cfg->max_pairs); return 1; } else if (!(f->value = strndup(h->line, m->written))) { fprintf(stderr, "%s: strndup(3): %s\n", __func__, strerror(errno)); return -1; } else if (!(pairs = realloc(m->pairs, n * sizeof *m->pairs))) { fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno)); return -1; } pairs[m->npairs++] = (const struct http_post_pair) { .name = f->name, .value = f->value }; m->pairs = pairs; 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; const size_t clen = strlen(c->boundary); if (b == c->boundary[m->blen]) { m->boundary[m->blen++] = b; if (m->blen >= clen) { /* 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', clen); m->blen = 0; m->state = MF_END_BOUNDARY; m->written = 0; return ret; } } else { fprintf(stderr, "%s: expected %hhx, got %hhx\n", __func__, c->boundary[m->blen], b); return 1; } 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 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 st; } static int read_mf_body_boundary(struct http_ctx *const h, const char *buf, size_t *const n) { struct ctx *const c = &h->ctx; struct multiform *const m = &c->u.mf; int res; if (m->blen && *buf != c->boundary[m->blen] && (res = reset_boundary(h))) return res; const char *const boundary = http_memmem(&c->boundary[m->blen], buf, *n); if (!boundary) { if ((res = reset_boundary(h)) || (res = dump_body(h, buf, *n))) return res; *n = 0; return 0; } const size_t prev = boundary - buf; if (prev && (res = reset_boundary(h))) return res; else if ((res = dump_body(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; } *n -= r; return 0; } static int read_mf_end_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; static const char end[] = "--", crlf[] = "\r\n"; const bool check = c->len >= strlen(end) || c->len >= strlen(crlf); h->line[c->len++] = *buf; (*n)--; if (c->len >= strlen(end) && !strncmp(h->line, end, strlen(end))) { c->len = 0; /* Found end boundary. */ if (!*n) c->response = end_boundary; else m->state = MF_END_BOUNDARY_CR_LINE; } else if (c->len >= strlen(crlf) && !strncmp(h->line, crlf, strlen(crlf))) { c->len = 0; m->state = MF_HEADER_CR_LINE; } else if (check) { fprintf(stderr, "%s: unexpected boundary delimiter: %.*s\n", __func__, (int)c->len, h->line); return 1; } return 0; } static int process_mf_end_crlf(struct http_ctx *const h) { if (*h->line) { fprintf(stderr, "%s: expected empty line\n", __func__); return 1; } h->ctx.response = end_boundary; return 0; } static int read_multiform(struct http_ctx *const h, const char *const buf, size_t *const sz) { struct multiform *const m = &h->ctx.u.mf; int res; switch (m->state) { case MF_START_BOUNDARY: /* Fall through. */ case MF_HEADER_CR_LINE: if ((res = update_lstate(h, process_mf_line, buf, sz))) return res; break; case MF_BODY_BOUNDARY_LINE: if ((res = read_mf_body_boundary(h, buf, sz))) return res; break; case MF_END_BOUNDARY: if ((res = read_mf_end_boundary(h, buf, sz))) return res; break; case MF_END_BOUNDARY_CR_LINE: if ((res = update_lstate(h, process_mf_end_crlf, buf, sz))) return res; break; } return 0; } static int send_line_payload(struct http_ctx *const h) { struct ctx *const c = &h->ctx; struct http_payload pl = ctx_to_payload(c); pl.u.post.data = h->line; return send_payload(h, &pl); } static int read_body_to_mem(struct http_ctx *const h, const char *const buf, size_t *const sz) { struct ctx *const c = &h->ctx; struct payload *const p = &c->u2.payload; const char b = *buf; if (p->read >= sizeof h->line - 1) { fprintf(stderr, "%s: exceeded maximum length\n", __func__); return 1; } h->line[p->read++] = b; (*sz)--; if (p->read >= p->len) { h->line[p->len] = '\0'; c->response = send_line_payload; } return 0; } static int ensure_put_tmp(struct http_ctx *const h) { int ret = -1; struct put *const put = &h->ctx.u.put; char *const tmpname = get_tmp(h->cfg.tmpdir); if (!tmpname) { fprintf(stderr, "%s: get_tmp failed\n", __func__); goto end; } put->tmpname = tmpname; ret = 0; end: if (ret) free(tmpname); return ret; } static int ensure_put_file(struct http_ctx *const h) { int ret = -1; struct put *const put = &h->ctx.u.put; int fd = -1; FILE *f = NULL; if ((fd = mkstemp(put->tmpname)) < 0) { fprintf(stderr, "%s: mkstemp(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(f = fdopen(fd, "wb"))) { fprintf(stderr, "%s: fdopen(3): %s\n", __func__, strerror(errno)); goto end; } put->f = f; ret = 0; end: if (ret) { if (f && fclose(f)) fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); else if (fd >= 0 && close(fd)) fprintf(stderr, "%s: close(2): %s\n", __func__, strerror(errno)); } return ret; } static int read_put_body_to_file(struct http_ctx *const h, const void *const buf, size_t *const sz) { struct ctx *const c = &h->ctx; struct put *const put = &c->u.put; if (!put->tmpname && ensure_put_tmp(h)) { fprintf(stderr, "%s: ensure_put_tmp failed\n", __func__); return -1; } else if (!put->f && ensure_put_file(h)) { fprintf(stderr, "%s: ensure_put_file failed\n", __func__); return -1; } else if (!fwrite(buf, *sz, 1, put->f)) { fprintf(stderr, "%s: fwrite(3) failed, feof=%d ferror=%d\n", __func__, feof(put->f), ferror(put->f)); return -1; } c->u2.payload.read += *sz; *sz = 0; return 0; } static int send_put_payload(struct http_ctx *const h) { struct ctx *const c = &h->ctx; struct http_payload pl = ctx_to_payload(c); pl.u.put.tmpname = c->u.put.tmpname; return send_payload(h, &pl); } static int read_to_file(struct http_ctx *const h, const char *const buf, size_t *const sz) { struct ctx *const c = &h->ctx; struct payload *const p = &c->u2.payload; if (read_put_body_to_file(h, buf, sz)) return -1; else if (p->read >= p->len) c->response = send_put_payload; return 0; } static int read_body(struct http_ctx *const h, const char *const buf, size_t *const sz) { const struct ctx *const c = &h->ctx; switch (c->op) { case HTTP_OP_POST: return c->boundary ? read_multiform(h, buf, sz) : read_body_to_mem(h, buf, sz); case HTTP_OP_PUT: return read_to_file(h, buf, sz); 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) { 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 read_buf(struct http_ctx *const h, const char *const buf, size_t *const sz) { switch (h->ctx.state) { case START_LINE: /* Fall through. */ case HEADER_CR_LINE: return update_lstate(h, process_line, buf, sz); case BODY_LINE: return read_body(h, buf, sz); } fprintf(stderr, "%s: unexpected state %d\n", __func__, h->ctx.state); return -1; } static int storebuf(struct http_ctx *const h, const void *const src, const size_t n) { struct ctx *const c = &h->ctx; void *const dst = malloc(n); if (!dst) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); return -1; } memcpy((c->buf = dst), src, (c->buflen = n)); return 0; } static int loadbuf(struct http_ctx *const h, void *const dst, const size_t n) { struct ctx *const c = &h->ctx; memcpy(dst, c->buf, c->buflen); free(c->buf); c->buf = NULL; return c->buflen; } int http_read(struct http_ctx *const h, bool *const close) { struct ctx *const c = &h->ctx; char buf[BUFSIZ]; int ret, r; if (c->buf) r = loadbuf(h, buf, sizeof buf); else if ((r = h->cfg.read(buf, sizeof buf, h->cfg.user)) <= 0) return rw_error(r, close); size_t rem = r; while (rem) { if ((ret = read_buf(h, &buf[r - rem], &rem))) goto failure; else if (c->response && rem) { if (storebuf(h, &buf[r - rem], rem)) { fprintf(stderr, "%s: storebuf failed\n", __func__); ret = -1; goto failure; } break; } } return 0; failure: ctx_free(&h->ctx); return ret; } static int append_expire(struct dynstr *const d, const struct tm *const exp) { int ret = -1; char s[sizeof "Thu, 01 Jan 1970 00:00:00 XXXX"]; const locale_t l = newlocale(LC_TIME_MASK, "POSIX", (locale_t)0); if (l == (locale_t)0) { fprintf(stderr, "%s: newlocale(3): %s\n", __func__, strerror(errno)); goto end; } else if (!strftime_l(s, sizeof s, "%a, %d %b %Y %H:%M:%S %Z", exp, l)) { fprintf(stderr, "%s: strftime_l(3) failed\n", __func__); return -1; } else if (dynstr_append(d, "; Expires=%s", s)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); return -1; } ret = 0; end: if (l != (locale_t)0) freelocale(l); return ret; } char *http_cookie_create(const char *const key, const char *const value, const struct tm *const exp) { struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "%s=%s; HttpOnly; SameSite=Strict", key, value)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto failure; } else if (exp && append_expire(&d, exp)) { fprintf(stderr, "%s: append_expire failed\n", __func__); goto failure; } return d.str; failure: dynstr_free(&d); return NULL; } int http_update(struct http_ctx *const h, bool *const write, bool *const close) { int ret; struct ctx *const c = &h->ctx; struct write_ctx *const w = &h->wctx; *close = false; if (c->response) ret = c->response(h); else ret = w->pending ? http_write(h, close) : http_read(h, close); *write = c->buf || c->response || 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; } int http_decode_url(const char *url, const bool spaces, char **out) { int ret = -1; char *str = NULL; size_t n = 0; while (*url) { char *const r = realloc(str, n + 1); if (!r) { fprintf(stderr, "%s: realloc(3) loop: %s\n", __func__, strerror(errno)); goto end; } str = r; if (spaces && *url == '+') { str[n++] = ' '; url++; } else if (*url != '%') str[n++] = *url++; else if (*(url + 1) && *(url + 2)) { const char buf[sizeof "00"] = {*(url + 1), *(url + 2)}; char *endptr; const unsigned long res = strtoul(buf, &endptr, 16); if (*endptr) { fprintf(stderr, "%s: invalid number %s\n", __func__, buf); ret = 1; goto end; } url += 3; str[n++] = res; } else { fprintf(stderr, "%s: unterminated %%\n", __func__); ret = 1; goto end; } } char *const r = realloc(str, n + 1); if (!r) { fprintf(stderr, "%s: realloc(3) end: %s\n", __func__, strerror(errno)); goto end; } str = r; str[n] = '\0'; *out = str; ret = 0; end: if (ret) free(str); return ret; } int http_strcasecmp(const char *s1, const char *s2) { int ret = -1; const locale_t l = newlocale(LC_CTYPE_MASK, "POSIX", (locale_t)0); if (l == (locale_t)0) { fprintf(stderr, "%s: newlocale(3): %s\n", __func__, strerror(errno)); goto end; } while (*s1 && *s2) if (tolower_l(*s1++, l) != tolower_l(*s2++, l)) { ret = 1; goto end; } ret = !!*s1 || !!*s2; end: if (l != (locale_t)0) freelocale(l); return ret; } int http_strncasecmp(const char *s1, const char *s2, const size_t n) { int ret = -1; const locale_t l = newlocale(LC_CTYPE_MASK, "POSIX", (locale_t)0); if (l == (locale_t)0) { fprintf(stderr, "%s: newlocale(3): %s\n", __func__, strerror(errno)); goto end; } for (size_t i = 0; i < n; i++, s1++, s2++) if (!*s1 || !*s2 || tolower_l(*s1, l) != tolower_l(*s2, l)) { ret = 1; goto end; } ret = 0; end: if (l != (locale_t)0) freelocale(l); return ret; }