/src/wget2/libwget/tls_session.c
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright (c) 2016-2024 Free Software Foundation, Inc. |
3 | | * |
4 | | * This file is part of libwget. |
5 | | * |
6 | | * Libwget is free software: you can redistribute it and/or modify |
7 | | * it under the terms of the GNU Lesser General Public License as published by |
8 | | * the Free Software Foundation, either version 3 of the License, or |
9 | | * (at your option) any later version. |
10 | | * |
11 | | * Libwget is distributed in the hope that it will be useful, |
12 | | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
14 | | * GNU Lesser General Public License for more details. |
15 | | * |
16 | | * You should have received a copy of the GNU Lesser General Public License |
17 | | * along with libwget. If not, see <https://www.gnu.org/licenses/>. |
18 | | * |
19 | | * |
20 | | * TLS session data cache for TLS resumption |
21 | | * |
22 | | * Changelog |
23 | | * 21.07.2016 Tim Ruehsen created |
24 | | * |
25 | | */ |
26 | | |
27 | | #include <config.h> |
28 | | |
29 | | #include <stdio.h> |
30 | | #include <stdlib.h> |
31 | | #include <string.h> |
32 | | #include <ctype.h> |
33 | | #include <time.h> |
34 | | #include <errno.h> |
35 | | #include <sys/stat.h> |
36 | | #include <sys/file.h> |
37 | | |
38 | | #include <wget.h> |
39 | | #include "private.h" |
40 | | |
41 | | struct wget_tls_session_db_st { |
42 | | wget_hashmap * |
43 | | entries; |
44 | | wget_thread_mutex |
45 | | mutex; |
46 | | int64_t |
47 | | load_time; |
48 | | bool |
49 | | changed : 1; // whether or not the db has been changed / needs saving |
50 | | }; |
51 | | |
52 | | struct wget_tls_session_st { |
53 | | const char * |
54 | | host; |
55 | | int64_t |
56 | | expires; // expiry time |
57 | | int64_t |
58 | | created; // creation time |
59 | | int64_t |
60 | | maxage; // max-age in seconds |
61 | | size_t |
62 | | data_size; |
63 | | const char * |
64 | | data; // session resumption data |
65 | | }; |
66 | | |
67 | | #ifdef __clang__ |
68 | | __attribute__((no_sanitize("integer"))) |
69 | | #endif |
70 | | WGET_GCC_PURE |
71 | | static unsigned int hash_tls_session(const wget_tls_session *tls_session) |
72 | 4.73k | { |
73 | 4.73k | unsigned int hash = 0; |
74 | 4.73k | const unsigned char *p; |
75 | | |
76 | 19.5k | for (p = (unsigned char *)tls_session->host; *p; p++) |
77 | 14.8k | hash = hash * 101 + *p; |
78 | | |
79 | 4.73k | return hash; |
80 | 4.73k | } |
81 | | |
82 | | WGET_GCC_NONNULL_ALL WGET_GCC_PURE |
83 | | static int compare_tls_session(const wget_tls_session *s1, const wget_tls_session *s2) |
84 | 1.79k | { |
85 | 1.79k | int n; |
86 | | |
87 | 1.79k | if ((n = strcmp(s1->host, s2->host))) |
88 | 1.35k | return n; |
89 | | |
90 | | // if (s1->data_size < s2->data_size) |
91 | | // return -1; |
92 | | |
93 | | // if (s1->data_size > s2->data_size) |
94 | | // return 1; |
95 | | |
96 | | // return memcmp(s1->data, s2->data, s1->data_size); |
97 | 448 | return 0; |
98 | 1.79k | } |
99 | | |
100 | | wget_tls_session *wget_tls_session_init(wget_tls_session *tls_session) |
101 | 2.87k | { |
102 | 2.87k | if (!tls_session) { |
103 | 0 | if (!(tls_session = wget_calloc(1, sizeof(wget_tls_session)))) |
104 | 0 | return NULL; |
105 | 0 | } else |
106 | 2.87k | memset(tls_session, 0, sizeof(*tls_session)); |
107 | | |
108 | 2.87k | tls_session->created = time(NULL); |
109 | | |
110 | 2.87k | return tls_session; |
111 | 2.87k | } |
112 | | |
113 | | void wget_tls_session_deinit(wget_tls_session *tls_session) |
114 | 2.87k | { |
115 | 2.87k | if (tls_session) { |
116 | 2.87k | xfree(tls_session->host); |
117 | 2.87k | xfree(tls_session->data); |
118 | 2.87k | } |
119 | 2.87k | } |
120 | | |
121 | | void wget_tls_session_free(wget_tls_session *tls_session) |
122 | 1.58k | { |
123 | 1.58k | if (tls_session) { |
124 | 1.58k | wget_tls_session_deinit(tls_session); |
125 | 1.58k | xfree(tls_session); |
126 | 1.58k | } |
127 | 1.58k | } |
128 | | |
129 | | wget_tls_session *wget_tls_session_new(const char *host, int64_t maxage, const void *data, size_t data_size) |
130 | 0 | { |
131 | 0 | wget_tls_session *tls_session = wget_tls_session_init(NULL); |
132 | |
|
133 | 0 | if (tls_session) { |
134 | 0 | tls_session->host = wget_strdup(host); |
135 | 0 | tls_session->data = wget_memdup(data, data_size); |
136 | 0 | tls_session->data_size = data_size; |
137 | |
|
138 | 0 | if (maxage <= 0 || maxage >= INT64_MAX / 2 || tls_session->created < 0 || tls_session->created >= INT64_MAX / 2) { |
139 | 0 | tls_session->maxage = 0; |
140 | 0 | tls_session->expires = 0; |
141 | 0 | } else { |
142 | 0 | tls_session->maxage = maxage; |
143 | 0 | tls_session->expires = tls_session->created + maxage; |
144 | 0 | } |
145 | 0 | } |
146 | |
|
147 | 0 | return tls_session; |
148 | 0 | } |
149 | | |
150 | | int wget_tls_session_get(const wget_tls_session_db *tls_session_db, const char *host, void **data, size_t *size) |
151 | 1.20k | { |
152 | 1.20k | if (tls_session_db) { |
153 | 1.20k | wget_tls_session tls_session, *tls_sessionp; |
154 | 1.20k | int64_t now = time(NULL); |
155 | | |
156 | 1.20k | tls_session.host = host; |
157 | 1.20k | if (wget_hashmap_get(tls_session_db->entries, &tls_session, &tls_sessionp) && tls_sessionp->expires >= now) { |
158 | 91 | if (data) |
159 | 91 | *data = wget_memdup(tls_sessionp->data, tls_sessionp->data_size); |
160 | 91 | if (size) |
161 | 91 | *size = tls_sessionp->data_size; |
162 | 91 | return 0; |
163 | 91 | } |
164 | 1.20k | } |
165 | | |
166 | 1.11k | return 1; |
167 | 1.20k | } |
168 | | |
169 | | wget_tls_session_db *wget_tls_session_db_init(wget_tls_session_db *tls_session_db) |
170 | 1.20k | { |
171 | 1.20k | wget_hashmap *entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_tls_session, (wget_hashmap_compare_fn *) compare_tls_session); |
172 | | |
173 | 1.20k | if (!entries) |
174 | 0 | return NULL; |
175 | | |
176 | 1.20k | if (!tls_session_db) { |
177 | 1.20k | if (!(tls_session_db = wget_calloc(1, sizeof(wget_tls_session_db)))) { |
178 | 0 | wget_hashmap_free(&entries); |
179 | 0 | return NULL; |
180 | 0 | } |
181 | 1.20k | } else |
182 | 0 | memset(tls_session_db, 0, sizeof(*tls_session_db)); |
183 | | |
184 | 1.20k | wget_hashmap_set_key_destructor(entries, (wget_hashmap_key_destructor *) wget_tls_session_free); |
185 | 1.20k | wget_hashmap_set_value_destructor(entries, (wget_hashmap_value_destructor *) wget_tls_session_free); |
186 | 1.20k | tls_session_db->entries = entries; |
187 | | |
188 | 1.20k | wget_thread_mutex_init(&tls_session_db->mutex); |
189 | | |
190 | 1.20k | return tls_session_db; |
191 | 1.20k | } |
192 | | |
193 | | void wget_tls_session_db_deinit(wget_tls_session_db *tls_session_db) |
194 | 11.3k | { |
195 | 11.3k | if (tls_session_db) { |
196 | 1.20k | wget_thread_mutex_lock(tls_session_db->mutex); |
197 | 1.20k | wget_hashmap_free(&tls_session_db->entries); |
198 | 1.20k | wget_thread_mutex_unlock(tls_session_db->mutex); |
199 | | |
200 | 1.20k | wget_thread_mutex_destroy(&tls_session_db->mutex); |
201 | 1.20k | } |
202 | 11.3k | } |
203 | | |
204 | | void wget_tls_session_db_free(wget_tls_session_db **tls_session_db) |
205 | 11.3k | { |
206 | 11.3k | if (tls_session_db) { |
207 | 11.3k | wget_tls_session_db_deinit(*tls_session_db); |
208 | 11.3k | xfree(*tls_session_db); |
209 | 11.3k | } |
210 | 11.3k | } |
211 | | |
212 | | void wget_tls_session_db_add(wget_tls_session_db *tls_session_db, wget_tls_session *tls_session) |
213 | 1.58k | { |
214 | 1.58k | if (!tls_session_db || !tls_session) |
215 | 0 | return; |
216 | | |
217 | 1.58k | wget_thread_mutex_lock(tls_session_db->mutex); |
218 | | |
219 | 1.58k | if (tls_session->maxage == 0) { |
220 | 0 | if (wget_hashmap_remove(tls_session_db->entries, tls_session)) { |
221 | 0 | tls_session_db->changed = 1; |
222 | 0 | debug_printf("removed TLS session data for %s\n", tls_session->host); |
223 | 0 | } |
224 | 0 | wget_tls_session_free(tls_session); |
225 | 0 | tls_session = NULL; |
226 | 1.58k | } else { |
227 | 1.58k | wget_tls_session *old; |
228 | | |
229 | 1.58k | if (wget_hashmap_get(tls_session_db->entries, tls_session, &old)) { |
230 | 357 | debug_printf("found TLS session data for %s\n", old->host); |
231 | 357 | if (wget_hashmap_remove(tls_session_db->entries, old)) |
232 | 357 | debug_printf("removed TLS session data for %s\n", tls_session->host); |
233 | 357 | } |
234 | | |
235 | 1.58k | debug_printf("add TLS session data for %s (maxage=%lld, size=%zu)\n", tls_session->host, (long long)tls_session->maxage, tls_session->data_size); |
236 | 1.58k | wget_hashmap_put(tls_session_db->entries, tls_session, tls_session); |
237 | 1.58k | tls_session_db->changed = 1; |
238 | 1.58k | } |
239 | | |
240 | 1.58k | wget_thread_mutex_unlock(tls_session_db->mutex); |
241 | 1.58k | } |
242 | | |
243 | | static int tls_session_db_load(wget_tls_session_db *tls_session_db, FILE *fp) |
244 | 1.20k | { |
245 | 1.20k | wget_tls_session tls_session; |
246 | 1.20k | struct stat st; |
247 | 1.20k | char *buf = NULL, *linep, *p; |
248 | 1.20k | size_t bufsize = 0; |
249 | 1.20k | ssize_t buflen; |
250 | 1.20k | int64_t now = time(NULL); |
251 | 1.20k | bool ok; |
252 | | |
253 | | // if the database file hasn't changed since the last read |
254 | | // there's no need to reload |
255 | | |
256 | 1.20k | if (fstat(fileno(fp), &st) == 0) { |
257 | 0 | if (st.st_mtime != tls_session_db->load_time) |
258 | 0 | tls_session_db->load_time = st.st_mtime; |
259 | 0 | else |
260 | 0 | return 0; |
261 | 0 | } |
262 | | |
263 | 5.64k | while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) { |
264 | 4.43k | linep = buf; |
265 | | |
266 | 4.43k | while (isspace(*linep)) linep++; // ignore leading whitespace |
267 | 4.43k | if (!*linep) continue; // skip empty lines |
268 | | |
269 | 3.07k | if (*linep == '#') |
270 | 196 | continue; // skip comments |
271 | | |
272 | | // strip off \r\n |
273 | 2.87k | while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r')) |
274 | 0 | buf[--buflen] = 0; |
275 | | |
276 | 2.87k | wget_tls_session_init(&tls_session); |
277 | 2.87k | ok = false; |
278 | | |
279 | | // parse host |
280 | 2.87k | if (*linep) { |
281 | 9.82k | for (p = linep; *linep && !isspace(*linep); ) |
282 | 6.94k | linep++; |
283 | 2.87k | tls_session.host = wget_strmemdup(p, linep - p); |
284 | 2.87k | } |
285 | | |
286 | | // parse creation time |
287 | 2.87k | if (*linep) { |
288 | 5.79k | for (p = ++linep; *linep && !isspace(*linep); ) |
289 | 3.28k | linep++; |
290 | 2.50k | tls_session.created = (int64_t) atoll(p); |
291 | 2.50k | if (tls_session.created < 0 || tls_session.created >= INT64_MAX / 2) |
292 | 325 | tls_session.created = 0; |
293 | 2.50k | } |
294 | | |
295 | | // parse max age |
296 | 2.87k | if (*linep) { |
297 | 15.4k | for (p = ++linep; *linep && !isspace(*linep); ) |
298 | 13.3k | linep++; |
299 | 2.13k | tls_session.maxage = (int64_t) atoll(p); |
300 | 2.13k | if (tls_session.maxage < 0 || tls_session.maxage >= INT64_MAX / 2) |
301 | 164 | tls_session.maxage = 0; // avoid integer overflow here |
302 | 2.13k | tls_session.expires = tls_session.maxage ? tls_session.created + tls_session.maxage : 0; |
303 | 2.13k | if (tls_session.expires < now) { |
304 | | // drop expired entry |
305 | 357 | wget_tls_session_deinit(&tls_session); |
306 | 357 | continue; |
307 | 357 | } |
308 | 2.13k | } |
309 | | |
310 | | // parse session data |
311 | 2.52k | if (*linep) { |
312 | 9.28k | for (p = ++linep; *linep && !isspace(*linep); ) |
313 | 7.69k | linep++; |
314 | | |
315 | 1.58k | size_t len = linep - p; |
316 | 1.58k | char *data = wget_malloc(wget_base64_get_decoded_length(len)); |
317 | 1.58k | if (data) { |
318 | 1.58k | tls_session.data_size = wget_base64_decode(data, p, len); |
319 | 1.58k | tls_session.data = data; |
320 | | |
321 | 1.58k | ok = true; |
322 | 1.58k | } |
323 | 1.58k | } |
324 | | |
325 | 2.52k | if (ok) { |
326 | 1.58k | bool no_change = wget_hashmap_size(tls_session_db->entries) == 0; |
327 | 1.58k | wget_tls_session_db_add(tls_session_db, wget_memdup(&tls_session, sizeof(tls_session))); |
328 | 1.58k | if (no_change) |
329 | 572 | tls_session_db->changed = 0; |
330 | 1.58k | } else { |
331 | 936 | wget_tls_session_deinit(&tls_session); |
332 | 936 | error_printf(_("Failed to parse HSTS line: '%s'\n"), buf); |
333 | 936 | } |
334 | 2.52k | } |
335 | | |
336 | 1.20k | xfree(buf); |
337 | | |
338 | 1.20k | if (ferror(fp)) { |
339 | 0 | tls_session_db->load_time = 0; // reload on next call to this function |
340 | 0 | return -1; |
341 | 0 | } |
342 | | |
343 | 1.20k | return 0; |
344 | 1.20k | } |
345 | | |
346 | | // Load the TLS session cache from a flat file |
347 | | // Protected by flock() |
348 | | |
349 | | int wget_tls_session_db_load(wget_tls_session_db *tls_session_db, const char *fname) |
350 | 1.20k | { |
351 | 1.20k | if (!tls_session_db || !fname || !*fname) |
352 | 0 | return 0; |
353 | | |
354 | 1.20k | if (wget_update_file(fname, (wget_update_load_fn *) tls_session_db_load, NULL, tls_session_db)) { |
355 | 0 | error_printf(_("Failed to read TLS session data\n")); |
356 | 0 | return -1; |
357 | 1.20k | } else { |
358 | 1.20k | debug_printf("Fetched TLS session data from '%s'\n", fname); |
359 | 1.20k | return 0; |
360 | 1.20k | } |
361 | 1.20k | } |
362 | | |
363 | | WGET_GCC_NONNULL_ALL |
364 | | static int tls_session_save(void *_fp, const void *_tls_session, WGET_GCC_UNUSED void *v) |
365 | 0 | { |
366 | 0 | FILE *fp = _fp; |
367 | 0 | const wget_tls_session *tls_session = _tls_session; |
368 | 0 | char tmp[1024], *session_b64 = tmp; |
369 | 0 | size_t b64_len = wget_base64_get_encoded_length(tls_session->data_size); |
370 | |
|
371 | 0 | if (b64_len > sizeof(tmp)) { |
372 | 0 | session_b64 = wget_malloc(b64_len); |
373 | 0 | if (!session_b64) { |
374 | 0 | error_printf(_("Failed to allocate %zu bytes\n"), b64_len); |
375 | 0 | return 0; |
376 | 0 | } |
377 | 0 | } |
378 | | |
379 | 0 | wget_base64_encode(session_b64, (const char *) tls_session->data, tls_session->data_size); |
380 | |
|
381 | 0 | wget_fprintf(fp, "%s %lld %lld %s\n", tls_session->host, (long long)tls_session->created, (long long)tls_session->maxage, session_b64); |
382 | |
|
383 | 0 | if (session_b64 != tmp) |
384 | 0 | xfree(session_b64); |
385 | |
|
386 | 0 | return 0; |
387 | 0 | } |
388 | | |
389 | | static int tls_session_db_save(void *tls_session_db, FILE *fp) |
390 | 0 | { |
391 | 0 | wget_hashmap *entries = ((wget_tls_session_db *)tls_session_db)->entries; |
392 | |
|
393 | 0 | if (wget_hashmap_size(entries) > 0) { |
394 | 0 | fputs("#TLSSession 1.0 file\n", fp); |
395 | 0 | fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp); |
396 | 0 | fputs("#<hostname> <created> <max-age> <session data>\n\n", fp); |
397 | |
|
398 | 0 | wget_hashmap_browse(entries, tls_session_save, fp); |
399 | |
|
400 | 0 | if (ferror(fp)) |
401 | 0 | return -1; |
402 | 0 | } |
403 | | |
404 | 0 | return 0; |
405 | 0 | } |
406 | | |
407 | | // Save the TLS session cache to a flat file |
408 | | // Protected by flock() |
409 | | |
410 | | int wget_tls_session_db_save(wget_tls_session_db *tls_session_db, const char *fname) |
411 | 0 | { |
412 | 0 | int size; |
413 | |
|
414 | 0 | if (!tls_session_db || !fname || !*fname) |
415 | 0 | return -1; |
416 | | |
417 | 0 | if (wget_update_file(fname, (wget_update_load_fn *) tls_session_db_load, tls_session_db_save, tls_session_db)) { |
418 | 0 | error_printf(_("Failed to write TLS session file '%s'\n"), fname); |
419 | 0 | return -1; |
420 | 0 | } |
421 | | |
422 | 0 | if ((size = wget_hashmap_size(tls_session_db->entries))) |
423 | 0 | debug_printf("Saved %d TLS session entr%s into '%s'\n", size, size != 1 ? "ies" : "y", fname); |
424 | 0 | else |
425 | 0 | debug_printf("No TLS session entries to save. Table is empty.\n"); |
426 | |
|
427 | 0 | tls_session_db->changed = 0; |
428 | |
|
429 | 0 | return 0; |
430 | 0 | } |
431 | | |
432 | | int wget_tls_session_db_changed(wget_tls_session_db *tls_session_db) |
433 | 0 | { |
434 | 0 | return tls_session_db ? tls_session_db->changed : 0; |
435 | 0 | } |