Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_url_safe.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
27#include "nilorea/n_url_safe.h"
28
29#include <stdio.h>
30#include <string.h>
31
32static int lc(int c) {
33 return (c >= 'A' && c <= 'Z') ? c + 32 : c;
34}
35
36/* Does \p s start with the lowercase literal \p lit? \p s must have at least
37 * strlen(lit) bytes (the caller guarantees it). Case-insensitive on \p s. */
38static int ci_prefix(const char* s, const char* lit) {
39 size_t i;
40 for (i = 0; lit[i]; i++)
41 if (lc((unsigned char)s[i]) != (unsigned char)lit[i])
42 return 0;
43 return 1;
44}
45
46/* Case-insensitive: is the header-name span [s, s+n) equal to \p lit? */
47static int name_eq(const char* s, size_t n, const char* lit) {
48 size_t i;
49 for (i = 0; i < n; i++) {
50 if (lit[i] == 0)
51 return 0;
52 if (lc((unsigned char)s[i]) != (unsigned char)lit[i])
53 return 0;
54 }
55 return lit[n] == 0;
56}
57
58static int parse_port(const char* s, size_t n, uint16_t* out) {
59 unsigned long v = 0;
60 size_t i;
61 if (n == 0 || n > 5)
62 return 0;
63 for (i = 0; i < n; i++) {
64 if (s[i] < '0' || s[i] > '9')
65 return 0;
66 v = v * 10u + (unsigned long)(s[i] - '0');
67 if (v > 65535u)
68 return 0;
69 }
70 if (v == 0)
71 return 0;
72 *out = (uint16_t)v;
73 return 1;
74}
75
76int n_url_safe_parse(const char* url, size_t len, n_url_safe* out) {
77 size_t i, hs, he;
78 if (!url || !out)
79 return 0;
80 memset(out, 0, sizeof(*out));
81 if (len < 8 || len > (size_t)N_URL_SAFE_MAX + 16)
82 return 0;
83 if (ci_prefix(url, "https://")) {
84 out->https = 1;
85 out->port = 443;
86 i = 8;
87 } else if (ci_prefix(url, "http://")) {
88 out->https = 0;
89 out->port = 80;
90 i = 7;
91 } else {
92 return 0;
93 }
94 /* authority up to '/', '?', '#', or end; userinfo and control bytes out */
95 hs = i;
96 while (i < len && url[i] != '/' && url[i] != '?' && url[i] != '#') {
97 if (url[i] == '@' || (unsigned char)url[i] <= 0x20)
98 return 0;
99 i++;
100 }
101 he = i;
102 {
103 const char* auth = url + hs;
104 size_t alen = he - hs, hostlen, k;
105 const char* host;
106 if (alen == 0)
107 return 0;
108 if (auth[0] == '[') { /* IPv6 literal [....](:port) */
109 size_t rb = 1;
110 while (rb < alen && auth[rb] != ']')
111 rb++;
112 if (rb >= alen || rb == 1)
113 return 0;
114 host = auth + 1;
115 hostlen = rb - 1;
116 if (rb + 1 < alen) {
117 if (auth[rb + 1] != ':' ||
118 !parse_port(auth + rb + 2, alen - rb - 2, &out->port))
119 return 0;
120 }
121 } else {
122 size_t colon = alen;
123 for (k = 0; k < alen; k++)
124 if (auth[k] == ':') {
125 colon = k;
126 break;
127 }
128 host = auth;
129 hostlen = colon;
130 if (colon < alen &&
131 !parse_port(auth + colon + 1, alen - colon - 1, &out->port))
132 return 0;
133 }
134 if (hostlen == 0 || hostlen > N_URL_SAFE_HOST_MAX)
135 return 0;
136 for (k = 0; k < hostlen; k++) {
137 unsigned char c = (unsigned char)host[k];
138 if (c <= 0x20 || c == '/' || c == '\\' || c == '@')
139 return 0;
140 }
141 memcpy(out->host, host, hostlen);
142 out->host[hostlen] = '\0';
143 }
144 /* path + query (fragment dropped), normalised to begin with '/' */
145 {
146 size_t oi = 0, k = he;
147 if (k >= len || url[k] == '#') {
148 out->path[0] = '/';
149 out->path[1] = '\0';
150 return 1;
151 }
152 if (url[k] == '?')
153 out->path[oi++] = '/'; /* query with no path */
154 for (; k < len && url[k] != '#'; k++) {
155 if ((unsigned char)url[k] <= 0x20)
156 return 0;
157 if (oi + 1 >= sizeof(out->path))
158 return 0;
159 out->path[oi++] = url[k];
160 }
161 out->path[oi] = '\0';
162 if (out->path[0] != '/')
163 return 0;
164 }
165 return 1;
166}
167
168/* Parse a non-negative Content-Length; -1 on empty/non-digit/overflow. */
169static long long parse_clen(const char* s, size_t n) {
170 long long v = 0;
171 size_t i;
172 if (n == 0)
173 return -1;
174 for (i = 0; i < n; i++) {
175 if (s[i] < '0' || s[i] > '9')
176 return -1;
177 if (v > (long long)1 << 50) /* absurd; the fetcher caps anyway */
178 return -1;
179 v = v * 10 + (s[i] - '0');
180 }
181 return v;
182}
183
184/* Case-insensitive: does the comma/space token list [s, s+n) contain \p tok? */
185static int has_token(const char* s, size_t n, const char* tok) {
186 size_t i = 0, tl = strlen(tok);
187 while (i < n) {
188 size_t j;
189 while (i < n && (s[i] == ' ' || s[i] == '\t' || s[i] == ','))
190 i++;
191 j = i;
192 while (j < n && s[j] != ',')
193 j++;
194 { /* trim trailing ws of [i, j) */
195 size_t e = j;
196 while (e > i && (s[e - 1] == ' ' || s[e - 1] == '\t'))
197 e--;
198 if (e - i == tl && name_eq(s + i, tl, tok))
199 return 1;
200 }
201 i = j + 1;
202 }
203 return 0;
204}
205
206/* Copy [v, v+n) into out (cap incl. NUL), truncating; drop control bytes. */
207static void copy_clean(char* out, size_t cap, const char* v, size_t n) {
208 size_t oi = 0, i;
209 if (cap == 0)
210 return;
211 for (i = 0; i < n && oi + 1 < cap; i++)
212 if ((unsigned char)v[i] >= 0x20)
213 out[oi++] = v[i];
214 out[oi] = '\0';
215}
216
217/* Copy a mime: up to ';' (drop params), trimmed, lowercased. */
218static void copy_mime(char* out, size_t cap, const char* v, size_t n) {
219 size_t oi = 0, i;
220 if (cap == 0)
221 return;
222 for (i = 0; i < n && v[i] != ';' && oi + 1 < cap; i++) {
223 unsigned char c = (unsigned char)v[i];
224 if (c > 0x20)
225 out[oi++] = (char)lc(c);
226 }
227 out[oi] = '\0';
228}
229
230int n_url_safe_head_parse(const char* buf, size_t len, n_url_safe_head* out) {
231 size_t scan, i, line_start, sl, p;
232 if (!buf || !out)
233 return 0;
234 memset(out, 0, sizeof(*out));
235 out->content_length = -1;
236 /* locate the end of the head (CRLFCRLF) within the cap */
237 scan = len < N_URL_SAFE_HEAD_MAX ? len : (size_t)N_URL_SAFE_HEAD_MAX;
238 /* nothing shorter than the CRLFCRLF terminator itself can hold a head: the
239 * loop below cannot run and the head_len test below rejects it.
240 * The older cppcheck used by CI folds the ternary assignment above down to
241 * a single value of 0 and then calls this walk dead code, a valueflow
242 * false positive: scan follows the caller's len, unknown here. Spelling
243 * the bound as an explicit "if (scan < 4)" guard only moves the same
244 * diagnostic onto that guard, hence the inline suppression. */
245 // cppcheck-suppress knownConditionTrueFalse ; scan is caller-controlled, the walk is reachable
246 for (i = 0; i + 3 < scan; i++)
247 if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' &&
248 buf[i + 3] == '\n') {
249 out->head_len = i + 4;
250 break;
251 }
252 if (out->head_len == 0)
253 return 0; /* incomplete, or larger than the cap */
254 /* status line: "HTTP/x.y SSS ..." */
255 sl = 0;
256 while (sl + 1 < out->head_len && !(buf[sl] == '\r' && buf[sl + 1] == '\n'))
257 sl++;
258 if (sl < 12 || !ci_prefix(buf, "http/"))
259 return 0;
260 p = 5;
261 while (p < sl && buf[p] != ' ')
262 p++;
263 while (p < sl && buf[p] == ' ')
264 p++;
265 if (p + 3 > sl || buf[p] < '0' || buf[p] > '9' || buf[p + 1] < '0' ||
266 buf[p + 1] > '9' || buf[p + 2] < '0' || buf[p + 2] > '9')
267 return 0;
268 out->status =
269 (buf[p] - '0') * 100 + (buf[p + 1] - '0') * 10 + (buf[p + 2] - '0');
270 /* header lines up to the blank line */
271 line_start = sl + 2;
272 while (line_start + 1 < out->head_len) {
273 size_t le = line_start, colon;
274 while (le + 1 < out->head_len &&
275 !(buf[le] == '\r' && buf[le + 1] == '\n'))
276 le++;
277 if (le == line_start)
278 break; /* blank line: end of headers */
279 colon = line_start;
280 while (colon < le && buf[colon] != ':')
281 colon++;
282 if (colon < le) {
283 const char* name = buf + line_start;
284 size_t nlen = colon - line_start;
285 size_t vs = colon + 1, vlen;
286 const char* val;
287 while (vs < le && (buf[vs] == ' ' || buf[vs] == '\t'))
288 vs++;
289 val = buf + vs;
290 vlen = le - vs;
291 while (vlen > 0 && (val[vlen - 1] == ' ' || val[vlen - 1] == '\t'))
292 vlen--;
293 if (name_eq(name, nlen, "location"))
294 copy_clean(out->location, sizeof(out->location), val, vlen);
295 else if (name_eq(name, nlen, "content-type"))
296 copy_mime(out->content_type, sizeof(out->content_type), val,
297 vlen);
298 else if (name_eq(name, nlen, "content-length"))
299 out->content_length = parse_clen(val, vlen);
300 else if (name_eq(name, nlen, "transfer-encoding"))
301 out->chunked = has_token(val, vlen, "chunked");
302 }
303 line_start = le + 2;
304 }
305 return 1;
306}
307
308int n_url_safe_resolve(const char* base, size_t base_len, const char* loc, size_t loc_len, char* out, size_t cap) {
309 n_url_safe b, tmp;
310 if (!base || !loc || !out || cap == 0)
311 return 0;
312 out[0] = '\0';
313 /* absolute target: copy as-is (still validated below) */
314 if ((loc_len >= 7 && ci_prefix(loc, "http://")) ||
315 (loc_len >= 8 && ci_prefix(loc, "https://"))) {
316 if (loc_len + 1 > cap)
317 return 0;
318 memcpy(out, loc, loc_len);
319 out[loc_len] = '\0';
320 } else {
321 const char* scheme;
322 int n;
323 if (!n_url_safe_parse(base, base_len, &b))
324 return 0;
325 scheme = b.https ? "https" : "http";
326 if (loc_len >= 2 && loc[0] == '/' && loc[1] == '/') {
327 n = snprintf(out, cap, "%s:%.*s", scheme, (int)loc_len, loc);
328 } else if (loc_len >= 1 && loc[0] == '/') {
329 if ((b.https && b.port == 443) || (!b.https && b.port == 80))
330 n = snprintf(out, cap, "%s://%s%.*s", scheme, b.host,
331 (int)loc_len, loc);
332 else
333 n = snprintf(out, cap, "%s://%s:%u%.*s", scheme, b.host,
334 (unsigned)b.port, (int)loc_len, loc);
335 } else {
336 return 0; /* bare relative path: not followed */
337 }
338 if (n < 0 || (size_t)n >= cap)
339 return 0;
340 }
341 return n_url_safe_parse(out, strlen(out), &tmp); /* must be a valid URL */
342}
343
344int n_url_safe_find(const char* text, size_t len, char* out, size_t cap) {
345 size_t i;
346 if (!text || !out || cap == 0)
347 return 0;
348 out[0] = '\0';
349 for (i = 0; i < len; i++) {
350 size_t e;
351 n_url_safe tmp;
352 if ((text[i] != 'h' && text[i] != 'H') || i + 7 > len)
353 continue;
354 if (!ci_prefix(text + i, "http://") &&
355 !(i + 8 <= len && ci_prefix(text + i, "https://")))
356 continue;
357 /* URL extent: up to whitespace / a delimiter / a control byte */
358 e = i;
359 while (e < len) {
360 unsigned char c = (unsigned char)text[e];
361 if (c <= 0x20 || c == '<' || c == '>' || c == '"' || c == '\\' ||
362 c == '`')
363 break;
364 e++;
365 }
366 /* trim trailing sentence punctuation ("see http://x.com.") */
367 while (e > i && strchr(".,;:!?)]}'\"", text[e - 1]))
368 e--;
369 if (e > i && (e - i) < cap) {
370 memcpy(out, text + i, e - i);
371 out[e - i] = '\0';
372 if (n_url_safe_parse(out, e - i, &tmp))
373 return 1; /* first valid URL wins */
374 out[0] = '\0';
375 }
376 i = e > i ? e - 1 : i; /* resume after this candidate */
377 }
378 return 0;
379}
static uint64_t le(const unsigned char *p, int n)
read a little endian unsigned value
Definition n_pth.c:233
static void copy_clean(char *out, size_t cap, const char *v, size_t n)
Definition n_url_safe.c:207
static int parse_port(const char *s, size_t n, uint16_t *out)
Definition n_url_safe.c:58
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
static void copy_mime(char *out, size_t cap, const char *v, size_t n)
Definition n_url_safe.c:218
static int ci_prefix(const char *s, const char *lit)
Definition n_url_safe.c:38
static long long parse_clen(const char *s, size_t n)
Definition n_url_safe.c:169
static int has_token(const char *s, size_t n, const char *tok)
Definition n_url_safe.c:185
static int name_eq(const char *s, size_t n, const char *lit)
Definition n_url_safe.c:47
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
static int lc(int c)
Definition n_url_safe.c:32
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
size_t head_len
bytes through the terminating CRLFCRLF
Definition n_url_safe.h:71
int chunked
Transfer-Encoding: chunked present.
Definition n_url_safe.h:70
#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
#define N_URL_SAFE_HEAD_MAX
Largest response head (status line + headers, through CRLFCRLF) accepted.
Definition n_url_safe.h:54
int https
1 = https (default port 443), 0 = http (80)
Definition n_url_safe.h:60
#define N_URL_SAFE_HOST_MAX
Longest hostname / IP literal accepted (RFC 1035 limit).
Definition n_url_safe.h:48
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