diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-09-30 23:50:33 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-10-02 15:52:14 +0200 |
| commit | a0f5f7509bb9040752fa61fe0fdb447608e22b1c (patch) | |
| tree | 53337bce28ed75f26953c5969af6bc76d8841f2b | |
| parent | bba0b62f4e9e17927b9a2cda51dd5a4aa1b1f14e (diff) | |
Implement form interface
This new interface allows library users to parse
application/x-www-form-urlencoded data conveniently.
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | doc/man7/Makefile | 1 | ||||
| -rw-r--r-- | doc/man7/libweb_form.7 | 123 | ||||
| -rw-r--r-- | examples/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | examples/form/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | examples/form/Makefile | 29 | ||||
| -rw-r--r-- | examples/form/README.md | 39 | ||||
| -rw-r--r-- | examples/form/main.c | 48 | ||||
| -rw-r--r-- | form.c | 193 | ||||
| -rw-r--r-- | include/libweb/form.h | 15 |
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 @@ -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; +} @@ -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 |
