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