Coverage Report

Created: 2025-07-23 07:18

/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
}