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