Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_form.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
29
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33#include <unistd.h>
34
35#include "nilorea/n_common.h"
36#include "nilorea/n_http_form.h"
37#include "nilorea/n_log.h"
38#include "nilorea/n_str.h"
39
41static int log_level = LOG_ERR;
43static int failures = 0;
44
48static void expect_true(const char* what, int ok) {
49 if (!ok) {
50 n_log(LOG_ERR, "FAIL: %s", what);
51 failures++;
52 } else {
53 n_log(LOG_INFO, "ok: %s", what);
54 }
55}
56
61static int process_args(int argc, char** argv) {
62 int opt = 0;
63
64 while ((opt = getopt(argc, argv, "V:")) != -1) {
65 if (opt == 'V') {
66 log_level = atoi(optarg);
67 } else {
68 return -1;
69 }
70 }
71 return 0;
72}
73
79static int decodes_to(const char* in, int flags, const char* want) {
80 N_STR* out = NULL;
81 int ok = n_http_percent_decode(in, strlen(in), flags, &out);
82 int res;
83 if (!want)
84 res = (ok == FALSE);
85 else
86 res = ok == TRUE && out && out->written == strlen(want) && memcmp(out->data, want, out->written) == 0;
87 free_nstr(&out);
88 return res;
89}
90
92static void test_decode(void) {
93 N_STR* out = NULL;
94
95 expect_true("plain text unchanged", decodes_to("abc", 0, "abc"));
96 expect_true("escapes decoded, any hex case", decodes_to("%2Fa%2fb%20c", 0, "/a/b c"));
97 expect_true("UTF-8 escapes decoded", decodes_to("caf%C3%A9", 0, "caf\xc3\xa9"));
98 expect_true("'+' kept without PLUS_SPACE", decodes_to("a+b", 0, "a+b"));
99 expect_true("'+' is a space with PLUS_SPACE", decodes_to("a+b", N_HTTP_FORM_PLUS_SPACE, "a b"));
100 expect_true("%2B stays '+' with PLUS_SPACE", decodes_to("a%2Bb", N_HTTP_FORM_PLUS_SPACE, "a+b"));
101 expect_true("empty input", decodes_to("", 0, ""));
102 expect_true("lone '%' refused", decodes_to("a%", 0, NULL));
103 expect_true("one hex digit refused", decodes_to("a%4", 0, NULL));
104 expect_true("non-hex escape refused", decodes_to("a%zz", 0, NULL));
105 expect_true("decoded NUL refused", decodes_to("a%00b", 0, NULL));
106 expect_true("decoded NUL refused even with RAW_BYTES", decodes_to("a%00b", N_HTTP_FORM_RAW_BYTES, NULL));
107 expect_true("ill-formed UTF-8 refused", decodes_to("%E9t%E9", 0, NULL));
108 expect_true("overlong '/' refused", decodes_to("%C0%AF", 0, NULL));
109 expect_true("ill-formed UTF-8 kept with RAW_BYTES", decodes_to("%E9", N_HTTP_FORM_RAW_BYTES, "\xe9"));
110
111 /* appends; a failure leaves the destination as it was */
112 out = char_to_nstr("x=");
113 expect_true("append", n_http_percent_decode("%41", 3, 0, &out) == TRUE && strcmp(out->data, "x=A") == 0);
114 expect_true("failed append returns FALSE", n_http_percent_decode("%4", 2, 0, &out) == FALSE);
115 expect_true("failed append leaves the destination", out->written == 3 && strcmp(out->data, "x=A") == 0);
116 free_nstr(&out);
117 expect_true("NULL destination refused", n_http_percent_decode("a", 1, 0, NULL) == FALSE);
118 expect_true("NULL source with a length refused", n_http_percent_decode(NULL, 1, 0, &out) == FALSE && out == NULL);
119}
120
122static void test_form(void) {
123 N_HTTP_FORM* f = NULL;
124 size_t vlen = 0;
125
126 f = n_http_form_parse("s=hello+world&page=2&&tag%5B%5D=a&tag%5B%5D=b&flag&e=&s=last", 60, 50, 0);
127 expect_true("query parses", f != NULL);
128 if (f) {
129 const char* v = NULL;
130 expect_true("empty pair skipped: 7 fields", f->nb_fields == 7);
131 expect_true("fields keep their order", strcmp(f->fields[0].name, "s") == 0 && strcmp(f->fields[1].name, "page") == 0);
132 v = n_http_form_get(f, "s", &vlen);
133 expect_true("last occurrence wins, like PHP", v && strcmp(v, "last") == 0 && vlen == 4);
134 expect_true("'+' decoded as a space in values", strcmp(f->fields[0].value, "hello world") == 0);
135 expect_true("bracket names decoded and counted", n_http_form_count(f, "tag[]") == 2);
136 v = n_http_form_get(f, "flag", &vlen);
137 expect_true("pair without '=' has an empty value", v && v[0] == '\0' && vlen == 0);
138 v = n_http_form_get(f, "e", &vlen);
139 expect_true("'name=' has an empty value", v && v[0] == '\0' && vlen == 0);
140 expect_true("absent field is NULL", n_http_form_get(f, "nope", &vlen) == NULL && vlen == 0);
141 expect_true("names are case-sensitive", n_http_form_get(f, "S", NULL) == NULL);
143 expect_true("free sets NULL", f == NULL);
144 }
145
146 f = n_http_form_parse("a=1&b=%zz&c=3", 13, 10, 0);
147 expect_true("bad escape fails the whole form", f == NULL);
148 f = n_http_form_parse("a=1&b=%zz&c=%00&d=%E9&=x&e=5", 28, 10, N_HTTP_FORM_SKIP_INVALID);
149 expect_true("SKIP_INVALID parses", f != NULL);
150 if (f) {
151 expect_true("SKIP_INVALID drops the bad pairs", f->nb_fields == 2 && n_http_form_get(f, "a", NULL) && n_http_form_get(f, "e", NULL));
153 }
154 f = n_http_form_parse("=x", 2, 10, 0);
155 expect_true("empty name refused", f == NULL);
156 f = n_http_form_parse("a=1&b=2&c=3", 11, 2, N_HTTP_FORM_SKIP_INVALID);
157 expect_true("more pairs than max_fields refused, even with SKIP_INVALID", f == NULL);
158 f = n_http_form_parse("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&a=1", 35, 1, 0);
159 expect_true("empty pairs do not count against max_fields", f != NULL && f->nb_fields == 1);
161 f = n_http_form_parse("v=%E9", 5, 4, N_HTTP_FORM_RAW_BYTES);
162 expect_true("RAW_BYTES keeps a latin-1 byte", f != NULL && f->fields[0].value_len == 1 && (unsigned char)f->fields[0].value[0] == 0xE9);
164 f = n_http_form_parse("", 0, 4, 0);
165 expect_true("empty query is an empty form", f != NULL && f->nb_fields == 0 && n_http_form_get(f, "a", NULL) == NULL);
167 f = n_http_form_parse(NULL, 0, 4, 0);
168 expect_true("NULL with length 0 is an empty form", f != NULL && f->nb_fields == 0);
170 expect_true("NULL with a length refused", n_http_form_parse(NULL, 3, 4, 0) == NULL);
171 expect_true("max_fields 0 refused", n_http_form_parse("a=1", 3, 0, 0) == NULL);
172 expect_true("count on NULL", n_http_form_count(NULL, "a") == 0);
173 n_http_form_free(NULL);
175}
176
182static int part_body_is(const char* buf, const N_HTTP_FORM_PART* p, const char* want) {
183 return p && p->body_len == strlen(want) && memcmp(buf + p->body_off, want, p->body_len) == 0;
184}
185
190static N_HTTP_FORM_MULTIPART* mp_parse(const char* body, size_t max) {
191 return n_http_form_multipart_parse(body, strlen(body), "XyZ", max);
192}
193
195#define BND(ct) n_http_form_multipart_boundary((ct), strlen(ct), b)
196
198static void test_multipart(void) {
199 char b[N_HTTP_FORM_BOUNDARY_MAX + 1];
201 static const char ok_body[] =
202 "--XyZ\r\n"
203 "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
204 "Hello\r\n"
205 "--XyZ\r\n"
206 "Content-Disposition: form-data; name=\"file\"; filename=\"a \\\"q\\\".png\"\r\n"
207 "Content-Type: image/png\r\n\r\n"
208 "\x89PNG\r\n--XyZnot-a-delimiter\r\n\r\n\x00\x01\r\n"
209 "--XyZ\r\n"
210 "Content-Disposition: form-data; name=\"empty\"\r\n\r\n"
211 "\r\n"
212 "--XyZ\r\n"
213 "content-disposition: FORM-DATA; filename=\"\"; name=\"tag[]\"\r\n\r\n"
214 "x\r\n"
215 "--XyZ\r\n"
216 "Content-Disposition: form-data; name=\"title\"\r\n\r\n"
217 "Second\r\n"
218 "--XyZ--\r\nepilogue ignored";
219 const size_t ok_len = sizeof(ok_body) - 1; /* the file part holds a NUL */
220
221 /* the boundary from Content-Type */
222 expect_true("boundary token", BND("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk") &&
223 strcmp(b, "----WebKitFormBoundary7MA4YWxk") == 0);
224 expect_true("boundary quoted, any case", BND("Multipart/Form-Data; charset=utf-8; BOUNDARY=\"a b:c\"") &&
225 strcmp(b, "a b:c") == 0);
226 expect_true("boundary inside another quoted parameter not taken",
227 BND("multipart/form-data; x=\"boundary=evil\"; boundary=real") && strcmp(b, "real") == 0);
228 expect_true("two boundaries refused", !BND("multipart/form-data; boundary=a; boundary=b"));
229 expect_true("other media type refused", !BND("multipart/mixed; boundary=a"));
230 expect_true("no boundary refused", !BND("multipart/form-data"));
231 expect_true("boundary over 70 refused",
232 !BND("multipart/form-data; boundary=12345678901234567890123456789012345678901234567890123456789012345678901"));
233 expect_true("boundary with a bad character refused", !BND("multipart/form-data; boundary=\"a<b\""));
234 expect_true("boundary ending in a space refused", !BND("multipart/form-data; boundary=\"ab \""));
235 expect_true("unterminated quote refused", !BND("multipart/form-data; boundary=\"ab"));
236
237 mp = n_http_form_multipart_parse(ok_body, ok_len, "XyZ", 10);
238 expect_true("browser body parses", mp && mp->nb_parts == 5);
239 if (mp) {
240 const N_HTTP_FORM_PART* f = n_http_form_multipart_get(mp, "file");
241 expect_true("first field", strcmp(mp->parts[0].name, "title") == 0 && !mp->parts[0].has_filename && part_body_is(ok_body, &mp->parts[0], "Hello"));
242 expect_true("file part: escaped quotes in filename", f && f->has_filename && strcmp(f->filename, "a \"q\".png") == 0 && strcmp(f->content_type, "image/png") == 0);
243 expect_true("file body binary-safe, look-alike delimiter kept", f && f->body_len == 4 + 2 + 20 + 4 + 2 && memcmp(ok_body + f->body_off, "\x89PNG\r\n--XyZnot-a-delimiter", 26) == 0);
244 expect_true("empty body", part_body_is(ok_body, n_http_form_multipart_get(mp, "empty"), ""));
245 expect_true("header names and form-data any case; filename=\"\" is a file field",
246 strcmp(mp->parts[3].name, "tag[]") == 0 && mp->parts[3].has_filename && mp->parts[3].filename[0] == '\0');
247 expect_true("last occurrence wins, count", part_body_is(ok_body, n_http_form_multipart_get(mp, "title"), "Second") && n_http_form_multipart_count(mp, "title") == 2);
248 expect_true("absent", n_http_form_multipart_get(mp, "nope") == NULL && n_http_form_multipart_count(mp, "nope") == 0);
250 expect_true("free sets NULL", mp == NULL);
251 }
252
253 /* the parameter confusion the old parser fell for */
254 mp = mp_parse("--XyZ\r\nContent-Disposition: form-data; filename=\"evil\"; name=\"real\"\r\n\r\nv\r\n--XyZ--", 4);
255 expect_true("name= inside filename= is not the name", mp && strcmp(mp->parts[0].name, "real") == 0 && strcmp(mp->parts[0].filename, "evil") == 0);
257 expect_true("two name= refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; name=\"b\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
258 expect_true("two filename= refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename=\"x\"; filename=\"y\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
259 expect_true("filename* refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename*=UTF-8''x\r\n\r\nv\r\n--XyZ--", 4) == NULL);
260 expect_true("two Content-Disposition refused",
261 mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
262 expect_true("no Content-Disposition refused", mp_parse("--XyZ\r\nContent-Type: text/plain\r\n\r\nv\r\n--XyZ--", 4) == NULL);
263 expect_true("not form-data refused", mp_parse("--XyZ\r\nContent-Disposition: attachment; name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
264 expect_true("empty name refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
265 expect_true("CR in a name refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\rb\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
266 expect_true("ill-formed UTF-8 filename refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"; filename=\"\xe9.png\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
267 expect_true("folded header refused", mp_parse("--XyZ\r\nContent-Disposition: form-data;\r\n name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
268 expect_true("header line without colon refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nbogus\r\n\r\nv\r\n--XyZ--", 4) == NULL);
269 /* truncation and limits */
270 expect_true("missing closing delimiter (truncated upload) refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nvalue", 4) == NULL);
271 expect_true("no blank line refused", mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n", 4) == NULL);
272 expect_true("more parts than max refused",
273 mp_parse("--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n1\r\n--XyZ\r\nContent-Disposition: form-data; name=\"b\"\r\n\r\n2\r\n--XyZ--", 1) == NULL);
274 {
275 static char big[N_HTTP_FORM_PART_HEAD_MAX + 200];
276 size_t o = (size_t)snprintf(big, sizeof(big), "--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\nX-Pad: ");
277 memset(big + o, 'p', N_HTTP_FORM_PART_HEAD_MAX);
279 memcpy(big + o, "\r\n\r\nv\r\n--XyZ--", 15);
280 expect_true("part header block over the cap refused", n_http_form_multipart_parse(big, o + 14, "XyZ", 4) == NULL);
281 }
282 {
283 char longname[300];
284 static char body[600];
285 memset(longname, 'n', N_HTTP_FORM_PART_NAME_MAX + 1);
286 longname[N_HTTP_FORM_PART_NAME_MAX + 1] = '\0';
287 snprintf(body, sizeof(body), "--XyZ\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\nv\r\n--XyZ--", longname);
288 expect_true("name over the cap refused", mp_parse(body, 4) == NULL);
289 }
290 /* framing details */
291 mp = mp_parse("preamble text\r\n--XyZ \r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nv\r\n--XyZ\t\r\nContent-Disposition: form-data; name=b\r\n\r\nw\r\n--XyZ--", 4);
292 expect_true("preamble, transport padding, token name", mp && mp->nb_parts == 2 && strcmp(mp->parts[1].name, "b") == 0);
294 expect_true("delimiter not at the start and no CRLF before it refused", mp_parse("junk--XyZ\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nv\r\n--XyZ--", 4) == NULL);
295 expect_true("closing delimiter first refused", mp_parse("--XyZ--", 4) == NULL);
296 expect_true("wrong boundary refused", n_http_form_multipart_parse("--AbC\r\n", 7, "XyZ", 4) == NULL);
297 expect_true("NULL / zero max refused", n_http_form_multipart_parse(NULL, 0, "XyZ", 4) == NULL && n_http_form_multipart_parse("--XyZ--", 7, "XyZ", 0) == NULL);
299}
300#undef BND
301
306int main(int argc, char** argv) {
307 if (process_args(argc, argv) != 0) {
308 fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
309 return 1;
310 }
312
313 test_decode();
314 test_form();
316
317 if (failures) {
318 n_log(LOG_ERR, "ex_http_form: %d failure(s)", failures);
319 return 1;
320 }
321 n_log(LOG_NOTICE, "ex_http_form: all checks passed");
322 return 0;
323}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
static N_HTTP_FORM_MULTIPART * mp_parse(const char *body, size_t max)
parse a NUL-terminated multipart body
static int decodes_to(const char *in, int flags, const char *want)
decode a NUL-terminated literal and compare with the expected result
static int part_body_is(const char *buf, const N_HTTP_FORM_PART *p, const char *want)
body of a part as a string comparison
static void test_multipart(void)
multipart/form-data
static void test_form(void)
query strings and urlencoded bodies
static void test_decode(void)
percent-decoding
#define BND(ct)
boundary of a NUL-terminated Content-Type literal, into b
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_INFO
informational
Definition n_log.h:106
size_t nb_parts
number of parts
size_t body_len
body length
size_t value_len
length of value
Definition n_http_form.h:74
char content_type[127+1]
the part's Content-Type as sent, "" when absent: untrusted
char * value
decoded value, "" for "name" or "name="
Definition n_http_form.h:72
int has_filename
non-zero when the part carried a filename parameter (a file field)
char * name
decoded name
Definition n_http_form.h:68
size_t body_off
body offset in the parsed buffer
N_HTTP_FIELD * fields
the fields, nb_fields of them
Definition n_http_form.h:80
char name[200+1]
field name (non-empty, UTF-8, no control byte)
char filename[255+1]
the filename as sent (UTF-8, no control byte, may be ""): untrusted, never use it as a path
N_HTTP_FORM_PART * parts
the parts, in order
size_t nb_fields
number of fields
Definition n_http_form.h:82
#define N_HTTP_FORM_SKIP_INVALID
n_http_form_parse(): drop a pair that fails to decode instead of failing
Definition n_http_form.h:63
N_HTTP_FORM_MULTIPART * n_http_form_multipart_parse(const char *buf, size_t len, const char *boundary, size_t max_parts)
parse a multipart/form-data body strictly, without copying it
#define N_HTTP_FORM_PART_HEAD_MAX
largest header block of one part
void n_http_form_multipart_free(N_HTTP_FORM_MULTIPART **mp)
free a parsed body and set *mp to NULL
size_t n_http_form_count(const N_HTTP_FORM *form, const char *name)
number of fields named name (exact, case-sensitive)
const N_HTTP_FORM_PART * n_http_form_multipart_get(const N_HTTP_FORM_MULTIPART *mp, const char *name)
the LAST part named name (PHP semantics)
const char * n_http_form_get(const N_HTTP_FORM *form, const char *name, size_t *value_len)
value of the LAST field named name (PHP semantics)
N_HTTP_FORM * n_http_form_parse(const char *s, size_t len, size_t max_fields, int flags)
decode a query string or an urlencoded body into fields
int n_http_percent_decode(const char *s, size_t len, int flags, N_STR **out)
percent-decode [s, s+len), appending to *out; malformed escapes, a decoded NUL and (without N_HTTP_FO...
size_t n_http_form_multipart_count(const N_HTTP_FORM_MULTIPART *mp, const char *name)
number of parts named name
#define N_HTTP_FORM_PLUS_SPACE
decode '+' as a space (query strings and urlencoded forms, not paths)
Definition n_http_form.h:59
#define N_HTTP_FORM_BOUNDARY_MAX
longest boundary RFC 2046 allows
#define N_HTTP_FORM_PART_NAME_MAX
longest multipart field name accepted
#define N_HTTP_FORM_RAW_BYTES
accept decoded bytes that are not well-formed UTF-8 (NUL stays refused)
Definition n_http_form.h:61
void n_http_form_free(N_HTTP_FORM **form)
free a form and set *form to NULL
decoded fields, in the order they were sent
Definition n_http_form.h:78
a parsed multipart/form-data body
one part of a multipart/form-data body; the body is NOT copied
size_t written
number of meaningful bytes in data, excluding the null terminator; the size including the null termin...
Definition n_str.h:68
char * data
the string
Definition n_str.h:63
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
Definition n_str.c:262
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
Strict percent-decoding, query strings and urlencoded forms.
Generic log system.
N_STR and string function declaration.