quickjs-tart

quickjs-based runtime for wallet-core logic
Log | Files | Refs | README | LICENSE

hsts.c (15852B)


      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 "sendf.h"
     37 #include "parsedate.h"
     38 #include "fopen.h"
     39 #include "rename.h"
     40 #include "share.h"
     41 #include "strdup.h"
     42 #include "curlx/strparse.h"
     43 
     44 /* The last 3 #include files should be in this order */
     45 #include "curl_printf.h"
     46 #include "curl_memory.h"
     47 #include "memdebug.h"
     48 
     49 #define MAX_HSTS_LINE 4095
     50 #define MAX_HSTS_HOSTLEN 2048
     51 #define MAX_HSTS_DATELEN 256
     52 #define UNLIMITED "unlimited"
     53 
     54 #if defined(DEBUGBUILD) || defined(UNITTESTS)
     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 hsts_debugtime(void *unused)
     59 {
     60   const char *timestr = getenv("CURL_TIME");
     61   (void)unused;
     62   if(timestr) {
     63     curl_off_t val;
     64     if(!curlx_str_number(&timestr, &val, TIME_T_MAX))
     65       val += (curl_off_t)deltatime;
     66     return (time_t)val;
     67   }
     68   return time(NULL);
     69 }
     70 #undef time
     71 #define time(x) hsts_debugtime(x)
     72 #endif
     73 
     74 struct hsts *Curl_hsts_init(void)
     75 {
     76   struct hsts *h = calloc(1, sizeof(struct hsts));
     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(CURL_UNCONST(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_node *e;
     94     struct Curl_llist_node *n;
     95     for(e = Curl_llist_head(&h->list); e; e = n) {
     96       struct stsentry *sts = Curl_node_elem(e);
     97       n = Curl_node_next(e);
     98       hsts_free(sts);
     99     }
    100     free(h->filename);
    101     free(h);
    102     *hp = NULL;
    103   }
    104 }
    105 
    106 static CURLcode hsts_create(struct hsts *h,
    107                             const char *hostname,
    108                             size_t hlen,
    109                             bool subdomains,
    110                             curl_off_t expires)
    111 {
    112   DEBUGASSERT(h);
    113   DEBUGASSERT(hostname);
    114 
    115   if(hlen && (hostname[hlen - 1] == '.'))
    116     /* strip off any trailing dot */
    117     --hlen;
    118   if(hlen) {
    119     char *duphost;
    120     struct stsentry *sts = calloc(1, sizeof(struct stsentry));
    121     if(!sts)
    122       return CURLE_OUT_OF_MEMORY;
    123 
    124     duphost = Curl_memdup0(hostname, hlen);
    125     if(!duphost) {
    126       free(sts);
    127       return CURLE_OUT_OF_MEMORY;
    128     }
    129 
    130     sts->host = duphost;
    131     sts->expires = expires;
    132     sts->includeSubDomains = subdomains;
    133     Curl_llist_append(&h->list, sts, &sts->node);
    134   }
    135   return CURLE_OK;
    136 }
    137 
    138 CURLcode Curl_hsts_parse(struct hsts *h, const char *hostname,
    139                          const char *header)
    140 {
    141   const char *p = header;
    142   curl_off_t expires = 0;
    143   bool gotma = FALSE;
    144   bool gotinc = FALSE;
    145   bool subdomains = FALSE;
    146   struct stsentry *sts;
    147   time_t now = time(NULL);
    148   size_t hlen = strlen(hostname);
    149 
    150   if(Curl_host_is_ipnum(hostname))
    151     /* "explicit IP address identification of all forms is excluded."
    152        / RFC 6797 */
    153     return CURLE_OK;
    154 
    155   do {
    156     curlx_str_passblanks(&p);
    157     if(curl_strnequal("max-age", p, 7)) {
    158       bool quoted = FALSE;
    159       int rc;
    160 
    161       if(gotma)
    162         return CURLE_BAD_FUNCTION_ARGUMENT;
    163 
    164       p += 7;
    165       curlx_str_passblanks(&p);
    166       if(curlx_str_single(&p, '='))
    167         return CURLE_BAD_FUNCTION_ARGUMENT;
    168       curlx_str_passblanks(&p);
    169 
    170       if(!curlx_str_single(&p, '\"'))
    171         quoted = TRUE;
    172 
    173       rc = curlx_str_number(&p, &expires, TIME_T_MAX);
    174       if(rc == STRE_OVERFLOW)
    175         expires = CURL_OFF_T_MAX;
    176       else if(rc)
    177         /* invalid max-age */
    178         return CURLE_BAD_FUNCTION_ARGUMENT;
    179 
    180       if(quoted) {
    181         if(*p != '\"')
    182           return CURLE_BAD_FUNCTION_ARGUMENT;
    183         p++;
    184       }
    185       gotma = TRUE;
    186     }
    187     else if(curl_strnequal("includesubdomains", p, 17)) {
    188       if(gotinc)
    189         return CURLE_BAD_FUNCTION_ARGUMENT;
    190       subdomains = TRUE;
    191       p += 17;
    192       gotinc = TRUE;
    193     }
    194     else {
    195       /* unknown directive, do a lame attempt to skip */
    196       while(*p && (*p != ';'))
    197         p++;
    198     }
    199 
    200     curlx_str_passblanks(&p);
    201     if(*p == ';')
    202       p++;
    203   } while(*p);
    204 
    205   if(!gotma)
    206     /* max-age is mandatory */
    207     return CURLE_BAD_FUNCTION_ARGUMENT;
    208 
    209   if(!expires) {
    210     /* remove the entry if present verbatim (without subdomain match) */
    211     sts = Curl_hsts(h, hostname, hlen, FALSE);
    212     if(sts) {
    213       Curl_node_remove(&sts->node);
    214       hsts_free(sts);
    215     }
    216     return CURLE_OK;
    217   }
    218 
    219   if(CURL_OFF_T_MAX - now < expires)
    220     /* would overflow, use maximum value */
    221     expires = CURL_OFF_T_MAX;
    222   else
    223     expires += now;
    224 
    225   /* check if it already exists */
    226   sts = Curl_hsts(h, hostname, hlen, FALSE);
    227   if(sts) {
    228     /* just update these fields */
    229     sts->expires = expires;
    230     sts->includeSubDomains = subdomains;
    231   }
    232   else
    233     return hsts_create(h, hostname, hlen, subdomains, expires);
    234 
    235   return CURLE_OK;
    236 }
    237 
    238 /*
    239  * Return TRUE if the given hostname is currently an HSTS one.
    240  *
    241  * The 'subdomain' argument tells the function if subdomain matching should be
    242  * attempted.
    243  */
    244 struct stsentry *Curl_hsts(struct hsts *h, const char *hostname,
    245                            size_t hlen, bool subdomain)
    246 {
    247   struct stsentry *bestsub = NULL;
    248   if(h) {
    249     time_t now = time(NULL);
    250     struct Curl_llist_node *e;
    251     struct Curl_llist_node *n;
    252     size_t blen = 0;
    253 
    254     if((hlen > MAX_HSTS_HOSTLEN) || !hlen)
    255       return NULL;
    256     if(hostname[hlen-1] == '.')
    257       /* remove the trailing dot */
    258       --hlen;
    259 
    260     for(e = Curl_llist_head(&h->list); e; e = n) {
    261       struct stsentry *sts = Curl_node_elem(e);
    262       size_t ntail;
    263       n = Curl_node_next(e);
    264       if(sts->expires <= now) {
    265         /* remove expired entries */
    266         Curl_node_remove(&sts->node);
    267         hsts_free(sts);
    268         continue;
    269       }
    270       ntail = strlen(sts->host);
    271       if((subdomain && sts->includeSubDomains) && (ntail < hlen)) {
    272         size_t offs = hlen - ntail;
    273         if((hostname[offs-1] == '.') &&
    274            curl_strnequal(&hostname[offs], sts->host, ntail) &&
    275            (ntail > blen)) {
    276           /* save the tail match with the longest tail */
    277           bestsub = sts;
    278           blen = ntail;
    279         }
    280       }
    281       /* avoid curl_strequal because the host name is not null-terminated */
    282       if((hlen == ntail) && curl_strnequal(hostname, sts->host, hlen))
    283         return sts;
    284     }
    285   }
    286   return bestsub;
    287 }
    288 
    289 /*
    290  * Send this HSTS entry to the write callback.
    291  */
    292 static CURLcode hsts_push(struct Curl_easy *data,
    293                           struct curl_index *i,
    294                           struct stsentry *sts,
    295                           bool *stop)
    296 {
    297   struct curl_hstsentry e;
    298   CURLSTScode sc;
    299   struct tm stamp;
    300   CURLcode result;
    301 
    302   e.name = (char *)CURL_UNCONST(sts->host);
    303   e.namelen = strlen(sts->host);
    304   e.includeSubDomains = sts->includeSubDomains;
    305 
    306   if(sts->expires != TIME_T_MAX) {
    307     result = Curl_gmtime((time_t)sts->expires, &stamp);
    308     if(result)
    309       return result;
    310 
    311     msnprintf(e.expire, sizeof(e.expire), "%d%02d%02d %02d:%02d:%02d",
    312               stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
    313               stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
    314   }
    315   else
    316     strcpy(e.expire, UNLIMITED);
    317 
    318   sc = data->set.hsts_write(data, &e, i,
    319                             data->set.hsts_write_userp);
    320   *stop = (sc != CURLSTS_OK);
    321   return sc == CURLSTS_FAIL ? CURLE_BAD_FUNCTION_ARGUMENT : CURLE_OK;
    322 }
    323 
    324 /*
    325  * Write this single hsts entry to a single output line
    326  */
    327 static CURLcode hsts_out(struct stsentry *sts, FILE *fp)
    328 {
    329   struct tm stamp;
    330   if(sts->expires != TIME_T_MAX) {
    331     CURLcode result = Curl_gmtime((time_t)sts->expires, &stamp);
    332     if(result)
    333       return result;
    334     fprintf(fp, "%s%s \"%d%02d%02d %02d:%02d:%02d\"\n",
    335             sts->includeSubDomains ? ".": "", sts->host,
    336             stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
    337             stamp.tm_hour, stamp.tm_min, stamp.tm_sec);
    338   }
    339   else
    340     fprintf(fp, "%s%s \"%s\"\n",
    341             sts->includeSubDomains ? ".": "", sts->host, UNLIMITED);
    342   return CURLE_OK;
    343 }
    344 
    345 
    346 /*
    347  * Curl_https_save() writes the HSTS cache to file and callback.
    348  */
    349 CURLcode Curl_hsts_save(struct Curl_easy *data, struct hsts *h,
    350                         const char *file)
    351 {
    352   struct Curl_llist_node *e;
    353   struct Curl_llist_node *n;
    354   CURLcode result = CURLE_OK;
    355   FILE *out;
    356   char *tempstore = NULL;
    357 
    358   if(!h)
    359     /* no cache activated */
    360     return CURLE_OK;
    361 
    362   /* if no new name is given, use the one we stored from the load */
    363   if(!file && h->filename)
    364     file = h->filename;
    365 
    366   if((h->flags & CURLHSTS_READONLYFILE) || !file || !file[0])
    367     /* marked as read-only, no file or zero length filename */
    368     goto skipsave;
    369 
    370   result = Curl_fopen(data, file, &out, &tempstore);
    371   if(!result) {
    372     fputs("# Your HSTS cache. https://curl.se/docs/hsts.html\n"
    373           "# This file was generated by libcurl! Edit at your own risk.\n",
    374           out);
    375     for(e = Curl_llist_head(&h->list); e; e = n) {
    376       struct stsentry *sts = Curl_node_elem(e);
    377       n = Curl_node_next(e);
    378       result = hsts_out(sts, out);
    379       if(result)
    380         break;
    381     }
    382     fclose(out);
    383     if(!result && tempstore && Curl_rename(tempstore, file))
    384       result = CURLE_WRITE_ERROR;
    385 
    386     if(result && tempstore)
    387       unlink(tempstore);
    388   }
    389   free(tempstore);
    390 skipsave:
    391   if(data->set.hsts_write) {
    392     /* if there is a write callback */
    393     struct curl_index i; /* count */
    394     i.total = Curl_llist_count(&h->list);
    395     i.index = 0;
    396     for(e = Curl_llist_head(&h->list); e; e = n) {
    397       struct stsentry *sts = Curl_node_elem(e);
    398       bool stop;
    399       n = Curl_node_next(e);
    400       result = hsts_push(data, &i, sts, &stop);
    401       if(result || stop)
    402         break;
    403       i.index++;
    404     }
    405   }
    406   return result;
    407 }
    408 
    409 /* only returns SERIOUS errors */
    410 static CURLcode hsts_add(struct hsts *h, const char *line)
    411 {
    412   /* Example lines:
    413      example.com "20191231 10:00:00"
    414      .example.net "20191231 10:00:00"
    415    */
    416   struct Curl_str host;
    417   struct Curl_str date;
    418 
    419   if(curlx_str_word(&line, &host, MAX_HSTS_HOSTLEN) ||
    420      curlx_str_singlespace(&line) ||
    421      curlx_str_quotedword(&line, &date, MAX_HSTS_DATELEN) ||
    422      curlx_str_newline(&line))
    423     ;
    424   else {
    425     CURLcode result = CURLE_OK;
    426     bool subdomain = FALSE;
    427     struct stsentry *e;
    428     char dbuf[MAX_HSTS_DATELEN + 1];
    429     time_t expires;
    430     const char *hp = curlx_str(&host);
    431 
    432     /* The date parser works on a null-terminated string. The maximum length
    433        is upheld by curlx_str_quotedword(). */
    434     memcpy(dbuf, curlx_str(&date), curlx_strlen(&date));
    435     dbuf[curlx_strlen(&date)] = 0;
    436 
    437     expires = strcmp(dbuf, UNLIMITED) ? Curl_getdate_capped(dbuf) :
    438       TIME_T_MAX;
    439 
    440     if(hp[0] == '.') {
    441       curlx_str_nudge(&host, 1);
    442       subdomain = TRUE;
    443     }
    444     /* only add it if not already present */
    445     e = Curl_hsts(h, curlx_str(&host), curlx_strlen(&host), subdomain);
    446     if(!e)
    447       result = hsts_create(h, curlx_str(&host), curlx_strlen(&host),
    448                            subdomain, expires);
    449     else if(curlx_str_casecompare(&host, 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  */
    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, strlen(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  */
    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     curlx_dyn_init(&buf, MAX_HSTS_LINE);
    529     while(Curl_get_line(&buf, fp)) {
    530       const char *lineptr = curlx_dyn_ptr(&buf);
    531       curlx_str_passblanks(&lineptr);
    532 
    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     curlx_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  */
    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  */
    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 
    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 #if defined(DEBUGBUILD) || defined(UNITTESTS)
    584 #undef time
    585 #endif
    586 
    587 #endif /* CURL_DISABLE_HTTP || CURL_DISABLE_HSTS */