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