1 /*
2 * Copyright (c) 2016-2021 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
hash_tls_session(const wget_tls_session * tls_session)71 static unsigned int hash_tls_session(const wget_tls_session *tls_session)
72 {
73 unsigned int hash = 0;
74 const unsigned char *p;
75
76 for (p = (unsigned char *)tls_session->host; *p; p++)
77 hash = hash * 101 + *p;
78
79 return hash;
80 }
81
82 WGET_GCC_NONNULL_ALL WGET_GCC_PURE
compare_tls_session(const wget_tls_session * s1,const wget_tls_session * s2)83 static int compare_tls_session(const wget_tls_session *s1, const wget_tls_session *s2)
84 {
85 int n;
86
87 if ((n = strcmp(s1->host, s2->host)))
88 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 return 0;
98 }
99
wget_tls_session_init(wget_tls_session * tls_session)100 wget_tls_session *wget_tls_session_init(wget_tls_session *tls_session)
101 {
102 if (!tls_session) {
103 if (!(tls_session = wget_calloc(1, sizeof(wget_tls_session))))
104 return NULL;
105 } else
106 memset(tls_session, 0, sizeof(*tls_session));
107
108 tls_session->created = time(NULL);
109
110 return tls_session;
111 }
112
wget_tls_session_deinit(wget_tls_session * tls_session)113 void wget_tls_session_deinit(wget_tls_session *tls_session)
114 {
115 if (tls_session) {
116 xfree(tls_session->host);
117 xfree(tls_session->data);
118 }
119 }
120
wget_tls_session_free(wget_tls_session * tls_session)121 void wget_tls_session_free(wget_tls_session *tls_session)
122 {
123 if (tls_session) {
124 wget_tls_session_deinit(tls_session);
125 xfree(tls_session);
126 }
127 }
128
wget_tls_session_new(const char * host,int64_t maxage,const void * data,size_t data_size)129 wget_tls_session *wget_tls_session_new(const char *host, int64_t maxage, const void *data, size_t data_size)
130 {
131 wget_tls_session *tls_session = wget_tls_session_init(NULL);
132
133 if (tls_session) {
134 tls_session->host = wget_strdup(host);
135 tls_session->data = wget_memdup(data, data_size);
136 tls_session->data_size = data_size;
137
138 if (maxage <= 0 || maxage >= INT64_MAX / 2 || tls_session->created < 0 || tls_session->created >= INT64_MAX / 2) {
139 tls_session->maxage = 0;
140 tls_session->expires = 0;
141 } else {
142 tls_session->maxage = maxage;
143 tls_session->expires = tls_session->created + maxage;
144 }
145 }
146
147 return tls_session;
148 }
149
wget_tls_session_get(const wget_tls_session_db * tls_session_db,const char * host,void ** data,size_t * size)150 int wget_tls_session_get(const wget_tls_session_db *tls_session_db, const char *host, void **data, size_t *size)
151 {
152 if (tls_session_db) {
153 wget_tls_session tls_session, *tls_sessionp;
154 int64_t now = time(NULL);
155
156 tls_session.host = host;
157 if (wget_hashmap_get(tls_session_db->entries, &tls_session, &tls_sessionp) && tls_sessionp->expires >= now) {
158 if (data)
159 *data = wget_memdup(tls_sessionp->data, tls_sessionp->data_size);
160 if (size)
161 *size = tls_sessionp->data_size;
162 return 0;
163 }
164 }
165
166 return 1;
167 }
168
wget_tls_session_db_init(wget_tls_session_db * tls_session_db)169 wget_tls_session_db *wget_tls_session_db_init(wget_tls_session_db *tls_session_db)
170 {
171 wget_hashmap *entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_tls_session, (wget_hashmap_compare_fn *) compare_tls_session);
172
173 if (!entries)
174 return NULL;
175
176 if (!tls_session_db) {
177 if (!(tls_session_db = wget_calloc(1, sizeof(wget_tls_session_db)))) {
178 wget_hashmap_free(&entries);
179 return NULL;
180 }
181 } else
182 memset(tls_session_db, 0, sizeof(*tls_session_db));
183
184 wget_hashmap_set_key_destructor(entries, (wget_hashmap_key_destructor *) wget_tls_session_free);
185 wget_hashmap_set_value_destructor(entries, (wget_hashmap_value_destructor *) wget_tls_session_free);
186 tls_session_db->entries = entries;
187
188 wget_thread_mutex_init(&tls_session_db->mutex);
189
190 return tls_session_db;
191 }
192
wget_tls_session_db_deinit(wget_tls_session_db * tls_session_db)193 void wget_tls_session_db_deinit(wget_tls_session_db *tls_session_db)
194 {
195 if (tls_session_db) {
196 wget_thread_mutex_lock(tls_session_db->mutex);
197 wget_hashmap_free(&tls_session_db->entries);
198 wget_thread_mutex_unlock(tls_session_db->mutex);
199
200 wget_thread_mutex_destroy(&tls_session_db->mutex);
201 }
202 }
203
wget_tls_session_db_free(wget_tls_session_db ** tls_session_db)204 void wget_tls_session_db_free(wget_tls_session_db **tls_session_db)
205 {
206 if (tls_session_db) {
207 wget_tls_session_db_deinit(*tls_session_db);
208 xfree(*tls_session_db);
209 }
210 }
211
wget_tls_session_db_add(wget_tls_session_db * tls_session_db,wget_tls_session * tls_session)212 void wget_tls_session_db_add(wget_tls_session_db *tls_session_db, wget_tls_session *tls_session)
213 {
214 if (!tls_session_db || !tls_session)
215 return;
216
217 wget_thread_mutex_lock(tls_session_db->mutex);
218
219 if (tls_session->maxage == 0) {
220 if (wget_hashmap_remove(tls_session_db->entries, tls_session)) {
221 tls_session_db->changed = 1;
222 debug_printf("removed TLS session data for %s\n", tls_session->host);
223 }
224 wget_tls_session_free(tls_session);
225 tls_session = NULL;
226 } else {
227 wget_tls_session *old;
228
229 if (wget_hashmap_get(tls_session_db->entries, tls_session, &old)) {
230 debug_printf("found TLS session data for %s\n", old->host);
231 if (wget_hashmap_remove(tls_session_db->entries, old))
232 debug_printf("removed TLS session data for %s\n", tls_session->host);
233 }
234
235 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 wget_hashmap_put(tls_session_db->entries, tls_session, tls_session);
237 tls_session_db->changed = 1;
238 }
239
240 wget_thread_mutex_unlock(tls_session_db->mutex);
241 }
242
tls_session_db_load(wget_tls_session_db * tls_session_db,FILE * fp)243 static int tls_session_db_load(wget_tls_session_db *tls_session_db, FILE *fp)
244 {
245 wget_tls_session tls_session;
246 struct stat st;
247 char *buf = NULL, *linep, *p;
248 size_t bufsize = 0;
249 ssize_t buflen;
250 int64_t now = time(NULL);
251 bool ok;
252
253 // if the database file hasn't changed since the last read
254 // there's no need to reload
255
256 if (fstat(fileno(fp), &st) == 0) {
257 if (st.st_mtime != tls_session_db->load_time)
258 tls_session_db->load_time = st.st_mtime;
259 else
260 return 0;
261 }
262
263 while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
264 linep = buf;
265
266 while (isspace(*linep)) linep++; // ignore leading whitespace
267 if (!*linep) continue; // skip empty lines
268
269 if (*linep == '#')
270 continue; // skip comments
271
272 // strip off \r\n
273 while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
274 buf[--buflen] = 0;
275
276 wget_tls_session_init(&tls_session);
277 ok = false;
278
279 // parse host
280 if (*linep) {
281 for (p = linep; *linep && !isspace(*linep); )
282 linep++;
283 tls_session.host = wget_strmemdup(p, linep - p);
284 }
285
286 // parse creation time
287 if (*linep) {
288 for (p = ++linep; *linep && !isspace(*linep); )
289 linep++;
290 tls_session.created = (int64_t) atoll(p);
291 if (tls_session.created < 0 || tls_session.created >= INT64_MAX / 2)
292 tls_session.created = 0;
293 }
294
295 // parse max age
296 if (*linep) {
297 for (p = ++linep; *linep && !isspace(*linep); )
298 linep++;
299 tls_session.maxage = (int64_t) atoll(p);
300 if (tls_session.maxage < 0 || tls_session.maxage >= INT64_MAX / 2)
301 tls_session.maxage = 0; // avoid integer overflow here
302 tls_session.expires = tls_session.maxage ? tls_session.created + tls_session.maxage : 0;
303 if (tls_session.expires < now) {
304 // drop expired entry
305 wget_tls_session_deinit(&tls_session);
306 continue;
307 }
308 }
309
310 // parse session data
311 if (*linep) {
312 for (p = ++linep; *linep && !isspace(*linep); )
313 linep++;
314
315 size_t len = linep - p;
316 char *data = wget_malloc(wget_base64_get_decoded_length(len));
317 if (data) {
318 tls_session.data_size = wget_base64_decode(data, p, len);
319 tls_session.data = data;
320
321 ok = true;
322 }
323 }
324
325 if (ok) {
326 bool no_change = wget_hashmap_size(tls_session_db->entries) == 0;
327 wget_tls_session_db_add(tls_session_db, wget_memdup(&tls_session, sizeof(tls_session)));
328 if (no_change)
329 tls_session_db->changed = 0;
330 } else {
331 wget_tls_session_deinit(&tls_session);
332 error_printf(_("Failed to parse HSTS line: '%s'\n"), buf);
333 }
334 }
335
336 xfree(buf);
337
338 if (ferror(fp)) {
339 tls_session_db->load_time = 0; // reload on next call to this function
340 return -1;
341 }
342
343 return 0;
344 }
345
346 // Load the TLS session cache from a flat file
347 // Protected by flock()
348
wget_tls_session_db_load(wget_tls_session_db * tls_session_db,const char * fname)349 int wget_tls_session_db_load(wget_tls_session_db *tls_session_db, const char *fname)
350 {
351 if (!tls_session_db || !fname || !*fname)
352 return 0;
353
354 if (wget_update_file(fname, (wget_update_load_fn *) tls_session_db_load, NULL, tls_session_db)) {
355 error_printf(_("Failed to read TLS session data\n"));
356 return -1;
357 } else {
358 debug_printf("Fetched TLS session data from '%s'\n", fname);
359 return 0;
360 }
361 }
362
363 WGET_GCC_NONNULL_ALL
tls_session_save(FILE * fp,const wget_tls_session * tls_session)364 static int tls_session_save(FILE *fp, const wget_tls_session *tls_session)
365 {
366 char session_b64[wget_base64_get_encoded_length(tls_session->data_size)];
367
368 wget_base64_encode(session_b64, (const char *) tls_session->data, tls_session->data_size);
369
370 wget_fprintf(fp, "%s %lld %lld %s\n", tls_session->host, (long long)tls_session->created, (long long)tls_session->maxage, session_b64);
371 return 0;
372 }
373
tls_session_db_save(void * tls_session_db,FILE * fp)374 static int tls_session_db_save(void *tls_session_db, FILE *fp)
375 {
376 wget_hashmap *entries = ((wget_tls_session_db *)tls_session_db)->entries;
377
378 if (wget_hashmap_size(entries) > 0) {
379 fputs("#TLSSession 1.0 file\n", fp);
380 fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
381 fputs("#<hostname> <created> <max-age> <session data>\n\n", fp);
382
383 wget_hashmap_browse(entries, (wget_hashmap_browse_fn *) tls_session_save, fp);
384
385 if (ferror(fp))
386 return -1;
387 }
388
389 return 0;
390 }
391
392 // Save the TLS session cache to a flat file
393 // Protected by flock()
394
wget_tls_session_db_save(wget_tls_session_db * tls_session_db,const char * fname)395 int wget_tls_session_db_save(wget_tls_session_db *tls_session_db, const char *fname)
396 {
397 int size;
398
399 if (!tls_session_db || !fname || !*fname)
400 return -1;
401
402 if (wget_update_file(fname, (wget_update_load_fn *) tls_session_db_load, tls_session_db_save, tls_session_db)) {
403 error_printf(_("Failed to write TLS session file '%s'\n"), fname);
404 return -1;
405 }
406
407 if ((size = wget_hashmap_size(tls_session_db->entries)))
408 debug_printf("Saved %d TLS session entr%s into '%s'\n", size, size != 1 ? "ies" : "y", fname);
409 else
410 debug_printf("No TLS session entries to save. Table is empty.\n");
411
412 tls_session_db->changed = 0;
413
414 return 0;
415 }
416
wget_tls_session_db_changed(wget_tls_session_db * tls_session_db)417 int wget_tls_session_db_changed(wget_tls_session_db *tls_session_db)
418 {
419 return tls_session_db ? tls_session_db->changed : 0;
420 }
421