Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_cookie.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
29
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33#include <unistd.h>
34
35#include "nilorea/n_common.h"
37#include "nilorea/n_log.h"
38#include "nilorea/n_str.h"
39
41static int log_level = LOG_ERR;
43static int failures = 0;
44
48static void expect_true(const char* what, int ok) {
49 if (!ok) {
50 n_log(LOG_ERR, "FAIL: %s", what);
51 failures++;
52 } else {
53 n_log(LOG_INFO, "ok: %s", what);
54 }
55}
56
61static int process_args(int argc, char** argv) {
62 int opt = 0;
63
64 while ((opt = getopt(argc, argv, "V:")) != -1) {
65 if (opt == 'V') {
66 log_level = atoi(optarg);
67 } else {
68 return -1;
69 }
70 }
71 return 0;
72}
73
79static int cookie_is(const char* header, const char* name, const char* want) {
80 size_t vl = 99;
81 const char* v = n_http_cookie_get(header, strlen(header), name, &vl);
82 if (!want)
83 return v == NULL && vl == 0;
84 return v && vl == strlen(want) && memcmp(v, want, vl) == 0;
85}
86
88static void test_read(void) {
89 const char* h = "a=first; b=x%41;a=second; c=\"q\"; bad name=1; =v; d; e = spaced ;;f=";
90
91 expect_true("first occurrence wins", cookie_is(h, "a", "first"));
92 expect_true("both occurrences counted", n_http_cookie_count(h, strlen(h), "a") == 2);
93 expect_true("value kept raw (percent escape)", cookie_is(h, "b", "x%41"));
94 expect_true("value kept raw (quotes)", cookie_is(h, "c", "\"q\""));
95 expect_true("pair with a non-token name skipped", cookie_is(h, "bad name", NULL) && cookie_is(h, "name", NULL));
96 expect_true("pair without '=' has an empty value", cookie_is(h, "d", ""));
97 expect_true("spaces around name and value trimmed", cookie_is(h, "e", "spaced"));
98 expect_true("empty value", cookie_is(h, "f", ""));
99 expect_true("absent cookie", cookie_is(h, "zz", NULL));
100 expect_true("names are case-sensitive", cookie_is(h, "A", NULL));
101 expect_true("a name prefix does not match", cookie_is("abc=1", "ab", NULL));
102 expect_true("__Host- name found", cookie_is("__Host-nsid=abc123; other=1", "__Host-nsid", "abc123"));
103 expect_true("empty header", cookie_is("", "a", NULL));
104 expect_true("NULL header", n_http_cookie_get(NULL, 0, "a", NULL) == NULL && n_http_cookie_count(NULL, 0, "a") == 0);
105 expect_true("empty name", n_http_cookie_get("a=1", 3, "", NULL) == NULL);
106}
107
112static int builds(const N_HTTP_SET_COOKIE* c, const char* want) {
113 N_STR* out = NULL;
114 int ok = n_http_set_cookie_build(&out, c);
115 int res = want ? (ok == TRUE && out && strcmp(out->data, want) == 0) : (ok == FALSE && out == NULL);
116 if (!res)
117 n_log(LOG_ERR, "got: %s", out ? out->data : "(nothing)");
118 free_nstr(&out);
119 return res;
120}
121
123static void test_build(void) {
125 N_STR* out = NULL;
126
127 memset(&c, 0, sizeof(c));
128 c.name = "__Host-nsid";
129 c.value = "AbC-123_x";
130 c.path = "/";
132 c.secure = 1;
133 c.http_only = 1;
135 expect_true("session cookie, as the site sends it",
136 builds(&c, "__Host-nsid=AbC-123_x; Path=/; Secure; HttpOnly; SameSite=Lax"));
137 c.max_age = 0;
138 c.value = "";
139 expect_true("deletion",
140 builds(&c, "__Host-nsid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax"));
141 c.max_age = 3600;
143 expect_true("Max-Age and Strict", builds(&c, "__Host-nsid=; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=Strict"));
144
145 memset(&c, 0, sizeof(c));
146 c.name = "pref";
147 c.value = "dark";
149 c.domain = "example.org";
150 c.path = "/a b";
151 expect_true("plain cookie with Domain, no flags", builds(&c, "pref=dark; Path=/a b; Domain=example.org"));
152
153 /* injection and invalid values */
154 c.domain = NULL;
155 c.path = NULL;
156 c.value = "a;b";
157 expect_true("';' in a value refused", builds(&c, NULL));
158 c.value = "a\r\nSet-Cookie: x=1";
159 expect_true("CRLF in a value refused", builds(&c, NULL));
160 c.value = "a b";
161 expect_true("space in a value refused", builds(&c, NULL));
162 c.value = "\"q\"";
163 expect_true("quotes in a value refused", builds(&c, NULL));
164 c.value = "ok";
165 c.name = "a=b";
166 expect_true("'=' in a name refused", builds(&c, NULL));
167 c.name = "";
168 expect_true("empty name refused", builds(&c, NULL));
169 c.name = NULL;
170 expect_true("NULL name refused", builds(&c, NULL));
171 c.name = "ok";
172 c.path = "/x;Domain=evil";
173 expect_true("';' in a path refused", builds(&c, NULL));
174 c.path = "/x\n";
175 expect_true("control byte in a path refused", builds(&c, NULL));
176 c.path = "";
177 expect_true("empty path refused", builds(&c, NULL));
178 c.path = NULL;
179 c.domain = "evil.org; Secure";
180 expect_true("bad domain refused", builds(&c, NULL));
181 c.domain = NULL;
182 c.max_age = -5;
183 expect_true("negative Max-Age refused", builds(&c, NULL));
186 expect_true("SameSite=None without Secure refused", builds(&c, NULL));
187 c.secure = 1;
188 expect_true("SameSite=None with Secure", builds(&c, "ok=ok; Secure; SameSite=None"));
190 expect_true("unknown SameSite refused", builds(&c, NULL));
192
193 /* cookie prefixes */
194 c.name = "__Host-x";
195 c.path = "/";
196 c.secure = 0;
197 expect_true("__Host- without Secure refused", builds(&c, NULL));
198 c.secure = 1;
199 c.path = "/admin";
200 expect_true("__Host- with Path other than / refused", builds(&c, NULL));
201 c.path = NULL;
202 expect_true("__Host- without Path refused", builds(&c, NULL));
203 c.path = "/";
204 c.domain = "example.org";
205 expect_true("__Host- with Domain refused", builds(&c, NULL));
206 c.domain = NULL;
207 c.name = "__Secure-x";
208 c.secure = 0;
209 expect_true("__Secure- without Secure refused", builds(&c, NULL));
210 c.secure = 1;
211 expect_true("__Secure- with Secure", builds(&c, "__Secure-x=ok; Path=/; Secure"));
212
213 /* appends; a refused cookie leaves the destination unchanged */
214 out = char_to_nstr("X: ");
215 c.name = "a";
216 c.path = NULL;
217 c.secure = 0;
218 expect_true("append", n_http_set_cookie_build(&out, &c) == TRUE && strcmp(out->data, "X: a=ok") == 0);
219 c.value = "b;c";
220 expect_true("refused append returns FALSE", n_http_set_cookie_build(&out, &c) == FALSE);
221 expect_true("refused append leaves the destination", strcmp(out->data, "X: a=ok") == 0);
222 free_nstr(&out);
223 expect_true("NULL arguments", n_http_set_cookie_build(NULL, &c) == FALSE && n_http_set_cookie_build(&out, NULL) == FALSE);
224}
225
230int main(int argc, char** argv) {
231 if (process_args(argc, argv) != 0) {
232 fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
233 return 1;
234 }
236
237 test_read();
238 test_build();
239
240 if (failures) {
241 n_log(LOG_ERR, "ex_http_cookie: %d failure(s)", failures);
242 return 1;
243 }
244 n_log(LOG_NOTICE, "ex_http_cookie: all checks passed");
245 return 0;
246}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
static int h(void *ctx, const N_ROUTE_MATCH *m)
a handler that does nothing (the test only routes)
#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
#define LOG_INFO
informational
Definition n_log.h:106
int http_only
non-zero: HttpOnly
const char * value
cookie value, RFC 6265 cookie-octets only (may be "")
int secure
non-zero: Secure
const char * path
Path attribute, or NULL.
const char * name
cookie name, an RFC 9110 token
const char * domain
Domain attribute, or NULL (host-only cookie).
N_HTTP_SAMESITE same_site
SameSite attribute.
long long max_age
Max-Age in seconds; N_HTTP_COOKIE_SESSION for a session cookie; 0 deletes the cookie (Max-Age=0 plus ...
size_t n_http_cookie_count(const char *header, size_t len, const char *name)
count the pairs named name in a Cookie header value
N_HTTP_SAMESITE
SameSite attribute of a Set-Cookie.
int n_http_set_cookie_build(N_STR **out, const N_HTTP_SET_COOKIE *c)
append a Set-Cookie header value, refusing anything that could break out of an attribute or that a br...
#define N_HTTP_COOKIE_SESSION
Max-Age value meaning "session cookie": neither Max-Age nor Expires is sent.
const char * n_http_cookie_get(const char *header, size_t len, const char *name, size_t *value_len)
find a cookie in a Cookie header value (first occurrence wins)
@ N_HTTP_SAMESITE_NONE
SameSite=None, needs Secure.
@ N_HTTP_SAMESITE_STRICT
SameSite=Strict.
@ N_HTTP_SAMESITE_LAX
SameSite=Lax.
@ N_HTTP_SAMESITE_UNSET
no SameSite attribute (browser default)
what a Set-Cookie should carry
char * data
the string
Definition n_str.h:63
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
Definition n_str.c:262
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
Generic log system.
N_STR and string function declaration.