aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-09-30 23:50:33 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2025-10-02 15:52:14 +0200
commita0f5f7509bb9040752fa61fe0fdb447608e22b1c (patch)
tree53337bce28ed75f26953c5969af6bc76d8841f2b
parentbba0b62f4e9e17927b9a2cda51dd5a4aa1b1f14e (diff)
Implement form interface
This new interface allows library users to parse application/x-www-form-urlencoded data conveniently.
-rw-r--r--CMakeLists.txt1
-rw-r--r--Makefile1
-rw-r--r--doc/man7/Makefile1
-rw-r--r--doc/man7/libweb_form.7123
-rw-r--r--examples/CMakeLists.txt1
-rw-r--r--examples/form/CMakeLists.txt4
-rw-r--r--examples/form/Makefile29
-rw-r--r--examples/form/README.md39
-rw-r--r--examples/form/main.c48
-rw-r--r--form.c193
-rw-r--r--include/libweb/form.h15
11 files changed, 455 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 34dfe40..640fbfc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.13.5)
option(BUILD_EXAMPLES "Build examples" ON)
project(web LANGUAGES C VERSION 0.4.0)
add_library(${PROJECT_NAME}
+ form.c
handler.c
html.c
http.c
diff --git a/Makefile b/Makefile
index 6027ed4..af79de3 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,7 @@ CFLAGS = $(O) $(CDEFS) -g -Iinclude -Idynstr/include -fPIC -MD -MF $(@:.o=.d)
LDFLAGS = -shared
DEPS = $(OBJECTS:.o=.d)
OBJECTS = \
+ form.o \
handler.o \
html.o \
http.o \
diff --git a/doc/man7/Makefile b/doc/man7/Makefile
index f9dc22c..bf588e6 100644
--- a/doc/man7/Makefile
+++ b/doc/man7/Makefile
@@ -5,6 +5,7 @@ datarootdir = $(prefix)/share
mandir = $(datarootdir)/man
man7dir = $(mandir)/man7
OBJECTS = \
+ $(DESTDIR)$(man7dir)/libweb_form.7 \
$(DESTDIR)$(man7dir)/libweb_handler.7 \
$(DESTDIR)$(man7dir)/libweb_html.7 \
$(DESTDIR)$(man7dir)/libweb_http.7
diff --git a/doc/man7/libweb_form.7 b/doc/man7/libweb_form.7
new file mode 100644
index 0000000..8b21810
--- /dev/null
+++ b/doc/man7/libweb_form.7
@@ -0,0 +1,123 @@
+.TH LIBWEB_FORM 7 2025-10-02 0.5.0 "libweb Library Reference"
+
+.SH NAME
+libweb_form \- libweb www-form decoding
+
+.SH SYNOPSIS
+.LP
+.nf
+#include <libweb/form.h>
+.fi
+
+.SH DESCRIPTION
+This component allows library users to decode payloads with
+.IR "Content-Type application/x-www-form-urlencoded" .
+Contents are stored into an opaque abstraction, namely
+.IR "struct form" ,
+and can be retrieved with the functions described below.
+
+.IR libweb_form (7)
+provides the following functions:
+
+.IP \(bu 2
+.IR form_alloc (3):
+Parses a human-readable string of
+.IR application/x-www-form-urlencoded -encoded
+data and, if valid, allocates a
+.I struct form
+instance that can be used to interact with other functions from this section.
+
+.IP \(bu 2
+.IR form_value (3):
+Returns a value for a given key from a
+.I struct form
+instance previously allocated by
+.IR form_alloc (3).
+
+.IP \(bu 2
+.IR form_foreach (3):
+Executes a user-defined function for each key inside a
+.I struct form
+instance previously allocated by
+.IR form_alloc (3).
+
+.IP \(bu 2
+.IR form_free (3):
+Deallocates a
+.I struct form
+instance and all the data associated to it.
+
+.SH EXAMPLE
+
+The following source code shows how to parse
+.IR application/x-www-form-urlencoded -encoded
+data using the interface described by this section:
+
+.PP
+.in +4n
+.EX
+#include <libweb/form.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+static int print(const char *const key, const char *const value,
+ void *const user)
+{
+ unsigned *const cnt = user;
+
+ printf("key=%s, value=%s, cnt=%u\en", key, value, ++(*cnt));
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_FAILURE;
+ const char *payload;
+ struct form *f = NULL;
+ unsigned cnt = 0;
+ int n;
+
+ if (argc != 2)
+ {
+ fprintf(stderr, "Usage: %s <payload>\en", *argv);
+ goto end;
+ }
+ else if ((n = form_alloc(payload = argv[1], &f)) < 0)
+ {
+ fprintf(stderr, "%s: form_alloc failed\en", __func__);
+ goto end;
+ }
+ else if (n)
+ {
+ fprintf(stderr, "%s: invalid user input: %s\en", __func__, payload);
+ goto end;
+ }
+ else if (form_foreach(f, print, &cnt))
+ {
+ fprintf(stderr, "%s: form_foreach failed\en", __func__);
+ goto end;
+ }
+
+ ret = EXIT_SUCCESS;
+
+end:
+ form_free(f);
+ return ret;
+}
+.EE
+.in
+.PP
+
+.SH SEE ALSO
+.BR form_alloc (3),
+.BR form_value (3),
+.BR form_foreach (3),
+.BR form_free (3).
+
+.SH COPYRIGHT
+Copyright (C) 2023-2025 libweb contributors
+.P
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
index 4c2a6a2..3370acb 100644
--- a/examples/CMakeLists.txt
+++ b/examples/CMakeLists.txt
@@ -1,4 +1,5 @@
cmake_minimum_required(VERSION 3.13)
+add_subdirectory(form)
add_subdirectory(headers)
add_subdirectory(hello)
add_subdirectory(html)
diff --git a/examples/form/CMakeLists.txt b/examples/form/CMakeLists.txt
new file mode 100644
index 0000000..d855a89
--- /dev/null
+++ b/examples/form/CMakeLists.txt
@@ -0,0 +1,4 @@
+cmake_minimum_required(VERSION 3.13)
+project(form C)
+add_executable(${PROJECT_NAME} main.c)
+target_link_libraries(${PROJECT_NAME} PRIVATE web)
diff --git a/examples/form/Makefile b/examples/form/Makefile
new file mode 100644
index 0000000..a965d76
--- /dev/null
+++ b/examples/form/Makefile
@@ -0,0 +1,29 @@
+.POSIX:
+
+PROJECT = form
+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/form/README.md b/examples/form/README.md
new file mode 100644
index 0000000..6be1969
--- /dev/null
+++ b/examples/form/README.md
@@ -0,0 +1,39 @@
+# HTML forms example
+
+This example shows how to parse `application/x-www-form-urlencoded`-encoded
+data using `libweb`s `form` API, using untrusted user input.
+
+## 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 with a `application/x-www-form-urlencoded`-encoded given
+as its only argument.
+
+For example:
+
+```sh
+$ ./form 'username=admin&password=mypassword'
+```
+
+Shall return:
+
+```
+key=username, value=admin, cnt=1
+key=password, value=mypassword, cnt=2
+```
+
+An invalid string shall be reported to `stderr`:
+
+```sh
+$ ./form 'username=admin&thisisawrongvalue'
+```
+
+```
+main: invalid user input: username=admin&thisisawrongvalue
+```
diff --git a/examples/form/main.c b/examples/form/main.c
new file mode 100644
index 0000000..afc1954
--- /dev/null
+++ b/examples/form/main.c
@@ -0,0 +1,48 @@
+#include <libweb/form.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+static int print(const char *const key, const char *const value,
+ void *const user)
+{
+ unsigned *const cnt = user;
+
+ printf("key=%s, value=%s, cnt=%u\n", key, value, ++(*cnt));
+ return 0;
+}
+
+int main(int argc, char *argv[])
+{
+ int ret = EXIT_FAILURE;
+ const char *payload;
+ struct form *f = NULL;
+ unsigned cnt = 0;
+ int n;
+
+ if (argc != 2)
+ {
+ fprintf(stderr, "Usage: %s <payload>\n", *argv);
+ goto end;
+ }
+ else if ((n = form_alloc(payload = argv[1], &f)) < 0)
+ {
+ fprintf(stderr, "%s: form_alloc failed\n", __func__);
+ goto end;
+ }
+ else if (n)
+ {
+ fprintf(stderr, "%s: invalid user input: %s\n", __func__, payload);
+ goto end;
+ }
+ else if (form_foreach(f, print, &cnt))
+ {
+ fprintf(stderr, "%s: form_foreach failed\n", __func__);
+ goto end;
+ }
+
+ ret = EXIT_SUCCESS;
+
+end:
+ form_free(f);
+ return ret;
+}
diff --git a/form.c b/form.c
new file mode 100644
index 0000000..784710b
--- /dev/null
+++ b/form.c
@@ -0,0 +1,193 @@
+#define _POSIX_C_SOURCE 200809L
+
+#include "libweb/form.h"
+#include "libweb/http.h"
+#include <errno.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+struct pair
+{
+ char *key, *value;
+};
+
+struct form
+{
+ struct pair *pairs;
+ size_t n;
+};
+
+void form_free(struct form *const f)
+{
+ if (!f)
+ return;
+
+ for (size_t i = 0; i < f->n; i++)
+ {
+ struct pair *const p = &f->pairs[i];
+
+ free(p->key);
+ free(p->value);
+ }
+
+ free(f->pairs);
+ free(f);
+}
+
+const char *form_value(const struct form *const f, const char *const key)
+{
+ for (size_t i = 0; i < f->n; i++)
+ {
+ const struct pair *const p = &f->pairs[i];
+
+ if (!strcmp(p->key, key))
+ return p->value;
+ }
+
+ return NULL;
+}
+
+static char *alloc_form_data(const char *const s, const char **const end)
+{
+ const char *const next = strchr(s, '&');
+ char *const data = next ? strndup(s, next - s) : strdup(s);
+
+ if (!data)
+ {
+ fprintf(stderr, "%s: strndup/strdup(3): %s\n", __func__,
+ strerror(errno));
+ return NULL;
+ }
+
+ *end = next ? next + 1 : s + strlen(s);
+ return data;
+}
+
+static int append(struct form *const forms, const char **const s)
+{
+ int ret = -1;
+ const char *end;
+ char *const data = alloc_form_data(*s, &end), *enckey = NULL,
+ *encvalue = NULL, *key = NULL, *value = NULL;
+ struct pair *p;
+
+ if (!data)
+ {
+ fprintf(stderr, "%s: alloc_form_data failed\n", __func__);
+ goto end;
+ }
+
+ const char *const sep = strchr(data, '=');
+
+ if (!sep)
+ {
+ ret = 1;
+ goto end;
+ }
+ else if (!data || !*(sep + 1))
+ {
+ ret = 1;
+ goto end;
+ }
+
+ const size_t keylen = sep - data;
+
+ if (!(enckey = strndup(data, keylen)))
+ {
+ fprintf(stderr, "%s: strndup(3) enckey: %s\n",
+ __func__, strerror(errno));
+ goto end;
+ }
+ else if (!(encvalue = strdup(sep + 1)))
+ {
+ fprintf(stderr, "%s: strdup(3) encvalue: %s\n",
+ __func__, strerror(errno));
+ goto end;
+ }
+ /* HTML input forms use '+' for whitespace, rather than %20. */
+ else if ((ret = http_decode_url(enckey, true, &key)))
+ {
+ fprintf(stderr, "%s: http_decode_url enckey failed\n", __func__);
+ goto end;
+ }
+ else if ((ret = http_decode_url(encvalue, true, &value)))
+ {
+ fprintf(stderr, "%s: http_decode_url encvalue failed\n", __func__);
+ goto end;
+ }
+ else if (!(p = realloc(forms->pairs, (forms->n + 1) * sizeof *p)))
+ {
+ fprintf(stderr, "%s: realloc(3): %s\n", __func__, strerror(errno));
+ goto end;
+ }
+
+ forms->pairs = p;
+ p[forms->n++] = (const struct pair)
+ {
+ .key = key,
+ .value = value
+ };
+
+ *s = end;
+ ret = 0;
+
+end:
+ if (ret)
+ {
+ free(key);
+ free(value);
+ }
+
+ free(enckey);
+ free(encvalue);
+ free(data);
+ return ret;
+}
+
+int form_foreach(const struct form *const f, const form_iter it,
+ void *const user)
+{
+ for (size_t i = 0; i < f->n; i++)
+ {
+ const struct pair *const p = &f->pairs[i];
+ const int ret = it(p->key, p->value, user);
+
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+
+int form_alloc(const char *data, struct form **const out)
+{
+ int ret = -1;
+ struct form *const f = malloc(sizeof *f);
+
+ if (!f)
+ {
+ fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
+ goto failure;
+ }
+
+ *f = (const struct form){0};
+
+ while (*data)
+ if ((ret = append(f, &data)))
+ {
+ if (ret < 0)
+ fprintf(stderr, "%s: append_form failed\n", __func__);
+
+ goto failure;
+ }
+
+ *out = f;
+ return 0;
+
+failure:
+ free(f);
+ return ret;
+}
diff --git a/include/libweb/form.h b/include/libweb/form.h
new file mode 100644
index 0000000..84ede89
--- /dev/null
+++ b/include/libweb/form.h
@@ -0,0 +1,15 @@
+#ifndef LIBWEB_FORM_H
+#define LIBWEB_FORM_H
+
+#include <stddef.h>
+
+typedef int (*form_iter)(const char *key, const char *value, void *user);
+
+struct form;
+
+int form_alloc(const char *data, struct form **f);
+const char *form_value(const struct form *f, const char *key);
+int form_foreach(const struct form *f, form_iter it, void *user);
+void form_free(struct form *f);
+
+#endif