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(sizeof(struct altsvc), 1);
101 size_t hlen;
102 size_t dlen;
103 if(!as)
104 return NULL;
105 hlen = strlen(s: srchost);
106 dlen = strlen(s: dsthost);
107 DEBUGASSERT(hlen);
108 DEBUGASSERT(dlen);
109 if(!hlen || !dlen)
110 /* bad input */
111 return NULL;
112 if((hlen > 2) && srchost[0] == '[') {
113 /* IPv6 address, strip off brackets */
114 srchost++;
115 hlen -= 2;
116 }
117 else if(srchost[hlen - 1] == '.')
118 /* strip off trailing dot */
119 hlen--;
120 if((dlen > 2) && dsthost[0] == '[') {
121 /* IPv6 address, strip off brackets */
122 dsthost++;
123 dlen -= 2;
124 }
125
126 as->src.host = Curl_memdup(src: srchost, buffer_length: hlen + 1);
127 if(!as->src.host)
128 goto error;
129 as->src.host[hlen] = 0;
130
131 as->dst.host = Curl_memdup(src: dsthost, buffer_length: dlen + 1);
132 if(!as->dst.host)
133 goto error;
134 as->dst.host[dlen] = 0;
135
136 as->src.alpnid = srcalpnid;
137 as->dst.alpnid = dstalpnid;
138 as->src.port = curlx_ultous(ulnum: srcport);
139 as->dst.port = curlx_ultous(ulnum: 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(name: dstalpn);
155 enum alpnid srcalpnid = alpn2alpnid(name: 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(s: line,
180 format: "%" 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(p: 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, node: &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(filename: file, FOPEN_READTEXT);
223 if(fp) {
224 line = malloc(MAX_ALTSVC_LINE);
225 if(!line)
226 goto fail;
227 while(Curl_get_line(buf: line, MAX_ALTSVC_LINE, input: 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, line: lineptr);
236 }
237 free(line); /* free the line buffer */
238 fclose(stream: fp);
239 }
240 return result;
241
242fail:
243 Curl_safefree(asi->filename);
244 free(line);
245 fclose(stream: 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(intime: as->expires, store: &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(fd: fp,
277 format: "%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(id: as->src.alpnid),
283 src6_pre, as->src.host, src6_post,
284 as->src.port,
285
286 Curl_alpnid2str(id: 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(sizeof(struct altsvcinfo), 1);
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 if(!ctrl)
339 /* unexpected */
340 return CURLE_BAD_FUNCTION_ARGUMENT;
341 asi->flags = ctrl;
342 return CURLE_OK;
343}
344
345/*
346 * Curl_altsvc_cleanup() frees an altsvc cache instance and all associated
347 * resources.
348 */
349void Curl_altsvc_cleanup(struct altsvcinfo **altsvcp)
350{
351 struct Curl_llist_element *e;
352 struct Curl_llist_element *n;
353 if(*altsvcp) {
354 struct altsvcinfo *altsvc = *altsvcp;
355 for(e = altsvc->list.head; e; e = n) {
356 struct altsvc *as = e->ptr;
357 n = e->next;
358 altsvc_free(as);
359 }
360 free(altsvc->filename);
361 free(altsvc);
362 *altsvcp = NULL; /* clear the pointer */
363 }
364}
365
366/*
367 * Curl_altsvc_save() writes the altsvc cache to a file.
368 */
369CURLcode Curl_altsvc_save(struct Curl_easy *data,
370 struct altsvcinfo *altsvc, const char *file)
371{
372 struct Curl_llist_element *e;
373 struct Curl_llist_element *n;
374 CURLcode result = CURLE_OK;
375 FILE *out;
376 char *tempstore = NULL;
377
378 if(!altsvc)
379 /* no cache activated */
380 return CURLE_OK;
381
382 /* if not new name is given, use the one we stored from the load */
383 if(!file && altsvc->filename)
384 file = altsvc->filename;
385
386 if((altsvc->flags & CURLALTSVC_READONLYFILE) || !file || !file[0])
387 /* marked as read-only, no file or zero length file name */
388 return CURLE_OK;
389
390 result = Curl_fopen(data, filename: file, fh: &out, tempname: &tempstore);
391 if(!result) {
392 fputs(s: "# Your alt-svc cache. https://curl.se/docs/alt-svc.html\n"
393 "# This file was generated by libcurl! Edit at your own risk.\n",
394 stream: out);
395 for(e = altsvc->list.head; e; e = n) {
396 struct altsvc *as = e->ptr;
397 n = e->next;
398 result = altsvc_out(as, fp: out);
399 if(result)
400 break;
401 }
402 fclose(stream: out);
403 if(!result && tempstore && Curl_rename(oldpath: tempstore, newpath: file))
404 result = CURLE_WRITE_ERROR;
405
406 if(result && tempstore)
407 unlink(name: tempstore);
408 }
409 free(tempstore);
410 return result;
411}
412
413static CURLcode getalnum(const char **ptr, char *alpnbuf, size_t buflen)
414{
415 size_t len;
416 const char *protop;
417 const char *p = *ptr;
418 while(*p && ISBLANK(*p))
419 p++;
420 protop = p;
421 while(*p && !ISBLANK(*p) && (*p != ';') && (*p != '='))
422 p++;
423 len = p - protop;
424 *ptr = p;
425
426 if(!len || (len >= buflen))
427 return CURLE_BAD_FUNCTION_ARGUMENT;
428 memcpy(dest: alpnbuf, src: protop, n: len);
429 alpnbuf[len] = 0;
430 return CURLE_OK;
431}
432
433/* hostcompare() returns true if 'host' matches 'check'. The first host
434 * argument may have a trailing dot present that will be ignored.
435 */
436static bool hostcompare(const char *host, const char *check)
437{
438 size_t hlen = strlen(s: host);
439 size_t clen = strlen(s: check);
440
441 if(hlen && (host[hlen - 1] == '.'))
442 hlen--;
443 if(hlen != clen)
444 /* they can't match if they have different lengths */
445 return FALSE;
446 return strncasecompare(host, check, hlen);
447}
448
449/* altsvc_flush() removes all alternatives for this source origin from the
450 list */
451static void altsvc_flush(struct altsvcinfo *asi, enum alpnid srcalpnid,
452 const char *srchost, unsigned short srcport)
453{
454 struct Curl_llist_element *e;
455 struct Curl_llist_element *n;
456 for(e = asi->list.head; e; e = n) {
457 struct altsvc *as = e->ptr;
458 n = e->next;
459 if((srcalpnid == as->src.alpnid) &&
460 (srcport == as->src.port) &&
461 hostcompare(host: srchost, check: as->src.host)) {
462 Curl_llist_remove(&asi->list, e, NULL);
463 altsvc_free(as);
464 }
465 }
466}
467
468#ifdef DEBUGBUILD
469/* to play well with debug builds, we can *set* a fixed time this will
470 return */
471static time_t altsvc_debugtime(void *unused)
472{
473 char *timestr = getenv("CURL_TIME");
474 (void)unused;
475 if(timestr) {
476 unsigned long val = strtol(timestr, NULL, 10);
477 return (time_t)val;
478 }
479 return time(NULL);
480}
481#undef time
482#define time(x) altsvc_debugtime(x)
483#endif
484
485#define ISNEWLINE(x) (((x) == '\n') || (x) == '\r')
486
487/*
488 * Curl_altsvc_parse() takes an incoming alt-svc response header and stores
489 * the data correctly in the cache.
490 *
491 * 'value' points to the header *value*. That's contents to the right of the
492 * header name.
493 *
494 * Currently this function rejects invalid data without returning an error.
495 * Invalid host name, port number will result in the specific alternative
496 * being rejected. Unknown protocols are skipped.
497 */
498CURLcode Curl_altsvc_parse(struct Curl_easy *data,
499 struct altsvcinfo *asi, const char *value,
500 enum alpnid srcalpnid, const char *srchost,
501 unsigned short srcport)
502{
503 const char *p = value;
504 size_t len;
505 char namebuf[MAX_ALTSVC_HOSTLEN] = "";
506 char alpnbuf[MAX_ALTSVC_ALPNLEN] = "";
507 struct altsvc *as;
508 unsigned short dstport = srcport; /* the same by default */
509 CURLcode result = getalnum(ptr: &p, alpnbuf, buflen: sizeof(alpnbuf));
510 size_t entries = 0;
511#ifdef CURL_DISABLE_VERBOSE_STRINGS
512 (void)data;
513#endif
514 if(result) {
515 infof(data, "Excessive alt-svc header, ignoring.");
516 return CURLE_OK;
517 }
518
519 DEBUGASSERT(asi);
520
521 /* "clear" is a magic keyword */
522 if(strcasecompare(alpnbuf, "clear")) {
523 /* Flush cached alternatives for this source origin */
524 altsvc_flush(asi, srcalpnid, srchost, srcport);
525 return CURLE_OK;
526 }
527
528 do {
529 if(*p == '=') {
530 /* [protocol]="[host][:port]" */
531 enum alpnid dstalpnid = alpn2alpnid(name: alpnbuf); /* the same by default */
532 p++;
533 if(*p == '\"') {
534 const char *dsthost = "";
535 const char *value_ptr;
536 char option[32];
537 unsigned long num;
538 char *end_ptr;
539 bool quoted = FALSE;
540 time_t maxage = 24 * 3600; /* default is 24 hours */
541 bool persist = FALSE;
542 bool valid = TRUE;
543 p++;
544 if(*p != ':') {
545 /* host name starts here */
546 const char *hostp = p;
547 if(*p == '[') {
548 /* pass all valid IPv6 letters - does not handle zone id */
549 len = strspn(s: ++p, accept: "0123456789abcdefABCDEF:.");
550 if(p[len] != ']')
551 /* invalid host syntax, bail out */
552 break;
553 /* we store the IPv6 numerical address *with* brackets */
554 len += 2;
555 p = &p[len-1];
556 }
557 else {
558 while(*p && (ISALNUM(*p) || (*p == '.') || (*p == '-')))
559 p++;
560 len = p - hostp;
561 }
562 if(!len || (len >= MAX_ALTSVC_HOSTLEN)) {
563 infof(data, "Excessive alt-svc host name, ignoring.");
564 valid = FALSE;
565 }
566 else {
567 memcpy(dest: namebuf, src: hostp, n: len);
568 namebuf[len] = 0;
569 dsthost = namebuf;
570 }
571 }
572 else {
573 /* no destination name, use source host */
574 dsthost = srchost;
575 }
576 if(*p == ':') {
577 unsigned long port = 0;
578 p++;
579 if(ISDIGIT(*p))
580 /* a port number */
581 port = strtoul(nptr: p, endptr: &end_ptr, base: 10);
582 else
583 end_ptr = (char *)p; /* not left uninitialized */
584 if(!port || port > USHRT_MAX || end_ptr == p || *end_ptr != '\"') {
585 infof(data, "Unknown alt-svc port number, ignoring.");
586 valid = FALSE;
587 }
588 else {
589 dstport = curlx_ultous(ulnum: port);
590 p = end_ptr;
591 }
592 }
593 if(*p++ != '\"')
594 break;
595 /* Handle the optional 'ma' and 'persist' flags. Unknown flags
596 are skipped. */
597 for(;;) {
598 while(ISBLANK(*p))
599 p++;
600 if(*p != ';')
601 break;
602 p++; /* pass the semicolon */
603 if(!*p || ISNEWLINE(*p))
604 break;
605 result = getalnum(ptr: &p, alpnbuf: option, buflen: sizeof(option));
606 if(result) {
607 /* skip option if name is too long */
608 option[0] = '\0';
609 }
610 while(*p && ISBLANK(*p))
611 p++;
612 if(*p != '=')
613 return CURLE_OK;
614 p++;
615 while(*p && ISBLANK(*p))
616 p++;
617 if(!*p)
618 return CURLE_OK;
619 if(*p == '\"') {
620 /* quoted value */
621 p++;
622 quoted = TRUE;
623 }
624 value_ptr = p;
625 if(quoted) {
626 while(*p && *p != '\"')
627 p++;
628 if(!*p++)
629 return CURLE_OK;
630 }
631 else {
632 while(*p && !ISBLANK(*p) && *p!= ';' && *p != ',')
633 p++;
634 }
635 num = strtoul(nptr: value_ptr, endptr: &end_ptr, base: 10);
636 if((end_ptr != value_ptr) && (num < ULONG_MAX)) {
637 if(strcasecompare("ma", option))
638 maxage = num;
639 else if(strcasecompare("persist", option) && (num == 1))
640 persist = TRUE;
641 }
642 }
643 if(dstalpnid && valid) {
644 if(!entries++)
645 /* Flush cached alternatives for this source origin, if any - when
646 this is the first entry of the line. */
647 altsvc_flush(asi, srcalpnid, srchost, srcport);
648
649 as = altsvc_createid(srchost, dsthost,
650 srcalpnid, dstalpnid,
651 srcport, dstport);
652 if(as) {
653 /* The expires time also needs to take the Age: value (if any) into
654 account. [See RFC 7838 section 3.1] */
655 as->expires = maxage + time(NULL);
656 as->persist = persist;
657 Curl_llist_insert_next(&asi->list, asi->list.tail, as, node: &as->node);
658 infof(data, "Added alt-svc: %s:%d over %s", dsthost, dstport,
659 Curl_alpnid2str(dstalpnid));
660 }
661 }
662 }
663 else
664 break;
665 /* after the double quote there can be a comma if there's another
666 string or a semicolon if no more */
667 if(*p == ',') {
668 /* comma means another alternative is presented */
669 p++;
670 result = getalnum(ptr: &p, alpnbuf, buflen: sizeof(alpnbuf));
671 if(result)
672 break;
673 }
674 }
675 else
676 break;
677 } while(*p && (*p != ';') && (*p != '\n') && (*p != '\r'));
678
679 return CURLE_OK;
680}
681
682/*
683 * Return TRUE on a match
684 */
685bool Curl_altsvc_lookup(struct altsvcinfo *asi,
686 enum alpnid srcalpnid, const char *srchost,
687 int srcport,
688 struct altsvc **dstentry,
689 const int versions) /* one or more bits */
690{
691 struct Curl_llist_element *e;
692 struct Curl_llist_element *n;
693 time_t now = time(NULL);
694 DEBUGASSERT(asi);
695 DEBUGASSERT(srchost);
696 DEBUGASSERT(dstentry);
697
698 for(e = asi->list.head; e; e = n) {
699 struct altsvc *as = e->ptr;
700 n = e->next;
701 if(as->expires < now) {
702 /* an expired entry, remove */
703 Curl_llist_remove(&asi->list, e, NULL);
704 altsvc_free(as);
705 continue;
706 }
707 if((as->src.alpnid == srcalpnid) &&
708 hostcompare(host: srchost, check: as->src.host) &&
709 (as->src.port == srcport) &&
710 (versions & as->dst.alpnid)) {
711 /* match */
712 *dstentry = as;
713 return TRUE;
714 }
715 }
716 return FALSE;
717}
718
719#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_ALTSVC */
720