1/***************************************************************************
2 * _ _ ____ _
3 * Project ___| | | | _ \| |
4 * / __| | | | |_) | |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
7 *
8 * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9 *
10 * This software is licensed as described in the file COPYING, which
11 * you should have received as part of this distribution. The terms
12 * are also available at https://curl.se/docs/copyright.html.
13 *
14 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
15 * copies of the Software, and permit persons to whom the Software is
16 * furnished to do so, under the terms of the COPYING file.
17 *
18 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19 * KIND, either express or implied.
20 *
21 * SPDX-License-Identifier: curl
22 *
23 ***************************************************************************/
24/*
25 * The Strict-Transport-Security header is defined in RFC 6797:
26 * https://datatracker.ietf.org/doc/html/rfc6797
27 */
28#include "curl_setup.h"
29
30#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_HSTS)
31#include <curl/curl.h>
32#include "urldata.h"
33#include "llist.h"
34#include "hsts.h"
35#include "curl_get_line.h"
36#include "strcase.h"
37#include "sendf.h"
38#include "strtoofft.h"
39#include "parsedate.h"
40#include "fopen.h"
41#include "rename.h"
42#include "share.h"
43
44/* The last 3 #include files should be in this order */
45#include "curl_printf.h"
46#include "curl_memory.h"
47#include "memdebug.h"
48
49#define MAX_HSTS_LINE 4095
50#define MAX_HSTS_HOSTLEN 256
51#define MAX_HSTS_HOSTLENSTR "256"
52#define MAX_HSTS_DATELEN 64
53#define MAX_HSTS_DATELENSTR "64"
54#define UNLIMITED "unlimited"
55
56#ifdef DEBUGBUILD
57/* to play well with debug builds, we can *set* a fixed time this will
58 return */
59time_t deltatime; /* allow for "adjustments" for unit test purposes */
60static time_t hsts_debugtime(void *unused)
61{
62 char *timestr = getenv("CURL_TIME");
63 (void)unused;
64 if(timestr) {
65 curl_off_t val;
66 (void)curlx_strtoofft(timestr, NULL, 10, &val);
67
68 val += (curl_off_t)deltatime;
69 return (time_t)val;
70 }
71 return time(NULL);
72}
73#undef time
74#define time(x) hsts_debugtime(x)
75#endif
76
77struct hsts *Curl_hsts_init(void)
78{
79 struct hsts *h = calloc(sizeof(struct hsts), 1);
80 if(h) {
81 Curl_llist_init(&h->list, NULL);
82 }
83 return h;
84}
85
86static void hsts_free(struct stsentry *e)
87{
88 free((char *)e->host);
89 free(e);
90}
91
92void Curl_hsts_cleanup(struct hsts **hp)
93{
94 struct hsts *h = *hp;
95 if(h) {
96 struct Curl_llist_element *e;
97 struct Curl_llist_element *n;
98 for(e = h->list.head; e; e = n) {
99 struct stsentry *sts = e->ptr;
100 n = e->next;
101 hsts_free(e: sts);
102 }
103 free(h->filename);
104 free(h);
105 *hp = NULL;
106 }
107}
108
109static struct stsentry *hsts_entry(void)
110{
111 return calloc(sizeof(struct stsentry), 1);
112}
113
114static CURLcode hsts_create(struct hsts *h,
115 const char *hostname,
116 bool subdomains,
117 curl_off_t expires)
118{
119 struct stsentry *sts = hsts_entry();
120 char *duphost;
121 size_t hlen;
122 if(!sts)
123 return CURLE_OUT_OF_MEMORY;
124
125 duphost = strdup(hostname);
126 if(!duphost) {
127 free(sts);
128 return CURLE_OUT_OF_MEMORY;
129 }
130
131 hlen = strlen(s: duphost);
132 if(duphost[hlen - 1] == '.')
133 /* strip off trailing any dot */
134 duphost[--hlen] = 0;
135
136 sts->host = duphost;
137 sts->expires = expires;
138 sts->includeSubDomains = subdomains;
139 Curl_llist_insert_next(&h->list, h->list.tail, sts, node: &sts->node);
140 return CURLE_OK;
141}
142
143CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
144 const char *header)
145{
146 const char *p = header;
147 curl_off_t expires = 0;
148 bool gotma = FALSE;
149 bool gotinc = FALSE;
150 bool subdomains = FALSE;
151 struct stsentry *sts;
152 time_t now = time(NULL);
153
154 if(Curl_host_is_ipnum(hostname))
155 /* "explicit IP address identification of all forms is excluded."
156 / RFC 6797 */
157 return CURLE_OK;
158
159 do {
160 while(*p && ISBLANK(*p))
161 p++;
162 if(strncasecompare("max-age=", p, 8)) {
163 bool quoted = FALSE;
164 CURLofft offt;
165 char *endp;
166
167 if(gotma)
168 return CURLE_BAD_FUNCTION_ARGUMENT;
169
170 p += 8;
171 while(*p && ISBLANK(*p))
172 p++;
173 if(*p == '\"') {
174 p++;
175 quoted = TRUE;
176 }
177 offt = curlx_strtoofft(str: p, endp: &endp, base: 10, num: &expires);
178 if(offt == CURL_OFFT_FLOW)
179 expires = CURL_OFF_T_MAX;
180 else if(offt)
181 /* invalid max-age */
182 return CURLE_BAD_FUNCTION_ARGUMENT;
183 p = endp;
184 if(quoted) {
185 if(*p != '\"')
186 return CURLE_BAD_FUNCTION_ARGUMENT;
187 p++;
188 }
189 gotma = TRUE;
190 }
191 else if(strncasecompare("includesubdomains", p, 17)) {
192 if(gotinc)
193 return CURLE_BAD_FUNCTION_ARGUMENT;
194 subdomains = TRUE;
195 p += 17;
196 gotinc = TRUE;
197 }
198 else {
199 /* unknown directive, do a lame attempt to skip */
200 while(*p && (*p != ';'))
201 p++;
202 }
203
204 while(*p && ISBLANK(*p))
205 p++;
206 if(*p == ';')
207 p++;
208 } while(*p);
209
210 if(!gotma)
211 /* max-age is mandatory */
212 return CURLE_BAD_FUNCTION_ARGUMENT;
213
214 if(!expires) {
215 /* remove the entry if present verbatim (without subdomain match) */
216 sts = Curl_hsts(h, hostname, FALSE);
217 if(sts) {
218 Curl_llist_remove(&h->list, &sts->node, NULL);
219 hsts_free(e: sts);
220 }
221 return CURLE_OK;
222 }
223
224 if(CURL_OFF_T_MAX - now < expires)
225 /* would overflow, use maximum value */
226 expires = CURL_OFF_T_MAX;
227 else
228 expires += now;
229
230 /* check if it already exists */
231 sts = Curl_hsts(h, hostname, FALSE);
232 if(sts) {
233 /* just update these fields */
234 sts->expires = expires;
235 sts->includeSubDomains = subdomains;
236 }
237 else
238 return hsts_create(h, hostname, subdomains, expires);
239
240 return CURLE_OK;
241}
242
243/*
244 * Return TRUE if the given host name is currently an HSTS one.
245 *
246 * The 'subdomain' argument tells the function if subdomain matching should be
247 * attempted.
248 */
249struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
250 bool subdomain)
251{
252 if(h) {
253 char buffer[MAX_HSTS_HOSTLEN + 1];
254 time_t now = time(NULL);
255 size_t hlen = strlen(s: hostname);
256 struct Curl_llist_element *e;
257 struct Curl_llist_element *n;
258
259 if((hlen > MAX_HSTS_HOSTLEN) || !hlen)
260 return NULL;
261 memcpy(dest: buffer, src: hostname, n: hlen);
262 if(hostname[hlen-1] == '.')
263 /* remove the trailing dot */
264 --hlen;
265 buffer[hlen] = 0;
266 hostname = buffer;
267
268 for(e = h->list.head; e; e = n) {
269 struct stsentry *sts = e->ptr;
270 n = e->next;
271 if(sts->expires <= now) {
272 /* remove expired entries */
273 Curl_llist_remove(&h->list, &sts->node, NULL);
274 hsts_free(e: sts);
275 continue;
276 }
277 if(subdomain && sts->includeSubDomains) {
278 size_t ntail = strlen(s: sts->host);
279 if(ntail < hlen) {
280 size_t offs = hlen - ntail;
281 if((hostname[offs-1] == '.') &&
282 strncasecompare(&hostname[offs], sts->host, ntail))
283 return sts;
284 }
285 }
286 if(strcasecompare(hostname, sts->host))
287 return sts;
288 }
289 }
290 return NULL; /* no match */
291}
292
293/*
294 * Send this HSTS entry to the write callback.
295 */
296static CURLcode hsts_push(struct Curl_easy *data,
297 struct curl_index *i,
298 struct stsentry *sts,
299 bool *stop)
300{
301 struct curl_hstsentry e;
302 CURLSTScode sc;
303 struct tm stamp;
304 CURLcode result;
305
306 e.name = (char *)sts->host;
307 e.namelen = strlen(s: sts->host);
308 e.includeSubDomains = sts->includeSubDomains;
309
310 if(sts->expires != TIME_T_MAX) {
311 result = Curl_gmtime(intime: (time_t)sts->expires, store: &stamp);
312 if(result)
313 return result;
314
315 msnprintf(buffer: e.expire, maxlength: sizeof(e.expire), format: "%d%02d%02d %02d:%02d:%02d",
316 stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
317 stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
318 }
319 else
320 strcpy(dest: e.expire, UNLIMITED);
321
322 sc = data->set.hsts_write(data, &e, i,
323 data->set.hsts_write_userp);
324 *stop = (sc != CURLSTS_OK);
325 return sc == CURLSTS_FAIL ? CURLE_BAD_FUNCTION_ARGUMENT : CURLE_OK;
326}
327
328/*
329 * Write this single hsts entry to a single output line
330 */
331static CURLcode hsts_out(struct stsentry *sts, FILE *fp)
332{
333 struct tm stamp;
334 if(sts->expires != TIME_T_MAX) {
335 CURLcode result = Curl_gmtime(intime: (time_t)sts->expires, store: &stamp);
336 if(result)
337 return result;
338 fprintf(fd: fp, format: "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n",
339 sts->includeSubDomains ? ".": "", sts->host,
340 stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
341 stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
342 }
343 else
344 fprintf(fd: fp, format: "%s%s \"%s\"\n",
345 sts->includeSubDomains ? ".": "", sts->host, UNLIMITED);
346 return CURLE_OK;
347}
348
349
350/*
351 * Curl_https_save() writes the HSTS cache to file and callback.
352 */
353CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
354 const char *file)
355{
356 struct Curl_llist_element *e;
357 struct Curl_llist_element *n;
358 CURLcode result = CURLE_OK;
359 FILE *out;
360 char *tempstore = NULL;
361
362 if(!h)
363 /* no cache activated */
364 return CURLE_OK;
365
366 /* if no new name is given, use the one we stored from the load */
367 if(!file && h->filename)
368 file = h->filename;
369
370 if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0])
371 /* marked as read-only, no file or zero length file name */
372 goto skipsave;
373
374 result = Curl_fopen(data, filename: file, fh: &out, tempname: &tempstore);
375 if(!result) {
376 fputs(s: "# Your HSTS cache. https://curl.se/docs/hsts.html\n"
377 "# This file was generated by libcurl! Edit at your own risk.\n",
378 stream: out);
379 for(e = h->list.head; e; e = n) {
380 struct stsentry *sts = e->ptr;
381 n = e->next;
382 result = hsts_out(sts, fp: out);
383 if(result)
384 break;
385 }
386 fclose(stream: out);
387 if(!result && tempstore && Curl_rename(oldpath: tempstore, newpath: file))
388 result = CURLE_WRITE_ERROR;
389
390 if(result && tempstore)
391 unlink(name: tempstore);
392 }
393 free(tempstore);
394skipsave:
395 if(data->set.hsts_write) {
396 /* if there's a write callback */
397 struct curl_index i; /* count */
398 i.total = h->list.size;
399 i.index = 0;
400 for(e = h->list.head; e; e = n) {
401 struct stsentry *sts = e->ptr;
402 bool stop;
403 n = e->next;
404 result = hsts_push(data, i: &i, sts, stop: &stop);
405 if(result || stop)
406 break;
407 i.index++;
408 }
409 }
410 return result;
411}
412
413/* only returns SERIOUS errors */
414static CURLcode hsts_add(struct hsts *h, char *line)
415{
416 /* Example lines:
417 example.com "20191231 10:00:00"
418 .example.net "20191231 10:00:00"
419 */
420 char host[MAX_HSTS_HOSTLEN + 1];
421 char date[MAX_HSTS_DATELEN + 1];
422 int rc;
423
424 rc = sscanf(s: line,
425 format: "%" MAX_HSTS_HOSTLENSTR "s \"%" MAX_HSTS_DATELENSTR "[^\"]\"",
426 host, date);
427 if(2 == rc) {
428 time_t expires = strcmp(s1: date, UNLIMITED) ? Curl_getdate_capped(p: date) :
429 TIME_T_MAX;
430 CURLcode result = CURLE_OK;
431 char *p = host;
432 bool subdomain = FALSE;
433 struct stsentry *e;
434 if(p[0] == '.') {
435 p++;
436 subdomain = TRUE;
437 }
438 /* only add it if not already present */
439 e = Curl_hsts(h, hostname: p, subdomain);
440 if(!e)
441 result = hsts_create(h, hostname: p, subdomains: subdomain, expires);
442 else {
443 /* the same host name, use the largest expire time */
444 if(expires > e->expires)
445 e->expires = expires;
446 }
447 if(result)
448 return result;
449 }
450
451 return CURLE_OK;
452}
453
454/*
455 * Load HSTS data from callback.
456 *
457 */
458static CURLcode hsts_pull(struct Curl_easy *data, struct hsts *h)
459{
460 /* if the HSTS read callback is set, use it */
461 if(data->set.hsts_read) {
462 CURLSTScode sc;
463 DEBUGASSERT(h);
464 do {
465 char buffer[MAX_HSTS_HOSTLEN + 1];
466 struct curl_hstsentry e;
467 e.name = buffer;
468 e.namelen = sizeof(buffer)-1;
469 e.includeSubDomains = FALSE; /* default */
470 e.expire[0] = 0;
471 e.name[0] = 0; /* just to make it clean */
472 sc = data->set.hsts_read(data, &e, data->set.hsts_read_userp);
473 if(sc == CURLSTS_OK) {
474 time_t expires;
475 CURLcode result;
476 if(!e.name[0])
477 /* bail out if no name was stored */
478 return CURLE_BAD_FUNCTION_ARGUMENT;
479 if(e.expire[0])
480 expires = Curl_getdate_capped(p: e.expire);
481 else
482 expires = TIME_T_MAX; /* the end of time */
483 result = hsts_create(h, hostname: e.name,
484 /* bitfield to bool conversion: */
485 subdomains: e.includeSubDomains ? TRUE : FALSE,
486 expires);
487 if(result)
488 return result;
489 }
490 else if(sc == CURLSTS_FAIL)
491 return CURLE_ABORTED_BY_CALLBACK;
492 } while(sc == CURLSTS_OK);
493 }
494 return CURLE_OK;
495}
496
497/*
498 * Load the HSTS cache from the given file. The text based line-oriented file
499 * format is documented here: https://curl.se/docs/hsts.html
500 *
501 * This function only returns error on major problems that prevent hsts
502 * handling to work completely. It will ignore individual syntactical errors
503 * etc.
504 */
505static CURLcode hsts_load(struct hsts *h, const char *file)
506{
507 CURLcode result = CURLE_OK;
508 char *line = NULL;
509 FILE *fp;
510
511 /* we need a private copy of the file name so that the hsts cache file
512 name survives an easy handle reset */
513 free(h->filename);
514 h->filename = strdup(file);
515 if(!h->filename)
516 return CURLE_OUT_OF_MEMORY;
517
518 fp = fopen(filename: file, FOPEN_READTEXT);
519 if(fp) {
520 line = malloc(MAX_HSTS_LINE);
521 if(!line)
522 goto fail;
523 while(Curl_get_line(buf: line, MAX_HSTS_LINE, input: fp)) {
524 char *lineptr = line;
525 while(*lineptr && ISBLANK(*lineptr))
526 lineptr++;
527 if(*lineptr == '#')
528 /* skip commented lines */
529 continue;
530
531 hsts_add(h, line: lineptr);
532 }
533 free(line); /* free the line buffer */
534 fclose(stream: fp);
535 }
536 return result;
537
538fail:
539 Curl_safefree(h->filename);
540 fclose(stream: fp);
541 return CURLE_OUT_OF_MEMORY;
542}
543
544/*
545 * Curl_hsts_loadfile() loads HSTS from file
546 */
547CURLcode Curl_hsts_loadfile(struct Curl_easy *data,
548 struct hsts *h, const char *file)
549{
550 DEBUGASSERT(h);
551 (void)data;
552 return hsts_load(h, file);
553}
554
555/*
556 * Curl_hsts_loadcb() loads HSTS from callback
557 */
558CURLcode Curl_hsts_loadcb(struct Curl_easy *data, struct hsts *h)
559{
560 if(h)
561 return hsts_pull(data, h);
562 return CURLE_OK;
563}
564
565void Curl_hsts_loadfiles(struct Curl_easy *data)
566{
567 struct curl_slist *l = data->set.hstslist;
568 if(l) {
569 Curl_share_lock(data, CURL_LOCK_DATA_HSTS, CURL_LOCK_ACCESS_SINGLE);
570
571 while(l) {
572 (void)Curl_hsts_loadfile(data, h: data->hsts, file: l->data);
573 l = l->next;
574 }
575 Curl_share_unlock(data, CURL_LOCK_DATA_HSTS);
576 }
577}
578
579#endif /* CURL_DISABLE_HTTP || CURL_DISABLE_HSTS */
580