File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / curl / lib / altsvc.c
Revision 1.1.1.1 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Wed Jun 3 10:01:15 2020 UTC (4 years, 1 month ago) by misho
Branches: curl, MAIN
CVS tags: v7_70_0p4, HEAD
curl

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

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>