3 ex__http__response_8c
gullradriel edited this page 2026-11-02 08:56:36 +01:00

examples/ex_http_response.c

Structures

Name
struct sink
an in-memory sink for n_http_response_write()
struct producer_state
what a test producer writes

Functions

Name
void test_write(void )
writing memory and file bodies, HEAD, partial and failing writes
void test_stream(void )
streamed bodies: chunked framing, HTTP/1.0, HEAD, failures, ownership
void test_status(void )
status codes and bodiless responses
void test_range(void )
Range header parsing.
void test_headers(void )
headers: validation, set/remove, framing
ssize_t sink_write(void * ctx, const void * buf, size_t len)
write function: accept at most s->chunk bytes
void producer_free(void * user)
free function of a producer's state
int producer(void * user, N_HTTP_STREAM * out)
producer: "data: N\n\n" pieces
int process_args(int argc, char ** argv)
read -V
int main(int argc, char ** argv)
run the checks
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
void expect_true(const char * what, int ok)
count a failed check
void count_piece(void * ctx)
before_piece: count the calls

Attributes

Name
int piece_calls
calls of count_piece()
int log_level
log level from -V
int failures
failed checks

Defines

Name
RANGE(s, total)

Functions Documentation

function test_write

static void test_write(
    void 
)

writing memory and file bodies, HEAD, partial and failing writes

function test_stream

static void test_stream(
    void 
)

streamed bodies: chunked framing, HTTP/1.0, HEAD, failures, ownership

function test_status

static void test_status(
    void 
)

status codes and bodiless responses

function test_range

static void test_range(
    void 
)

Range header parsing.

function test_headers

static void test_headers(
    void 
)

headers: validation, set/remove, framing

function sink_write

static ssize_t sink_write(
    void * ctx,
    const void * buf,
    size_t len
)

write function: accept at most s->chunk bytes

Parameters:

  • ctx the sink
  • buf bytes
  • len how many

Return: bytes taken, -1 to simulate a broken connection

function producer_free

static void producer_free(
    void * user
)

free function of a producer's state

Parameters:

function producer

static int producer(
    void * user,
    N_HTTP_STREAM * out
)

producer: "data: N\n\n" pieces

Parameters:

Return: the state's ok

function process_args

static int process_args(
    int argc,
    char ** argv
)

read -V

Parameters:

  • argc argument count
  • argv arguments

Return: 0, or -1 on a bad argument

function main

int main(
    int argc,
    char ** argv
)

run the checks

Parameters:

  • argc argument count
  • argv arguments

Return: 0 when every check passes

function head_is

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

Parameters:

  • r the response
  • want expected head, with the Date line removed

Return: non-zero when equal

function expect_true

static void expect_true(
    const char * what,
    int ok
)

count a failed check

Parameters:

  • what the check
  • ok its result

function count_piece

static void count_piece(
    void * ctx
)

before_piece: count the calls

Parameters:

  • ctx the sink (unused)

Attributes Documentation

variable piece_calls

static int piece_calls = 0;

calls of count_piece()

variable log_level

static int log_level = 3;

log level from -V

variable failures

static int failures = 0;

failed checks

Macros Documentation

define RANGE

#define RANGE(
    s,
    total
)
n_http_parse_range((s), strlen(s), (total), &a, &b)

Source code

/*
 * 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_common.h"
#include "nilorea/n_http_response.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);
        failures++;
    } 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;
    if (!n_http_response_head(r, &h))
        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);
    free_nstr(&h);
    return res;
}

static void test_headers(void) {
    N_HTTP_RESP* r = n_http_response_new(200);
    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 */
        N_HTTP_RESP* e = n_http_response_new(200);
        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"));
        n_http_response_free(&e);
    }
    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",
                head_is(r,
                        "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"));
    n_http_response_set_connection(r, 1, 1, 0);
    expect_true("HTTP/1.1 keep-alive sends no Connection",
                head_is(r,
                        "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"));
    n_http_response_set_connection(r, 1, 0, 0);
    expect_true("HTTP/1.0 keep-alive says so",
                head_is(r,
                        "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);
    free_nstr(&h);
    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:"));
    free_nstr(&h);
    {
        int i, full = 1;
        N_HTTP_RESP* m = n_http_response_new(200);
        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"));
        n_http_response_free(&m);
    }
    n_http_response_free(&r);
    expect_true("free sets NULL", r == NULL);
}

static void test_status(void) {
    N_HTTP_RESP* r;

    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);
    r = n_http_response_new(304);
    n_http_response_add_header(r, "ETag", "\"abc\"");
    n_http_response_append(r, "ignored", 7);
    n_http_response_set_connection(r, 1, 1, 0);
    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);
        free_nstr(&s.data);
    }
    expect_true("set_status", n_http_response_set_status(r, 204) && !n_http_response_set_status(r, 1000));
    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_status(r, 302);
    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"));
    n_http_response_free(&r);
    r = n_http_response_new(404);
    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"));
    n_http_response_free(&r);
}

static void test_write(void) {
    N_HTTP_RESP* r = n_http_response_new(200);
    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);
        free_nstr(&s.data);
    }
    n_http_response_set_connection(r, 0, 1, 1);
    {
        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);
        free_nstr(&s.data);
    }
    n_http_response_set_connection(r, 0, 1, 0);
    {
        sink s = {NULL, 3, 20};
        expect_true("a failing write fails the response", n_http_response_write(r, sink_write, &s) == FALSE);
        free_nstr(&s.data);
    }

    /* 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);
            free_nstr(&s.data);
        }
        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);
            free_nstr(&s.data);
        }
        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);
            free_nstr(&s.data);
        }
        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);
    }
    n_http_response_free(&r);
    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);
    n_http_response_free(NULL);
}

typedef struct {
    int pieces; 
    int ok;     
    int freed;  
    int seen;   
} producer_state;

static int piece_calls = 0;

static int producer(void* user, N_HTTP_STREAM* out) {
    producer_state* p = (producer_state*)user;
    char piece[32];
    for (int i = 0; i < p->pieces; i++) {
        snprintf(piece, sizeof(piece), "data: %d\n\n", i);
        if (!n_http_stream_write(out, piece, strlen(piece)))
            return FALSE;
        p->seen++;
    }
    expect_true("an empty piece writes nothing", n_http_stream_write(out, "", 0));
    return p->ok;
}

static void producer_free(void* user) {
    ((producer_state*)user)->freed = 1;
}

static void count_piece(void* ctx) {
    (void)ctx;
    piece_calls++;
}

static void test_stream(void) {
    producer_state st = {3, TRUE, 0, 0};
    N_HTTP_RESP* r = n_http_response_new(200);
    n_http_response_set_header(r, "Content-Type", "text/event-stream");
    expect_true("stream body", n_http_response_stream(r, producer, &st, producer_free));
    expect_true("no declared length", n_http_response_body_length(r) == -1);
    n_http_response_set_connection(r, 1, 1, 0);
    expect_true("chunked head", head_is(r, "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked\r\n\r\n"));
    {
        sink s = {NULL, 5, -1};
        static const char want[] = "\r\n\r\n9\r\ndata: 0\n\n\r\n9\r\ndata: 1\n\n\r\n9\r\ndata: 2\n\n\r\n0\r\n\r\n";
        expect_true("streamed in chunks", n_http_response_write_ex(r, sink_write, &s, count_piece) && s.data && st.seen == 3);
        expect_true("before_piece before each piece and the last chunk", piece_calls == 4);
        expect_true("chunk framing and the last chunk",
                    s.data && s.data->written > sizeof(want) - 1 && memcmp(s.data->data + s.data->written - (sizeof(want) - 1), want, sizeof(want) - 1) == 0);
        free_nstr(&s.data);
    }
    /* HTTP/1.0: raw bytes, the connection closes after them */
    st.seen = 0;
    n_http_response_set_connection(r, 1, 0, 0);
    expect_true("HTTP/1.0 head closes", head_is(r, "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nConnection: close\r\n\r\n"));
    {
        sink s = {NULL, 1000, -1};
        expect_true("HTTP/1.0 raw body", n_http_response_write(r, sink_write, &s) && s.data &&
                                             memcmp(s.data->data + s.data->written - 27, "data: 0\n\ndata: 1\n\ndata: 2\n\n", 27) == 0);
        free_nstr(&s.data);
    }
    /* HEAD: the producer does not run */
    st.seen = 0;
    n_http_response_set_connection(r, 1, 1, 1);
    {
        sink s = {NULL, 1000, -1};
        expect_true("HEAD skips the producer", n_http_response_write(r, sink_write, &s) && st.seen == 0);
        free_nstr(&s.data);
    }
    /* a broken connection stops the producer; a producer giving up fails the response */
    n_http_response_set_connection(r, 1, 1, 0);
    st.seen = 0;
    {
        sink s = {NULL, 1000, 70};
        st.pieces = 100;
        expect_true("a failing write stops the producer", n_http_response_write(r, sink_write, &s) == FALSE && st.seen < 100);
        free_nstr(&s.data);
    }
    st.pieces = 1;
    st.ok = FALSE;
    {
        sink s = {NULL, 1000, -1};
        expect_true("a producer giving up fails the response", n_http_response_write(r, sink_write, &s) == FALSE);
        free_nstr(&s.data);
    }
    expect_true("not freed while in use", st.freed == 0);
    expect_true("a memory body replaces the stream and frees its state", n_http_response_append(r, "x", 1) && st.freed == 1 && n_http_response_body_length(r) == 1);
    st.freed = 0;
    expect_true("stream again", n_http_response_stream(r, producer, &st, producer_free));
    n_http_response_free(&r);
    expect_true("freed with the response", st.freed == 1);
    st.freed = 0;
    expect_true("bad arguments refused, state freed", !n_http_response_stream(NULL, producer, &st, producer_free) && st.freed == 1);
    expect_true("NULL stream", n_http_stream_write(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;
    }
    set_log_level(log_level);

    test_headers();
    test_status();
    test_write();
    test_stream();
    test_range();

    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;
}