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