Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_url_safe.c

n_url_safe regression: URL parse (incl.

n_url_safe regression: URL parse (incl. SSRF/phishing rejections), response-head parse, redirect resolution and URL finding.

Author
Castagnier Mickael
Version
1.0
/*
* 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_log.h"
#include <string.h>
static int failures = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
n_log(LOG_ERR, "FAIL: %s", (msg)); \
failures++; \
} \
} while (0)
static int uparse(const char* s, n_url_safe* out) {
return n_url_safe_parse(s, strlen(s), out);
}
int main(void) {
/* URL parse: components */
{
CHECK(uparse("https://e.org/a/b?x=1#frag", &u), "valid https parses");
CHECK(u.https == 1 && u.port == 443, "https default port");
CHECK(strcmp(u.host, "e.org") == 0, "host");
CHECK(strcmp(u.path, "/a/b?x=1") == 0, "path, fragment dropped");
CHECK(uparse("http://h:8080/", &u) && u.https == 0 && u.port == 8080,
"http with explicit port");
/* an IPv6 literal has its brackets stripped */
CHECK(uparse("https://[2606:4700::1]/x", &u) &&
strcmp(u.host, "2606:4700::1") == 0,
"IPv6 literal brackets stripped");
/* rejections */
CHECK(!uparse("ftp://e.org/", &u), "non-http(s) scheme rejected");
CHECK(!uparse("https://user@e.org/", &u),
"userinfo rejected (SSRF/phishing)");
CHECK(!uparse("https:///x", &u), "empty host rejected");
CHECK(!uparse("https://e.org:99999/", &u), "bad port rejected");
}
/* response head parse */
{
const char* r =
"HTTP/1.1 301 Moved\r\nLocation: https://e.org/new\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: 12\r\n\r\nbodybytes...";
CHECK(n_url_safe_head_parse(r, strlen(r), &h), "response head parses");
CHECK(h.status == 301, "status 301");
CHECK(h.content_length == 12, "content-length");
CHECK(strcmp(h.location, "https://e.org/new") == 0, "location");
CHECK(strcmp(h.content_type, "text/html") == 0,
"content-type, params dropped");
CHECK(!n_url_safe_head_parse("", 0, &h), "empty head rejected");
CHECK(!n_url_safe_head_parse("\r\n\r", 3, &h),
"head shorter than CRLFCRLF rejected");
CHECK(!n_url_safe_head_parse("HTTP/1.1 200 OK\r\nHost: e", 24, &h),
"partial head returns 0");
}
/* redirect resolution */
{
char out[N_URL_SAFE_MAX + 1];
const char* base = "https://e.org/dir/page";
const char* abs = "https://x.org/y";
const char* rel = "//x.org/y";
const char* root = "/abs";
const char* bare = "rel/path";
CHECK(n_url_safe_resolve(base, strlen(base), abs, strlen(abs), out,
sizeof(out)) &&
strcmp(out, "https://x.org/y") == 0,
"absolute redirect");
CHECK(n_url_safe_resolve(base, strlen(base), rel, strlen(rel), out,
sizeof(out)) &&
strcmp(out, "https://x.org/y") == 0,
"scheme-relative redirect");
CHECK(n_url_safe_resolve(base, strlen(base), root, strlen(root), out,
sizeof(out)) &&
strcmp(out, "https://e.org/abs") == 0,
"root-relative redirect");
CHECK(!n_url_safe_resolve(base, strlen(base), bare, strlen(bare), out,
sizeof(out)),
"bare relative redirect refused");
}
/* URL finding in text */
{
char out[N_URL_SAFE_MAX + 1];
const char* t = "see https://e.org/p. thanks";
CHECK(n_url_safe_find(t, strlen(t), out, sizeof(out)) &&
strcmp(out, "https://e.org/p") == 0,
"find URL, trailing '.' trimmed");
CHECK(!n_url_safe_find("x https://a@b/", 14, out, sizeof(out)),
"malformed URL not surfaced");
}
if (failures) {
n_log(LOG_ERR, "ex_url_safe: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_url_safe: all checks passed");
return 0;
}
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
#define CHECK(cond, msg)
static int uparse(const char *s, n_url_safe *out)
Definition ex_url_safe.c:43
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
Generic log system.
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
Definition n_url_safe.c:308
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
Definition n_url_safe.c:344
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)
Definition n_url_safe.c:230
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
Definition n_url_safe.c:76
Hardened, bounded URL + HTTP/1.x response-head parsing for a safe fetcher.
char host[253u+1]
Definition n_url_safe.h:62
uint16_t port
Definition n_url_safe.h:61
char location[1024u+1]
Location (redirects).
Definition n_url_safe.h:72
long long content_length
Content-Length, or -1 if absent.
Definition n_url_safe.h:69
#define N_URL_SAFE_MAX
Longest URL / path / Location accepted.
Definition n_url_safe.h:50
int status
HTTP status code, e.g.
Definition n_url_safe.h:68
char content_type[127u+1]
mime, params dropped
Definition n_url_safe.h:73
int https
1 = https (default port 443), 0 = http (80)
Definition n_url_safe.h:60
char path[1024u+1]
Definition n_url_safe.h:63
The parts of an absolute http(s) URL.
Definition n_url_safe.h:59
The response-head fields a fetcher acts on.
Definition n_url_safe.h:67