Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_response.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
30
31#include <fcntl.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35#include <unistd.h>
36
37#include "nilorea/n_common.h"
39#include "nilorea/n_log.h"
40#include "nilorea/n_str.h"
41
43static int log_level = LOG_ERR;
45static int failures = 0;
46
50static void expect_true(const char* what, int ok) {
51 if (!ok) {
52 n_log(LOG_ERR, "FAIL: %s", what);
53 failures++;
54 } else {
55 n_log(LOG_INFO, "ok: %s", what);
56 }
57}
58
63static int process_args(int argc, char** argv) {
64 int opt = 0;
65
66 while ((opt = getopt(argc, argv, "V:")) != -1) {
67 if (opt == 'V') {
68 log_level = atoi(optarg);
69 } else {
70 return -1;
71 }
72 }
73 return 0;
74}
75
77typedef struct {
79 size_t chunk;
80 long fail_at;
81} sink;
82
88static ssize_t sink_write(void* ctx, const void* buf, size_t len) {
89 sink* s = (sink*)ctx;
90 size_t n = len < s->chunk ? len : s->chunk;
91 if (s->fail_at >= 0 && s->data && (long)s->data->written >= s->fail_at)
92 return -1;
93 if (!nstrcat_ex(&s->data, (void*)buf, n, 1))
94 return -1;
95 return (ssize_t)n;
96}
97
102static int head_is(const N_HTTP_RESP* r, const char* want) {
103 N_STR* h = NULL;
104 char* date;
105 int res;
106 if (!n_http_response_head(r, &h))
107 return 0;
108 /* drop the Date line, which changes every second */
109 date = strstr(h->data, "Date: ");
110 if (date) {
111 char* end = strstr(date, "\r\n");
112 memmove(date, end + 2, strlen(end + 2) + 1);
113 h->written = strlen(h->data);
114 }
115 res = strcmp(h->data, want) == 0;
116 if (!res)
117 n_log(LOG_ERR, "head was:\n%s", h->data);
118 free_nstr(&h);
119 return res;
120}
121
123static void test_headers(void) {
125 N_STR* h = NULL;
126
127 expect_true("new", r != NULL);
128 if (!r)
129 return;
130 expect_true("add", n_http_response_add_header(r, "Content-Type", "text/html; charset=utf-8"));
131 expect_true("repeat allowed", n_http_response_add_header(r, "Set-Cookie", "a=1") && n_http_response_add_header(r, "Set-Cookie", "b=2"));
132 expect_true("value trimmed", n_http_response_add_header(r, "X-Trim", " v \t") && strcmp(n_http_response_get_header(r, "x-trim"), "v") == 0);
133 expect_true("CRLF in a value refused (response splitting)", !n_http_response_add_header(r, "Location", "/a\r\nSet-Cookie: evil=1"));
134 expect_true("LF in a value refused", !n_http_response_add_header(r, "Location", "/a\nb"));
135 expect_true("NUL-free control byte refused", !n_http_response_add_header(r, "X-A", "a\x01"));
136 expect_true("DEL refused", !n_http_response_add_header(r, "X-A", "a\x7f"));
137 expect_true("tab and UTF-8 accepted", n_http_response_add_header(r, "X-B", "a\tcaf\xc3\xa9"));
138 {
139 /* found by fuzzing: an empty value used to make the whole head fail */
141 expect_true("empty value accepted and serialized", n_http_response_add_header(e, "X-Empty", " ") &&
142 head_is(e, "HTTP/1.1 200 OK\r\nX-Empty: \r\nContent-Length: 0\r\nConnection: close\r\n\r\n"));
144 }
145 expect_true("bad name refused", !n_http_response_add_header(r, "X A", "v") && !n_http_response_add_header(r, "", "v") && !n_http_response_add_header(r, "X:A", "v"));
146 expect_true("Content-Length is the builder's", !n_http_response_add_header(r, "content-length", "5"));
147 expect_true("Transfer-Encoding is the builder's", !n_http_response_add_header(r, "Transfer-Encoding", "chunked"));
148 expect_true("Connection is the builder's", !n_http_response_add_header(r, "Connection", "keep-alive"));
149 expect_true("NULL value refused", !n_http_response_add_header(r, "X-C", NULL));
150 expect_true("set replaces every older one", n_http_response_set_header(r, "set-cookie", "c=3") && strcmp(n_http_response_get_header(r, "Set-Cookie"), "c=3") == 0);
151 expect_true("refused set keeps the old value", !n_http_response_set_header(r, "Content-Type", "x\r\n") && strcmp(n_http_response_get_header(r, "content-type"), "text/html; charset=utf-8") == 0);
152 expect_true("remove", n_http_response_remove_header(r, "X-B") == 1 && n_http_response_get_header(r, "X-B") == NULL);
153 expect_true("body", n_http_response_append(r, "hello", 5) && n_http_response_append(r, " world", 6));
154 expect_true("default: HTTP/1.1, close",
155 head_is(r,
156 "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
157 "Content-Length: 11\r\nConnection: close\r\n\r\n"));
159 expect_true("HTTP/1.1 keep-alive sends no Connection",
160 head_is(r,
161 "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
162 "Content-Length: 11\r\n\r\n"));
164 expect_true("HTTP/1.0 keep-alive says so",
165 head_is(r,
166 "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
167 "Content-Length: 11\r\nConnection: keep-alive\r\n\r\n"));
168 expect_true("bad version refused", !n_http_response_set_connection(r, 1, 2, 0));
169 expect_true("Date added", n_http_response_head(r, &h) && strstr(h->data, "\r\nDate: ") != NULL && strstr(h->data, " GMT\r\n") != NULL);
170 free_nstr(&h);
171 expect_true("caller's Date kept", n_http_response_set_header(r, "Date", "Thu, 01 Jan 1970 00:00:00 GMT") && n_http_response_head(r, &h) &&
172 strstr(h->data, "Date: Thu, 01 Jan 1970 00:00:00 GMT\r\n") != NULL && strstr(h->data + 20, "Date:") == strstr(h->data, "Date:"));
173 free_nstr(&h);
174 {
175 int i, full = 1;
177 for (i = 0; i < N_HTTP_RESPONSE_HEADERS_MAX; i++)
178 full = full && n_http_response_add_header(m, "X-N", "v");
179 expect_true("header cap reached", full && !n_http_response_add_header(m, "X-N", "v"));
181 }
183 expect_true("free sets NULL", r == NULL);
184}
185
187static void test_status(void) {
188 N_HTTP_RESP* r;
189
190 expect_true("reasons", strcmp(n_http_status_reason(404), "Not Found") == 0 && strcmp(n_http_status_reason(429), "Too Many Requests") == 0 &&
191 strcmp(n_http_status_reason(413), "Content Too Large") == 0 && strcmp(n_http_status_reason(299), "Success") == 0 &&
192 strcmp(n_http_status_reason(99), "") == 0 && strcmp(n_http_status_reason(600), "") == 0);
193 expect_true("status out of range refused", n_http_response_new(99) == NULL && n_http_response_new(600) == NULL);
194 r = n_http_response_new(304);
195 n_http_response_add_header(r, "ETag", "\"abc\"");
196 n_http_response_append(r, "ignored", 7);
198 expect_true("304: no Content-Length", head_is(r, "HTTP/1.1 304 Not Modified\r\nETag: \"abc\"\r\n\r\n"));
199 expect_true("304: body length 0", n_http_response_body_length(r) == 0);
200 {
201 sink s = {NULL, 1000, -1};
202 expect_true("304: no body written", n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "ignored") == NULL);
203 free_nstr(&s.data);
204 }
205 expect_true("set_status", n_http_response_set_status(r, 204) && !n_http_response_set_status(r, 1000));
206 expect_true("204: no Content-Length", head_is(r, "HTTP/1.1 204 No Content\r\nETag: \"abc\"\r\n\r\n"));
208 n_http_response_set_header(r, "Location", "/x/");
209 expect_true("302 with its body length", head_is(r, "HTTP/1.1 302 Found\r\nETag: \"abc\"\r\nLocation: /x/\r\nContent-Length: 7\r\n\r\n"));
211 r = n_http_response_new(404);
212 expect_true("empty body: Content-Length 0", head_is(r, "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"));
214}
215
217static void test_write(void) {
219 char path[] = "ex_http_response_XXXXXX";
220 int fd;
221 char data[200000];
222 size_t i;
223
224 n_http_response_append(r, "0123456789", 10);
225 {
226 sink s = {NULL, 7, -1};
227 expect_true("memory body written in 7-byte pieces", n_http_response_write(r, sink_write, &s) && s.data);
228 expect_true("ends with the body", s.data && s.data->written > 10 && memcmp(s.data->data + s.data->written - 14, "\r\n\r\n0123456789", 14) == 0);
229 free_nstr(&s.data);
230 }
232 {
233 sink s = {NULL, 1000, -1};
234 expect_true("HEAD writes the head only, with the full length",
235 n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "Content-Length: 10\r\n") &&
236 memcmp(s.data->data + s.data->written - 4, "\r\n\r\n", 4) == 0);
237 free_nstr(&s.data);
238 }
240 {
241 sink s = {NULL, 3, 20};
242 expect_true("a failing write fails the response", n_http_response_write(r, sink_write, &s) == FALSE);
243 free_nstr(&s.data);
244 }
245
246 /* a file body: 200000 bytes, streamed in chunks, then a middle range */
247 for (i = 0; i < sizeof(data); i++)
248 data[i] = (char)('a' + i % 26);
249 fd = mkstemp(path);
250 expect_true("temp file", fd >= 0);
251 if (fd >= 0) {
252 int fd2;
253 expect_true("temp file written", write(fd, data, sizeof(data)) == (ssize_t)sizeof(data));
254 close(fd);
255 fd2 = open(path, O_RDONLY);
256 expect_true("file body", n_http_response_file(r, fd2, 0, (long long)sizeof(data)));
257 expect_true("file body length", n_http_response_body_length(r) == (long long)sizeof(data));
258 {
259 sink s = {NULL, 4093, -1};
260 expect_true("file body written", n_http_response_write(r, sink_write, &s) && s.data);
261 expect_true("file body bytes", s.data && s.data->written > sizeof(data) && memcmp(s.data->data + s.data->written - sizeof(data), data, sizeof(data)) == 0);
262 free_nstr(&s.data);
263 }
264 fd2 = open(path, O_RDONLY);
265 expect_true("file range", n_http_response_file(r, fd2, 100001, 26));
266 {
267 sink s = {NULL, 1000, -1};
268 expect_true("file range written", n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "Content-Length: 26\r\n"));
269 expect_true("file range bytes", s.data && memcmp(s.data->data + s.data->written - 26, data + 100001, 26) == 0);
270 free_nstr(&s.data);
271 }
272 fd2 = open(path, O_RDONLY);
273 n_http_response_file(r, fd2, (long long)sizeof(data) - 5, 10);
274 {
275 sink s = {NULL, 1000, -1};
276 expect_true("file shorter than declared fails", n_http_response_write(r, sink_write, &s) == FALSE);
277 free_nstr(&s.data);
278 }
279 expect_true("bad file arguments refused", !n_http_response_file(r, -1, 0, 1) && !n_http_response_file(r, open(path, O_RDONLY), -1, 1));
280 expect_true("memory body replaces the file", n_http_response_append(r, "x", 1) && n_http_response_body_length(r) == 1);
281 unlink(path);
282 }
284 expect_true("NULL arguments", n_http_response_write(NULL, sink_write, NULL) == FALSE && n_http_response_head(NULL, NULL) == FALSE &&
285 n_http_response_body_length(NULL) == -1 && n_http_response_append(NULL, "a", 1) == FALSE);
287}
288
290static void test_range(void) {
291 long long a = -1, b = -1;
292#define RANGE(s, total) n_http_parse_range((s), strlen(s), (total), &a, &b)
293 expect_true("a-b", RANGE("bytes=0-99", 1000) == N_HTTP_RANGE_OK && a == 0 && b == 99);
294 expect_true("a-", RANGE("bytes=900-", 1000) == N_HTTP_RANGE_OK && a == 900 && b == 999);
295 expect_true("suffix", RANGE("bytes=-100", 1000) == N_HTTP_RANGE_OK && a == 900 && b == 999);
296 expect_true("suffix longer than the body", RANGE("bytes=-5000", 1000) == N_HTTP_RANGE_OK && a == 0 && b == 999);
297 expect_true("end clamped", RANGE("bytes=990-5000", 1000) == N_HTTP_RANGE_OK && a == 990 && b == 999);
298 expect_true("start past the end", RANGE("bytes=1000-", 1000) == N_HTTP_RANGE_UNSATISFIABLE);
299 expect_true("empty suffix", RANGE("bytes=-0", 1000) == N_HTTP_RANGE_UNSATISFIABLE);
300 expect_true("empty body", RANGE("bytes=0-", 0) == N_HTTP_RANGE_UNSATISFIABLE);
301 expect_true("several ranges: whole body", RANGE("bytes=0-1,5-6", 1000) == N_HTTP_RANGE_NONE);
302 expect_true("other unit: whole body", RANGE("items=0-1", 1000) == N_HTTP_RANGE_NONE);
303 expect_true("reversed: whole body", RANGE("bytes=50-10", 1000) == N_HTTP_RANGE_NONE);
304 expect_true("garbage: whole body", RANGE("bytes=a-b", 1000) == N_HTTP_RANGE_NONE && RANGE("bytes=-", 1000) == N_HTTP_RANGE_NONE &&
305 RANGE("bytes=", 1000) == N_HTTP_RANGE_NONE && RANGE("bytes= 1-2", 1000) == N_HTTP_RANGE_NONE);
306 expect_true("huge number: whole body", RANGE("bytes=99999999999999999999-", 1000) == N_HTTP_RANGE_NONE);
307 expect_true("NULL: whole body", n_http_parse_range(NULL, 0, 10, &a, &b) == N_HTTP_RANGE_NONE);
308#undef RANGE
309}
310
315int main(int argc, char** argv) {
316 if (process_args(argc, argv) != 0) {
317 fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
318 return 1;
319 }
321
322 test_headers();
323 test_status();
324 test_write();
325 test_range();
326
327 if (failures) {
328 n_log(LOG_ERR, "ex_http_response: %d failure(s)", failures);
329 return 1;
330 }
331 n_log(LOG_NOTICE, "ex_http_response: all checks passed");
332 return 0;
333}
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 void test_headers(void)
headers: validation, set/remove, framing
#define RANGE(s, total)
static void test_status(void)
status codes and bodiless responses
static int head_is(const N_HTTP_RESP *r, const char *want)
text of the head of r, up to and excluding the Date line's value
long fail_at
fail once this many bytes were written, -1 never
static ssize_t sink_write(void *ctx, const void *buf, size_t len)
write function: accept at most s->chunk bytes
size_t chunk
most bytes accepted per call (partial writes)
static void test_write(void)
writing memory and file bodies, HEAD, partial and failing writes
static void test_range(void)
Range header parsing.
N_STR * data
everything written
an in-memory sink for n_http_response_write()
static int h(void *ctx, const N_ROUTE_MATCH *m)
a handler that does nothing (the test only routes)
#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
N_HTTP_RANGE n_http_parse_range(const char *value, size_t len, long long total, long long *first, long long *last)
interpret a single-range Range header for a body of total bytes
#define N_HTTP_RESPONSE_HEADERS_MAX
most headers a response may carry
int n_http_response_append(N_HTTP_RESP *r, const void *data, size_t len)
append bytes to the memory body (drops a file body)
const char * n_http_status_reason(int status)
reason phrase of a status code, a class phrase for an unlisted one
int n_http_response_head(const N_HTTP_RESP *r, N_STR **out)
append the status line and headers, through the blank line
int n_http_response_set_header(N_HTTP_RESP *r, const char *name, const char *value)
set a header: remove every header of that name, then add it
int n_http_response_add_header(N_HTTP_RESP *r, const char *name, const char *value)
add a header line after validating name and value
int n_http_response_file(N_HTTP_RESP *r, int fd, long long offset, long long length)
use a file range as the body; the response owns fd from now on
int n_http_response_set_status(N_HTTP_RESP *r, int status)
change the status code
long long n_http_response_body_length(const N_HTTP_RESP *r)
body length the response declares, 0 for a bodiless status
size_t n_http_response_remove_header(N_HTTP_RESP *r, const char *name)
remove every header of that name (ASCII case-insensitive)
int n_http_response_write(const N_HTTP_RESP *r, n_http_write_fn fn, void *ctx)
write the whole response (head, then body unless HEAD or bodiless)
int n_http_response_set_connection(N_HTTP_RESP *r, int keep_alive, int version_minor, int head_only)
connection handling for this response
void n_http_response_free(N_HTTP_RESP **r)
free a response (closing its file body) and set *r to NULL
const char * n_http_response_get_header(const N_HTTP_RESP *r, const char *name)
value of the first header of that name (ASCII case-insensitive)
N_HTTP_RESP * n_http_response_new(int status)
new response with a status, keep-alive off, HTTP/1.1
@ N_HTTP_RANGE_NONE
no usable Range: send the whole body (200)
@ N_HTTP_RANGE_UNSATISFIABLE
send 416, with a Content-Range that gives only the total
@ N_HTTP_RANGE_OK
one satisfiable range: send 206
a response under construction (fields are private, use the functions)
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 * nstrcat_ex(N_STR **dest, void *src, NSTRBYTE size, int resize_flag)
Append data into N_STR using internal N_STR size and cursor position.
Definition n_str.c:1096
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
HTTP/1.1 response: validated headers, memory or file body, framing.
Generic log system.
N_STR and string function declaration.