File: hsts.c

package info (click to toggle)
wget2 1.99.1-2
  • links: PTS, VCS
  • area: main
  • in suites: buster
  • size: 13,468 kB
  • sloc: ansic: 88,607; sh: 10,241; makefile: 501; xml: 182; sed: 16
file content (569 lines) | stat: -rw-r--r-- 16,416 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
/*
 * Copyright(c) 2014 Tim Ruehsen
 * Copyright(c) 2015-2018 Free Software Foundation, Inc.
 *
 * This file is part of libwget.
 *
 * Libwget is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Libwget is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with libwget.  If not, see <https://www.gnu.org/licenses/>.
 *
 *
 * HSTS routines
 *
 * Changelog
 * 28.01.2014  Tim Ruehsen  created
 *
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/file.h>

#include <wget.h>
#include "private.h"

/**
 * \file
 * \brief HTTP Strict Transport Security (RFC 6797) routines
 * \defgroup libwget-hsts HTTP Strict Transport Security (RFC 6797) routines
 * @{
 *
 * This is an implementation of RFC 6797.
 */

typedef struct {
	wget_hsts_db_t
		parent;
	char *
		fname;
	wget_hashmap_t *
		entries;
	wget_thread_mutex_t
		mutex;
	int64_t
		load_time;
} _hsts_db_impl_t;

typedef struct {
	const char *
		host;
	int64_t
		expires; // expiry time
	int64_t
		created; // creation time
	int64_t
		maxage; // max-age in seconds
	uint16_t
		port;
	bool
		include_subdomains : 1; // whether or not subdomains are included
} _hsts_t;

#ifdef __clang__
__attribute__((no_sanitize("integer")))
#endif
static unsigned int G_GNUC_WGET_PURE _hash_hsts(const _hsts_t *hsts)
{
	unsigned int hash = hsts->port;
	const unsigned char *p;

	for (p = (unsigned char *)hsts->host; *p; p++)
		hash = hash * 101 + *p;

	return hash;
}

static int G_GNUC_WGET_NONNULL_ALL G_GNUC_WGET_PURE _compare_hsts(const _hsts_t *h1, const _hsts_t *h2)
{
	int n;

	if ((n = strcmp(h1->host, h2->host)))
		return n;

	return h1->port < h2->port ? -1 : (h1->port > h2->port ? 1 : 0);
}

static _hsts_t *_init_hsts(_hsts_t *hsts)
{
	if (!hsts)
		hsts = xmalloc(sizeof(_hsts_t));

	memset(hsts, 0, sizeof(*hsts));
	hsts->created = time(NULL);

	return hsts;
}

static void _deinit_hsts(_hsts_t *hsts)
{
	if (hsts) {
		xfree(hsts->host);
	}
}

static void _free_hsts(_hsts_t *hsts)
{
	if (hsts) {
		_deinit_hsts(hsts);
		xfree(hsts);
	}
}

static _hsts_t *_new_hsts(const char *host, uint16_t port, time_t maxage, int include_subdomains)
{
	_hsts_t *hsts = _init_hsts(NULL);

	hsts->host = wget_strdup(host);
	hsts->port = port ? port : 443;
	hsts->include_subdomains = !!include_subdomains;

	if (maxage <= 0 || maxage >= INT64_MAX / 2 || hsts->created < 0 || hsts->created >= INT64_MAX / 2) {
		hsts->maxage = 0;
		hsts->expires = 0;
	} else {
		hsts->maxage = maxage;
		hsts->expires = hsts->created + maxage;
	}

	return hsts;
}

/**
 * \param[in] hsts_db An HSTS database
 * \param[in] host Hostname to search for
 * \param[in] port Port number in the original URI/IRI.
 *                 Port number 80 is treated similar to 443, as 80 is default port for HTTP.
 * \return 1 if the host must be accessed only through TLS, 0 if there is no such condition.
 *
 * Searches for a given host in the database for any previously added entry.
 *
 * HSTS entries older than amount of time specified by `maxage` are considered `expired` and are ignored.
 *
 * This function is thread-safe and can be called from multiple threads concurrently.
 * Any implementation for this function must be thread-safe as well.
 */
int wget_hsts_host_match(const wget_hsts_db_t *hsts_db, const char *host, uint16_t port)
{
	if (hsts_db && hsts_db->vtable && hsts_db->vtable->host_match)
		return hsts_db->vtable->host_match(hsts_db, host, port);
	else
		return 0;
}
static int impl_hsts_db_host_match(const wget_hsts_db_t *hsts_db, const char *host, uint16_t port)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	_hsts_t hsts, *hstsp;
	const char *p;
	int64_t now = time(NULL);

	// first look for an exact match
	// if it's the default port, "normalize" it
	// we assume the scheme is HTTP
	hsts.port = (port == 80 ? 443 : port);
	hsts.host = host;
	if ((hstsp = wget_hashmap_get(hsts_db_priv->entries, &hsts)) && hstsp->expires >= now)
		return 1;

	// now look for a valid subdomain match
	for (p = host; (p = strchr(p, '.')); ) {
		hsts.host = ++p;
		if ((hstsp = wget_hashmap_get(hsts_db_priv->entries, &hsts))
				&& hstsp->include_subdomains && hstsp->expires >= now)
			return 1;
	}

	return 0;
}

/**
 * \param[in] hsts_db HSTS database created by wget_hsts_db_init()
 *
 * Frees all resources allocated for HSTS database, except for the structure itself. The `hsts_db` pointer can then
 * be passed to wget_hsts_db_init() for reinitialization.
 *
 * If `hsts_db` is NULL this function does nothing.
 *
 * This function only works with databases created by wget_hsts_db_init().
 */
void wget_hsts_db_deinit(wget_hsts_db_t *hsts_db)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	if (hsts_db_priv) {
		xfree(hsts_db_priv->fname);
		wget_thread_mutex_lock(hsts_db_priv->mutex);
		wget_hashmap_free(&hsts_db_priv->entries);
		wget_thread_mutex_unlock(hsts_db_priv->mutex);

		wget_thread_mutex_destroy(&hsts_db_priv->mutex);
	}
}

/**
 * \param[in] hsts_db Pointer to the HSTS database handle (will be set to NULL)
 *
 * Frees all resources allocated for the HSTS database.
 *
 * A double pointer is required because this function will set the handle (pointer) to the HPKP database to NULL
 * to prevent potential use-after-free conditions.
 *
 * If `hsts_db` or pointer it points to is NULL, then the function does nothing.
 *
 * Newly added entries will be lost unless commited to persistent storage using wget_hsts_db_save().
 */
void wget_hsts_db_free(wget_hsts_db_t **hsts_db)
{
	if (hsts_db && *hsts_db) {
		if ((*hsts_db)->vtable)
			(*hsts_db)->vtable->free(*hsts_db);
		*hsts_db = NULL;
	}
}
static void impl_hsts_db_free(wget_hsts_db_t *hsts_db)
{
	wget_hsts_db_deinit(hsts_db);
	xfree(hsts_db);
}

static void _hsts_db_add_entry(_hsts_db_impl_t *hsts_db_priv, _hsts_t *hsts)
{
	wget_thread_mutex_lock(hsts_db_priv->mutex);

	if (hsts->maxage == 0) {
		if (wget_hashmap_remove(hsts_db_priv->entries, hsts))
			debug_printf("removed HSTS %s:%hu\n", hsts->host, hsts->port);
		_free_hsts(hsts);
		hsts = NULL;
	} else {
		_hsts_t *old = wget_hashmap_get(hsts_db_priv->entries, hsts);

		if (old) {
			if (old->created < hsts->created || old->maxage != hsts->maxage || old->include_subdomains != hsts->include_subdomains) {
				old->created = hsts->created;
				old->expires = hsts->expires;
				old->maxage = hsts->maxage;
				old->include_subdomains = hsts->include_subdomains;
				debug_printf("update HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", old->host, old->port, (long long)old->maxage, old->include_subdomains);
			}
			_free_hsts(hsts);
			hsts = NULL;
		} else {
			// key and value are the same to make wget_hashmap_get() return old 'hsts'
			// debug_printf("add HSTS %s:%hu (maxage=%lld, includeSubDomains=%d)\n", hsts->host, hsts->port, (long long)hsts->maxage, hsts->include_subdomains);
			wget_hashmap_put_noalloc(hsts_db_priv->entries, hsts, hsts);
			// no need to free anything here
		}
	}

	wget_thread_mutex_unlock(hsts_db_priv->mutex);
}

/**
 * \param[in] hsts_db An HSTS database
 * \param[in] host Hostname from where `Strict-Transport-Security` header was received
 * \param[in] port Port number used for connecting to the host
 * \param[in] maxage The time from now till the entry is valid, in seconds, or 0 to remove existing entry.
 *                   Corresponds to the `max-age` directive in `Strict-Transport-Security` header.
 * \param[in] include_subdomains Nonzero if `includeSubDomains` directive was present in the header, zero otherwise
 *
 * Add an entry to the HSTS database. An entry corresponds to the `Strict-Transport-Security` HTTP response header.
 * Any existing entry with same `host` and `port` is replaced. If `maxage` is zero, any existing entry with
 * matching `host` and `port` is removed.
 *
 * This function is thread-safe and can be called from multiple threads concurrently.
 * Any implementation for this function must be thread-safe as well.
 */
void wget_hsts_db_add(wget_hsts_db_t *hsts_db, const char *host, uint16_t port, time_t maxage, int include_subdomains)
{
	if (hsts_db && hsts_db->vtable)
		hsts_db->vtable->add(hsts_db, host, port, maxage, include_subdomains);
}
static void impl_hsts_db_add(wget_hsts_db_t *hsts_db, const char *host, uint16_t port, time_t maxage, int include_subdomains)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	_hsts_t *hsts = _new_hsts(host, port, maxage, include_subdomains);

	_hsts_db_add_entry(hsts_db_priv, hsts);
}

static int _hsts_db_load(_hsts_db_impl_t *hsts_db_priv, FILE *fp)
{
	_hsts_t hsts;
	struct stat st;
	char *buf = NULL, *linep, *p;
	size_t bufsize = 0;
	ssize_t buflen;
	int64_t now = time(NULL);
	int ok;

	// if the database file hasn't changed since the last read
	// there's no need to reload

	if (fstat(fileno(fp), &st) == 0) {
		if (st.st_mtime != hsts_db_priv->load_time)
			hsts_db_priv->load_time = st.st_mtime;
		else
			return 0;
	}

	while ((buflen = wget_getline(&buf, &bufsize, fp)) >= 0) {
		linep = buf;

		while (isspace(*linep)) linep++; // ignore leading whitespace
		if (!*linep) continue; // skip empty lines

		if (*linep == '#')
			continue; // skip comments

		// strip off \r\n
		while (buflen > 0 && (buf[buflen] == '\n' || buf[buflen] == '\r'))
			buf[--buflen] = 0;

		_init_hsts(&hsts);
		ok = 0;

		// parse host
		if (*linep) {
			for (p = linep; *linep && !isspace(*linep); )
				linep++;
			hsts.host = wget_strmemdup(p, linep - p);
		}

		// parse port
		if (*linep) {
			for (p = ++linep; *linep && !isspace(*linep); )
				linep++;
			hsts.port = (uint16_t) atoi(p);
			if (hsts.port == 0)
				hsts.port = 443;
		}

		// parse includeSubDomains
		if (*linep) {
			for (p = ++linep; *linep && !isspace(*linep); )
				linep++;
			hsts.include_subdomains = atoi(p) ? 1 : 0;
		}

		// parse creation time
		if (*linep) {
			for (p = ++linep; *linep && !isspace(*linep); )
				linep++;
			hsts.created = atoll(p);
			if (hsts.created < 0 || hsts.created >= INT64_MAX / 2)
				hsts.created = 0;
		}

		// parse max age
		if (*linep) {
			for (p = ++linep; *linep && !isspace(*linep); )
				linep++;
			hsts.maxage = atoll(p);
			if (hsts.maxage < 0 || hsts.maxage >= INT64_MAX / 2)
				hsts.maxage = 0; // avoid integer overflow here
			hsts.expires = hsts.maxage ? hsts.created + hsts.maxage : 0;
			if (hsts.expires < now) {
				// drop expired entry
				_deinit_hsts(&hsts);
				continue;
			}
			ok = 1;
		}

		if (ok) {
			_hsts_db_add_entry(hsts_db_priv, wget_memdup(&hsts, sizeof(hsts)));
		} else {
			_deinit_hsts(&hsts);
			error_printf(_("Failed to parse HSTS line: '%s'\n"), buf);
		}
	}

	xfree(buf);

	if (ferror(fp)) {
		hsts_db_priv->load_time = 0; // reload on next call to this function
		return -1;
	}

	return 0;
}

/**
 * \param[in] hsts_db An HSTS database
 * \return 0 if the operation succeded, -1 in case of error
 *
 * Performs all operations necessary to access the HSTS database entries from persistent storage
 * using wget_hsts_host_match() for example.
 *
 * For database created by wget_hsts_db_init() this function will load all the entries from the file specified
 * in `fname` parameter of wget_hsts_db_init().
 *
 * If `hsts_db` is NULL this function does nothing and returns 0.
 */
int wget_hsts_db_load(wget_hsts_db_t *hsts_db)
{
	if (!hsts_db || !hsts_db->vtable)
		return 0;

	return hsts_db->vtable->load(hsts_db);
}
// Load the HSTS cache from a flat file
// Protected by flock()
static int impl_hsts_db_load(wget_hsts_db_t *hsts_db)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	if (!hsts_db_priv->fname || !*hsts_db_priv->fname)
		return 0;

	if (wget_update_file(hsts_db_priv->fname, (wget_update_load_t) _hsts_db_load, NULL, hsts_db_priv)) {
		error_printf(_("Failed to read HSTS data\n"));
		return -1;
	} else {
		debug_printf(_("Fetched HSTS data from '%s'\n"), hsts_db_priv->fname);
		return 0;
	}
}

static int G_GNUC_WGET_NONNULL_ALL _hsts_save(FILE *fp, const _hsts_t *hsts)
{
	wget_fprintf(fp, "%s %hu %d %lld %lld\n", hsts->host, hsts->port, hsts->include_subdomains, (long long)hsts->created, (long long)hsts->maxage);
	return 0;
}

static int _hsts_db_save(void *hsts_db_priv, FILE *fp)
{
	wget_hashmap_t *entries = ((_hsts_db_impl_t *) hsts_db_priv)->entries;

	if (wget_hashmap_size(entries) > 0) {
		fputs("#HSTS 1.0 file\n", fp);
		fputs("#Generated by Wget2 " PACKAGE_VERSION ". Edit at your own risk.\n", fp);
		fputs("# <hostname> <port> <incl. subdomains> <created> <max-age>\n", fp);

		wget_hashmap_browse(entries, (wget_hashmap_browse_t) _hsts_save, fp);

		if (ferror(fp))
			return -1;
	}

	return 0;
}

/**
 * \param[in] hsts_db HSTS database
 * \return 0 if the operation succeded, -1 otherwise
 *
 * Saves all changes to the HSTS database (via wget_hsts_db_add() for example) to persistent storage.
 *
 * For databases created by wget_hsts_db_init(), the data is stored into file specified by `fname` parameter
 * of wget_hsts_db_init().
 *
 * If `hsts_db` is NULL this function does nothing.
 */
int wget_hsts_db_save(wget_hsts_db_t *hsts_db)
{
	if (hsts_db && hsts_db->vtable)
		return hsts_db->vtable->save(hsts_db);

	return -1;
}
// Save the HSTS cache to a flat file
// Protected by flock()
static int impl_hsts_db_save(wget_hsts_db_t *hsts_db)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	int size;

	if (!hsts_db_priv->fname || !*hsts_db_priv->fname)
		return -1;

	if (wget_update_file(hsts_db_priv->fname, (wget_update_load_t) _hsts_db_load, _hsts_db_save, hsts_db_priv)) {
		error_printf(_("Failed to write HSTS file '%s'\n"), hsts_db_priv->fname);
		return -1;
	}

	if ((size = wget_hashmap_size(hsts_db_priv->entries)))
		debug_printf(_("Saved %d HSTS entr%s into '%s'\n"), size, size != 1 ? "ies" : "y", hsts_db_priv->fname);
	else
		debug_printf(_("No HSTS entries to save. Table is empty.\n"));

	return 0;
}

//vtable
static struct wget_hsts_db_vtable vtable = {
	.load = impl_hsts_db_load,
	.save = impl_hsts_db_save,
	.host_match = impl_hsts_db_host_match,
	.add = impl_hsts_db_add,
	.free = impl_hsts_db_free
};

/**
 * \param[in] hsts_db Previously created HSTS database on which wget_hsts_db_deinit() has been called, or NULL
 * \param[in] fname The file where the data is stored, or NULL.
 * \return A new wget_hsts_db_t
 *
 * Constructor for the default implementation of HSTS database.
 *
 * This function does no file IO, data is read only when \ref wget_hsts_db_load "wget_hsts_db_load()" is called.
 */
wget_hsts_db_t *wget_hsts_db_init(wget_hsts_db_t *hsts_db, const char *fname)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	if (!hsts_db_priv)
		hsts_db_priv = xmalloc(sizeof(_hsts_db_impl_t));

	memset(hsts_db_priv, 0, sizeof(*hsts_db_priv));
	hsts_db_priv->parent.vtable = &vtable;
	if (fname)
		hsts_db_priv->fname = wget_strdup(fname);
	hsts_db_priv->entries = wget_hashmap_create(16, (wget_hashmap_hash_t)_hash_hsts, (wget_hashmap_compare_t)_compare_hsts);
	wget_hashmap_set_key_destructor(hsts_db_priv->entries, (wget_hashmap_key_destructor_t)_free_hsts);
	wget_hashmap_set_value_destructor(hsts_db_priv->entries, (wget_hashmap_value_destructor_t)_free_hsts);
	wget_thread_mutex_init(&hsts_db_priv->mutex);

	return (wget_hsts_db_t *) hsts_db_priv;
}

/**
 * \param[in] hsts_db HSTS database created by wget_hsts_db_init().
 * \param[in] fname Filename where database should be stored, or NULL
 *
 * Changes the file where HSTS database entries are stored.
 *
 * Works only for the HSTS databases created by wget_hsts_db_init().
 * This function does no file IO, data is read or written only when wget_hsts_db_load() or wget_hsts_db_save()
 * is called.
 */
void wget_hsts_db_set_fname(wget_hsts_db_t *hsts_db, const char *fname)
{
	_hsts_db_impl_t *hsts_db_priv = (_hsts_db_impl_t *) hsts_db;

	xfree(hsts_db_priv->fname);
	if (fname)
		hsts_db_priv->fname = wget_strdup(fname);
}

/**@}*/