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

examples/ex_http_req.c

Functions

Name
n_http_req_status parse(const char * s, n_http_request * out)
int main(void )

Attributes

Name
int failures

Defines

Name
WANT(expr, want, msg)
PARSE_EX(s)

Functions Documentation

function parse

static n_http_req_status parse(
    const char * s,
    n_http_request * out
)

function main

int main(
    void 
)

Attributes Documentation

variable failures

static int failures = 0;

Macros Documentation

define WANT

#define WANT(
    expr,
    want,
    msg
)
do {                                       \
    if ((expr) != (want)) {                \
        n_log(LOG_ERR, "FAIL: %s", (msg)); \
        failures++;                        \
    }                                      \
} while (0)

define PARSE_EX

#define PARSE_EX(
    s
)
n_http_request_parse_ex((s), strlen(s), &lim, &r)

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

#include <string.h>

static int failures = 0;

/* Parse a NUL-terminated request literal (its byte length is the wire length). */
static n_http_req_status parse(const char* s, n_http_request* out) {
    return n_http_request_parse(s, strlen(s), out);
}

#define WANT(expr, want, msg)                  \
    do {                                       \
        if ((expr) != (want)) {                \
            n_log(LOG_ERR, "FAIL: %s", (msg)); \
            failures++;                        \
        }                                      \
    } while (0)

int main(void) {
    n_http_request r;
    set_log_level(LOG_NOTICE);

    /* a valid GET */
    WANT(parse("GET /v1/me?x=1 HTTP/1.1\r\nHost: e\r\n\r\n", &r), N_HTTP_REQ_OK,
         "valid GET should parse");
    WANT(r.method, N_HTTP_REQ_GET, "method GET");
    if (strcmp(r.path, "/v1/me?x=1") != 0) {
        n_log(LOG_ERR, "FAIL: GET path");
        failures++;
    }

    /* a valid POST with a body + an Authorization header */
    WANT(parse("POST /v1/send HTTP/1.1\r\nContent-Length: 5\r\n"
               "Authorization: Bearer tok\r\n\r\nhello",
               &r),
         N_HTTP_REQ_OK, "valid POST should parse");
    WANT(r.method, N_HTTP_REQ_POST, "method POST");
    if (r.content_length != 5) {
        n_log(LOG_ERR, "FAIL: content_length");
        failures++;
    }
    if (strcmp(r.authorization, "Bearer tok") != 0) {
        n_log(LOG_ERR, "FAIL: authorization capture");
        failures++;
    }

    /* rejections */
    WANT(parse("PUT / HTTP/1.1\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "unsupported method rejected");
    WANT(parse("GET http://e/x HTTP/1.1\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "non-origin-form target rejected");
    WANT(parse("GET /a#frag HTTP/1.1\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "fragment in target rejected");
    WANT(parse("GET / HTTP/2.0\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "unsupported version rejected");
    WANT(parse("POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n", &r),
         N_HTTP_REQ_ERROR, "Transfer-Encoding rejected (smuggling)");
    WANT(parse("POST / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n"
               "\r\n",
               &r),
         N_HTTP_REQ_ERROR, "duplicate Content-Length rejected (smuggling)");
    WANT(parse("POST / HTTP/1.1\r\nContent-Length: abc\r\n\r\n", &r),
         N_HTTP_REQ_ERROR, "non-numeric Content-Length rejected");

    /* incomplete: no CRLFCRLF yet, still under the cap -> read more */
    WANT(parse("GET / HTTP/1.1\r\nHost: e", &r), N_HTTP_REQ_INCOMPLETE,
         "partial head is INCOMPLETE");
    /* shorter than the CRLFCRLF terminator: still just "read more" */
    WANT(n_http_request_parse("", 0, &r), N_HTTP_REQ_INCOMPLETE,
         "empty buffer is INCOMPLETE");
    WANT(n_http_request_parse("\r\n\r", 3, &r), N_HTTP_REQ_INCOMPLETE,
         "buffer shorter than CRLFCRLF is INCOMPLETE");

    /* NULL args */
    WANT(n_http_request_parse(NULL, 0, &r), N_HTTP_REQ_ERROR, "NULL buf");

    /* header values and names: control bytes and non-token names refused */
    WANT(parse("GET / HTTP/1.1\r\nX-A: a\nb\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "bare LF inside a header value rejected");
    WANT(parse("GET / HTTP/1.1\r\nX-A: a\rb\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "bare CR inside a header value rejected");
    /* 28 bytes: the literal holds a NUL, so strlen() cannot measure it */
    WANT(n_http_request_parse("GET / HTTP/1.1\r\nX-A: a\0b\r\n\r\n", 28, &r),
         N_HTTP_REQ_ERROR, "NUL inside a header value rejected");
    WANT(parse("GET / HTTP/1.1\r\nX(A): a\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "separator in a header name rejected");
    WANT(parse("GET / HTTP/1.1\r\n folded: a\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "obs-fold continuation line rejected");
    WANT(parse("GET / HTTP/1.1\r\nX-A: caf\xc3\xa9\t ok\r\n\r\n", &r),
         N_HTTP_REQ_OK, "obs-text and tabs inside a value accepted");

    /* the default entry point keeps refusing HEAD (GET and POST only) */
    WANT(parse("HEAD / HTTP/1.1\r\n\r\n", &r), N_HTTP_REQ_ERROR,
         "HEAD refused by n_http_request_parse");

    /* header table and lookup */
    {
        const char* req =
            "GET /x HTTP/1.0\r\nHost: www.example\r\ncookie: a=1\r\n"
            "X-Empty:\r\nCOOKIE:  b=2 \r\n\r\n";
        const char* v;
        size_t vlen = 99;
        WANT(parse(req, &r), N_HTTP_REQ_OK, "header table request parses");
        WANT(r.version_minor, 0, "HTTP/1.0 version_minor");
        WANT(r.nb_headers, (size_t)4, "four headers recorded");
        v = n_http_request_header(&r, req, "HOST", &vlen);
        if (!v || vlen != 11 || memcmp(v, "www.example", 11) != 0) {
            n_log(LOG_ERR, "FAIL: Host lookup");
            failures++;
        }
        v = n_http_request_header(&r, req, "Cookie", &vlen);
        if (!v || vlen != 3 || memcmp(v, "a=1", 3) != 0) {
            n_log(LOG_ERR, "FAIL: first Cookie lookup");
            failures++;
        }
        WANT(n_http_request_header_count(&r, req, "cookie"), (size_t)2,
             "two Cookie lines counted");
        v = n_http_request_header(&r, req, "x-empty", &vlen);
        WANT(v != NULL && vlen == 0, 1, "empty header value found, length 0");
        WANT(n_http_request_header(&r, req, "Referer", &vlen) == NULL, 1,
             "absent header is NULL");
        WANT(vlen, (size_t)0, "absent header length is 0");
        WANT(n_http_request_header(&r, req, "Hos", NULL) == NULL, 1,
             "a name prefix does not match");
        WANT(n_http_request_header(NULL, req, "Host", NULL) == NULL, 1,
             "NULL request");
        WANT(n_http_request_header_count(&r, NULL, "Host"), (size_t)0,
             "NULL buffer counts 0");
    }

    /* n_http_request_parse_ex: methods, body cap, Host rule */
    {
        n_http_req_limits lim;
        lim.methods = N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_GET) |
                      N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_HEAD) |
                      N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_POST);
        lim.body_max = 22LL * 1024 * 1024;
        lim.require_host = 1;
#define PARSE_EX(s) n_http_request_parse_ex((s), strlen(s), &lim, &r)
        WANT(PARSE_EX("HEAD /a HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_OK,
             "HEAD accepted by parse_ex when allowed");
        WANT(r.method, N_HTTP_REQ_HEAD, "method HEAD");
        WANT(r.version_minor, 1, "HTTP/1.1 version_minor");
        WANT(PARSE_EX("POST /u HTTP/1.1\r\nHost: e\r\nContent-Length: 23068672\r\n\r\n"),
             N_HTTP_REQ_OK, "22 MiB body accepted under a 22 MiB cap");
        WANT(r.content_length, 23068672LL, "22 MiB content_length");
        WANT(PARSE_EX("POST /u HTTP/1.1\r\nHost: e\r\nContent-Length: 23068673\r\n\r\n"),
             N_HTTP_REQ_ERROR, "body one byte over the cap rejected");
        WANT(PARSE_EX("GET / HTTP/1.1\r\n\r\n"), N_HTTP_REQ_ERROR,
             "HTTP/1.1 without Host rejected when required");
        WANT(PARSE_EX("GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n"),
             N_HTTP_REQ_ERROR, "two Host headers rejected when required");
        WANT(PARSE_EX("GET / HTTP/1.0\r\n\r\n"), N_HTTP_REQ_OK,
             "HTTP/1.0 without Host accepted");
        WANT(PARSE_EX("PUT / HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_ERROR,
             "PUT still rejected by parse_ex");
        lim.methods = N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_GET);
        WANT(PARSE_EX("POST / HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_ERROR,
             "method outside the allowed set rejected");
        lim.methods = 0;
        WANT(PARSE_EX("GET / HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_ERROR,
             "empty method set is an error");
        lim.methods = N_HTTP_REQ_METHOD_BIT(N_HTTP_REQ_GET);
        lim.body_max = N_HTTP_REQ_BODY_LIMIT_MAX + 1;
        WANT(PARSE_EX("GET / HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_ERROR,
             "body cap above the limit is an error");
        lim.body_max = -1;
        WANT(PARSE_EX("GET / HTTP/1.1\r\nHost: e\r\n\r\n"), N_HTTP_REQ_ERROR,
             "negative body cap is an error");
#undef PARSE_EX
        WANT(n_http_request_parse_ex("GET / HTTP/1.1\r\n\r\n", 18, NULL, &r),
             N_HTTP_REQ_ERROR, "NULL limits");
    }

    if (failures) {
        n_log(LOG_ERR, "ex_http_req: %d failure(s)", failures);
        return 1;
    }
    n_log(LOG_NOTICE, "ex_http_req: all checks passed");
    return 0;
}