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