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