Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_response.c

n_http_response: header validation (no response splitting), framing (Content-Length, Connection, Date), HEAD and bodiless statuses, memory and file bodies written through a write function that takes a few bytes at a time, single-range parsing

n_http_response: header validation (no response splitting), framing (Content-Length, Connection, Date), HEAD and bodiless statuses, memory and file bodies written through a write function that takes a few bytes at a time, single-range parsing

Author
Castagnier Mickael
Version
1.0
Date
23/10/2026
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "nilorea/n_log.h"
#include "nilorea/n_str.h"
static int log_level = LOG_ERR;
static int failures = 0;
static void expect_true(const char* what, int ok) {
if (!ok) {
n_log(LOG_ERR, "FAIL: %s", what);
} else {
n_log(LOG_INFO, "ok: %s", what);
}
}
static int process_args(int argc, char** argv) {
int opt = 0;
while ((opt = getopt(argc, argv, "V:")) != -1) {
if (opt == 'V') {
log_level = atoi(optarg);
} else {
return -1;
}
}
return 0;
}
typedef struct {
N_STR* data;
size_t chunk;
long fail_at;
} sink;
static ssize_t sink_write(void* ctx, const void* buf, size_t len) {
sink* s = (sink*)ctx;
size_t n = len < s->chunk ? len : s->chunk;
if (s->fail_at >= 0 && s->data && (long)s->data->written >= s->fail_at)
return -1;
if (!nstrcat_ex(&s->data, (void*)buf, n, 1))
return -1;
return (ssize_t)n;
}
static int head_is(const N_HTTP_RESP* r, const char* want) {
N_STR* h = NULL;
char* date;
int res;
return 0;
/* drop the Date line, which changes every second */
date = strstr(h->data, "Date: ");
if (date) {
char* end = strstr(date, "\r\n");
memmove(date, end + 2, strlen(end + 2) + 1);
h->written = strlen(h->data);
}
res = strcmp(h->data, want) == 0;
if (!res)
n_log(LOG_ERR, "head was:\n%s", h->data);
return res;
}
static void test_headers(void) {
N_STR* h = NULL;
expect_true("new", r != NULL);
if (!r)
return;
expect_true("add", n_http_response_add_header(r, "Content-Type", "text/html; charset=utf-8"));
expect_true("repeat allowed", n_http_response_add_header(r, "Set-Cookie", "a=1") && n_http_response_add_header(r, "Set-Cookie", "b=2"));
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);
expect_true("CRLF in a value refused (response splitting)", !n_http_response_add_header(r, "Location", "/a\r\nSet-Cookie: evil=1"));
expect_true("LF in a value refused", !n_http_response_add_header(r, "Location", "/a\nb"));
expect_true("NUL-free control byte refused", !n_http_response_add_header(r, "X-A", "a\x01"));
expect_true("DEL refused", !n_http_response_add_header(r, "X-A", "a\x7f"));
expect_true("tab and UTF-8 accepted", n_http_response_add_header(r, "X-B", "a\tcaf\xc3\xa9"));
{
/* found by fuzzing: an empty value used to make the whole head fail */
expect_true("empty value accepted and serialized", n_http_response_add_header(e, "X-Empty", " ") &&
head_is(e, "HTTP/1.1 200 OK\r\nX-Empty: \r\nContent-Length: 0\r\nConnection: close\r\n\r\n"));
}
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"));
expect_true("Content-Length is the builder's", !n_http_response_add_header(r, "content-length", "5"));
expect_true("Transfer-Encoding is the builder's", !n_http_response_add_header(r, "Transfer-Encoding", "chunked"));
expect_true("Connection is the builder's", !n_http_response_add_header(r, "Connection", "keep-alive"));
expect_true("NULL value refused", !n_http_response_add_header(r, "X-C", NULL));
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);
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);
expect_true("remove", n_http_response_remove_header(r, "X-B") == 1 && n_http_response_get_header(r, "X-B") == NULL);
expect_true("body", n_http_response_append(r, "hello", 5) && n_http_response_append(r, " world", 6));
expect_true("default: HTTP/1.1, close",
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
"Content-Length: 11\r\nConnection: close\r\n\r\n"));
expect_true("HTTP/1.1 keep-alive sends no Connection",
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
"Content-Length: 11\r\n\r\n"));
expect_true("HTTP/1.0 keep-alive says so",
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nX-Trim: v\r\nset-cookie: c=3\r\n"
"Content-Length: 11\r\nConnection: keep-alive\r\n\r\n"));
expect_true("bad version refused", !n_http_response_set_connection(r, 1, 2, 0));
expect_true("Date added", n_http_response_head(r, &h) && strstr(h->data, "\r\nDate: ") != NULL && strstr(h->data, " GMT\r\n") != NULL);
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) &&
strstr(h->data, "Date: Thu, 01 Jan 1970 00:00:00 GMT\r\n") != NULL && strstr(h->data + 20, "Date:") == strstr(h->data, "Date:"));
{
int i, full = 1;
for (i = 0; i < N_HTTP_RESPONSE_HEADERS_MAX; i++)
full = full && n_http_response_add_header(m, "X-N", "v");
expect_true("header cap reached", full && !n_http_response_add_header(m, "X-N", "v"));
}
expect_true("free sets NULL", r == NULL);
}
static void test_status(void) {
expect_true("reasons", strcmp(n_http_status_reason(404), "Not Found") == 0 && strcmp(n_http_status_reason(429), "Too Many Requests") == 0 &&
strcmp(n_http_status_reason(413), "Content Too Large") == 0 && strcmp(n_http_status_reason(299), "Success") == 0 &&
strcmp(n_http_status_reason(99), "") == 0 && strcmp(n_http_status_reason(600), "") == 0);
expect_true("status out of range refused", n_http_response_new(99) == NULL && n_http_response_new(600) == NULL);
n_http_response_add_header(r, "ETag", "\"abc\"");
n_http_response_append(r, "ignored", 7);
expect_true("304: no Content-Length", head_is(r, "HTTP/1.1 304 Not Modified\r\nETag: \"abc\"\r\n\r\n"));
expect_true("304: body length 0", n_http_response_body_length(r) == 0);
{
sink s = {NULL, 1000, -1};
expect_true("304: no body written", n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "ignored") == NULL);
}
expect_true("204: no Content-Length", head_is(r, "HTTP/1.1 204 No Content\r\nETag: \"abc\"\r\n\r\n"));
n_http_response_set_header(r, "Location", "/x/");
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"));
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"));
}
static void test_write(void) {
char path[] = "ex_http_response_XXXXXX";
int fd;
char data[200000];
size_t i;
n_http_response_append(r, "0123456789", 10);
{
sink s = {NULL, 7, -1};
expect_true("memory body written in 7-byte pieces", n_http_response_write(r, sink_write, &s) && s.data);
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);
}
{
sink s = {NULL, 1000, -1};
expect_true("HEAD writes the head only, with the full length",
n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "Content-Length: 10\r\n") &&
memcmp(s.data->data + s.data->written - 4, "\r\n\r\n", 4) == 0);
}
{
sink s = {NULL, 3, 20};
expect_true("a failing write fails the response", n_http_response_write(r, sink_write, &s) == FALSE);
}
/* a file body: 200000 bytes, streamed in chunks, then a middle range */
for (i = 0; i < sizeof(data); i++)
data[i] = (char)('a' + i % 26);
fd = mkstemp(path);
expect_true("temp file", fd >= 0);
if (fd >= 0) {
int fd2;
expect_true("temp file written", write(fd, data, sizeof(data)) == (ssize_t)sizeof(data));
close(fd);
fd2 = open(path, O_RDONLY);
expect_true("file body", n_http_response_file(r, fd2, 0, (long long)sizeof(data)));
expect_true("file body length", n_http_response_body_length(r) == (long long)sizeof(data));
{
sink s = {NULL, 4093, -1};
expect_true("file body written", n_http_response_write(r, sink_write, &s) && s.data);
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);
}
fd2 = open(path, O_RDONLY);
expect_true("file range", n_http_response_file(r, fd2, 100001, 26));
{
sink s = {NULL, 1000, -1};
expect_true("file range written", n_http_response_write(r, sink_write, &s) && s.data && strstr(s.data->data, "Content-Length: 26\r\n"));
expect_true("file range bytes", s.data && memcmp(s.data->data + s.data->written - 26, data + 100001, 26) == 0);
}
fd2 = open(path, O_RDONLY);
n_http_response_file(r, fd2, (long long)sizeof(data) - 5, 10);
{
sink s = {NULL, 1000, -1};
expect_true("file shorter than declared fails", n_http_response_write(r, sink_write, &s) == FALSE);
}
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));
expect_true("memory body replaces the file", n_http_response_append(r, "x", 1) && n_http_response_body_length(r) == 1);
unlink(path);
}
expect_true("NULL arguments", n_http_response_write(NULL, sink_write, NULL) == FALSE && n_http_response_head(NULL, NULL) == FALSE &&
n_http_response_body_length(NULL) == -1 && n_http_response_append(NULL, "a", 1) == FALSE);
}
static void test_range(void) {
long long a = -1, b = -1;
#define RANGE(s, total) n_http_parse_range((s), strlen(s), (total), &a, &b)
expect_true("a-b", RANGE("bytes=0-99", 1000) == N_HTTP_RANGE_OK && a == 0 && b == 99);
expect_true("a-", RANGE("bytes=900-", 1000) == N_HTTP_RANGE_OK && a == 900 && b == 999);
expect_true("suffix", RANGE("bytes=-100", 1000) == N_HTTP_RANGE_OK && a == 900 && b == 999);
expect_true("suffix longer than the body", RANGE("bytes=-5000", 1000) == N_HTTP_RANGE_OK && a == 0 && b == 999);
expect_true("end clamped", RANGE("bytes=990-5000", 1000) == N_HTTP_RANGE_OK && a == 990 && b == 999);
expect_true("start past the end", RANGE("bytes=1000-", 1000) == N_HTTP_RANGE_UNSATISFIABLE);
expect_true("empty suffix", RANGE("bytes=-0", 1000) == N_HTTP_RANGE_UNSATISFIABLE);
expect_true("empty body", RANGE("bytes=0-", 0) == N_HTTP_RANGE_UNSATISFIABLE);
expect_true("several ranges: whole body", RANGE("bytes=0-1,5-6", 1000) == N_HTTP_RANGE_NONE);
expect_true("other unit: whole body", RANGE("items=0-1", 1000) == N_HTTP_RANGE_NONE);
expect_true("reversed: whole body", RANGE("bytes=50-10", 1000) == N_HTTP_RANGE_NONE);
expect_true("garbage: whole body", RANGE("bytes=a-b", 1000) == N_HTTP_RANGE_NONE && RANGE("bytes=-", 1000) == N_HTTP_RANGE_NONE &&
RANGE("bytes=", 1000) == N_HTTP_RANGE_NONE && RANGE("bytes= 1-2", 1000) == N_HTTP_RANGE_NONE);
expect_true("huge number: whole body", RANGE("bytes=99999999999999999999-", 1000) == N_HTTP_RANGE_NONE);
expect_true("NULL: whole body", n_http_parse_range(NULL, 0, 10, &a, &b) == N_HTTP_RANGE_NONE);
#undef RANGE
}
int main(int argc, char** argv) {
if (process_args(argc, argv) != 0) {
fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
return 1;
}
if (failures) {
n_log(LOG_ERR, "ex_http_response: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_http_response: all checks passed");
return 0;
}
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.