8 n__http__req_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00

src/n_http_req.c

HTTP/1.1 request-head parser (see n_http_req.h). More...

Functions

Name
int value_ok(const char * s, size_t n)
int target_ok(unsigned char c)
int parse_body_len(const char * s, size_t n, long long max, long long * out)
int name_token_ok(const char * s, size_t n)
int name_ieq(const char * a, const char * b, size_t n)
int name_eq(const char * s, size_t n, const char * lit)
int lc(int c)

Detailed Description

HTTP/1.1 request-head parser (see n_http_req.h).

Author: Castagnier Mickael

Version: 1.0

A small, strict scanner, bounded and over-read safe on every path.

Functions Documentation

function value_ok

static int value_ok(
    const char * s,
    size_t n
)

function target_ok

static int target_ok(
    unsigned char c
)

function parse_body_len

static int parse_body_len(
    const char * s,
    size_t n,
    long long max,
    long long * out
)

function name_token_ok

static int name_token_ok(
    const char * s,
    size_t n
)

function name_ieq

static int name_ieq(
    const char * a,
    const char * b,
    size_t n
)

function name_eq

static int name_eq(
    const char * s,
    size_t n,
    const char * lit
)

function lc

static int lc(
    int c
)

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_req.h"

#include <string.h>

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

/* Case-insensitive compare of an \p n-byte field name against a lowercase,
 * NUL-terminated literal. */
static int name_eq(const char* s, size_t n, const char* lit) {
    size_t i;
    for (i = 0; i < n; i++)
        if (lit[i] == '\0' || lc((unsigned char)s[i]) != lit[i])
            return 0;
    return lit[n] == '\0';
}

/* Case-insensitive compare of two \p n-byte names, either case on both sides. */
static int name_ieq(const char* a, const char* b, size_t n) {
    size_t i;
    for (i = 0; i < n; i++)
        if (lc((unsigned char)a[i]) != lc((unsigned char)b[i]))
            return 0;
    return 1;
}

/* A field-name must be a non-empty RFC 9110 token: letters, digits and
 * !#$%&'*+-.^_`|~ only. Controls, whitespace and separators are refused, so
 * "Content-Length " with a trailing space is rejected, not silently ignored,
 * a request-smuggling vector against a downstream proxy. */
static int name_token_ok(const char* s, size_t n) {
    size_t i;
    if (n == 0)
        return 0;
    for (i = 0; i < n; i++) {
        unsigned char c = (unsigned char)s[i];
        if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
            continue;
        if (!strchr("!#$%&'*+-.^_`|~", c) || c == '\0')
            return 0;
    }
    return 1;
}

/* A field-value may hold visible ASCII, spaces, tabs and obs-text (0x80-0xff),
 * never a control byte: a bare CR, LF or NUL inside a value is how a request
 * is split or a header injected further down the chain. */
static int value_ok(const char* s, size_t n) {
    size_t i;
    for (i = 0; i < n; i++) {
        unsigned char c = (unsigned char)s[i];
        if ((c < 0x20 && c != '\t') || c == 0x7f)
            return 0;
    }
    return 1;
}

/* Parse a bounded non-negative decimal [s, s+n) into *out. Returns 1 on a clean
 * parse within max, else 0 (empty / non-digit / over cap).
 */
static int parse_body_len(const char* s, size_t n, long long max, long long* out) {
    unsigned long long v = 0;
    size_t i;
    if (n == 0)
        return 0;
    for (i = 0; i < n; i++) {
        if (s[i] < '0' || s[i] > '9')
            return 0;
        v = v * 10u + (unsigned)(s[i] - '0');
        if (v > (unsigned long long)max)
            return 0;
    }
    *out = (long long)v;
    return 1;
}

/* A valid byte in an origin-form request target: printable ASCII, no space, no
 * '#' (a fragment must never reach the server). */
static int target_ok(unsigned char c) {
    return c > 0x20 && c < 0x7f && c != '#';
}

n_http_req_status n_http_request_parse_ex(const char* buf, size_t len, const n_http_req_limits* limits, n_http_request* out) {
    size_t scan, i, sl, p, tstart, tend, line_start, nheaders = 0, nhost = 0;
    int have_clen = 0;

    if (!buf || !out || !limits)
        return N_HTTP_REQ_ERROR;
    memset(out, 0, sizeof(*out));
    if (limits->methods == 0 || limits->body_max < 0 ||
        limits->body_max > N_HTTP_REQ_BODY_LIMIT_MAX)
        return N_HTTP_REQ_ERROR;

    /* locate the end of the head (CRLFCRLF) within the cap */
    scan = len < N_HTTP_REQ_HEAD_MAX ? len : (size_t)N_HTTP_REQ_HEAD_MAX;
    /* nothing shorter than the CRLFCRLF terminator itself can hold a head: the
     * loop below cannot run, head_len stays 0, and since scan < 4 implies
     * len < N_HTTP_REQ_HEAD_MAX the head_len test reports INCOMPLETE.
     * The older cppcheck used by CI folds the ternary assignment above down to
     * a single value of 0 and then calls this walk dead code, a valueflow
     * false positive: scan follows the caller's len, unknown here. Spelling
     * the bound as an explicit "if (scan < 4)" guard only moves the same
     * diagnostic onto that guard, hence the inline suppression. */
    // cppcheck-suppress knownConditionTrueFalse ; scan is caller-controlled, the walk is reachable
    for (i = 0; i + 3 < scan; i++)
        if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' &&
            buf[i + 3] == '\n') {
            out->head_len = i + 4;
            break;
        }
    if (out->head_len == 0)
        return len < N_HTTP_REQ_HEAD_MAX ? N_HTTP_REQ_INCOMPLETE
                                         : N_HTTP_REQ_ERROR;

    /* Request line: METHOD SP target SP HTTP/1.x CRLF */
    sl = 0;
    while (sl + 1 < out->head_len && !(buf[sl] == '\r' && buf[sl + 1] == '\n'))
        sl++;
    if (sl >= 4 && memcmp(buf, "GET ", 4) == 0) {
        out->method = N_HTTP_REQ_GET;
        p = 4;
    } else if (sl >= 5 && memcmp(buf, "POST ", 5) == 0) {
        out->method = N_HTTP_REQ_POST;
        p = 5;
    } else if (sl >= 5 && memcmp(buf, "HEAD ", 5) == 0) {
        out->method = N_HTTP_REQ_HEAD;
        p = 5;
    } else {
        return N_HTTP_REQ_ERROR;
    }
    if (!(limits->methods & N_HTTP_REQ_METHOD_BIT(out->method)))
        return N_HTTP_REQ_ERROR;
    /* origin-form target, from p to the next space */
    tstart = p;
    while (p < sl && buf[p] != ' ') {
        if (!target_ok((unsigned char)buf[p]))
            return N_HTTP_REQ_ERROR;
        p++;
    }
    tend = p;
    if (tend == tstart || buf[tstart] != '/' ||
        tend - tstart > N_HTTP_REQ_PATH_MAX)
        return N_HTTP_REQ_ERROR;
    /* exactly one SP, then the version filling the rest of the line */
    if (p >= sl || buf[p] != ' ')
        return N_HTTP_REQ_ERROR;
    p++;
    if (sl - p != 8 || memcmp(buf + p, "HTTP/1.", 7) != 0 ||
        (buf[p + 7] != '0' && buf[p + 7] != '1'))
        return N_HTTP_REQ_ERROR;
    out->version_minor = buf[p + 7] - '0';
    memcpy(out->path, buf + tstart, tend - tstart);
    out->path[tend - tstart] = '\0';

    /* Header lines up to the blank line */
    line_start = sl + 2;
    while (line_start + 1 < out->head_len) {
        size_t le = line_start, colon, vs, vlen, nlen;
        const char *name, *val;
        while (le + 1 < out->head_len &&
               !(buf[le] == '\r' && buf[le + 1] == '\n'))
            le++;
        if (le == line_start)
            break; /* blank line: end of headers */
        if (++nheaders > N_HTTP_REQ_HEADERS_MAX)
            return N_HTTP_REQ_ERROR;
        colon = line_start;
        while (colon < le && buf[colon] != ':')
            colon++;
        if (colon >= le)
            return N_HTTP_REQ_ERROR; /* header without a colon */
        name = buf + line_start;
        nlen = colon - line_start;
        if (!name_token_ok(name, nlen))
            return N_HTTP_REQ_ERROR;
        vs = colon + 1;
        while (vs < le && (buf[vs] == ' ' || buf[vs] == '\t'))
            vs++;
        val = buf + vs;
        vlen = le - vs;
        while (vlen > 0 && (val[vlen - 1] == ' ' || val[vlen - 1] == '\t'))
            vlen--;
        if (!value_ok(val, vlen))
            return N_HTTP_REQ_ERROR;
        out->headers[out->nb_headers].name_off = line_start;
        out->headers[out->nb_headers].name_len = nlen;
        out->headers[out->nb_headers].value_off = vs;
        out->headers[out->nb_headers].value_len = vlen;
        out->nb_headers++;
        if (name_eq(name, nlen, "host"))
            nhost++;
        if (name_eq(name, nlen, "transfer-encoding"))
            return N_HTTP_REQ_ERROR; /* no chunked, smuggling defense */
        if (name_eq(name, nlen, "content-length")) {
            long long cl;
            if (have_clen) /* duplicate Content-Length, smuggling */
                return N_HTTP_REQ_ERROR;
            if (!parse_body_len(val, vlen, limits->body_max, &cl))
                return N_HTTP_REQ_ERROR;
            out->content_length = cl;
            have_clen = 1;
        } else if (name_eq(name, nlen, "authorization")) {
            /* capture the credential; refuse rather than truncate an over-long
             * one (a truncated bearer token must never be treated as valid) */
            if (out->authorization[0] != '\0' || vlen > N_HTTP_REQ_AUTH_MAX)
                return N_HTTP_REQ_ERROR;
            memcpy(out->authorization, val, vlen);
            out->authorization[vlen] = '\0';
        }
        line_start = le + 2;
    }
    if (limits->require_host &&
        (nhost > 1 || (nhost == 0 && out->version_minor == 1)))
        return N_HTTP_REQ_ERROR;
    return N_HTTP_REQ_OK;
}

n_http_req_status n_http_request_parse(const char* buf, size_t len, n_http_request* out) {
    n_http_req_limits limits;
    limits.methods = N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_GET) | N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_POST);
    limits.body_max = (long long)N_HTTP_REQ_BODY_MAX;
    limits.require_host = 0;
    return n_http_request_parse_ex(buf, len, &limits, out);
}

const char* n_http_request_header(const n_http_request* req, const char* buf, const char* name, size_t* value_len) {
    size_t i, n;
    if (value_len)
        *value_len = 0;
    if (!req || !buf || !name)
        return NULL;
    n = strlen(name);
    for (i = 0; i < req->nb_headers && i < N_HTTP_REQ_HEADERS_MAX; i++) {
        const n_http_req_header* h = &req->headers[i];
        if (h->name_len == n && name_ieq(buf + h->name_off, name, n)) {
            if (value_len)
                *value_len = h->value_len;
            return buf + h->value_off;
        }
    }
    return NULL;
}

size_t n_http_request_header_count(const n_http_request* req, const char* buf, const char* name) {
    size_t i, n, count = 0;
    if (!req || !buf || !name)
        return 0;
    n = strlen(name);
    for (i = 0; i < req->nb_headers && i < N_HTTP_REQ_HEADERS_MAX; i++) {
        const n_http_req_header* h = &req->headers[i];
        if (h->name_len == n && name_ieq(buf + h->name_off, name, n))
            count++;
    }
    return count;
}