7 n__url__safe_8c
gullradriel edited this page 2026-10-29 20:54:40 +01:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

src/n_url_safe.c

Hardened URL + HTTP/1.x response-head parsing (see n_url_safe.h). More...

Functions

Name
int parse_port(const char * s, size_t n, uint16_t * out)
long long parse_clen(const char * s, size_t n)
int name_eq(const char * s, size_t n, const char * lit)
int n_url_safe_resolve(const char * base, size_t base_len, const char * loc, size_t loc_len, char * out, size_t cap)
resolve a redirect Location against a request URL
int n_url_safe_parse(const char * url, size_t len, n_url_safe * out)
parse an absolute http(s) URL [url, url+len) into a bounded struct
int n_url_safe_head_parse(const char * buf, size_t len, n_url_safe_head * out)
parse an HTTP/1.x response head from [buf, buf+len)
int n_url_safe_find(const char * text, size_t len, char * out, size_t cap)
find the first http(s) URL in a block of text
int lc(int c)
int has_token(const char * s, size_t n, const char * tok)
void copy_mime(char * out, size_t cap, const char * v, size_t n)
void copy_clean(char * out, size_t cap, const char * v, size_t n)
int ci_prefix(const char * s, const char * lit)

Detailed Description

Hardened URL + HTTP/1.x response-head parsing (see n_url_safe.h).

Author: Castagnier Mickael

Version: 1.0

Pure, bounded, non-allocating, over-read safe.

Functions Documentation

function parse_port

static int parse_port(
    const char * s,
    size_t n,
    uint16_t * out
)

function parse_clen

static long long parse_clen(
    const char * s,
    size_t n
)

function name_eq

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

function n_url_safe_resolve

int n_url_safe_resolve(
    const char * base,
    size_t base_len,
    const char * loc,
    size_t loc_len,
    char * out,
    size_t cap
)

resolve a redirect Location against a request URL

Parameters:

  • base the request URL
  • base_len its length
  • loc the Location value
  • loc_len its length
  • out output buffer for the absolute URL
  • cap capacity of out (including the NUL)

Return: 1 on success, 0 otherwise

Handles an absolute http(s) target, a scheme-relative "//host/..." and a root-relative "/path"; a bare relative path is refused (returns 0). The result is re-validated with n_url_safe_parse.

function n_url_safe_parse

int n_url_safe_parse(
    const char * url,
    size_t len,
    n_url_safe * out
)

parse an absolute http(s) URL [url, url+len) into a bounded struct

Parameters:

  • url the URL bytes
  • len number of bytes in url
  • out the parsed URL to fill

Return: 1 on success, 0 otherwise

Rejects a non-http(s) scheme, userinfo ("user@host" – an SSRF / phishing vector), an empty or over-long host, a bad port, control bytes, and anything longer than the caps. IPv6 literals in brackets are accepted (brackets stripped into host).

function n_url_safe_head_parse

int n_url_safe_head_parse(
    const char * buf,
    size_t len,
    n_url_safe_head * out
)

parse an HTTP/1.x response head from [buf, buf+len)

Parameters:

  • buf the received bytes
  • len number of bytes in buf
  • out the parsed head to fill

Return: 1 on success, 0 otherwise

Returns 1 only when a complete head (through CRLFCRLF) is present and the status line is well-formed; 0 if incomplete, malformed, or larger than N_URL_SAFE_HEAD_MAX. On success out->head_len is the offset of the body. Header names are matched case-insensitively; unknown headers are ignored. Bounded and over-read safe.

function n_url_safe_find

int n_url_safe_find(
    const char * text,
    size_t len,
    char * out,
    size_t cap
)

find the first http(s) URL in a block of text

Parameters:

  • text the text to scan
  • len its length
  • out output buffer for the found URL
  • cap capacity of out (including the NUL)

Return: 1 if a URL was found, 0 otherwise

Writes it to out (cap includes the NUL). Trailing sentence punctuation is trimmed. The candidate must pass n_url_safe_parse (so a userinfo / malformed URL is skipped, and scanning continues), which makes this a safe trigger: it never surfaces a URL n_url_safe_parse would reject.

function lc

static int lc(
    int c
)

function has_token

static int has_token(
    const char * s,
    size_t n,
    const char * tok
)

function copy_mime

static void copy_mime(
    char * out,
    size_t cap,
    const char * v,
    size_t n
)

function copy_clean

static void copy_clean(
    char * out,
    size_t cap,
    const char * v,
    size_t n
)

function ci_prefix

static int ci_prefix(
    const char * s,
    const char * lit
)

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

#include <stdio.h>
#include <string.h>

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

/* Does \p s start with the lowercase literal \p lit? \p s must have at least
 * strlen(lit) bytes (the caller guarantees it). Case-insensitive on \p s. */
static int ci_prefix(const char* s, const char* lit) {
    size_t i;
    for (i = 0; lit[i]; i++)
        if (lc((unsigned char)s[i]) != (unsigned char)lit[i])
            return 0;
    return 1;
}

/* Case-insensitive: is the header-name span [s, s+n) equal to \p lit? */
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)
            return 0;
        if (lc((unsigned char)s[i]) != (unsigned char)lit[i])
            return 0;
    }
    return lit[n] == 0;
}

static int parse_port(const char* s, size_t n, uint16_t* out) {
    unsigned long v = 0;
    size_t i;
    if (n == 0 || n > 5)
        return 0;
    for (i = 0; i < n; i++) {
        if (s[i] < '0' || s[i] > '9')
            return 0;
        v = v * 10u + (unsigned long)(s[i] - '0');
        if (v > 65535u)
            return 0;
    }
    if (v == 0)
        return 0;
    *out = (uint16_t)v;
    return 1;
}

int n_url_safe_parse(const char* url, size_t len, n_url_safe* out) {
    size_t i, hs, he;
    if (!url || !out)
        return 0;
    memset(out, 0, sizeof(*out));
    if (len < 8 || len > (size_t)N_URL_SAFE_MAX + 16)
        return 0;
    if (ci_prefix(url, "https://")) {
        out->https = 1;
        out->port = 443;
        i = 8;
    } else if (ci_prefix(url, "http://")) {
        out->https = 0;
        out->port = 80;
        i = 7;
    } else {
        return 0;
    }
    /* authority up to '/', '?', '#', or end; userinfo and control bytes out */
    hs = i;
    while (i < len && url[i] != '/' && url[i] != '?' && url[i] != '#') {
        if (url[i] == '@' || (unsigned char)url[i] <= 0x20)
            return 0;
        i++;
    }
    he = i;
    {
        const char* auth = url + hs;
        size_t alen = he - hs, hostlen, k;
        const char* host;
        if (alen == 0)
            return 0;
        if (auth[0] == '[') { /* IPv6 literal [....](:port) */
            size_t rb = 1;
            while (rb < alen && auth[rb] != ']')
                rb++;
            if (rb >= alen || rb == 1)
                return 0;
            host = auth + 1;
            hostlen = rb - 1;
            if (rb + 1 < alen) {
                if (auth[rb + 1] != ':' ||
                    !parse_port(auth + rb + 2, alen - rb - 2, &out->port))
                    return 0;
            }
        } else {
            size_t colon = alen;
            for (k = 0; k < alen; k++)
                if (auth[k] == ':') {
                    colon = k;
                    break;
                }
            host = auth;
            hostlen = colon;
            if (colon < alen &&
                !parse_port(auth + colon + 1, alen - colon - 1, &out->port))
                return 0;
        }
        if (hostlen == 0 || hostlen > N_URL_SAFE_HOST_MAX)
            return 0;
        for (k = 0; k < hostlen; k++) {
            unsigned char c = (unsigned char)host[k];
            if (c <= 0x20 || c == '/' || c == '\\' || c == '@')
                return 0;
        }
        memcpy(out->host, host, hostlen);
        out->host[hostlen] = '\0';
    }
    /* path + query (fragment dropped), normalised to begin with '/' */
    {
        size_t oi = 0, k = he;
        if (k >= len || url[k] == '#') {
            out->path[0] = '/';
            out->path[1] = '\0';
            return 1;
        }
        if (url[k] == '?')
            out->path[oi++] = '/'; /* query with no path */
        for (; k < len && url[k] != '#'; k++) {
            if ((unsigned char)url[k] <= 0x20)
                return 0;
            if (oi + 1 >= sizeof(out->path))
                return 0;
            out->path[oi++] = url[k];
        }
        out->path[oi] = '\0';
        if (out->path[0] != '/')
            return 0;
    }
    return 1;
}

/* Parse a non-negative Content-Length; -1 on empty/non-digit/overflow. */
static long long parse_clen(const char* s, size_t n) {
    long long v = 0;
    size_t i;
    if (n == 0)
        return -1;
    for (i = 0; i < n; i++) {
        if (s[i] < '0' || s[i] > '9')
            return -1;
        if (v > (long long)1 << 50) /* absurd; the fetcher caps anyway */
            return -1;
        v = v * 10 + (s[i] - '0');
    }
    return v;
}

/* Case-insensitive: does the comma/space token list [s, s+n) contain \p tok? */
static int has_token(const char* s, size_t n, const char* tok) {
    size_t i = 0, tl = strlen(tok);
    while (i < n) {
        size_t j;
        while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == ','))
            i++;
        j = i;
        while (j < n && s[j] != ',')
            j++;
        { /* trim trailing ws of [i, j) */
            size_t e = j;
            while (e > i && (s[e - 1] == ' ' || s[e - 1] == '\t'))
                e--;
            if (e - i == tl && name_eq(s + i, tl, tok))
                return 1;
        }
        i = j + 1;
    }
    return 0;
}

/* Copy [v, v+n) into out (cap incl. NUL), truncating; drop control bytes. */
static void copy_clean(char* out, size_t cap, const char* v, size_t n) {
    size_t oi = 0, i;
    if (cap == 0)
        return;
    for (i = 0; i < n && oi + 1 < cap; i++)
        if ((unsigned char)v[i] >= 0x20)
            out[oi++] = v[i];
    out[oi] = '\0';
}

/* Copy a mime: up to ';' (drop params), trimmed, lowercased. */
static void copy_mime(char* out, size_t cap, const char* v, size_t n) {
    size_t oi = 0, i;
    if (cap == 0)
        return;
    for (i = 0; i < n && v[i] != ';' && oi + 1 < cap; i++) {
        unsigned char c = (unsigned char)v[i];
        if (c > 0x20)
            out[oi++] = (char)lc(c);
    }
    out[oi] = '\0';
}

int n_url_safe_head_parse(const char* buf, size_t len, n_url_safe_head* out) {
    size_t scan, i, line_start, sl, p;
    if (!buf || !out)
        return 0;
    memset(out, 0, sizeof(*out));
    out->content_length = -1;
    /* locate the end of the head (CRLFCRLF) within the cap */
    scan = len < N_URL_SAFE_HEAD_MAX ? len : (size_t)N_URL_SAFE_HEAD_MAX;
    /* nothing shorter than the CRLFCRLF terminator itself can hold a head: the
     * loop below cannot run and the head_len test below rejects it.
     * 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 0; /* incomplete, or larger than the cap */
    /* status line: "HTTP/x.y SSS ..." */
    sl = 0;
    while (sl + 1 < out->head_len && !(buf[sl] == '\r' && buf[sl + 1] == '\n'))
        sl++;
    if (sl < 12 || !ci_prefix(buf, "http/"))
        return 0;
    p = 5;
    while (p < sl && buf[p] != ' ')
        p++;
    while (p < sl && buf[p] == ' ')
        p++;
    if (p + 3 > sl || buf[p] < '0' || buf[p] > '9' || buf[p + 1] < '0' ||
        buf[p + 1] > '9' || buf[p + 2] < '0' || buf[p + 2] > '9')
        return 0;
    out->status =
        (buf[p] - '0') * 100 + (buf[p + 1] - '0') * 10 + (buf[p + 2] - '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;
        while (le + 1 < out->head_len &&
               !(buf[le] == '\r' && buf[le + 1] == '\n'))
            le++;
        if (le == line_start)
            break; /* blank line: end of headers */
        colon = line_start;
        while (colon < le && buf[colon] != ':')
            colon++;
        if (colon < le) {
            const char* name = buf + line_start;
            size_t nlen = colon - line_start;
            size_t vs = colon + 1, vlen;
            const char* val;
            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 (name_eq(name, nlen, "location"))
                copy_clean(out->location, sizeof(out->location), val, vlen);
            else if (name_eq(name, nlen, "content-type"))
                copy_mime(out->content_type, sizeof(out->content_type), val,
                          vlen);
            else if (name_eq(name, nlen, "content-length"))
                out->content_length = parse_clen(val, vlen);
            else if (name_eq(name, nlen, "transfer-encoding"))
                out->chunked = has_token(val, vlen, "chunked");
        }
        line_start = le + 2;
    }
    return 1;
}

int n_url_safe_resolve(const char* base, size_t base_len, const char* loc, size_t loc_len, char* out, size_t cap) {
    n_url_safe b, tmp;
    if (!base || !loc || !out || cap == 0)
        return 0;
    out[0] = '\0';
    /* absolute target: copy as-is (still validated below) */
    if ((loc_len >= 7 && ci_prefix(loc, "http://")) ||
        (loc_len >= 8 && ci_prefix(loc, "https://"))) {
        if (loc_len + 1 > cap)
            return 0;
        memcpy(out, loc, loc_len);
        out[loc_len] = '\0';
    } else {
        const char* scheme;
        int n;
        if (!n_url_safe_parse(base, base_len, &b))
            return 0;
        scheme = b.https ? "https" : "http";
        if (loc_len >= 2 && loc[0] == '/' && loc[1] == '/') {
            n = snprintf(out, cap, "%s:%.*s", scheme, (int)loc_len, loc);
        } else if (loc_len >= 1 && loc[0] == '/') {
            if ((b.https && b.port == 443) || (!b.https && b.port == 80))
                n = snprintf(out, cap, "%s://%s%.*s", scheme, b.host,
                             (int)loc_len, loc);
            else
                n = snprintf(out, cap, "%s://%s:%u%.*s", scheme, b.host,
                             (unsigned)b.port, (int)loc_len, loc);
        } else {
            return 0; /* bare relative path: not followed */
        }
        if (n < 0 || (size_t)n >= cap)
            return 0;
    }
    return n_url_safe_parse(out, strlen(out), &tmp); /* must be a valid URL */
}

int n_url_safe_find(const char* text, size_t len, char* out, size_t cap) {
    size_t i;
    if (!text || !out || cap == 0)
        return 0;
    out[0] = '\0';
    for (i = 0; i < len; i++) {
        size_t e;
        n_url_safe tmp;
        if ((text[i] != 'h' && text[i] != 'H') || i + 7 > len)
            continue;
        if (!ci_prefix(text + i, "http://") &&
            !(i + 8 <= len && ci_prefix(text + i, "https://")))
            continue;
        /* URL extent: up to whitespace / a delimiter / a control byte */
        e = i;
        while (e < len) {
            unsigned char c = (unsigned char)text[e];
            if (c <= 0x20 || c == '<' || c == '>' || c == '"' || c == '\\' ||
                c == '`')
                break;
            e++;
        }
        /* trim trailing sentence punctuation ("see http://x.com.") */
        while (e > i && strchr(".,;:!?)]}'\"", text[e - 1]))
            e--;
        if (e > i && (e - i) < cap) {
            memcpy(out, text + i, e - i);
            out[e - i] = '\0';
            if (n_url_safe_parse(out, e - i, &tmp))
                return 1; /* first valid URL wins */
            out[0] = '\0';
        }
        i = e > i ? e - 1 : i; /* resume after this candidate */
    }
    return 0;
}