aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-10-06 01:23:20 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-10-06 15:51:00 +0200
commit3e4c7c993bbbe2bdeb563fa888b900d01c4be4a1 (patch)
treebda2c376c19b11f8f76ef6aad84dea067f491934
parenta0f5f7509bb9040752fa61fe0fdb447608e22b1c (diff)
Fix design issues with async responses, add async example
struct http_response did not provide users any void * that could be used to maintain a state between calls to an asynchronous HTTP response. On the other hand, the user pointer could not be used for this purpose, since it is shared among all HTTP clients for a given struct handler instance. Moreover, the length callback was still not supporting this feature, which in fact might be required by some users. Implementing this was particularly challenging, as this broke the current assumption that all bytes on a call to http_read were being processed. Now, since a client request can only be partially processed because of the length callback, http_read must take this into account so that the remaining bytes are still available for future calls, before reading again from the file descriptor.
-rw-r--r--doc/man7/libweb_http.750
-rw-r--r--examples/CMakeLists.txt1
-rw-r--r--examples/Makefile1
-rw-r--r--examples/async/CMakeLists.txt4
-rw-r--r--examples/async/Makefile29
-rw-r--r--examples/async/README.md14
-rw-r--r--examples/async/main.c232
-rw-r--r--handler.c33
-rw-r--r--http.c152
-rw-r--r--include/libweb/http.h10
10 files changed, 470 insertions, 56 deletions
diff --git a/doc/man7/libweb_http.7 b/doc/man7/libweb_http.7
index c3bec8b..a8e315d 100644
--- a/doc/man7/libweb_http.7
+++ b/doc/man7/libweb_http.7
@@ -562,7 +562,13 @@ struct http_response
unsigned long long \fIn\fP;
size_t \fIn_headers\fP;
void (*\fIfree\fP)(void *);
- int (*\fIstep\fP)(const struct http_payload *, struct http_response *, void *);
+ void *\fIstep_args\fP;
+
+ union http_step
+ {
+ int (*\fIlength\fP)(unsigned long long \fIlen\fP, const struct http_cookie *\fIc\fP, struct http_response *\fIr\fP, void *\fIuser\fP, void *\fIstep_args\fP);
+ int (*\fIpayload\fP)(const struct http_payload *\fIp\fP, struct http_response *\fIr\fP, void *\fIuser\fP, void *\fIstep_args\fP);
+ } \fIstep\fP;
};
.EE
.in
@@ -671,22 +677,54 @@ is a valid pointer. Otherwise,
must be a null pointer.
.I step
-allows implementations to generate a response in a non-blocking manner.
-When a response is not immediately available when a payload is received,
+allows implementations to deal with responses asynchronously
+i.e., without blocking other clients.
+For example, this can be useful to
+generate a response in a non-blocking manner.
+In other words, when a response is not immediately available
+when a payload is received,
.I step
must be assigned to a function that can generate it later.
.I libweb
shall then call this function immediately later,
without blocking other clients.
+
Assigning
-.I step
+.I step.length
+or
+.I step.payload
to a null pointer falls back to the default behaviour
i.e., a response is returned immediately.
Note that a non-null
-.I step
-shall always take priority, thus ignoring any other information inside the
+.I step.length
+or
+.I step.payload
+shall always take priority,
+.B thus ignoring any other information inside the
.I "struct http_response"
instance.
+.I step
+can be configured under the following situations:
+
+.IP \(bu 2
+Inside the
+.I length
+callback defined by the
+.I struct http_cfg
+instance. In this case,
+.I step.length
+must be assigned to a function to be later called by
+.IR libweb .
+
+.IP \(bu 2
+Inside the
+.I payload
+callback defined by the
+.I struct http_cfg
+instance. In this case,
+.I step.payload
+must be assigned to a function to be later called by
+.IR libweb .
.SS Transport Layer Security (TLS)
By design,
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 3370acb..b64430c 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -1,4 +1,5 @@
cmake_minimum_required(VERSION 3.13)
+add_subdirectory(async)
add_subdirectory(form)
add_subdirectory(headers)
add_subdirectory(hello)
diff --git a/examples/Makefile b/examples/Makefile
index b06f0a5..688895a 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -1,6 +1,7 @@
.POSIX:
DIRS = \
+ async \
form \
headers \
hello \
diff --git a/examples/async/CMakeLists.txt b/examples/async/CMakeLists.txt
new file mode 100644
index 0000000..4afd5ba
--- /dev/null
+++ b/examples/async/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.13)
+project(async C)
+add_executable(${PROJECT_NAME} main.c)
+target_link_libraries(${PROJECT_NAME} PRIVATE web)
diff --git a/examples/async/Makefile b/examples/async/Makefile
new file mode 100644
index 0000000..d0a42c2
--- /dev/null
+++ b/examples/async/Makefile
@@ -0,0 +1,29 @@
+.POSIX:
+
+PROJECT = async
+DEPS = \
+ main.o
+LIBWEB = ../../libweb.a
+DYNSTR = ../../dynstr/libdynstr.a
+CFLAGS = -I ../../include -I ../../dynstr/include
+LIBWEB_FLAGS = -L ../../ -l web
+DYNSTR_FLAGS = -L ../../dynstr -l dynstr
+
+all: $(PROJECT)
+
+clean:
+ rm -f $(DEPS)
+
+distclean: clean
+ rm -f $(PROJECT)
+
+FORCE:
+
+$(PROJECT): $(DEPS) $(LIBWEB) $(DYNSTR)
+ $(CC) $(LDFLAGS) $(DEPS) $(LIBWEB_FLAGS) $(DYNSTR_FLAGS) -o $@
+
+$(LIBWEB): FORCE
+ +cd ../../ && $(MAKE)
+
+$(DYNSTR): FORCE
+ +cd ../../dynstr && $(MAKE)
diff --git a/examples/async/README.md b/examples/async/README.md
new file mode 100644
index 0000000..ece15e7
--- /dev/null
+++ b/examples/async/README.md
@@ -0,0 +1,14 @@
+# Asynchronous HTTP response example
+
+This example shows how to generate HTTP responses asynchronously.
+
+## How to build
+
+If using `make(1)`, just run `make` from this directory.
+
+If using CMake, examples are built by default when configuring the project
+from [the top-level `CMakeLists.txt`](../../CMakeLists.txt).
+
+## How to run
+
+Run the executable without any command line arguments.
diff --git a/examples/async/main.c b/examples/async/main.c
new file mode 100644
index 0000000..dbf69a8
--- /dev/null
+++ b/examples/async/main.c
@@ -0,0 +1,232 @@
+/* As of FreeBSD 13.2, sigaction(2) still conforms to IEEE Std
+ * 1003.1-1990 (POSIX.1), which did not define SA_RESTART.
+ * FreeBSD supports it as an extension, but then _POSIX_C_SOURCE must
+ * not be defined. */
+#ifndef __FreeBSD__
+#define _POSIX_C_SOURCE 200809L
+#endif
+
+#include <dynstr.h>
+#include <libweb/handler.h>
+#include <libweb/html.h>
+#include <libweb/http.h>
+#include <errno.h>
+#include <signal.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int send_response(const unsigned cnt, struct http_response *const r)
+{
+ int ret = -1;
+
+ struct html_node *html = html_node_alloc("html"), *body, *p;
+ struct dynstr d, text;
+
+ dynstr_init(&d);
+ dynstr_init(&text);
+
+ if (!html)
+ {
+ fprintf(stderr, "%s: html_node_alloc failed\n", __func__);
+ goto end;
+ }
+ else if (!(body = html_node_add_child(html, "body")))
+ {
+ fprintf(stderr, "%s: html_node_add_child body failed\n", __func__);
+ goto end;
+ }
+ else if (!(p = html_node_add_child(body, "p")))
+ {
+ fprintf(stderr, "%s: html_node_add_child p failed\n", __func__);
+ goto end;
+ }
+ else if (dynstr_append(&text, "It took %u iterations to generate "
+ "this response!", cnt))
+ {
+ fprintf(stderr, "%s: dynstr_append failed\n", __func__);
+ goto end;
+ }
+ else if (html_node_set_value(p, text.str))
+ {
+ fprintf(stderr, "%s: html_node_set_value p failed\n", __func__);
+ goto end;
+ }
+ else if (html_serialize(html, &d))
+ {
+ fprintf(stderr, "%s: html_serialize failed\n", __func__);
+ goto end;
+ }
+
+ *r = (const struct http_response)
+ {
+ .status = HTTP_STATUS_OK,
+ .buf.rw = d.str,
+ .n = d.len,
+ .free = free
+ };
+
+ if (http_response_add_header(r, "Content-Type", "text/html"))
+ {
+ fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
+ goto end;
+ }
+
+ ret = 0;
+
+end:
+ dynstr_free(&text);
+ html_node_free(html);
+
+ if (ret)
+ dynstr_free(&d);
+
+ return ret;
+}
+
+static int step(const struct http_payload *const pl,
+ struct http_response *const r, void *const user, void *step_args)
+{
+ unsigned *const cnt = step_args;
+ const unsigned max = 10;
+
+ fprintf(stderr, "%s: step %u\n", __func__, *cnt);
+
+ if (++(*cnt) >= max)
+ return send_response(*cnt, r);
+
+ return 0;
+}
+
+static int hello(const struct http_payload *const pl,
+ struct http_response *const r, void *const user)
+{
+ unsigned *const cnt = malloc(sizeof *cnt);
+
+ if (!cnt)
+ {
+ fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
+ return -1;
+ }
+
+ *r = (const struct http_response)
+ {
+ .step.payload = step,
+ .step_args = cnt
+ };
+
+ *cnt = 0;
+ return 0;
+}
+
+static int on_length(const unsigned long long len,
+ const struct http_cookie *const c, struct http_response *const r,
+ void *const user)
+{
+ *r = (const struct http_response)
+ {
+ .status = HTTP_STATUS_FORBIDDEN
+ };
+
+ return 1;
+}
+
+struct handler *handler;
+
+static void handle_signal(const int signum)
+{
+ switch (signum)
+ {
+ case SIGINT:
+ /* Fall through. */
+ case SIGTERM:
+ handler_notify_close(handler);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static int init_signals(void)
+{
+ struct sigaction sa =
+ {
+ .sa_handler = handle_signal,
+ .sa_flags = SA_RESTART
+ };
+
+ sigemptyset(&sa.sa_mask);
+
+ static const struct signal
+ {
+ int signal;
+ const char *name;
+ } signals[] =
+ {
+ {.signal = SIGINT, .name = "SIGINT"},
+ {.signal = SIGTERM, .name = "SIGTERM"},
+ {.signal = SIGPIPE, .name = "SIGPIPE"}
+ };
+
+ for (size_t i = 0; i < sizeof signals / sizeof *signals; i++)
+ {
+ const struct signal *const s = &signals[i];
+
+ if (sigaction(s->signal, &sa, NULL))
+ {
+ fprintf(stderr, "%s: sigaction(2) %s: %s\n",
+ __func__, s->name, strerror(errno));
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_FAILURE;
+ const struct handler_cfg cfg =
+ {
+ .length = on_length
+ };
+
+ static const char *const urls[] = {"/", "/index.html"};
+
+ if (!(handler = handler_alloc(&cfg)))
+ {
+ fprintf(stderr, "%s: handler_alloc failed\n", __func__);
+ goto end;
+ }
+
+ for (size_t i = 0; i < sizeof urls / sizeof *urls; i++)
+ if (handler_add(handler, urls[i], HTTP_OP_GET, hello, NULL))
+ {
+ fprintf(stderr, "%s: handler_add failed\n", __func__);
+ goto end;
+ }
+
+ unsigned short outport;
+
+ if (handler_listen(handler, 0, &outport))
+ {
+ fprintf(stderr, "%s: handler_listen failed\n", __func__);
+ goto end;
+ }
+
+ printf("Listening on port %hu\n", outport);
+
+ if (handler_loop(handler))
+ {
+ fprintf(stderr, "%s: handler_loop failed\n", __func__);
+ goto end;
+ }
+
+ ret = EXIT_SUCCESS;
+
+end:
+ handler_free(handler);
+ return ret;
+}
diff --git a/handler.c b/handler.c
index 8a777fa..98e6118 100644
--- a/handler.c
+++ b/handler.c
@@ -28,7 +28,8 @@ struct handler
struct handler *h;
struct server_client *c;
struct http_ctx *http;
- int (*fn)(const struct http_payload *, struct http_response *, void *);
+ union http_step step;
+ void *step_args;
struct client *next;
} *clients;
@@ -61,15 +62,19 @@ static int on_payload(const struct http_payload *const p,
if (e->op == p->op && !wildcard_cmp(p->resource, e->url, true))
{
+ union http_step *const s = &c->step;
int ret;
- if (c->fn)
- ret = c->fn(p, r, e->user);
+ if (s->payload)
+ ret = s->payload(p, r, e->user, c->step_args);
else
ret = e->f(p, r, e->user);
if (!ret)
- c->fn = r->step;
+ {
+ s->payload = r->step.payload;
+ c->step_args = r->step_args;
+ }
return ret;
}
@@ -92,10 +97,24 @@ static int on_length(const unsigned long long len,
struct client *const cl = user;
struct handler *const h = cl->h;
- if (h->cfg.length)
- return h->cfg.length(len, c, r, h->cfg.user);
+ if (!h->cfg.length)
+ return 0;
- return 0;
+ union http_step *const s = &cl->step;
+ int ret;
+
+ if (s->length)
+ ret = s->length(len, c, r, h->cfg.user, cl->step_args);
+ else
+ ret = h->cfg.length(len, c, r, h->cfg.user);
+
+ if (!ret)
+ {
+ s->length = r->step.length;
+ cl->step_args = r->step_args;
+ }
+
+ return ret;
}
static struct client *find_or_alloc_client(struct handler *const h,
diff --git a/http.c b/http.c
index 67aa62e..c173c3d 100644
--- a/http.c
+++ b/http.c
@@ -103,6 +103,8 @@ struct http_ctx
struct http_header *headers;
bool has_length, expect_continue;
int (*response)(struct http_ctx *);
+ void *buf;
+ size_t buflen;
} ctx;
struct write_ctx
@@ -653,6 +655,7 @@ static void ctx_free(struct ctx *const c)
free(c->headers);
free(c->args);
+ free(c->buf);
*c = (const struct ctx){0};
}
@@ -1220,7 +1223,7 @@ static int process_payload(struct http_ctx *const h)
ctx_free(c);
return ret;
}
- else if (!r->step)
+ else if (!r->step.payload)
{
h->wctx.op = c->op;
ctx_free(c);
@@ -1475,29 +1478,15 @@ static int check_length(struct http_ctx *const h)
return h->cfg.length(c->u2.payload.len, &cookie, &h->wctx.r, h->cfg.user);
}
-static int process_expect(struct http_ctx *const h)
+static int process_payload_expect(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;
- return start_response(h);
- }
-
+ struct http_response *const r = &h->wctx.r;
struct http_payload p = ctx_to_payload(c);
p.expect_continue = true;
- struct http_response *const r = &h->wctx.r;
const int ret = h->cfg.payload(&p, r, h->cfg.user);
if (ret)
@@ -1505,7 +1494,7 @@ static int process_expect(struct http_ctx *const h)
ctx_free(c);
return ret;
}
- else if (!r->step)
+ else if (!r->step.payload)
{
w->op = c->op;
c->state = BODY_LINE;
@@ -1515,6 +1504,30 @@ static int process_expect(struct http_ctx *const 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;
@@ -1530,7 +1543,7 @@ static int process_get(struct http_ctx *const h)
ctx_free(c);
return ret;
}
- else if (!r->step)
+ else if (!r->step.payload)
{
w->op = c->op;
ctx_free(c);
@@ -1544,6 +1557,34 @@ static int process_get(struct http_ctx *const 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;
@@ -1575,20 +1616,8 @@ static int header_cr_line(struct http_ctx *const h)
}
else if (c->boundary)
{
- const int res = check_length(h);
-
- if (res)
- {
- if (res < 0)
- {
- fprintf(stderr, "%s: check_length failed\n",
- __func__);
- return res;
- }
-
- h->wctx.close = true;
- return start_response(h);
- }
+ c->response = process_boundary;
+ return 0;
}
c->state = BODY_LINE;
@@ -1640,7 +1669,7 @@ static int send_payload(struct http_ctx *const h,
ctx_free(c);
return ret;
}
- else if (!r->step)
+ else if (!r->step.payload)
{
ctx_free(c);
return start_response(h);
@@ -2524,28 +2553,67 @@ static int read_buf(struct http_ctx *const h, const char *const buf,
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];
- const int r = h->cfg.read(buf, sizeof buf, h->cfg.user);
+ int ret, r;
- if (r <= 0)
+ 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)
{
- const int ret = read_buf(h, &buf[r - rem], &rem);
-
- if (ret)
+ if ((ret = read_buf(h, &buf[r - rem], &rem)))
+ goto failure;
+ else if (c->response && rem)
{
- ctx_free(&h->ctx);
- return ret;
+ 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)
@@ -2615,7 +2683,7 @@ int http_update(struct http_ctx *const h, bool *const write, bool *const close)
else
ret = w->pending ? http_write(h, close) : http_read(h, close);
- *write = c->response || w->pending;
+ *write = c->buf || c->response || w->pending;
return ret;
}
diff --git a/include/libweb/http.h b/include/libweb/http.h
index f18b5c4..87edf8a 100644
--- a/include/libweb/http.h
+++ b/include/libweb/http.h
@@ -109,7 +109,15 @@ struct http_response
unsigned long long n;
size_t n_headers;
void (*free)(void *);
- int (*step)(const struct http_payload *, struct http_response *, void *);
+ void *step_args;
+
+ union http_step
+ {
+ int (*length)(unsigned long long len, const struct http_cookie *c,
+ struct http_response *r, void *user, void *step_args);
+ int (*payload)(const struct http_payload *p, struct http_response *r,
+ void *user, void *step_args);
+ } step;
};
struct http_cfg