Annotation of embedaddon/curl/lib/altsvc.c, revision 1.1
1.1 ! misho 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>