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