1/***************************************************************************
2 * _ _ ____ _
3 * Project ___| | | | _ \| |
4 * / __| | | | |_) | |
5 * | (__| |_| | _ <| |___
6 * \___|\___/|_| \_\_____|
7 *
8 * Copyright (C) 2019, 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.haxx.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 Alt-Svc: header is defined in RFC 7838:
24 * https://tools.ietf.org/html/rfc7838
25 */
26#include "curl_setup.h"
27
28#if !defined(CURL_DISABLE_HTTP) && defined(USE_ALTSVC)
29#include <curl/curl.h>
30#include "urldata.h"
31#include "altsvc.h"
32#include "curl_get_line.h"
33#include "strcase.h"
34#include "parsedate.h"
35#include "sendf.h"
36#include "warnless.h"
37
38/* The last 3 #include files should be in this order */
39#include "curl_printf.h"
40#include "curl_memory.h"
41#include "memdebug.h"
42
43#define MAX_ALTSVC_LINE 4095
44#define MAX_ALTSVC_DATELENSTR "64"
45#define MAX_ALTSVC_DATELEN 64
46#define MAX_ALTSVC_HOSTLENSTR "512"
47#define MAX_ALTSVC_HOSTLEN 512
48#define MAX_ALTSVC_ALPNLENSTR "10"
49#define MAX_ALTSVC_ALPNLEN 10
50
51static enum alpnid alpn2alpnid(char *name)
52{
53 if(strcasecompare(name, "h1"))
54 return ALPN_h1;
55 if(strcasecompare(name, "h2"))
56 return ALPN_h2;
57#if (defined(USE_QUICHE) || defined(USE_NGTCP2)) && !defined(UNITTESTS)
58 if(strcasecompare(name, "h3-24"))
59 return ALPN_h3;
60#else
61 if(strcasecompare(name, "h3"))
62 return ALPN_h3;
63#endif
64 return ALPN_none; /* unknown, probably rubbish input */
65}
66
67/* Given the ALPN ID, return the name */
68const char *Curl_alpnid2str(enum alpnid id)
69{
70 switch(id) {
71 case ALPN_h1:
72 return "h1";
73 case ALPN_h2:
74 return "h2";
75 case ALPN_h3:
76#if (defined(USE_QUICHE) || defined(USE_NGTCP2)) && !defined(UNITTESTS)
77 return "h3-24";
78#else
79 return "h3";
80#endif
81 default:
82 return ""; /* bad */
83 }
84}
85
86
87static void altsvc_free(struct altsvc *as)
88{
89 free(as->src.host);
90 free(as->dst.host);
91 free(as);
92}
93
94static struct altsvc *altsvc_createid(const char *srchost,
95 const char *dsthost,
96 enum alpnid srcalpnid,
97 enum alpnid dstalpnid,
98 unsigned int srcport,
99 unsigned int dstport)
100{
101 struct altsvc *as = calloc(sizeof(struct altsvc), 1);
102 if(!as)
103 return NULL;
104
105 as->src.host = strdup(srchost);
106 if(!as->src.host)
107 goto error;
108 as->dst.host = strdup(dsthost);
109 if(!as->dst.host)
110 goto error;
111
112 as->src.alpnid = srcalpnid;
113 as->dst.alpnid = dstalpnid;
114 as->src.port = curlx_ultous(srcport);
115 as->dst.port = curlx_ultous(dstport);
116
117 return as;
118 error:
119 altsvc_free(as);
120 return NULL;
121}
122
123static struct altsvc *altsvc_create(char *srchost,
124 char *dsthost,
125 char *srcalpn,
126 char *dstalpn,
127 unsigned int srcport,
128 unsigned int dstport)
129{
130 enum alpnid dstalpnid = alpn2alpnid(dstalpn);
131 enum alpnid srcalpnid = alpn2alpnid(srcalpn);
132 if(!srcalpnid || !dstalpnid)
133 return NULL;
134 return altsvc_createid(srchost, dsthost, srcalpnid, dstalpnid,
135 srcport, dstport);
136}
137
138/* only returns SERIOUS errors */
139static CURLcode altsvc_add(struct altsvcinfo *asi, char *line)
140{
141 /* Example line:
142 h2 example.com 443 h3 shiny.example.com 8443 "20191231 10:00:00" 1
143 */
144 char srchost[MAX_ALTSVC_HOSTLEN + 1];
145 char dsthost[MAX_ALTSVC_HOSTLEN + 1];
146 char srcalpn[MAX_ALTSVC_ALPNLEN + 1];
147 char dstalpn[MAX_ALTSVC_ALPNLEN + 1];
148 char date[MAX_ALTSVC_DATELEN + 1];
149 unsigned int srcport;
150 unsigned int dstport;
151 unsigned int prio;
152 unsigned int persist;
153 int rc;
154
155 rc = sscanf(line,
156 "%" MAX_ALTSVC_ALPNLENSTR "s %" MAX_ALTSVC_HOSTLENSTR "s %u "
157 "%" MAX_ALTSVC_ALPNLENSTR "s %" MAX_ALTSVC_HOSTLENSTR "s %u "
158 "\"%" MAX_ALTSVC_DATELENSTR "[^\"]\" %u %u",
159 srcalpn, srchost, &srcport,
160 dstalpn, dsthost, &dstport,
161 date, &persist, &prio);
162 if(9 == rc) {
163 struct altsvc *as;
164 time_t expires = Curl_getdate_capped(date);
165 as = altsvc_create(srchost, dsthost, srcalpn, dstalpn, srcport, dstport);
166 if(as) {
167 as->expires = expires;
168 as->prio = prio;
169 as->persist = persist ? 1 : 0;
170 Curl_llist_insert_next(&asi->list, asi->list.tail, as, &as->node);
171 asi->num++; /* one more entry */
172 }
173 }
174
175 return CURLE_OK;
176}
177
178/*
179 * Load alt-svc entries from the given file. The text based line-oriented file
180 * format is documented here:
181 * https://github.com/curl/curl/wiki/QUIC-implementation
182 *
183 * This function only returns error on major problems that prevents alt-svc
184 * handling to work completely. It will ignore individual syntactical errors
185 * etc.
186 */
187static CURLcode altsvc_load(struct altsvcinfo *asi, const char *file)
188{
189 CURLcode result = CURLE_OK;
190 char *line = NULL;
191 FILE *fp = fopen(file, FOPEN_READTEXT);
192 if(fp) {
193 line = malloc(MAX_ALTSVC_LINE);
194 if(!line)
195 goto fail;
196 while(Curl_get_line(line, MAX_ALTSVC_LINE, fp)) {
197 char *lineptr = line;
198 while(*lineptr && ISBLANK(*lineptr))
199 lineptr++;
200 if(*lineptr == '#')
201 /* skip commented lines */
202 continue;
203
204 altsvc_add(asi, lineptr);
205 }
206 free(line); /* free the line buffer */
207 fclose(fp);
208 }
209 return result;
210
211 fail:
212 free(line);
213 fclose(fp);
214 return CURLE_OUT_OF_MEMORY;
215}
216
217/*
218 * Write this single altsvc entry to a single output line
219 */
220
221static CURLcode altsvc_out(struct altsvc *as, FILE *fp)
222{
223 struct tm stamp;
224 CURLcode result = Curl_gmtime(as->expires, &stamp);
225 if(result)
226 return result;
227
228 fprintf(fp,
229 "%s %s %u "
230 "%s %s %u "
231 "\"%d%02d%02d "
232 "%02d:%02d:%02d\" "
233 "%u %d\n",
234 Curl_alpnid2str(as->src.alpnid), as->src.host, as->src.port,
235 Curl_alpnid2str(as->dst.alpnid), as->dst.host, as->dst.port,
236 stamp.tm_year + 1900, stamp.tm_mon + 1, stamp.tm_mday,
237 stamp.tm_hour, stamp.tm_min, stamp.tm_sec,
238 as->persist, as->prio);
239 return CURLE_OK;
240}
241
242/* ---- library-wide functions below ---- */
243
244/*
245 * Curl_altsvc_init() creates a new altsvc cache.
246 * It returns the new instance or NULL if something goes wrong.
247 */
248struct altsvcinfo *Curl_altsvc_init(void)
249{
250 struct altsvcinfo *asi = calloc(sizeof(struct altsvcinfo), 1);
251 if(!asi)
252 return NULL;
253 Curl_llist_init(&asi->list, NULL);
254
255 /* set default behavior */
256 asi->flags = CURLALTSVC_H1
257#ifdef USE_NGHTTP2
258 | CURLALTSVC_H2
259#endif
260#ifdef ENABLE_QUIC
261 | CURLALTSVC_H3
262#endif
263 ;
264 return asi;
265}
266
267/*
268 * Curl_altsvc_load() loads alt-svc from file.
269 */
270CURLcode Curl_altsvc_load(struct altsvcinfo *asi, const char *file)
271{
272 CURLcode result;
273 DEBUGASSERT(asi);
274 result = altsvc_load(asi, file);
275 return result;
276}
277
278/*
279 * Curl_altsvc_ctrl() passes on the external bitmask.
280 */
281CURLcode Curl_altsvc_ctrl(struct altsvcinfo *asi, const long ctrl)
282{
283 DEBUGASSERT(asi);
284 if(!ctrl)
285 /* unexpected */
286 return CURLE_BAD_FUNCTION_ARGUMENT;
287 asi->flags = ctrl;
288 return CURLE_OK;
289}
290
291/*
292 * Curl_altsvc_cleanup() frees an altsvc cache instance and all associated
293 * resources.
294 */
295void Curl_altsvc_cleanup(struct altsvcinfo *altsvc)
296{
297 struct curl_llist_element *e;
298 struct curl_llist_element *n;
299 if(altsvc) {
300 for(e = altsvc->list.head; e; e = n) {
301 struct altsvc *as = e->ptr;
302 n = e->next;
303 altsvc_free(as);
304 }
305 free(altsvc);
306 }
307}
308
309/*
310 * Curl_altsvc_save() writes the altsvc cache to a file.
311 */
312CURLcode Curl_altsvc_save(struct altsvcinfo *altsvc, const char *file)
313{
314 struct curl_llist_element *e;
315 struct curl_llist_element *n;
316 CURLcode result = CURLE_OK;
317 FILE *out;
318
319 if(!altsvc)
320 /* no cache activated */
321 return CURLE_OK;
322
323 if((altsvc->flags & CURLALTSVC_READONLYFILE) || !file[0])
324 /* marked as read-only or zero length file name */
325 return CURLE_OK;
326 out = fopen(file, FOPEN_WRITETEXT);
327 if(!out)
328 return CURLE_WRITE_ERROR;
329 fputs("# Your alt-svc cache. https://curl.haxx.se/docs/alt-svc.html\n"
330 "# This file was generated by libcurl! Edit at your own risk.\n",
331 out);
332 for(e = altsvc->list.head; e; e = n) {
333 struct altsvc *as = e->ptr;
334 n = e->next;
335 result = altsvc_out(as, out);
336 if(result)
337 break;
338 }
339 fclose(out);
340 return result;
341}
342
343static CURLcode getalnum(const char **ptr, char *alpnbuf, size_t buflen)
344{
345 size_t len;
346 const char *protop;
347 const char *p = *ptr;
348 while(*p && ISBLANK(*p))
349 p++;
350 protop = p;
351 while(*p && !ISBLANK(*p) && (*p != ';') && (*p != '='))
352 p++;
353 len = p - protop;
354
355 if(!len || (len >= buflen))
356 return CURLE_BAD_FUNCTION_ARGUMENT;
357 memcpy(alpnbuf, protop, len);
358 alpnbuf[len] = 0;
359 *ptr = p;
360 return CURLE_OK;
361}
362
363/* altsvc_flush() removes all alternatives for this source origin from the
364 list */
365static void altsvc_flush(struct altsvcinfo *asi, enum alpnid srcalpnid,
366 const char *srchost, unsigned short srcport)
367{
368 struct curl_llist_element *e;
369 struct curl_llist_element *n;
370 for(e = asi->list.head; e; e = n) {
371 struct altsvc *as = e->ptr;
372 n = e->next;
373 if((srcalpnid == as->src.alpnid) &&
374 (srcport == as->src.port) &&
375 strcasecompare(srchost, as->src.host)) {
376 Curl_llist_remove(&asi->list, e, NULL);
377 altsvc_free(as);
378 asi->num--;
379 }
380 }
381}
382
383#ifdef DEBUGBUILD
384/* to play well with debug builds, we can *set* a fixed time this will
385 return */
386static time_t debugtime(void *unused)
387{
388 char *timestr = getenv("CURL_TIME");
389 (void)unused;
390 if(timestr) {
391 unsigned long val = strtol(timestr, NULL, 10);
392 return (time_t)val;
393 }
394 return time(NULL);
395}
396#define time(x) debugtime(x)
397#endif
398
399/*
400 * Curl_altsvc_parse() takes an incoming alt-svc response header and stores
401 * the data correctly in the cache.
402 *
403 * 'value' points to the header *value*. That's contents to the right of the
404 * header name.
405 */
406CURLcode Curl_altsvc_parse(struct Curl_easy *data,
407 struct altsvcinfo *asi, const char *value,
408 enum alpnid srcalpnid, const char *srchost,
409 unsigned short srcport)
410{
411 const char *p = value;
412 size_t len;
413 enum alpnid dstalpnid = srcalpnid; /* the same by default */
414 char namebuf[MAX_ALTSVC_HOSTLEN] = "";
415 char alpnbuf[MAX_ALTSVC_ALPNLEN] = "";
416 struct altsvc *as;
417 unsigned short dstport = srcport; /* the same by default */
418 const char *semip;
419 time_t maxage = 24 * 3600; /* default is 24 hours */
420 bool persist = FALSE;
421 CURLcode result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
422 if(result)
423 return result;
424
425 DEBUGASSERT(asi);
426
427 /* Flush all cached alternatives for this source origin, if any */
428 altsvc_flush(asi, srcalpnid, srchost, srcport);
429
430 /* "clear" is a magic keyword */
431 if(strcasecompare(alpnbuf, "clear")) {
432 return CURLE_OK;
433 }
434
435 /* The 'ma' and 'persist' flags are annoyingly meant for all alternatives
436 but are set after the list on the line. Scan for the semicolons and get
437 those fields first! */
438 semip = p;
439 do {
440 semip = strchr(semip, ';');
441 if(semip) {
442 char option[32];
443 unsigned long num;
444 char *end_ptr;
445 bool quoted = FALSE;
446 semip++; /* pass the semicolon */
447 result = getalnum(&semip, option, sizeof(option));
448 if(result)
449 break;
450 while(*semip && ISBLANK(*semip))
451 semip++;
452 if(*semip != '=')
453 continue;
454 semip++;
455 while(*semip && ISBLANK(*semip))
456 semip++;
457 if(*semip == '\"') {
458 /* quoted value */
459 semip++;
460 quoted = TRUE;
461 }
462 num = strtoul(semip, &end_ptr, 10);
463 if((end_ptr != semip) && num && (num < ULONG_MAX)) {
464 if(strcasecompare("ma", option))
465 maxage = num;
466 else if(strcasecompare("persist", option) && (num == 1))
467 persist = TRUE;
468 if(quoted && (*end_ptr == '\"'))
469 end_ptr++;
470 }
471 semip = end_ptr;
472 }
473 } while(semip);
474
475 do {
476 if(*p == '=') {
477 /* [protocol]="[host][:port]" */
478 dstalpnid = alpn2alpnid(alpnbuf);
479 if(!dstalpnid) {
480 infof(data, "Unknown alt-svc protocol \"%s\", ignoring...\n", alpnbuf);
481 return CURLE_OK;
482 }
483 p++;
484 if(*p == '\"') {
485 const char *dsthost;
486 p++;
487 if(*p != ':') {
488 /* host name starts here */
489 const char *hostp = p;
490 while(*p && (ISALNUM(*p) || (*p == '.') || (*p == '-')))
491 p++;
492 len = p - hostp;
493 if(!len || (len >= MAX_ALTSVC_HOSTLEN))
494 return CURLE_BAD_FUNCTION_ARGUMENT;
495 memcpy(namebuf, hostp, len);
496 namebuf[len] = 0;
497 dsthost = namebuf;
498 }
499 else {
500 /* no destination name, use source host */
501 dsthost = srchost;
502 }
503 if(*p == ':') {
504 /* a port number */
505 char *end_ptr;
506 unsigned long port = strtoul(++p, &end_ptr, 10);
507 if(port > USHRT_MAX || end_ptr == p || *end_ptr != '\"') {
508 infof(data, "Unknown alt-svc port number, ignoring...\n");
509 return CURLE_OK;
510 }
511 p = end_ptr;
512 dstport = curlx_ultous(port);
513 }
514 if(*p++ != '\"')
515 return CURLE_BAD_FUNCTION_ARGUMENT;
516 as = altsvc_createid(srchost, dsthost,
517 srcalpnid, dstalpnid,
518 srcport, dstport);
519 if(as) {
520 /* The expires time also needs to take the Age: value (if any) into
521 account. [See RFC 7838 section 3.1] */
522 as->expires = maxage + time(NULL);
523 as->persist = persist;
524 Curl_llist_insert_next(&asi->list, asi->list.tail, as, &as->node);
525 asi->num++; /* one more entry */
526 infof(data, "Added alt-svc: %s:%d over %s\n", dsthost, dstport,
527 Curl_alpnid2str(dstalpnid));
528 }
529 }
530 /* after the double quote there can be a comma if there's another
531 string or a semicolon if no more */
532 if(*p == ',') {
533 /* comma means another alternative is presented */
534 p++;
535 result = getalnum(&p, alpnbuf, sizeof(alpnbuf));
536 if(result)
537 /* failed to parse, but since we already did at least one host we
538 return OK */
539 return CURLE_OK;
540 }
541 }
542 } while(*p && (*p != ';') && (*p != '\n') && (*p != '\r'));
543
544 return CURLE_OK;
545}
546
547/*
548 * Return TRUE on a match
549 */
550bool Curl_altsvc_lookup(struct altsvcinfo *asi,
551 enum alpnid srcalpnid, const char *srchost,
552 int srcport,
553 struct altsvc **dstentry,
554 const int versions) /* one or more bits */
555{
556 struct curl_llist_element *e;
557 struct curl_llist_element *n;
558 time_t now = time(NULL);
559 DEBUGASSERT(asi);
560 DEBUGASSERT(srchost);
561 DEBUGASSERT(dstentry);
562
563 for(e = asi->list.head; e; e = n) {
564 struct altsvc *as = e->ptr;
565 n = e->next;
566 if(as->expires < now) {
567 /* an expired entry, remove */
568 Curl_llist_remove(&asi->list, e, NULL);
569 altsvc_free(as);
570 continue;
571 }
572 if((as->src.alpnid == srcalpnid) &&
573 strcasecompare(as->src.host, srchost) &&
574 (as->src.port == srcport) &&
575 (versions & as->dst.alpnid)) {
576 /* match */
577 *dstentry = as;
578 return TRUE;
579 }
580 }
581 return FALSE;
582}
583
584#endif /* CURL_DISABLE_HTTP || USE_ALTSVC */
585