xref: /third_party/curl/lib/altsvc.c (revision 13498266)
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 Alt-Svc: header is defined in RFC 7838:
26 * https://datatracker.ietf.org/doc/html/rfc7838
27 */
28#include "curl_setup.h"
29
30#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_ALTSVC)
31#include <curl/curl.h>
32#include "urldata.h"
33#include "altsvc.h"
34#include "curl_get_line.h"
35#include "strcase.h"
36#include "parsedate.h"
37#include "sendf.h"
38#include "warnless.h"
39#include "fopen.h"
40#include "rename.h"
41#include "strdup.h"
42#include "inet_pton.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_ALTSVC_LINE 4095
50#define MAX_ALTSVC_DATELENSTR "64"
51#define MAX_ALTSVC_DATELEN 64
52#define MAX_ALTSVC_HOSTLENSTR "512"
53#define MAX_ALTSVC_HOSTLEN 512
54#define MAX_ALTSVC_ALPNLENSTR "10"
55#define MAX_ALTSVC_ALPNLEN 10
56
57#define H3VERSION "h3"
58
59static enum alpnid alpn2alpnid(char *name)
60{
61  if(strcasecompare(name, "h1"))
62    return ALPN_h1;
63  if(strcasecompare(name, "h2"))
64    return ALPN_h2;
65  if(strcasecompare(name, H3VERSION))
66    return ALPN_h3;
67  return ALPN_none; /* unknown, probably rubbish input */
68}
69
70/* Given the ALPN ID, return the name */
71const char *Curl_alpnid2str(enum alpnid id)
72{
73  switch(id) {
74  case ALPN_h1:
75    return "h1";
76  case ALPN_h2:
77    return "h2";
78  case ALPN_h3:
79    return H3VERSION;
80  default:
81    return ""; /* bad */
82  }
83}
84
85
86static void altsvc_free(struct altsvc *as)
87{
88  free(as->src.host);
89  free(as->dst.host);
90  free(as);
91}
92
93static struct altsvc *altsvc_createid(const char *srchost,
94                                      const char *dsthost,
95                                      enum alpnid srcalpnid,
96                                      enum alpnid dstalpnid,
97                                      unsigned int srcport,
98                                      unsigned int dstport)
99{
100  struct altsvc *as = calloc(1, sizeof(struct altsvc));
101  size_t hlen;
102  size_t dlen;
103  if(!as)
104    return NULL;
105  hlen = strlen(srchost);
106  dlen = strlen(dsthost);
107  DEBUGASSERT(hlen);
108  DEBUGASSERT(dlen);
109  if(!hlen || !dlen) {
110    /* bad input */
111    free(as);
112    return NULL;
113  }
114  if((hlen > 2) && srchost[0] == '[') {
115    /* IPv6 address, strip off brackets */
116    srchost++;
117    hlen -= 2;
118  }
119  else if(srchost[hlen - 1] == '.')
120    /* strip off trailing dot */
121    hlen--;
122  if((dlen > 2) && dsthost[0] == '[') {
123    /* IPv6 address, strip off brackets */
124    dsthost++;
125    dlen -= 2;
126  }
127
128  as->src.host = Curl_memdup0(srchost, hlen);
129  if(!as->src.host)
130    goto error;
131
132  as->dst.host = Curl_memdup0(dsthost, dlen);
133  if(!as->dst.host)
134    goto error;
135
136  as->src.alpnid = srcalpnid;
137  as->dst.alpnid = dstalpnid;
138  as->src.port = curlx_ultous(srcport);
139  as->dst.port = curlx_ultous(dstport);
140
141  return as;
142error:
143  altsvc_free(as);
144  return NULL;
145}
146
147static struct altsvc *altsvc_create(char *srchost,
148                                    char *dsthost,
149                                    char *srcalpn,
150                                    char *dstalpn,
151                                    unsigned int srcport,
152                                    unsigned int dstport)
153{
154  enum alpnid dstalpnid = alpn2alpnid(dstalpn);
155  enum alpnid srcalpnid = alpn2alpnid(srcalpn);
156  if(!srcalpnid || !dstalpnid)
157    return NULL;
158  return altsvc_createid(srchost, dsthost, srcalpnid, dstalpnid,
159                         srcport, dstport);
160}
161
162/* only returns SERIOUS errors */
163static CURLcode altsvc_add(struct altsvcinfo *asi, char *line)
164{
165  /* Example line:
166     h2 example.com 443 h3 shiny.example.com 8443 "20191231 10:00:00" 1
167   */
168  char srchost[MAX_ALTSVC_HOSTLEN + 1];
169  char dsthost[MAX_ALTSVC_HOSTLEN + 1];
170  char srcalpn[MAX_ALTSVC_ALPNLEN + 1];
171  char dstalpn[MAX_ALTSVC_ALPNLEN + 1];
172  char date[MAX_ALTSVC_DATELEN + 1];
173  unsigned int srcport;
174  unsigned int dstport;
175  unsigned int prio;
176  unsigned int persist;
177  int rc;
178
179  rc = sscanf(line,
180              "%" MAX_ALTSVC_ALPNLENSTR "s %" MAX_ALTSVC_HOSTLENSTR "s %u "
181              "%" MAX_ALTSVC_ALPNLENSTR "s %" MAX_ALTSVC_HOSTLENSTR "s %u "
182              "\"%" MAX_ALTSVC_DATELENSTR "[^\"]\" %u %u",
183              srcalpn, srchost, &srcport,
184              dstalpn, dsthost, &dstport,
185              date, &persist, &prio);
186  if(9 == rc) {
187    struct altsvc *as;
188    time_t expires = Curl_getdate_capped(date);
189    as = altsvc_create(srchost, dsthost, srcalpn, dstalpn, srcport, dstport);
190    if(as) {
191      as->expires = expires;
192      as->prio = prio;
193      as->persist = persist ? 1 : 0;
194      Curl_llist_insert_next(&asi->list, asi->list.tail, as, &as->node);
195    }
196  }
197
198  return CURLE_OK;
199}
200
201/*
202 * Load alt-svc entries from the given file. The text based line-oriented file
203 * format is documented here: https://curl.se/docs/alt-svc.html
204 *
205 * This function only returns error on major problems that prevent alt-svc
206 * handling to work completely. It will ignore individual syntactical errors
207 * etc.
208 */
209static CURLcode altsvc_load(struct altsvcinfo *asi, const char *file)
210{
211  CURLcode result = CURLE_OK;
212  char *line = NULL;
213  FILE *fp;
214
215  /* we need a private copy of the file name so that the altsvc cache file
216     name survives an easy handle reset */
217  free(asi->filename);
218  asi->filename = strdup(file);
219  if(!asi->filename)
220    return CURLE_OUT_OF_MEMORY;
221
222  fp = fopen(file, FOPEN_READTEXT);
223  if(fp) {
224    line = malloc(MAX_ALTSVC_LINE);
225    if(!line)
226      goto fail;
227    while(Curl_get_line(line, MAX_ALTSVC_LINE, fp)) {
228      char *lineptr = line;
229      while(*lineptr && ISBLANK(*lineptr))
230        lineptr++;
231      if(*lineptr == '#')
232        /* skip commented lines */
233        continue;
234
235      altsvc_add(asi, lineptr);
236    }
237    free(line); /* free the line buffer */
238    fclose(fp);
239  }
240  return result;
241
242fail:
243  Curl_safefree(asi->filename);
244  free(line);
245  fclose(fp);
246  return CURLE_OUT_OF_MEMORY;
247}
248
249/*
250 * Write this single altsvc entry to a single output line
251 */
252
253static CURLcode altsvc_out(struct altsvc *as, FILE *fp)
254{
255  struct tm stamp;
256  const char *dst6_pre = "";
257  const char *dst6_post = "";
258  const char *src6_pre = "";
259  const char *src6_post = "";
260  CURLcode result = Curl_gmtime(as->expires, &stamp);
261  if(result)
262    return result;
263#ifdef ENABLE_IPV6
264  else {
265    char ipv6_unused[16];
266    if(1 == Curl_inet_pton(AF_INET6, as->dst.host, ipv6_unused)) {
267      dst6_pre = "[";
268      dst6_post = "]";
269    }
270    if(1 == Curl_inet_pton(AF_INET6, as->src.host, ipv6_unused)) {
271      src6_pre = "[";
272      src6_post = "]";
273    }
274  }
275#endif
276  fprintf(fp,
277          "%s %s%s%s %u "
278          "%s %s%s%s %u "
279          "\"%d%02d%02d "
280          "%02d:%02d:%02d\" "
281          "%u %d\n",
282          Curl_alpnid2str(as->src.alpnid),
283          src6_pre, as->src.host, src6_post,
284          as->src.port,
285
286          Curl_alpnid2str(as->dst.alpnid),
287          dst6_pre, as->dst.host, dst6_post,
288          as->dst.port,
289
290          stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
291          stamp.tm_hour, stamp.tm_min, stamp.tm_sec,
292          as->persist, as->prio);
293  return CURLE_OK;
294}
295
296/* ---- library-wide functions below ---- */
297
298/*
299 * Curl_altsvc_init() creates a new altsvc cache.
300 * It returns the new instance or NULL if something goes wrong.
301 */
302struct altsvcinfo *Curl_altsvc_init(void)
303{
304  struct altsvcinfo *asi = calloc(1, sizeof(struct altsvcinfo));
305  if(!asi)
306    return NULL;
307  Curl_llist_init(&asi->list, NULL);
308
309  /* set default behavior */
310  asi->flags = CURLALTSVC_H1
311#ifdef USE_HTTP2
312    | CURLALTSVC_H2
313#endif
314#ifdef ENABLE_QUIC
315    | CURLALTSVC_H3
316#endif
317    ;
318  return asi;
319}
320
321/*
322 * Curl_altsvc_load() loads alt-svc from file.
323 */
324CURLcode Curl_altsvc_load(struct altsvcinfo *asi, const char *file)
325{
326  CURLcode result;
327  DEBUGASSERT(asi);
328  result = altsvc_load(asi, file);
329  return result;
330}
331
332/*
333 * Curl_altsvc_ctrl() passes on the external bitmask.
334 */
335CURLcode Curl_altsvc_ctrl(struct altsvcinfo *asi, const long ctrl)
336{
337  DEBUGASSERT(asi);
338  asi->flags = ctrl;
339  return CURLE_OK;
340}
341
342/*
343 * Curl_altsvc_cleanup() frees an altsvc cache instance and all associated
344 * resources.
345 */
346void Curl_altsvc_cleanup(struct altsvcinfo **altsvcp)
347{
348  struct Curl_llist_element *e;
349  struct Curl_llist_element *n;
350  if(*altsvcp) {
351    struct altsvcinfo *altsvc = *altsvcp;
352    for(e = altsvc->list.head; e; e = n) {
353      struct altsvc *as = e->ptr;
354      n = e->next;
355      altsvc_free(as);
356    }
357    free(altsvc->filename);
358    free(altsvc);
359    *altsvcp = NULL; /* clear the pointer */
360  }
361}
362
363/*
364 * Curl_altsvc_save() writes the altsvc cache to a file.
365 */
366CURLcode Curl_altsvc_save(struct Curl_easy *data,
367                          struct altsvcinfo *altsvc, const char *file)
368{
369  struct Curl_llist_element *e;
370  struct Curl_llist_element *n;
371  CURLcode result = CURLE_OK;
372  FILE *out;
373  char *tempstore = NULL;
374
375  if(!altsvc)
376    /* no cache activated */
377    return CURLE_OK;
378
379  /* if not new name is given, use the one we stored from the load */
380  if(!file && altsvc->filename)
381    file = altsvc->filename;
382
383  if((altsvc->flags & CURLALTSVC_READONLYFILE) || !file || !file[0])
384    /* marked as read-only, no file or zero length file name */
385    return CURLE_OK;
386
387  result = Curl_fopen(data, file, &out, &tempstore);
388  if(!result) {
389    fputs("# Your alt-svc cache. https://curl.se/docs/alt-svc.html\n"
390          "# This file was generated by libcurl! Edit at your own risk.\n",
391          out);
392    for(e = altsvc->list.head; e; e = n) {
393      struct altsvc *as = e->ptr;
394      n = e->next;
395      result = altsvc_out(as, out);
396      if(result)
397        break;
398    }
399    fclose(out);
400    if(!result && tempstore && Curl_rename(tempstore, file))
401      result = CURLE_WRITE_ERROR;
402
403    if(result && tempstore)
404      unlink(tempstore);
405  }
406  free(tempstore);
407  return result;
408}
409
410static CURLcode getalnum(const char **ptr, char *alpnbuf, size_t buflen)
411{
412  size_t len;
413  const char *protop;
414  const char *p = *ptr;
415  while(*p && ISBLANK(*p))
416    p++;
417  protop = p;
418  while(*p && !ISBLANK(*p) && (*p != ';') && (*p != '='))
419    p++;
420  len = p - protop;
421  *ptr = p;
422
423  if(!len || (len >= buflen))
424    return CURLE_BAD_FUNCTION_ARGUMENT;
425  memcpy(alpnbuf, protop, len);
426  alpnbuf[len] = 0;
427  return CURLE_OK;
428}
429
430/* hostcompare() returns true if 'host' matches 'check'. The first host
431 * argument may have a trailing dot present that will be ignored.
432 */
433static bool hostcompare(const char *host, const char *check)
434{
435  size_t hlen = strlen(host);
436  size_t clen = strlen(check);
437
438  if(hlen && (host[hlen - 1] == '.'))
439    hlen--;
440  if(hlen != clen)
441    /* they can't match if they have different lengths */
442    return FALSE;
443  return strncasecompare(host, check, hlen);
444}
445
446/* altsvc_flush() removes all alternatives for this source origin from the
447   list */
448static void altsvc_flush(struct altsvcinfo *asi, enum alpnid srcalpnid,
449                         const char *srchost, unsigned short srcport)
450{
451  struct Curl_llist_element *e;
452  struct Curl_llist_element *n;
453  for(e = asi->list.head; e; e = n) {
454    struct altsvc *as = e->ptr;
455    n = e->next;
456    if((srcalpnid == as->src.alpnid) &&
457       (srcport == as->src.port) &&
458       hostcompare(srchost, as->src.host)) {
459      Curl_llist_remove(&asi->list, e, NULL);
460      altsvc_free(as);
461    }
462  }
463}
464
465#ifdef DEBUGBUILD
466/* to play well with debug builds, we can *set* a fixed time this will
467   return */
468static time_t altsvc_debugtime(void *unused)
469{
470  char *timestr = getenv("CURL_TIME");
471  (void)unused;
472  if(timestr) {
473    unsigned long val = strtol(timestr, NULL, 10);
474    return (time_t)val;
475  }
476  return time(NULL);
477}
478#undef time
479#define time(x) altsvc_debugtime(x)
480#endif
481
482#define ISNEWLINE(x) (((x) == '\n') || (x) == '\r')
483
484/*
485 * Curl_altsvc_parse() takes an incoming alt-svc response header and stores
486 * the data correctly in the cache.
487 *
488 * 'value' points to the header *value*. That's contents to the right of the
489 * header name.
490 *
491 * Currently this function rejects invalid data without returning an error.
492 * Invalid host name, port number will result in the specific alternative
493 * being rejected. Unknown protocols are skipped.
494 */
495CURLcode Curl_altsvc_parse(struct Curl_easy *data,
496                           struct altsvcinfo *asi, const char *value,
497                           enum alpnid srcalpnid, const char *srchost,
498                           unsigned short srcport)
499{
500  const char *p = value;
501  size_t len;
502  char namebuf[MAX_ALTSVC_HOSTLEN] = "";
503  char alpnbuf[MAX_ALTSVC_ALPNLEN] = "";
504  struct altsvc *as;
505  unsigned short dstport = srcport; /* the same by default */
506  CURLcode result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
507  size_t entries = 0;
508#ifdef CURL_DISABLE_VERBOSE_STRINGS
509  (void)data;
510#endif
511  if(result) {
512    infof(data, "Excessive alt-svc header, ignoring.");
513    return CURLE_OK;
514  }
515
516  DEBUGASSERT(asi);
517
518  /* "clear" is a magic keyword */
519  if(strcasecompare(alpnbuf, "clear")) {
520    /* Flush cached alternatives for this source origin */
521    altsvc_flush(asi, srcalpnid, srchost, srcport);
522    return CURLE_OK;
523  }
524
525  do {
526    if(*p == '=') {
527      /* [protocol]="[host][:port]" */
528      enum alpnid dstalpnid = alpn2alpnid(alpnbuf); /* the same by default */
529      p++;
530      if(*p == '\"') {
531        const char *dsthost = "";
532        const char *value_ptr;
533        char option[32];
534        unsigned long num;
535        char *end_ptr;
536        bool quoted = FALSE;
537        time_t maxage = 24 * 3600; /* default is 24 hours */
538        bool persist = FALSE;
539        bool valid = TRUE;
540        p++;
541        if(*p != ':') {
542          /* host name starts here */
543          const char *hostp = p;
544          if(*p == '[') {
545            /* pass all valid IPv6 letters - does not handle zone id */
546            len = strspn(++p, "0123456789abcdefABCDEF:.");
547            if(p[len] != ']')
548              /* invalid host syntax, bail out */
549              break;
550            /* we store the IPv6 numerical address *with* brackets */
551            len += 2;
552            p = &p[len-1];
553          }
554          else {
555            while(*p && (ISALNUM(*p) || (*p == '.') || (*p == '-')))
556              p++;
557            len = p - hostp;
558          }
559          if(!len || (len >= MAX_ALTSVC_HOSTLEN)) {
560            infof(data, "Excessive alt-svc host name, ignoring.");
561            valid = FALSE;
562          }
563          else {
564            memcpy(namebuf, hostp, len);
565            namebuf[len] = 0;
566            dsthost = namebuf;
567          }
568        }
569        else {
570          /* no destination name, use source host */
571          dsthost = srchost;
572        }
573        if(*p == ':') {
574          unsigned long port = 0;
575          p++;
576          if(ISDIGIT(*p))
577            /* a port number */
578            port = strtoul(p, &end_ptr, 10);
579          else
580            end_ptr = (char *)p; /* not left uninitialized */
581          if(!port || port > USHRT_MAX || end_ptr == p || *end_ptr != '\"') {
582            infof(data, "Unknown alt-svc port number, ignoring.");
583            valid = FALSE;
584          }
585          else {
586            dstport = curlx_ultous(port);
587            p = end_ptr;
588          }
589        }
590        if(*p++ != '\"')
591          break;
592        /* Handle the optional 'ma' and 'persist' flags. Unknown flags
593           are skipped. */
594        for(;;) {
595          while(ISBLANK(*p))
596            p++;
597          if(*p != ';')
598            break;
599          p++; /* pass the semicolon */
600          if(!*p || ISNEWLINE(*p))
601            break;
602          result = getalnum(&p, option, sizeof(option));
603          if(result) {
604            /* skip option if name is too long */
605            option[0] = '\0';
606          }
607          while(*p && ISBLANK(*p))
608            p++;
609          if(*p != '=')
610            return CURLE_OK;
611          p++;
612          while(*p && ISBLANK(*p))
613            p++;
614          if(!*p)
615            return CURLE_OK;
616          if(*p == '\"') {
617            /* quoted value */
618            p++;
619            quoted = TRUE;
620          }
621          value_ptr = p;
622          if(quoted) {
623            while(*p && *p != '\"')
624              p++;
625            if(!*p++)
626              return CURLE_OK;
627          }
628          else {
629            while(*p && !ISBLANK(*p) && *p!= ';' && *p != ',')
630              p++;
631          }
632          num = strtoul(value_ptr, &end_ptr, 10);
633          if((end_ptr != value_ptr) && (num < ULONG_MAX)) {
634            if(strcasecompare("ma", option))
635              maxage = num;
636            else if(strcasecompare("persist", option) && (num == 1))
637              persist = TRUE;
638          }
639        }
640        if(dstalpnid && valid) {
641          if(!entries++)
642            /* Flush cached alternatives for this source origin, if any - when
643               this is the first entry of the line. */
644            altsvc_flush(asi, srcalpnid, srchost, srcport);
645
646          as = altsvc_createid(srchost, dsthost,
647                               srcalpnid, dstalpnid,
648                               srcport, dstport);
649          if(as) {
650            /* The expires time also needs to take the Age: value (if any) into
651               account. [See RFC 7838 section 3.1] */
652            as->expires = maxage + time(NULL);
653            as->persist = persist;
654            Curl_llist_insert_next(&asi->list, asi->list.tail, as, &as->node);
655            infof(data, "Added alt-svc: %s:%d over %s", dsthost, dstport,
656                  Curl_alpnid2str(dstalpnid));
657          }
658        }
659      }
660      else
661        break;
662      /* after the double quote there can be a comma if there's another
663         string or a semicolon if no more */
664      if(*p == ',') {
665        /* comma means another alternative is presented */
666        p++;
667        result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
668        if(result)
669          break;
670      }
671    }
672    else
673      break;
674  } while(*p && (*p != ';') && (*p != '\n') && (*p != '\r'));
675
676  return CURLE_OK;
677}
678
679/*
680 * Return TRUE on a match
681 */
682bool Curl_altsvc_lookup(struct altsvcinfo *asi,
683                        enum alpnid srcalpnid, const char *srchost,
684                        int srcport,
685                        struct altsvc **dstentry,
686                        const int versions) /* one or more bits */
687{
688  struct Curl_llist_element *e;
689  struct Curl_llist_element *n;
690  time_t now = time(NULL);
691  DEBUGASSERT(asi);
692  DEBUGASSERT(srchost);
693  DEBUGASSERT(dstentry);
694
695  for(e = asi->list.head; e; e = n) {
696    struct altsvc *as = e->ptr;
697    n = e->next;
698    if(as->expires < now) {
699      /* an expired entry, remove */
700      Curl_llist_remove(&asi->list, e, NULL);
701      altsvc_free(as);
702      continue;
703    }
704    if((as->src.alpnid == srcalpnid) &&
705       hostcompare(srchost, as->src.host) &&
706       (as->src.port == srcport) &&
707       (versions & as->dst.alpnid)) {
708      /* match */
709      *dstentry = as;
710      return TRUE;
711    }
712  }
713  return FALSE;
714}
715
716#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_ALTSVC */
717