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