1 /*
2  * Copyright (c) 2014 Tim Ruehsen
3  * Copyright (c) 2015-2021 Free Software Foundation, Inc.
4  *
5  * This file is part of libwget.
6  *
7  * Libwget is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Lesser General Public License as published by
9  * the Free Software Foundation, either version 3 of the License, or
10  * (at your option) any later version.
11  *
12  * Libwget is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public License
18  * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  *
21  * HSTS routines
22  *
23  * Changelog
24  * 28.01.2014  Tim Ruehsen  created
25  *
26  */
27 
28 #include <config.h>
29 
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <ctype.h>
34 #include <time.h>
35 #include <errno.h>
36 #include <sys/stat.h>
37 #include <sys/file.h>
38 
39 #include <wget.h>
40 #include "private.h"
41 
42 /**
43  * \file
44  * \brief HTTP Strict Transport Security (RFC 6797) routines
45  * \defgroup libwget-hsts HTTP Strict Transport Security (RFC 6797) routines
46  * @{
47  *
48  * This is an implementation of RFC 6797.
49  */
50 
51 struct wget_hsts_db_st {
52 	const char *
53 		fname;
54 	wget_hashmap *
55 		entries;
56 	wget_thread_mutex
57 		mutex;
58 	int64_t
59 		load_time;
60 };
61 
62 typedef struct {
63 	const char *
64 		host;
65 	int64_t
66 		expires; // expiry time
67 	int64_t
68 		created; // creation time
69 	int64_t
70 		maxage; // max-age in seconds
71 	uint16_t
72 		port;
73 	bool
74 		include_subdomains : 1; // whether or not subdomains are included
75 } hsts_entry;
76 
77 /// Pointer to the function table
78 static const wget_hsts_db_vtable
79 	*plugin_vtable;
80 
wget_hsts_set_plugin(const wget_hsts_db_vtable * vtable)81 void wget_hsts_set_plugin(const wget_hsts_db_vtable *vtable)
82 {
83 	plugin_vtable = vtable;
84 }
85 
86 #ifdef __clang__
87 __attribute__((no_sanitize("integer")))
88 #endif
89 WGET_GCC_PURE
hash_hsts(const hsts_entry * hsts)90 static unsigned int hash_hsts(const hsts_entry *hsts)
91 {
92 	unsigned int hash = hsts->port;
93 	const unsigned char *p;
94 
95 	for (p = (unsigned char *)hsts->host; *p; p++)
96 		hash = hash * 101 + *p;
97 
98 	return hash;
99 }
100 
101 WGET_GCC_NONNULL_ALL WGET_GCC_PURE
compare_hsts(const hsts_entry * h1,const hsts_entry * h2)102 static int compare_hsts(const hsts_entry *h1, const hsts_entry *h2)
103 {
104 	int n;
105 
106 	if ((n = strcmp(h1->host, h2->host)))
107 		return n;
108 
109 	return h1->port < h2->port ? -1 : (h1->port > h2->port ? 1 : 0);
110 }
111 
init_hsts(hsts_entry * hsts)112 static hsts_entry *init_hsts(hsts_entry *hsts)
113 {
114 	if (!hsts) {
115 		if (!(hsts = wget_calloc(1, sizeof(hsts_entry))))
116 			return NULL;
117 	} else
118 		memset(hsts, 0, sizeof(*hsts));
119 
120 	hsts->created = time(NULL);
121 
122 	return hsts;
123 }
124 
deinit_hsts(hsts_entry * hsts)125 static void deinit_hsts(hsts_entry *hsts)
126 {
127 	if (hsts) {
128 		xfree(hsts->host);
129 	}
130 }
131 
free_hsts(hsts_entry * hsts)132 static void free_hsts(hsts_entry *hsts)
133 {
134 	if (hsts) {
135 		deinit_hsts(hsts);
136 		xfree(hsts);
137 	}
138 }
139 
new_hsts(const char * host,uint16_t port,int64_t maxage,bool include_subdomains)140 static hsts_entry *new_hsts(const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
141 {
142 	hsts_entry *hsts = init_hsts(NULL);
143 
144 	if (!hsts)
145 		return NULL;
146 
147 	hsts->host = wget_strdup(host);
148 	hsts->port = port ? port : 443;
149 	hsts->include_subdomains = include_subdomains;
150 
151 	if (maxage <= 0 || maxage >= INT64_MAX / 2 || hsts->created < 0 || hsts->created >= INT64_MAX / 2) {
152 		hsts->maxage = 0;
153 		hsts->expires = 0;
154 	} else {
155 		hsts->maxage = maxage;
156 		hsts->expires = hsts->created + maxage;
157 	}
158 
159 	return hsts;
160 }
161 
162 /**
163  * \param[in] hsts_db An HSTS database
164  * \param[in] host Hostname to search for
165  * \param[in] port Port number in the original URI/IRI.
166  *                 Port number 80 is treated similar to 443, as 80 is default port for HTTP.
167  * \return 1 if the host must be accessed only through TLS, 0 if there is no such condition.
168  *
169  * Searches for a given host in the database for any previously added entry.
170  *
171  * HSTS entries older than amount of time specified by `maxage` are considered `expired` and are ignored.
172  *
173  * This function is thread-safe and can be called from multiple threads concurrently.
174  * Any implementation for this function must be thread-safe as well.
175  */
wget_hsts_host_match(const wget_hsts_db * hsts_db,const char * host,uint16_t port)176 int wget_hsts_host_match(const wget_hsts_db *hsts_db, const char *host, uint16_t port)
177 {
178 	if (plugin_vtable)
179 		return plugin_vtable->host_match(hsts_db, host, port);
180 
181 	if (!hsts_db)
182 		return 0;
183 
184 	hsts_entry hsts, *hstsp;
185 	const char *p;
186 	int64_t now = time(NULL);
187 
188 	// first look for an exact match
189 	// if it's the default port, "normalize" it
190 	// we assume the scheme is HTTP
191 	hsts.port = (port == 80 ? 443 : port);
192 	hsts.host = host;
193 	if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp) && hstsp->expires >= now)
194 		return 1;
195 
196 	// now look for a valid subdomain match
197 	for (p = host; (p = strchr(p, '.')); ) {
198 		hsts.host = ++p;
199 		if (wget_hashmap_get(hsts_db->entries, &hsts, &hstsp)
200 				&& hstsp->include_subdomains && hstsp->expires >= now)
201 			return 1;
202 	}
203 
204 	return 0;
205 }
206 
207 /**
208  * \param[in] hsts_db HSTS database created by wget_hsts_db_init()
209  *
210  * Frees all resources allocated for HSTS database, except for the structure itself. The `hsts_db` pointer can then
211  * be passed to wget_hsts_db_init() for reinitialization.
212  *
213  * If `hsts_db` is NULL this function does nothing.
214  *
215  * This function only works with databases created by wget_hsts_db_init().
216  */
wget_hsts_db_deinit(wget_hsts_db * hsts_db)217 void wget_hsts_db_deinit(wget_hsts_db *hsts_db)
218 {
219 	if (plugin_vtable) {
220 		plugin_vtable->deinit(hsts_db);
221 		return;
222 	}
223 
224 	if (hsts_db) {
225 		xfree(hsts_db->fname);
226 		wget_thread_mutex_lock(hsts_db->mutex);
227 		wget_hashmap_free(&hsts_db->entries);
228 		wget_thread_mutex_unlock(hsts_db->mutex);
229 
230 		wget_thread_mutex_destroy(&hsts_db->mutex);
231 	}
232 }
233 
234 /**
235  * \param[in] hsts_db Pointer to the HSTS database handle (will be set to NULL)
236  *
237  * Frees all resources allocated for the HSTS database.
238  *
239  * A double pointer is required because this function will set the handle (pointer) to the HPKP database to NULL
240  * to prevent potential use-after-free conditions.
241  *
242  * If `hsts_db` or pointer it points to is NULL, then the function does nothing.
243  *
244  * Newly added entries will be lost unless committed to persistent storage using wget_hsts_db_save().
245  */
wget_hsts_db_free(wget_hsts_db ** hsts_db)246 void wget_hsts_db_free(wget_hsts_db **hsts_db)
247 {
248 	if (plugin_vtable) {
249 		plugin_vtable->free(hsts_db);
250 		return;
251 	}
252 
253 	if (hsts_db && *hsts_db) {
254 		wget_hsts_db_deinit(*hsts_db);
255 		xfree(*hsts_db);
256 	}
257 }
258 
hsts_db_add_entry(wget_hsts_db * hsts_db,hsts_entry * hsts)259 static void hsts_db_add_entry(wget_hsts_db *hsts_db, hsts_entry *hsts)
260 {
261 	if (!hsts)
262 		return;
263 
264 	wget_thread_mutex_lock(hsts_db->mutex);
265 
266 	if (hsts->maxage == 0) {
267 		if (wget_hashmap_remove(hsts_db->entries, hsts))
268 			debug_printf("removed HSTS %s:%hu\n", hsts->host, hsts->port);
269 		free_hsts(hsts);
270 		hsts = NULL;
271 	} else {
272 		hsts_entry *old;
273 
274 		if (wget_hashmap_get(hsts_db->entries, hsts, &old)) {
275 			if (old->created < hsts->created || old->maxage != hsts->maxage || old->include_subdomains != hsts->include_subdomains) {
276 				old->created = hsts->created;
277 				old->expires = hsts->expires;
278 				old->maxage = hsts->maxage;
279 				old->include_subdomains = hsts->include_subdomains;
280 				debug_printf("update HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", old->host, old->port, (long long)old->maxage, old->include_subdomains);
281 			}
282 			free_hsts(hsts);
283 			hsts = NULL;
284 		} else {
285 			// key and value are the same to make wget_hashmap_get() return old 'hsts'
286 			// debug_printf("add HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", hsts->host, hsts->port, (long long)hsts->maxage, hsts->include_subdomains);
287 			wget_hashmap_put(hsts_db->entries, hsts, hsts);
288 			// no need to free anything here
289 		}
290 	}
291 
292 	wget_thread_mutex_unlock(hsts_db->mutex);
293 }
294 
295 /**
296  * \param[in] hsts_db An HSTS database
297  * \param[in] host Hostname from where `Strict-Transport-Security` header was received
298  * \param[in] port Port number used for connecting to the host
299  * \param[in] maxage The time from now till the entry is valid, in seconds, or 0 to remove existing entry.
300  *                   Corresponds to the `max-age` directive in `Strict-Transport-Security` header.
301  * \param[in] include_subdomains Nonzero if `includeSubDomains` directive was present in the header, zero otherwise
302  *
303  * Add an entry to the HSTS database. An entry corresponds to the `Strict-Transport-Security` HTTP response header.
304  * Any existing entry with same `host` and `port` is replaced. If `maxage` is zero, any existing entry with
305  * matching `host` and `port` is removed.
306  *
307  * This function is thread-safe and can be called from multiple threads concurrently.
308  * Any implementation for this function must be thread-safe as well.
309  */
wget_hsts_db_add(wget_hsts_db * hsts_db,const char * host,uint16_t port,int64_t maxage,bool include_subdomains)310 void wget_hsts_db_add(wget_hsts_db *hsts_db, const char *host, uint16_t port, int64_t maxage, bool include_subdomains)
311 {
312 	if (plugin_vtable) {
313 		plugin_vtable->add(hsts_db, host, port, maxage, include_subdomains);
314 		return;
315 	}
316 
317 	if (hsts_db) {
318 		hsts_entry *hsts = new_hsts(host, port, maxage, include_subdomains);
319 
320 		hsts_db_add_entry(hsts_db, hsts);
321 	}
322 }
323 
hsts_db_load(wget_hsts_db * hsts_db,FILE * fp)324 static int hsts_db_load(wget_hsts_db *hsts_db, FILE *fp)
325 {
326 	hsts_entry hsts;
327 	struct stat st;
328 	char *buf = NULL, *linep, *p;
329 	size_t bufsize = 0;
330 	ssize_t buflen;
331 	int64_t now = time(NULL);
332 	int ok;
333 
334 	// if the database file hasn't changed since the last read
335 	// there's no need to reload
336 
337 	if (fstat(fileno(fp), &st) == 0) {
338 		if (st.st_mtime != hsts_db->load_time)
339 			hsts_db->load_time = st.st_mtime;
340 		else
341 			return 0;
342 	}
343 
344 	while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
345 		linep = buf;
346 
347 		while (isspace(*linep)) linep++; // ignore leading whitespace
348 		if (!*linep) continue; // skip empty lines
349 
350 		if (*linep == '#')
351 			continue; // skip comments
352 
353 		// strip off \r\n
354 		while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
355 			buf[--buflen] = 0;
356 
357 		init_hsts(&hsts);
358 		ok = 0;
359 
360 		// parse host
361 		if (*linep) {
362 			for (p = linep; *linep && !isspace(*linep); )
363 				linep++;
364 			hsts.host = wget_strmemdup(p, linep - p);
365 		}
366 
367 		// parse port
368 		if (*linep) {
369 			for (p = ++linep; *linep && !isspace(*linep); )
370 				linep++;
371 			hsts.port = (uint16_t) atoi(p);
372 			if (hsts.port == 0)
373 				hsts.port = 443;
374 		}
375 
376 		// parse includeSubDomains
377 		if (*linep) {
378 			for (p = ++linep; *linep && !isspace(*linep); )
379 				linep++;
380 			hsts.include_subdomains = atoi(p) ? 1 : 0;
381 		}
382 
383 		// parse creation time
384 		if (*linep) {
385 			for (p = ++linep; *linep && !isspace(*linep); )
386 				linep++;
387 			hsts.created = atoll(p);
388 			if (hsts.created < 0 || hsts.created >= INT64_MAX / 2)
389 				hsts.created = 0;
390 		}
391 
392 		// parse max age
393 		if (*linep) {
394 			for (p = ++linep; *linep && !isspace(*linep); )
395 				linep++;
396 			hsts.maxage = atoll(p);
397 			if (hsts.maxage < 0 || hsts.maxage >= INT64_MAX / 2)
398 				hsts.maxage = 0; // avoid integer overflow here
399 			hsts.expires = hsts.maxage ? hsts.created + hsts.maxage : 0;
400 			if (hsts.expires < now) {
401 				// drop expired entry
402 				deinit_hsts(&hsts);
403 				continue;
404 			}
405 			ok = 1;
406 		}
407 
408 		if (ok) {
409 			hsts_db_add_entry(hsts_db, wget_memdup(&hsts, sizeof(hsts)));
410 		} else {
411 			deinit_hsts(&hsts);
412 			error_printf(_("Failed to parse HSTS line: '%s'\n"), buf);
413 		}
414 	}
415 
416 	xfree(buf);
417 
418 	if (ferror(fp)) {
419 		hsts_db->load_time = 0; // reload on next call to this function
420 		return -1;
421 	}
422 
423 	return 0;
424 }
425 
426 /**
427  * \param[in] hsts_db An HSTS database
428  * \return 0 if the operation succeeded, -1 in case of error
429  *
430  * Performs all operations necessary to access the HSTS database entries from persistent storage
431  * using wget_hsts_host_match() for example.
432  *
433  * For database created by wget_hsts_db_init() this function will load all the entries from the file specified
434  * in `fname` parameter of wget_hsts_db_init().
435  *
436  * If `hsts_db` is NULL this function does nothing and returns 0.
437  */
wget_hsts_db_load(wget_hsts_db * hsts_db)438 int wget_hsts_db_load(wget_hsts_db *hsts_db)
439 {
440 	if (plugin_vtable)
441 		return plugin_vtable->load(hsts_db);
442 
443 	if (!hsts_db)
444 		return -1;
445 
446 	if (!hsts_db->fname || !*hsts_db->fname)
447 		return 0;
448 
449 	// Load the HSTS cache from a flat file
450 	// Protected by flock()
451 	if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, NULL, hsts_db)) {
452 		error_printf(_("Failed to read HSTS data\n"));
453 		return -1;
454 	} else {
455 		debug_printf("Fetched HSTS data from '%s'\n", hsts_db->fname);
456 		return 0;
457 	}
458 }
459 
460 WGET_GCC_NONNULL_ALL
hsts_save(FILE * fp,const hsts_entry * hsts)461 static int hsts_save(FILE *fp, const hsts_entry *hsts)
462 {
463 	wget_fprintf(fp, "%s %hu %d %lld %lld\n", hsts->host, hsts->port, hsts->include_subdomains, (long long)hsts->created, (long long)hsts->maxage);
464 	return 0;
465 }
466 
hsts_db_save(void * hsts_db,FILE * fp)467 static int hsts_db_save(void *hsts_db, FILE *fp)
468 {
469 	wget_hashmap *entries = ((wget_hsts_db *) hsts_db)->entries;
470 
471 	if (wget_hashmap_size(entries) > 0) {
472 		fputs("#HSTS 1.0 file\n", fp);
473 		fputs("#Generated by libwget " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
474 		fputs("# <hostname> <port> <incl. subdomains> <created> <max-age>\n", fp);
475 
476 		wget_hashmap_browse(entries, (wget_hashmap_browse_fn *) hsts_save, fp);
477 
478 		if (ferror(fp))
479 			return -1;
480 	}
481 
482 	return 0;
483 }
484 
485 /**
486  * \param[in] hsts_db HSTS database
487  * \return 0 if the operation succeeded, -1 otherwise
488  *
489  * Saves all changes to the HSTS database (via wget_hsts_db_add() for example) to persistent storage.
490  *
491  * For databases created by wget_hsts_db_init(), the data is stored into file specified by `fname` parameter
492  * of wget_hsts_db_init().
493  *
494  * If `hsts_db` is NULL this function does nothing.
495  */
wget_hsts_db_save(wget_hsts_db * hsts_db)496 int wget_hsts_db_save(wget_hsts_db *hsts_db)
497 {
498 	int size;
499 
500 	if (plugin_vtable)
501 		return plugin_vtable->save(hsts_db);
502 
503 	if (!hsts_db)
504 		return -1;
505 
506 	if (!hsts_db->fname || !*hsts_db->fname)
507 		return -1;
508 
509 	// Save the HSTS cache to a flat file
510 	// Protected by flock()
511 	if (wget_update_file(hsts_db->fname, (wget_update_load_fn *) hsts_db_load, hsts_db_save, hsts_db)) {
512 		error_printf(_("Failed to write HSTS file '%s'\n"), hsts_db->fname);
513 		return -1;
514 	}
515 
516 	if ((size = wget_hashmap_size(hsts_db->entries)))
517 		debug_printf("Saved %d HSTS entr%s into '%s'\n", size, size != 1 ? "ies" : "y", hsts_db->fname);
518 	else
519 		debug_printf("No HSTS entries to save. Table is empty.\n");
520 
521 	return 0;
522 }
523 
524 /**
525  * \param[in] hsts_db Previously created HSTS database on which wget_hsts_db_deinit() has been called, or NULL
526  * \param[in] fname The file where the data is stored, or NULL.
527  * \return A new wget_hsts_db
528  *
529  * Constructor for the default implementation of HSTS database.
530  *
531  * This function does no file IO, data is read only when \ref wget_hsts_db_load "wget_hsts_db_load()" is called.
532  */
wget_hsts_db_init(wget_hsts_db * hsts_db,const char * fname)533 wget_hsts_db *wget_hsts_db_init(wget_hsts_db *hsts_db, const char *fname)
534 {
535 	if (plugin_vtable)
536 		return plugin_vtable->init(hsts_db, fname);
537 
538 	if (fname) {
539 		if (!(fname = wget_strdup(fname)))
540 			return NULL;
541 	}
542 
543 	wget_hashmap *entries = wget_hashmap_create(16, (wget_hashmap_hash_fn *) hash_hsts, (wget_hashmap_compare_fn *) compare_hsts);
544 	if (!entries) {
545 		xfree(fname);
546 		return NULL;
547 	}
548 
549 	if (!hsts_db) {
550 		if (!(hsts_db = wget_calloc(1, sizeof(struct wget_hsts_db_st)))) {
551 			wget_hashmap_free(&entries);
552 			xfree(fname);
553 			return NULL;
554 		}
555 	} else
556 		memset(hsts_db, 0, sizeof(*hsts_db));
557 
558 	hsts_db->fname = fname;
559 	hsts_db->entries = entries;
560 	wget_hashmap_set_key_destructor(hsts_db->entries, (wget_hashmap_key_destructor *) free_hsts);
561 	wget_hashmap_set_value_destructor(hsts_db->entries, (wget_hashmap_value_destructor *) free_hsts);
562 	wget_thread_mutex_init(&hsts_db->mutex);
563 
564 	return hsts_db;
565 }
566 
567 /**
568  * \param[in] hsts_db HSTS database created by wget_hsts_db_init().
569  * \param[in] fname Filename where database should be stored, or NULL
570  *
571  * Changes the file where HSTS database entries are stored.
572  *
573  * Works only for the HSTS databases created by wget_hsts_db_init().
574  * This function does no file IO, data is read or written only when wget_hsts_db_load() or wget_hsts_db_save()
575  * is called.
576  */
wget_hsts_db_set_fname(wget_hsts_db * hsts_db,const char * fname)577 void wget_hsts_db_set_fname(wget_hsts_db *hsts_db, const char *fname)
578 {
579 	xfree(hsts_db->fname);
580 	hsts_db->fname = wget_strdup(fname);
581 }
582 
583 /**@}*/
584