• 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 
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