Annotation of embedaddon/lighttpd/src/mod_trigger_b4_dl.c, revision 1.1
1.1 ! misho 1: #include "base.h"
! 2: #include "log.h"
! 3: #include "buffer.h"
! 4:
! 5: #include "plugin.h"
! 6: #include "response.h"
! 7: #include "inet_ntop_cache.h"
! 8:
! 9: #include <ctype.h>
! 10: #include <stdlib.h>
! 11: #include <fcntl.h>
! 12: #include <string.h>
! 13:
! 14: #if defined(HAVE_GDBM_H)
! 15: # include <gdbm.h>
! 16: #endif
! 17:
! 18: #if defined(HAVE_PCRE_H)
! 19: # include <pcre.h>
! 20: #endif
! 21:
! 22: #if defined(HAVE_MEMCACHE_H)
! 23: # include <memcache.h>
! 24: #endif
! 25:
! 26: /**
! 27: * this is a trigger_b4_dl for a lighttpd plugin
! 28: *
! 29: */
! 30:
! 31: /* plugin config for all request/connections */
! 32:
! 33: typedef struct {
! 34: buffer *db_filename;
! 35:
! 36: buffer *trigger_url;
! 37: buffer *download_url;
! 38: buffer *deny_url;
! 39:
! 40: array *mc_hosts;
! 41: buffer *mc_namespace;
! 42: #if defined(HAVE_PCRE_H)
! 43: pcre *trigger_regex;
! 44: pcre *download_regex;
! 45: #endif
! 46: #if defined(HAVE_GDBM_H)
! 47: GDBM_FILE db;
! 48: #endif
! 49:
! 50: #if defined(HAVE_MEMCACHE_H)
! 51: struct memcache *mc;
! 52: #endif
! 53:
! 54: unsigned short trigger_timeout;
! 55: unsigned short debug;
! 56: } plugin_config;
! 57:
! 58: typedef struct {
! 59: PLUGIN_DATA;
! 60:
! 61: buffer *tmp_buf;
! 62:
! 63: plugin_config **config_storage;
! 64:
! 65: plugin_config conf;
! 66: } plugin_data;
! 67:
! 68: /* init the plugin data */
! 69: INIT_FUNC(mod_trigger_b4_dl_init) {
! 70: plugin_data *p;
! 71:
! 72: p = calloc(1, sizeof(*p));
! 73:
! 74: p->tmp_buf = buffer_init();
! 75:
! 76: return p;
! 77: }
! 78:
! 79: /* detroy the plugin data */
! 80: FREE_FUNC(mod_trigger_b4_dl_free) {
! 81: plugin_data *p = p_d;
! 82:
! 83: UNUSED(srv);
! 84:
! 85: if (!p) return HANDLER_GO_ON;
! 86:
! 87: if (p->config_storage) {
! 88: size_t i;
! 89: for (i = 0; i < srv->config_context->used; i++) {
! 90: plugin_config *s = p->config_storage[i];
! 91:
! 92: if (!s) continue;
! 93:
! 94: buffer_free(s->db_filename);
! 95: buffer_free(s->download_url);
! 96: buffer_free(s->trigger_url);
! 97: buffer_free(s->deny_url);
! 98:
! 99: buffer_free(s->mc_namespace);
! 100: array_free(s->mc_hosts);
! 101:
! 102: #if defined(HAVE_PCRE_H)
! 103: if (s->trigger_regex) pcre_free(s->trigger_regex);
! 104: if (s->download_regex) pcre_free(s->download_regex);
! 105: #endif
! 106: #if defined(HAVE_GDBM_H)
! 107: if (s->db) gdbm_close(s->db);
! 108: #endif
! 109: #if defined(HAVE_MEMCACHE_H)
! 110: if (s->mc) mc_free(s->mc);
! 111: #endif
! 112:
! 113: free(s);
! 114: }
! 115: free(p->config_storage);
! 116: }
! 117:
! 118: buffer_free(p->tmp_buf);
! 119:
! 120: free(p);
! 121:
! 122: return HANDLER_GO_ON;
! 123: }
! 124:
! 125: /* handle plugin config and check values */
! 126:
! 127: SETDEFAULTS_FUNC(mod_trigger_b4_dl_set_defaults) {
! 128: plugin_data *p = p_d;
! 129: size_t i = 0;
! 130:
! 131:
! 132: config_values_t cv[] = {
! 133: { "trigger-before-download.gdbm-filename", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
! 134: { "trigger-before-download.trigger-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
! 135: { "trigger-before-download.download-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
! 136: { "trigger-before-download.deny-url", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
! 137: { "trigger-before-download.trigger-timeout", NULL, T_CONFIG_SHORT, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
! 138: { "trigger-before-download.memcache-hosts", NULL, T_CONFIG_ARRAY, T_CONFIG_SCOPE_CONNECTION }, /* 5 */
! 139: { "trigger-before-download.memcache-namespace", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 6 */
! 140: { "trigger-before-download.debug", NULL, T_CONFIG_BOOLEAN, T_CONFIG_SCOPE_CONNECTION }, /* 7 */
! 141: { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
! 142: };
! 143:
! 144: if (!p) return HANDLER_ERROR;
! 145:
! 146: p->config_storage = calloc(1, srv->config_context->used * sizeof(specific_config *));
! 147:
! 148: for (i = 0; i < srv->config_context->used; i++) {
! 149: plugin_config *s;
! 150: #if defined(HAVE_PCRE_H)
! 151: const char *errptr;
! 152: int erroff;
! 153: #endif
! 154:
! 155: s = calloc(1, sizeof(plugin_config));
! 156: s->db_filename = buffer_init();
! 157: s->download_url = buffer_init();
! 158: s->trigger_url = buffer_init();
! 159: s->deny_url = buffer_init();
! 160: s->mc_hosts = array_init();
! 161: s->mc_namespace = buffer_init();
! 162:
! 163: cv[0].destination = s->db_filename;
! 164: cv[1].destination = s->trigger_url;
! 165: cv[2].destination = s->download_url;
! 166: cv[3].destination = s->deny_url;
! 167: cv[4].destination = &(s->trigger_timeout);
! 168: cv[5].destination = s->mc_hosts;
! 169: cv[6].destination = s->mc_namespace;
! 170: cv[7].destination = &(s->debug);
! 171:
! 172: p->config_storage[i] = s;
! 173:
! 174: if (0 != config_insert_values_global(srv, ((data_config *)srv->config_context->data[i])->value, cv)) {
! 175: return HANDLER_ERROR;
! 176: }
! 177: #if defined(HAVE_GDBM_H)
! 178: if (!buffer_is_empty(s->db_filename)) {
! 179: if (NULL == (s->db = gdbm_open(s->db_filename->ptr, 4096, GDBM_WRCREAT | GDBM_NOLOCK, S_IRUSR | S_IWUSR, 0))) {
! 180: log_error_write(srv, __FILE__, __LINE__, "s",
! 181: "gdbm-open failed");
! 182: return HANDLER_ERROR;
! 183: }
! 184: #ifdef FD_CLOEXEC
! 185: fcntl(gdbm_fdesc(s->db), F_SETFD, FD_CLOEXEC);
! 186: #endif
! 187: }
! 188: #endif
! 189: #if defined(HAVE_PCRE_H)
! 190: if (!buffer_is_empty(s->download_url)) {
! 191: if (NULL == (s->download_regex = pcre_compile(s->download_url->ptr,
! 192: 0, &errptr, &erroff, NULL))) {
! 193:
! 194: log_error_write(srv, __FILE__, __LINE__, "sbss",
! 195: "compiling regex for download-url failed:",
! 196: s->download_url, "pos:", erroff);
! 197: return HANDLER_ERROR;
! 198: }
! 199: }
! 200:
! 201: if (!buffer_is_empty(s->trigger_url)) {
! 202: if (NULL == (s->trigger_regex = pcre_compile(s->trigger_url->ptr,
! 203: 0, &errptr, &erroff, NULL))) {
! 204:
! 205: log_error_write(srv, __FILE__, __LINE__, "sbss",
! 206: "compiling regex for trigger-url failed:",
! 207: s->trigger_url, "pos:", erroff);
! 208:
! 209: return HANDLER_ERROR;
! 210: }
! 211: }
! 212: #endif
! 213:
! 214: if (s->mc_hosts->used) {
! 215: #if defined(HAVE_MEMCACHE_H)
! 216: size_t k;
! 217: s->mc = mc_new();
! 218:
! 219: for (k = 0; k < s->mc_hosts->used; k++) {
! 220: data_string *ds = (data_string *)s->mc_hosts->data[k];
! 221:
! 222: if (0 != mc_server_add4(s->mc, ds->value->ptr)) {
! 223: log_error_write(srv, __FILE__, __LINE__, "sb",
! 224: "connection to host failed:",
! 225: ds->value);
! 226:
! 227: return HANDLER_ERROR;
! 228: }
! 229: }
! 230: #else
! 231: log_error_write(srv, __FILE__, __LINE__, "s",
! 232: "memcache support is not compiled in but trigger-before-download.memcache-hosts is set, aborting");
! 233: return HANDLER_ERROR;
! 234: #endif
! 235: }
! 236:
! 237:
! 238: #if (!defined(HAVE_GDBM_H) && !defined(HAVE_MEMCACHE_H)) || !defined(HAVE_PCRE_H)
! 239: log_error_write(srv, __FILE__, __LINE__, "s",
! 240: "(either gdbm or libmemcache) and pcre are require, but were not found, aborting");
! 241: return HANDLER_ERROR;
! 242: #endif
! 243: }
! 244:
! 245: return HANDLER_GO_ON;
! 246: }
! 247:
! 248: #define PATCH(x) \
! 249: p->conf.x = s->x;
! 250: static int mod_trigger_b4_dl_patch_connection(server *srv, connection *con, plugin_data *p) {
! 251: size_t i, j;
! 252: plugin_config *s = p->config_storage[0];
! 253:
! 254: #if defined(HAVE_GDBM)
! 255: PATCH(db);
! 256: #endif
! 257: #if defined(HAVE_PCRE_H)
! 258: PATCH(download_regex);
! 259: PATCH(trigger_regex);
! 260: #endif
! 261: PATCH(trigger_timeout);
! 262: PATCH(deny_url);
! 263: PATCH(mc_namespace);
! 264: PATCH(debug);
! 265: #if defined(HAVE_MEMCACHE_H)
! 266: PATCH(mc);
! 267: #endif
! 268:
! 269: /* skip the first, the global context */
! 270: for (i = 1; i < srv->config_context->used; i++) {
! 271: data_config *dc = (data_config *)srv->config_context->data[i];
! 272: s = p->config_storage[i];
! 273:
! 274: /* condition didn't match */
! 275: if (!config_check_cond(srv, con, dc)) continue;
! 276:
! 277: /* merge config */
! 278: for (j = 0; j < dc->value->used; j++) {
! 279: data_unset *du = dc->value->data[j];
! 280:
! 281: if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.download-url"))) {
! 282: #if defined(HAVE_PCRE_H)
! 283: PATCH(download_regex);
! 284: #endif
! 285: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-url"))) {
! 286: # if defined(HAVE_PCRE_H)
! 287: PATCH(trigger_regex);
! 288: # endif
! 289: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.gdbm-filename"))) {
! 290: #if defined(HAVE_GDBM_H)
! 291: PATCH(db);
! 292: #endif
! 293: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.trigger-timeout"))) {
! 294: PATCH(trigger_timeout);
! 295: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.debug"))) {
! 296: PATCH(debug);
! 297: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.deny-url"))) {
! 298: PATCH(deny_url);
! 299: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-namespace"))) {
! 300: PATCH(mc_namespace);
! 301: } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("trigger-before-download.memcache-hosts"))) {
! 302: #if defined(HAVE_MEMCACHE_H)
! 303: PATCH(mc);
! 304: #endif
! 305: }
! 306: }
! 307: }
! 308:
! 309: return 0;
! 310: }
! 311: #undef PATCH
! 312:
! 313: URIHANDLER_FUNC(mod_trigger_b4_dl_uri_handler) {
! 314: plugin_data *p = p_d;
! 315: const char *remote_ip;
! 316: data_string *ds;
! 317:
! 318: #if defined(HAVE_PCRE_H)
! 319: int n;
! 320: # define N 10
! 321: int ovec[N * 3];
! 322:
! 323: if (con->mode != DIRECT) return HANDLER_GO_ON;
! 324:
! 325: if (con->uri.path->used == 0) return HANDLER_GO_ON;
! 326:
! 327: mod_trigger_b4_dl_patch_connection(srv, con, p);
! 328:
! 329: if (!p->conf.trigger_regex || !p->conf.download_regex) return HANDLER_GO_ON;
! 330:
! 331: # if !defined(HAVE_GDBM_H) && !defined(HAVE_MEMCACHE_H)
! 332: return HANDLER_GO_ON;
! 333: # elif defined(HAVE_GDBM_H) && defined(HAVE_MEMCACHE_H)
! 334: if (!p->conf.db && !p->conf.mc) return HANDLER_GO_ON;
! 335: if (p->conf.db && p->conf.mc) {
! 336: /* can't decide which one */
! 337:
! 338: return HANDLER_GO_ON;
! 339: }
! 340: # elif defined(HAVE_GDBM_H)
! 341: if (!p->conf.db) return HANDLER_GO_ON;
! 342: # else
! 343: if (!p->conf.mc) return HANDLER_GO_ON;
! 344: # endif
! 345:
! 346: if (NULL != (ds = (data_string *)array_get_element(con->request.headers, "X-Forwarded-For"))) {
! 347: /* X-Forwarded-For contains the ip behind the proxy */
! 348:
! 349: remote_ip = ds->value->ptr;
! 350:
! 351: /* memcache can't handle spaces */
! 352: } else {
! 353: remote_ip = inet_ntop_cache_get_ip(srv, &(con->dst_addr));
! 354: }
! 355:
! 356: if (p->conf.debug) {
! 357: log_error_write(srv, __FILE__, __LINE__, "ss", "(debug) remote-ip:", remote_ip);
! 358: }
! 359:
! 360: /* check if URL is a trigger -> insert IP into DB */
! 361: if ((n = pcre_exec(p->conf.trigger_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) {
! 362: if (n != PCRE_ERROR_NOMATCH) {
! 363: log_error_write(srv, __FILE__, __LINE__, "sd",
! 364: "execution error while matching:", n);
! 365:
! 366: return HANDLER_ERROR;
! 367: }
! 368: } else {
! 369: # if defined(HAVE_GDBM_H)
! 370: if (p->conf.db) {
! 371: /* the trigger matched */
! 372: datum key, val;
! 373:
! 374: key.dptr = (char *)remote_ip;
! 375: key.dsize = strlen(remote_ip);
! 376:
! 377: val.dptr = (char *)&(srv->cur_ts);
! 378: val.dsize = sizeof(srv->cur_ts);
! 379:
! 380: if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
! 381: log_error_write(srv, __FILE__, __LINE__, "s",
! 382: "insert failed");
! 383: }
! 384: }
! 385: # endif
! 386: # if defined(HAVE_MEMCACHE_H)
! 387: if (p->conf.mc) {
! 388: size_t i;
! 389: buffer_copy_string_buffer(p->tmp_buf, p->conf.mc_namespace);
! 390: buffer_append_string(p->tmp_buf, remote_ip);
! 391:
! 392: for (i = 0; i < p->tmp_buf->used - 1; i++) {
! 393: if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
! 394: }
! 395:
! 396: if (p->conf.debug) {
! 397: log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) triggered IP:", p->tmp_buf);
! 398: }
! 399:
! 400: if (0 != mc_set(p->conf.mc,
! 401: CONST_BUF_LEN(p->tmp_buf),
! 402: (char *)&(srv->cur_ts), sizeof(srv->cur_ts),
! 403: p->conf.trigger_timeout, 0)) {
! 404: log_error_write(srv, __FILE__, __LINE__, "s",
! 405: "insert failed");
! 406: }
! 407: }
! 408: # endif
! 409: }
! 410:
! 411: /* check if URL is a download -> check IP in DB, update timestamp */
! 412: if ((n = pcre_exec(p->conf.download_regex, NULL, con->uri.path->ptr, con->uri.path->used - 1, 0, 0, ovec, 3 * N)) < 0) {
! 413: if (n != PCRE_ERROR_NOMATCH) {
! 414: log_error_write(srv, __FILE__, __LINE__, "sd",
! 415: "execution error while matching: ", n);
! 416: return HANDLER_ERROR;
! 417: }
! 418: } else {
! 419: /* the download uri matched */
! 420: # if defined(HAVE_GDBM_H)
! 421: if (p->conf.db) {
! 422: datum key, val;
! 423: time_t last_hit;
! 424:
! 425: key.dptr = (char *)remote_ip;
! 426: key.dsize = strlen(remote_ip);
! 427:
! 428: val = gdbm_fetch(p->conf.db, key);
! 429:
! 430: if (val.dptr == NULL) {
! 431: /* not found, redirect */
! 432:
! 433: response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
! 434: con->http_status = 307;
! 435: con->file_finished = 1;
! 436:
! 437: return HANDLER_FINISHED;
! 438: }
! 439:
! 440: memcpy(&last_hit, val.dptr, sizeof(time_t));
! 441:
! 442: free(val.dptr);
! 443:
! 444: if (srv->cur_ts - last_hit > p->conf.trigger_timeout) {
! 445: /* found, but timeout, redirect */
! 446:
! 447: response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
! 448: con->http_status = 307;
! 449: con->file_finished = 1;
! 450:
! 451: if (p->conf.db) {
! 452: if (0 != gdbm_delete(p->conf.db, key)) {
! 453: log_error_write(srv, __FILE__, __LINE__, "s",
! 454: "delete failed");
! 455: }
! 456: }
! 457:
! 458: return HANDLER_FINISHED;
! 459: }
! 460:
! 461: val.dptr = (char *)&(srv->cur_ts);
! 462: val.dsize = sizeof(srv->cur_ts);
! 463:
! 464: if (0 != gdbm_store(p->conf.db, key, val, GDBM_REPLACE)) {
! 465: log_error_write(srv, __FILE__, __LINE__, "s",
! 466: "insert failed");
! 467: }
! 468: }
! 469: # endif
! 470:
! 471: # if defined(HAVE_MEMCACHE_H)
! 472: if (p->conf.mc) {
! 473: void *r;
! 474: size_t i;
! 475:
! 476: buffer_copy_string_buffer(p->tmp_buf, p->conf.mc_namespace);
! 477: buffer_append_string(p->tmp_buf, remote_ip);
! 478:
! 479: for (i = 0; i < p->tmp_buf->used - 1; i++) {
! 480: if (p->tmp_buf->ptr[i] == ' ') p->tmp_buf->ptr[i] = '-';
! 481: }
! 482:
! 483: if (p->conf.debug) {
! 484: log_error_write(srv, __FILE__, __LINE__, "sb", "(debug) checking IP:", p->tmp_buf);
! 485: }
! 486:
! 487: /**
! 488: *
! 489: * memcached is do expiration for us, as long as we can fetch it every thing is ok
! 490: * and the timestamp is updated
! 491: *
! 492: */
! 493: if (NULL == (r = mc_aget(p->conf.mc,
! 494: CONST_BUF_LEN(p->tmp_buf)
! 495: ))) {
! 496:
! 497: response_header_insert(srv, con, CONST_STR_LEN("Location"), CONST_BUF_LEN(p->conf.deny_url));
! 498:
! 499: con->http_status = 307;
! 500: con->file_finished = 1;
! 501:
! 502: return HANDLER_FINISHED;
! 503: }
! 504:
! 505: free(r);
! 506:
! 507: /* set a new timeout */
! 508: if (0 != mc_set(p->conf.mc,
! 509: CONST_BUF_LEN(p->tmp_buf),
! 510: (char *)&(srv->cur_ts), sizeof(srv->cur_ts),
! 511: p->conf.trigger_timeout, 0)) {
! 512: log_error_write(srv, __FILE__, __LINE__, "s",
! 513: "insert failed");
! 514: }
! 515: }
! 516: # endif
! 517: }
! 518:
! 519: #else
! 520: UNUSED(srv);
! 521: UNUSED(con);
! 522: UNUSED(p_d);
! 523: #endif
! 524:
! 525: return HANDLER_GO_ON;
! 526: }
! 527:
! 528: #if defined(HAVE_GDBM_H)
! 529: TRIGGER_FUNC(mod_trigger_b4_dl_handle_trigger) {
! 530: plugin_data *p = p_d;
! 531: size_t i;
! 532:
! 533: /* check DB each minute */
! 534: if (srv->cur_ts % 60 != 0) return HANDLER_GO_ON;
! 535:
! 536: /* cleanup */
! 537: for (i = 0; i < srv->config_context->used; i++) {
! 538: plugin_config *s = p->config_storage[i];
! 539: datum key, val, okey;
! 540:
! 541: if (!s->db) continue;
! 542:
! 543: okey.dptr = NULL;
! 544:
! 545: /* according to the manual this loop + delete does delete all entries on its way
! 546: *
! 547: * we don't care as the next round will remove them. We don't have to perfect here.
! 548: */
! 549: for (key = gdbm_firstkey(s->db); key.dptr; key = gdbm_nextkey(s->db, okey)) {
! 550: time_t last_hit;
! 551: if (okey.dptr) {
! 552: free(okey.dptr);
! 553: okey.dptr = NULL;
! 554: }
! 555:
! 556: val = gdbm_fetch(s->db, key);
! 557:
! 558: memcpy(&last_hit, val.dptr, sizeof(time_t));
! 559:
! 560: free(val.dptr);
! 561:
! 562: if (srv->cur_ts - last_hit > s->trigger_timeout) {
! 563: gdbm_delete(s->db, key);
! 564: }
! 565:
! 566: okey = key;
! 567: }
! 568: if (okey.dptr) free(okey.dptr);
! 569:
! 570: /* reorg once a day */
! 571: if ((srv->cur_ts % (60 * 60 * 24) != 0)) gdbm_reorganize(s->db);
! 572: }
! 573: return HANDLER_GO_ON;
! 574: }
! 575: #endif
! 576:
! 577: /* this function is called at dlopen() time and inits the callbacks */
! 578:
! 579: int mod_trigger_b4_dl_plugin_init(plugin *p);
! 580: int mod_trigger_b4_dl_plugin_init(plugin *p) {
! 581: p->version = LIGHTTPD_VERSION_ID;
! 582: p->name = buffer_init_string("trigger_b4_dl");
! 583:
! 584: p->init = mod_trigger_b4_dl_init;
! 585: p->handle_uri_clean = mod_trigger_b4_dl_uri_handler;
! 586: p->set_defaults = mod_trigger_b4_dl_set_defaults;
! 587: #if defined(HAVE_GDBM_H)
! 588: p->handle_trigger = mod_trigger_b4_dl_handle_trigger;
! 589: #endif
! 590: p->cleanup = mod_trigger_b4_dl_free;
! 591:
! 592: p->data = NULL;
! 593:
! 594: return 0;
! 595: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>