b9fa6f7af540d93aec38a0bfa03ec0f91a502d91
[platform/upstream/cmake.git] / Utilities / cmcurl / lib / hsts.c
1 /***************************************************************************
2  *                                  _   _ ____  _
3  *  Project                     ___| | | |  _ \| |
4  *                             / __| | | | |_) | |
5  *                            | (__| |_| |  _ <| |___
6  *                             \___|\___/|_| \_\_____|
7  *
8  * Copyright (C) 2020 - 2022, 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://datatracker.ietf.org/doc/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 "rand.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 #define UNLIMITED "unlimited"
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 */
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
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
83 static void hsts_free(struct stsentry *e)
84 {
85   free((char *)e->host);
86   free(e);
87 }
88
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
106 static struct stsentry *hsts_entry(void)
107 {
108   return calloc(sizeof(struct stsentry), 1);
109 }
110
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   char *duphost;
118   size_t hlen;
119   if(!sts)
120     return CURLE_OUT_OF_MEMORY;
121
122   duphost = strdup(hostname);
123   if(!duphost) {
124     free(sts);
125     return CURLE_OUT_OF_MEMORY;
126   }
127
128   hlen = strlen(duphost);
129   if(duphost[hlen - 1] == '.')
130     /* strip off trailing any dot */
131     duphost[--hlen] = 0;
132
133   sts->host = duphost;
134   sts->expires = expires;
135   sts->includeSubDomains = subdomains;
136   Curl_llist_insert_next(&h->list, h->list.tail, sts, &sts->node);
137   return CURLE_OK;
138 }
139
140 CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
141                          const char *header)
142 {
143   const char *p = header;
144   curl_off_t expires = 0;
145   bool gotma = FALSE;
146   bool gotinc = FALSE;
147   bool subdomains = FALSE;
148   struct stsentry *sts;
149   time_t now = time(NULL);
150
151   if(Curl_host_is_ipnum(hostname))
152     /* "explicit IP address identification of all forms is excluded."
153        / RFC 6797 */
154     return CURLE_OK;
155
156   do {
157     while(*p && ISSPACE(*p))
158       p++;
159     if(Curl_strncasecompare("max-age=", p, 8)) {
160       bool quoted = FALSE;
161       CURLofft offt;
162       char *endp;
163
164       if(gotma)
165         return CURLE_BAD_FUNCTION_ARGUMENT;
166
167       p += 8;
168       while(*p && ISSPACE(*p))
169         p++;
170       if(*p == '\"') {
171         p++;
172         quoted = TRUE;
173       }
174       offt = curlx_strtoofft(p, &endp, 10, &expires);
175       if(offt == CURL_OFFT_FLOW)
176         expires = CURL_OFF_T_MAX;
177       else if(offt)
178         /* invalid max-age */
179         return CURLE_BAD_FUNCTION_ARGUMENT;
180       p = endp;
181       if(quoted) {
182         if(*p != '\"')
183           return CURLE_BAD_FUNCTION_ARGUMENT;
184         p++;
185       }
186       gotma = TRUE;
187     }
188     else if(Curl_strncasecompare("includesubdomains", p, 17)) {
189       if(gotinc)
190         return CURLE_BAD_FUNCTION_ARGUMENT;
191       subdomains = TRUE;
192       p += 17;
193       gotinc = TRUE;
194     }
195     else {
196       /* unknown directive, do a lame attempt to skip */
197       while(*p && (*p != ';'))
198         p++;
199     }
200
201     while(*p && ISSPACE(*p))
202       p++;
203     if(*p == ';')
204       p++;
205   } while (*p);
206
207   if(!gotma)
208     /* max-age is mandatory */
209     return CURLE_BAD_FUNCTION_ARGUMENT;
210
211   if(!expires) {
212     /* remove the entry if present verbatim (without subdomain match) */
213     sts = Curl_hsts(h, hostname, FALSE);
214     if(sts) {
215       Curl_llist_remove(&h->list, &sts->node, NULL);
216       hsts_free(sts);
217     }
218     return CURLE_OK;
219   }
220
221   if(CURL_OFF_T_MAX - now < expires)
222     /* would overflow, use maximum value */
223     expires = CURL_OFF_T_MAX;
224   else
225     expires += now;
226
227   /* check if it already exists */
228   sts = Curl_hsts(h, hostname, FALSE);
229   if(sts) {
230     /* just update these fields */
231     sts->expires = expires;
232     sts->includeSubDomains = subdomains;
233   }
234   else
235     return hsts_create(h, hostname, subdomains, expires);
236
237   return CURLE_OK;
238 }
239
240 /*
241  * Return TRUE if the given host name is currently an HSTS one.
242  *
243  * The 'subdomain' argument tells the function if subdomain matching should be
244  * attempted.
245  */
246 struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
247                            bool subdomain)
248 {
249   if(h) {
250     char buffer[MAX_HSTS_HOSTLEN + 1];
251     time_t now = time(NULL);
252     size_t hlen = strlen(hostname);
253     struct Curl_llist_element *e;
254     struct Curl_llist_element *n;
255
256     if((hlen > MAX_HSTS_HOSTLEN) || !hlen)
257       return NULL;
258     memcpy(buffer, hostname, hlen);
259     if(hostname[hlen-1] == '.')
260       /* remove the trailing dot */
261       --hlen;
262     buffer[hlen] = 0;
263     hostname = buffer;
264
265     for(e = h->list.head; e; e = n) {
266       struct stsentry *sts = e->ptr;
267       n = e->next;
268       if(sts->expires <= now) {
269         /* remove expired entries */
270         Curl_llist_remove(&h->list, &sts->node, NULL);
271         hsts_free(sts);
272         continue;
273       }
274       if(subdomain && sts->includeSubDomains) {
275         size_t ntail = strlen(sts->host);
276         if(ntail < hlen) {
277           size_t offs = hlen - ntail;
278           if((hostname[offs-1] == '.') &&
279              Curl_strncasecompare(&hostname[offs], sts->host, ntail))
280             return sts;
281         }
282       }
283       if(Curl_strcasecompare(hostname, sts->host))
284         return sts;
285     }
286   }
287   return NULL; /* no match */
288 }
289
290 /*
291  * Send this HSTS entry to the write callback.
292  */
293 static CURLcode hsts_push(struct Curl_easy *data,
294                           struct curl_index *i,
295                           struct stsentry *sts,
296                           bool *stop)
297 {
298   struct curl_hstsentry e;
299   CURLSTScode sc;
300   struct tm stamp;
301   CURLcode result;
302
303   e.name = (char *)sts->host;
304   e.namelen = strlen(sts->host);
305   e.includeSubDomains = sts->includeSubDomains;
306
307   if(sts->expires != TIME_T_MAX) {
308     result = Curl_gmtime((time_t)sts->expires, &stamp);
309     if(result)
310       return result;
311
312     msnprintf(e.expire, sizeof(e.expire), "%d%02d%02d %02d:%02d:%02d",
313               stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
314               stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
315   }
316   else
317     strcpy(e.expire, UNLIMITED);
318
319   sc = data->set.hsts_write(data, &e, i,
320                             data->set.hsts_write_userp);
321   *stop = (sc != CURLSTS_OK);
322   return sc == CURLSTS_FAIL ? CURLE_BAD_FUNCTION_ARGUMENT : CURLE_OK;
323 }
324
325 /*
326  * Write this single hsts entry to a single output line
327  */
328 static CURLcode hsts_out(struct stsentry *sts, FILE *fp)
329 {
330   struct tm stamp;
331   if(sts->expires != TIME_T_MAX) {
332     CURLcode result = Curl_gmtime((time_t)sts->expires, &stamp);
333     if(result)
334       return result;
335     fprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n",
336             sts->includeSubDomains ? ".": "", sts->host,
337             stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
338             stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
339   }
340   else
341     fprintf(fp, "%s%s \"%s\"\n",
342             sts->includeSubDomains ? ".": "", sts->host, UNLIMITED);
343   return CURLE_OK;
344 }
345
346
347 /*
348  * Curl_https_save() writes the HSTS cache to file and callback.
349  */
350 CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
351                         const char *file)
352 {
353   struct Curl_llist_element *e;
354   struct Curl_llist_element *n;
355   CURLcode result = CURLE_OK;
356   FILE *out;
357   char *tempstore;
358   unsigned char randsuffix[9];
359
360   if(!h)
361     /* no cache activated */
362     return CURLE_OK;
363
364   /* if no new name is given, use the one we stored from the load */
365   if(!file && h->filename)
366     file = h->filename;
367
368   if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0])
369     /* marked as read-only, no file or zero length file name */
370     goto skipsave;
371
372   if(Curl_rand_hex(data, randsuffix, sizeof(randsuffix)))
373     return CURLE_FAILED_INIT;
374
375   tempstore = aprintf("%s.%s.tmp", file, randsuffix);
376   if(!tempstore)
377     return CURLE_OUT_OF_MEMORY;
378
379   out = fopen(tempstore, FOPEN_WRITETEXT);
380   if(!out)
381     result = CURLE_WRITE_ERROR;
382   else {
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 = h->list.head; e; e = n) {
387       struct stsentry *sts = e->ptr;
388       n = e->next;
389       result = hsts_out(sts, out);
390       if(result)
391         break;
392     }
393     fclose(out);
394     if(!result && Curl_rename(tempstore, file))
395       result = CURLE_WRITE_ERROR;
396
397     if(result)
398       unlink(tempstore);
399   }
400   free(tempstore);
401   skipsave:
402   if(data->set.hsts_write) {
403     /* if there's a write callback */
404     struct curl_index i; /* count */
405     i.total = h->list.size;
406     i.index = 0;
407     for(e = h->list.head; e; e = n) {
408       struct stsentry *sts = e->ptr;
409       bool stop;
410       n = e->next;
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 */
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;
438     char *p = host;
439     bool subdomain = FALSE;
440     if(p[0] == '.') {
441       p++;
442       subdomain = TRUE;
443     }
444     result = hsts_create(h, p, subdomain, expires);
445     if(result)
446       return result;
447   }
448
449   return CURLE_OK;
450 }
451
452 /*
453  * Load HSTS data from callback.
454  *
455  */
456 static CURLcode hsts_pull(struct Curl_easy *data, struct hsts *h)
457 {
458   /* if the HSTS read callback is set, use it */
459   if(data->set.hsts_read) {
460     CURLSTScode sc;
461     DEBUGASSERT(h);
462     do {
463       char buffer[MAX_HSTS_HOSTLEN + 1];
464       struct curl_hstsentry e;
465       e.name = buffer;
466       e.namelen = sizeof(buffer)-1;
467       e.includeSubDomains = FALSE; /* default */
468       e.expire[0] = 0;
469       e.name[0] = 0; /* just to make it clean */
470       sc = data->set.hsts_read(data, &e, data->set.hsts_read_userp);
471       if(sc == CURLSTS_OK) {
472         time_t expires;
473         CURLcode result;
474         if(!e.name[0])
475           /* bail out if no name was stored */
476           return CURLE_BAD_FUNCTION_ARGUMENT;
477         if(e.expire[0])
478           expires = Curl_getdate_capped(e.expire);
479         else
480           expires = TIME_T_MAX; /* the end of time */
481         result = hsts_create(h, e.name,
482                              /* bitfield to bool conversion: */
483                              e.includeSubDomains ? TRUE : FALSE,
484                              expires);
485         if(result)
486           return result;
487       }
488       else if(sc == CURLSTS_FAIL)
489         return CURLE_ABORTED_BY_CALLBACK;
490     } while(sc == CURLSTS_OK);
491   }
492   return CURLE_OK;
493 }
494
495 /*
496  * Load the HSTS cache from the given file. The text based line-oriented file
497  * format is documented here:
498  * https://github.com/curl/curl/wiki/HSTS
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  */
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  */
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  */
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
564 #endif /* CURL_DISABLE_HTTP || CURL_DISABLE_HSTS */