aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi.dcr@tutanota.com>2024-11-10 23:56:57 +0100
committerXavier Del Campo Romero <xavi.dcr@tutanota.com>2024-11-11 00:04:39 +0100
commit5d47b2d12caba33793a078d2eafae6ae3d2ad921 (patch)
tree6928e070b939a37a4bf5af6ff7222e87f00ccae9
parentb8cd00d00fa4dd2c45615c6b0367e3b57e12e98d (diff)
Implement HTTP byte serving
This commit allows the HTTP server to return partial content to clients, rather than returning the whole resource. This can be particularly useful for applications such as audio/video playback or showing large PDF files. Notes: - Applications must not care about partial contents i.e., if a valid user request was made, applications must still return HTTP status 200 ("OK"), as usual. The HTTP server will then translate the status code to 206 ("Partial Content") if required.
-rw-r--r--http.c285
-rw-r--r--include/libweb/http.h16
2 files changed, 275 insertions, 26 deletions
diff --git a/http.c b/http.c
index 2360c41..fbdba54 100644
--- a/http.c
+++ b/http.c
@@ -37,11 +37,16 @@ struct http_ctx
char *resource, *field, *value, *boundary;
size_t len;
- struct payload
+ union
{
- char *path;
- unsigned long long len, read;
- } payload;
+ struct http_get get;
+
+ struct payload
+ {
+ char *path;
+ unsigned long long len, read;
+ } payload;
+ } u2;
union
{
@@ -104,6 +109,7 @@ struct http_ctx
bool pending, close;
enum state state;
struct http_response r;
+ struct http_get_range gr;
off_t n;
struct dynstr d;
enum http_op op;
@@ -116,6 +122,134 @@ struct http_ctx
struct http_cfg cfg;
};
+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
+ {
+ 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 (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)
@@ -611,20 +745,14 @@ static int write_start_line(struct http_ctx *const h, bool *const close)
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);
+ int res;
- dynstr_free(d);
+ dynstr_free(&w->d);
- if (res < 0 || res >= sizeof len)
+ if ((res = send_length(w)))
{
- 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;
+ fprintf(stderr, "%s: send_length failed\n", __func__);
+ return res;
}
else if (prepare_headers(h))
{
@@ -907,7 +1035,7 @@ static int set_length(struct http_ctx *const h, const char *const len)
case HTTP_OP_POST:
/* Fall through. */
case HTTP_OP_PUT:
- c->payload.len = value;
+ c->u2.payload.len = value;
c->u.put = (const struct put){0};
break;
@@ -1152,6 +1280,84 @@ static int expect(struct http_ctx *const h, const char *const value)
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)
{
@@ -1229,6 +1435,11 @@ static int process_header(struct http_ctx *const h, const char *const line,
{
.header = "Content-Type",
.f = set_content_type
+ },
+
+ {
+ .header = "Range",
+ .f = set_range
}
};
@@ -1253,7 +1464,7 @@ static int check_length(struct http_ctx *const h)
.value = c->value
};
- return h->cfg.length(c->payload.len, &cookie, &h->wctx.r, h->cfg.user);
+ return h->cfg.length(c->u2.payload.len, &cookie, &h->wctx.r, h->cfg.user);
}
static int process_expect(struct http_ctx *const h)
@@ -1288,6 +1499,27 @@ static int process_expect(struct http_ctx *const h)
return start_response(h);
}
+static int process_get(struct http_ctx *const h)
+{
+ struct ctx *const c = &h->ctx;
+ struct http_payload p = ctx_to_payload(c);
+ struct write_ctx *const w = &h->wctx;
+
+ p.u.get.range = w->gr = c->u2.get.range;
+
+ const int ret = h->cfg.payload(&p, &h->wctx.r, h->cfg.user);
+
+ w->op = c->op;
+ ctx_free(c);
+
+ if (ret)
+ return ret;
+ else if (w->gr.state)
+ w->r.status = HTTP_STATUS_PARTIAL_CONTENT;
+
+ return start_response(h);
+}
+
static int header_cr_line(struct http_ctx *const h)
{
const char *const line = (const char *)h->line;
@@ -1298,13 +1530,14 @@ static int header_cr_line(struct http_ctx *const h)
switch (c->op)
{
case HTTP_OP_GET:
- /* Fall through. */
+ return process_get(h);
+
case HTTP_OP_HEAD:
return process_payload(h);
case HTTP_OP_POST:
{
- if (!c->payload.len)
+ if (!c->u2.payload.len)
return process_payload(h);
else if (c->expect_continue)
return process_expect(h);
@@ -1337,7 +1570,7 @@ static int header_cr_line(struct http_ctx *const h)
__func__);
return 1;
}
- else if (!c->payload.len)
+ else if (!c->u2.payload.len)
return process_payload(h);
else if (c->expect_continue)
return process_expect(h);
@@ -1616,7 +1849,7 @@ static int process_mf_line(struct http_ctx *const h)
[MF_HEADER_CR_LINE] = mf_header_cr_line
};
- h->ctx.payload.read += strlen(h->line) + strlen("\r\n");
+ h->ctx.u2.payload.read += strlen(h->line) + strlen("\r\n");
return state[h->ctx.u.mf.state](h);
}
@@ -1691,7 +1924,7 @@ static int read_mf_body_to_mem(struct http_ctx *const h,
memcpy(h->line, buf, n);
m->written += n;
m->len += n;
- c->payload.read += n;
+ c->u2.payload.read += n;
return 0;
}
@@ -1715,7 +1948,7 @@ static int read_mf_body_to_file(struct http_ctx *const h,
m->written += n;
m->len += n;
- c->payload.read += n;
+ c->u2.payload.read += n;
return 0;
}
@@ -2055,7 +2288,7 @@ 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->payload;
+ struct payload *const p = &c->u2.payload;
const char b = *buf;
if (p->read >= sizeof h->line - 1)
@@ -2158,7 +2391,7 @@ static int read_put_body_to_file(struct http_ctx *const h,
return -1;
}
- c->payload.read += *sz;
+ c->u2.payload.read += *sz;
*sz = 0;
return 0;
}
@@ -2167,7 +2400,7 @@ 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->payload;
+ struct payload *const p = &c->u2.payload;
if (read_put_body_to_file(h, buf, sz))
return -1;
diff --git a/include/libweb/http.h b/include/libweb/http.h
index 4e80570..736a561 100644
--- a/include/libweb/http.h
+++ b/include/libweb/http.h
@@ -49,6 +49,21 @@ struct http_payload
{
const char *tmpname;
} put;
+
+ struct http_get
+ {
+ struct http_get_range
+ {
+ enum
+ {
+ HTTP_GET_RANGE_NONE,
+ HTTP_GET_RANGE_START = 1 << 0,
+ HTTP_GET_RANGE_END = 1 << 1
+ } state;
+
+ unsigned long long start, end;
+ } range;
+ } get;
} u;
const struct http_arg
@@ -64,6 +79,7 @@ struct http_payload
#define HTTP_STATUSES \
X(CONTINUE, "Continue", 100) \
X(OK, "OK", 200) \
+ X(PARTIAL_CONTENT, "Partial Content", 206) \
X(SEE_OTHER, "See other", 303) \
X(BAD_REQUEST, "Bad Request", 400) \
X(UNAUTHORIZED, "Unauthorized", 401) \