4 n__http__response_8c
gullradriel edited this page 2026-11-02 08:56:36 +01:00

src/n_http_response.c

HTTP/1.1 response builder and writer (see n_http_response.h). More...

Structures

Name
struct N_HTTP_STREAM
a streamed body being written

Functions

Name
int _resp_write_all(n_http_write_fn fn, void * ctx, const char * buf, size_t len)
int _resp_token(const char * s)
ssize_t _resp_pread(int fd, char * buf, size_t len, long long offset)
int _resp_num(const char * s, size_t n, long long * v)
int _resp_lc(int c)
int _resp_ieq(const char * a, const char * b)
void _resp_drop_body(N_HTTP_RESP * r)
int _resp_cat(N_STR ** h, const char * str)
int _resp_bodiless(int status)

Defines

Name
N_HTTP_RESPONSE_CHUNK

Detailed Description

HTTP/1.1 response builder and writer (see n_http_response.h).

Author: Castagnier Mickael

Version: 1.0

Date: 2026

Functions Documentation

function _resp_write_all

static int _resp_write_all(
    n_http_write_fn fn,
    void * ctx,
    const char * buf,
    size_t len
)

function _resp_token

static int _resp_token(
    const char * s
)

function _resp_pread

static ssize_t _resp_pread(
    int fd,
    char * buf,
    size_t len,
    long long offset
)

function _resp_num

static int _resp_num(
    const char * s,
    size_t n,
    long long * v
)

function _resp_lc

static int _resp_lc(
    int c
)

function _resp_ieq

static int _resp_ieq(
    const char * a,
    const char * b
)

function _resp_drop_body

static void _resp_drop_body(
    N_HTTP_RESP * r
)

function _resp_cat

static int _resp_cat(
    N_STR ** h,
    const char * str
)

function _resp_bodiless

static int _resp_bodiless(
    int status
)

Macros Documentation

define N_HTTP_RESPONSE_CHUNK

#define N_HTTP_RESPONSE_CHUNK 65536

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 "nilorea/n_http_response.h"

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_str.h"
#include "nilorea/n_time.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifdef __windows__
#include <io.h>
#endif

/* bytes read from a file body per write call */
#define N_HTTP_RESPONSE_CHUNK 65536

const char* n_http_status_reason(int status) {
    switch (status) {
        case 100:
            return "Continue";
        case 101:
            return "Switching Protocols";
        case 200:
            return "OK";
        case 201:
            return "Created";
        case 202:
            return "Accepted";
        case 204:
            return "No Content";
        case 206:
            return "Partial Content";
        case 301:
            return "Moved Permanently";
        case 302:
            return "Found";
        case 303:
            return "See Other";
        case 304:
            return "Not Modified";
        case 307:
            return "Temporary Redirect";
        case 308:
            return "Permanent Redirect";
        case 400:
            return "Bad Request";
        case 401:
            return "Unauthorized";
        case 403:
            return "Forbidden";
        case 404:
            return "Not Found";
        case 405:
            return "Method Not Allowed";
        case 406:
            return "Not Acceptable";
        case 408:
            return "Request Timeout";
        case 409:
            return "Conflict";
        case 410:
            return "Gone";
        case 411:
            return "Length Required";
        case 412:
            return "Precondition Failed";
        case 413:
            return "Content Too Large";
        case 414:
            return "URI Too Long";
        case 415:
            return "Unsupported Media Type";
        case 416:
            return "Range Not Satisfiable";
        case 417:
            return "Expectation Failed";
        case 421:
            return "Misdirected Request";
        case 422:
            return "Unprocessable Content";
        case 426:
            return "Upgrade Required";
        case 428:
            return "Precondition Required";
        case 429:
            return "Too Many Requests";
        case 431:
            return "Request Header Fields Too Large";
        case 451:
            return "Unavailable For Legal Reasons";
        case 500:
            return "Internal Server Error";
        case 501:
            return "Not Implemented";
        case 502:
            return "Bad Gateway";
        case 503:
            return "Service Unavailable";
        case 504:
            return "Gateway Timeout";
        case 505:
            return "HTTP Version Not Supported";
        default:
            break;
    }
    if (status >= 100 && status < 200)
        return "Informational";
    if (status >= 200 && status < 300)
        return "Success";
    if (status >= 300 && status < 400)
        return "Redirection";
    if (status >= 400 && status < 500)
        return "Client Error";
    if (status >= 500 && status < 600)
        return "Server Error";
    return "";
}

static int _resp_lc(int c) {
    return (c >= 'A' && c <= 'Z') ? c - 'A' + 'a' : c;
}

static int _resp_ieq(const char* a, const char* b) {
    while (*a && *b) {
        if (_resp_lc((unsigned char)*a) != _resp_lc((unsigned char)*b))
            return 0;
        a++;
        b++;
    }
    return *a == *b;
}

static int _resp_token(const char* s) {
    if (!s || !*s)
        return 0;
    for (; *s; s++) {
        unsigned char c = (unsigned char)*s;
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
            continue;
        if (!strchr("!#$%&'*+-.^_`|~", c))
            return 0;
    }
    return 1;
}

/* No body and no Content-Length for these (RFC 9110 sections 6.4.1, 15.3.5, 15.4.5). */
static int _resp_bodiless(int status) {
    return (status >= 100 && status < 200) || status == 204 || status == 304;
}

N_HTTP_RESP* n_http_response_new(int status) {
    N_HTTP_RESP* r = NULL;
    if (status < 100 || status > 599)
        return NULL;
    Malloc(r, N_HTTP_RESP, 1);
    __n_assert(r, return NULL);
    r->status = status;
    r->fd = -1;
    r->version_minor = 1;
    return r;
}

static void _resp_drop_body(N_HTTP_RESP* r) {
    if (r->stream_free && r->stream_user)
        r->stream_free(r->stream_user);
    r->stream = NULL;
    r->stream_user = NULL;
    r->stream_free = NULL;
    free_nstr(&r->body);
    if (r->fd >= 0)
        close(r->fd);
    r->fd = -1;
    r->fd_offset = 0;
    r->fd_length = 0;
}

void n_http_response_free(N_HTTP_RESP** r) {
    size_t i;
    if (!r || !*r)
        return;
    for (i = 0; i < (*r)->nb_headers; i++) {
        FreeNoLog((*r)->names[i]);
        FreeNoLog((*r)->values[i]);
    }
    _resp_drop_body(*r);
    FreeNoLog(*r);
}

int n_http_response_set_status(N_HTTP_RESP* r, int status) {
    if (!r || status < 100 || status > 599)
        return FALSE;
    r->status = status;
    return TRUE;
}

int n_http_response_add_header(N_HTTP_RESP* r, const char* name, const char* value) {
    size_t vs, ve, i;
    char *n = NULL, *v = NULL;
    if (!r || !value || !_resp_token(name))
        return FALSE;
    if (_resp_ieq(name, "content-length") || _resp_ieq(name, "transfer-encoding") || _resp_ieq(name, "connection")) {
        n_log(LOG_DEBUG, "response: %s is set by the builder", name);
        return FALSE;
    }
    if (r->nb_headers >= N_HTTP_RESPONSE_HEADERS_MAX)
        return FALSE;
    ve = strlen(value);
    if (ve > N_HTTP_RESPONSE_VALUE_MAX)
        return FALSE;
    for (i = 0; i < ve; i++) {
        unsigned char c = (unsigned char)value[i];
        if ((c < 0x20 && c != '\t') || c == 0x7f) {
            n_log(LOG_DEBUG, "response: control byte 0x%02x in %s refused", c, name);
            return FALSE;
        }
    }
    vs = 0;
    while (vs < ve && (value[vs] == ' ' || value[vs] == '\t'))
        vs++;
    while (ve > vs && (value[ve - 1] == ' ' || value[ve - 1] == '\t'))
        ve--;
    n = strdup(name);
    Malloc(v, char, ve - vs + 1);
    if (!n || !v) {
        FreeNoLog(n);
        FreeNoLog(v);
        return FALSE;
    }
    memcpy(v, value + vs, ve - vs);
    v[ve - vs] = '\0';
    r->names[r->nb_headers] = n;
    r->values[r->nb_headers] = v;
    r->nb_headers++;
    return TRUE;
}

size_t n_http_response_remove_header(N_HTTP_RESP* r, const char* name) {
    size_t i, o = 0, removed = 0;
    if (!r || !name)
        return 0;
    for (i = 0; i < r->nb_headers; i++) {
        if (_resp_ieq(r->names[i], name)) {
            FreeNoLog(r->names[i]);
            FreeNoLog(r->values[i]);
            removed++;
            continue;
        }
        r->names[o] = r->names[i];
        r->values[o] = r->values[i];
        o++;
    }
    for (i = o; i < r->nb_headers; i++) {
        r->names[i] = NULL;
        r->values[i] = NULL;
    }
    r->nb_headers = o;
    return removed;
}

int n_http_response_set_header(N_HTTP_RESP* r, const char* name, const char* value) {
    size_t before;
    if (!r || !name)
        return FALSE;
    before = r->nb_headers;
    if (!n_http_response_add_header(r, name, value))
        return FALSE;
    /* the new header is last: drop the older ones of that name before it */
    {
        size_t i, o = 0;
        for (i = 0; i < before; i++) {
            if (_resp_ieq(r->names[i], name)) {
                FreeNoLog(r->names[i]);
                FreeNoLog(r->values[i]);
                continue;
            }
            r->names[o] = r->names[i];
            r->values[o] = r->values[i];
            o++;
        }
        r->names[o] = r->names[before];
        r->values[o] = r->values[before];
        o++;
        for (i = o; i < r->nb_headers; i++) {
            r->names[i] = NULL;
            r->values[i] = NULL;
        }
        r->nb_headers = o;
    }
    return TRUE;
}

const char* n_http_response_get_header(const N_HTTP_RESP* r, const char* name) {
    size_t i;
    if (!r || !name)
        return NULL;
    for (i = 0; i < r->nb_headers; i++)
        if (_resp_ieq(r->names[i], name))
            return r->values[i];
    return NULL;
}

int n_http_response_append(N_HTTP_RESP* r, const void* data, size_t len) {
    if (!r || (!data && len > 0))
        return FALSE;
    if (r->fd >= 0 || r->stream)
        _resp_drop_body(r);
    if (!r->body) {
        r->body = new_nstr(len + 1);
        if (!r->body)
            return FALSE;
    }
    if (len == 0)
        return TRUE;
    return nstrcat_ex(&r->body, (void*)data, len, 1) ? TRUE : FALSE;
}

int n_http_response_file(N_HTTP_RESP* r, int fd, long long offset, long long length) {
    if (!r || fd < 0 || offset < 0 || length < 0) {
        if (fd >= 0)
            close(fd);
        return FALSE;
    }
    _resp_drop_body(r);
    r->fd = fd;
    r->fd_offset = offset;
    r->fd_length = length;
    return TRUE;
}

int n_http_response_set_connection(N_HTTP_RESP* r, int keep_alive, int version_minor, int head_only) {
    if (!r || (version_minor != 0 && version_minor != 1))
        return FALSE;
    r->keep_alive = keep_alive ? 1 : 0;
    r->version_minor = version_minor;
    r->head_only = head_only ? 1 : 0;
    return TRUE;
}

long long n_http_response_body_length(const N_HTTP_RESP* r) {
    if (!r)
        return -1;
    if (_resp_bodiless(r->status))
        return 0;
    if (r->stream)
        return -1;
    if (r->fd >= 0)
        return r->fd_length;
    return r->body ? (long long)r->body->written : 0;
}

/* Append a C string; unlike nstrcat_bytes(), an empty string is a no-op
 * success (an empty header value is legal). */
static int _resp_cat(N_STR** h, const char* str) {
    if (!str)
        return FALSE;
    size_t n = strlen(str);
    if (n == 0)
        return *h != NULL || (*h = new_nstr(1)) != NULL;
    return nstrcat_ex(h, (void*)str, n, 1) != NULL;
}

int n_http_response_head(const N_HTTP_RESP* r, N_STR** out) {
    char line[128];
    size_t i;
    N_STR* h = NULL;
    if (!r || !out)
        return FALSE;
    snprintf(line, sizeof(line), "HTTP/1.1 %d %s\r\n", r->status, n_http_status_reason(r->status));
    if (!_resp_cat(&h, line))
        goto fail;
    if (!n_http_response_get_header(r, "date")) {
        char date[N_TIME_HTTP_DATE_LEN];
        if (n_time_http_date(time(NULL), date, sizeof(date)) &&
            (!_resp_cat(&h, "Date: ") || !_resp_cat(&h, date) || !_resp_cat(&h, "\r\n")))
            goto fail;
    }
    for (i = 0; i < r->nb_headers; i++)
        if (!_resp_cat(&h, r->names[i]) || !_resp_cat(&h, ": ") || !_resp_cat(&h, r->values[i]) ||
            !_resp_cat(&h, "\r\n"))
            goto fail;
    if (!_resp_bodiless(r->status) && r->stream) {
        if (r->version_minor == 1 && !_resp_cat(&h, "Transfer-Encoding: chunked\r\n"))
            goto fail;
    } else if (!_resp_bodiless(r->status)) {
        snprintf(line, sizeof(line), "Content-Length: %lld\r\n", n_http_response_body_length(r));
        if (!_resp_cat(&h, line))
            goto fail;
    }
    /* an HTTP/1.0 streamed body ends when the connection closes */
    if (!r->keep_alive || (r->stream && r->version_minor == 0 && !_resp_bodiless(r->status))) {
        if (!_resp_cat(&h, "Connection: close\r\n"))
            goto fail;
    } else if (r->version_minor == 0) {
        if (!_resp_cat(&h, "Connection: keep-alive\r\n"))
            goto fail;
    }
    if (!_resp_cat(&h, "\r\n"))
        goto fail;
    if (!*out) {
        *out = h;
        return TRUE;
    }
    if (!nstrcat_ex(out, h->data, h->written, 1))
        goto fail;
    free_nstr(&h);
    return TRUE;
fail:
    free_nstr(&h);
    return FALSE;
}

static int _resp_write_all(n_http_write_fn fn, void* ctx, const char* buf, size_t len) {
    while (len > 0) {
        ssize_t w = fn(ctx, buf, len);
        if (w <= 0 || (size_t)w > len)
            return FALSE;
        buf += w;
        len -= (size_t)w;
    }
    return TRUE;
}

/* Read up to len bytes at offset, without moving a shared file position
 * where the platform allows it. */
static ssize_t _resp_pread(int fd, char* buf, size_t len, long long offset) {
#ifdef __windows__
    if (_lseeki64(fd, offset, SEEK_SET) < 0)
        return -1;
    return (ssize_t)_read(fd, buf, (unsigned)len);
#else
    return pread(fd, buf, len, (off_t)offset);
#endif
}

struct N_HTTP_STREAM {
    n_http_write_fn fn;              
    void* ctx;                       
    void (*before_piece)(void* ctx); 
    int chunked;                     
    int failed;                      
};

int n_http_response_stream(N_HTTP_RESP* r, n_http_stream_fn fn, void* user, void (*free_user)(void* user)) {
    if (!r || !fn) {
        if (free_user && user)
            free_user(user);
        return FALSE;
    }
    _resp_drop_body(r);
    r->stream = fn;
    r->stream_user = user;
    r->stream_free = free_user;
    return TRUE;
}

int n_http_stream_write(N_HTTP_STREAM* s, const void* data, size_t len) {
    if (!s || s->failed || (!data && len > 0))
        return FALSE;
    if (len == 0)
        return TRUE;
    if (s->before_piece)
        s->before_piece(s->ctx);
    if (s->chunked) {
        char size[24];
        snprintf(size, sizeof(size), "%zx\r\n", len);
        if (!_resp_write_all(s->fn, s->ctx, size, strlen(size)) || !_resp_write_all(s->fn, s->ctx, (const char*)data, len) ||
            !_resp_write_all(s->fn, s->ctx, "\r\n", 2))
            s->failed = 1;
    } else if (!_resp_write_all(s->fn, s->ctx, (const char*)data, len)) {
        s->failed = 1;
    }
    return !s->failed;
}

int n_http_response_write(const N_HTTP_RESP* r, n_http_write_fn fn, void* ctx) {
    return n_http_response_write_ex(r, fn, ctx, NULL);
}

int n_http_response_write_ex(const N_HTTP_RESP* r, n_http_write_fn fn, void* ctx, void (*before_piece)(void* ctx)) {
    N_STR* head = NULL;
    int ok;
    if (!r || !fn)
        return FALSE;
    if (!n_http_response_head(r, &head))
        return FALSE;
    ok = _resp_write_all(fn, ctx, head->data, head->written);
    free_nstr(&head);
    if (!ok)
        return FALSE;
    if (r->head_only || _resp_bodiless(r->status))
        return TRUE;
    if (r->stream) {
        N_HTTP_STREAM s;
        int done;
        s.fn = fn;
        s.ctx = ctx;
        s.before_piece = before_piece;
        s.chunked = r->version_minor == 1;
        s.failed = 0;
        done = r->stream(r->stream_user, &s);
        if (!done || s.failed)
            return FALSE;
        if (s.chunked) {
            if (before_piece)
                before_piece(ctx);
            return _resp_write_all(fn, ctx, "0\r\n\r\n", 5);
        }
        return TRUE;
    }
    if (r->fd >= 0) {
        char* chunk = NULL;
        long long done = 0;
        Malloc(chunk, char, N_HTTP_RESPONSE_CHUNK);
        __n_assert(chunk, return FALSE);
        while (done < r->fd_length) {
            long long want = r->fd_length - done;
            ssize_t got;
            if (want > N_HTTP_RESPONSE_CHUNK)
                want = N_HTTP_RESPONSE_CHUNK;
            got = _resp_pread(r->fd, chunk, (size_t)want, r->fd_offset + done);
            if (got <= 0 || !_resp_write_all(fn, ctx, chunk, (size_t)got)) {
                n_log(LOG_DEBUG, "response: file body stopped at %lld of %lld bytes", done, r->fd_length);
                Free(chunk);
                return FALSE;
            }
            done += got;
        }
        Free(chunk);
        return TRUE;
    }
    if (r->body && r->body->written > 0)
        return _resp_write_all(fn, ctx, r->body->data, r->body->written);
    return TRUE;
}

/* Parse a non-negative decimal [s, s+n) without overflow. */
static int _resp_num(const char* s, size_t n, long long* v) {
    size_t i;
    *v = 0;
    if (n == 0 || n > 18)
        return FALSE;
    for (i = 0; i < n; i++) {
        if (s[i] < '0' || s[i] > '9')
            return FALSE;
        *v = *v * 10 + (s[i] - '0');
    }
    return TRUE;
}

N_HTTP_RANGE n_http_parse_range(const char* value, size_t len, long long total, long long* first, long long* last) {
    size_t dash;
    long long a, b;
    if (!value || !first || !last || total < 0 || len < 7 || memcmp(value, "bytes=", 6) != 0)
        return N_HTTP_RANGE_NONE;
    value += 6;
    len -= 6;
    for (dash = 0; dash < len && value[dash] != '-'; dash++);
    if (dash == len)
        return N_HTTP_RANGE_NONE;
    if (dash == 0) {
        /* suffix range: the last n bytes */
        if (!_resp_num(value + 1, len - 1, &b))
            return N_HTTP_RANGE_NONE;
        if (b == 0 || total == 0)
            return N_HTTP_RANGE_UNSATISFIABLE;
        *first = b >= total ? 0 : total - b;
        *last = total - 1;
        return N_HTTP_RANGE_OK;
    }
    if (!_resp_num(value, dash, &a))
        return N_HTTP_RANGE_NONE;
    if (dash + 1 == len) {
        b = total - 1;
    } else if (!_resp_num(value + dash + 1, len - dash - 1, &b) || b < a) {
        return N_HTTP_RANGE_NONE;
    }
    if (a >= total)
        return N_HTTP_RANGE_UNSATISFIABLE;
    if (b >= total)
        b = total - 1;
    *first = a;
    *last = b;
    return N_HTTP_RANGE_OK;
}