File:  [ELWIX - Embedded LightWeight unIX -] / libaitwww / src / aitwww.c
Revision 1.6: download - view: text, annotated - select for diffs - revision graph
Wed Sep 14 15:12:22 2016 UTC (7 years, 8 months ago) by misho
Branches: MAIN
CVS tags: www3_4, WWW3_3, HEAD
version 3.3

    1: /*************************************************************************
    2: * (C) 2012 AITNET ltd - Sofia/Bulgaria - <misho@aitnet.org>
    3: *  by Michael Pounov <misho@elwix.org>
    4: *
    5: * $Author: misho $
    6: * $Id: aitwww.c,v 1.6 2016/09/14 15:12:22 misho Exp $
    7: *
    8: **************************************************************************
    9: The ELWIX and AITNET software is distributed under the following
   10: terms:
   11: 
   12: All of the documentation and software included in the ELWIX and AITNET
   13: Releases is copyrighted by ELWIX - Sofia/Bulgaria <info@elwix.org>
   14: 
   15: Copyright 2004 - 2016
   16: 	by Michael Pounov <misho@elwix.org>.  All rights reserved.
   17: 
   18: Redistribution and use in source and binary forms, with or without
   19: modification, are permitted provided that the following conditions
   20: are met:
   21: 1. Redistributions of source code must retain the above copyright
   22:    notice, this list of conditions and the following disclaimer.
   23: 2. Redistributions in binary form must reproduce the above copyright
   24:    notice, this list of conditions and the following disclaimer in the
   25:    documentation and/or other materials provided with the distribution.
   26: 3. All advertising materials mentioning features or use of this software
   27:    must display the following acknowledgement:
   28: This product includes software developed by Michael Pounov <misho@elwix.org>
   29: ELWIX - Embedded LightWeight unIX and its contributors.
   30: 4. Neither the name of AITNET nor the names of its contributors
   31:    may be used to endorse or promote products derived from this software
   32:    without specific prior written permission.
   33: 
   34: THIS SOFTWARE IS PROVIDED BY AITNET AND CONTRIBUTORS ``AS IS'' AND
   35: ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   36: IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   37: ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
   38: FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   39: DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
   40: OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
   41: HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
   42: LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
   43: OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   44: SUCH DAMAGE.
   45: */
   46: #include "global.h"
   47: #include "mime.h"
   48: 
   49: 
   50: #pragma GCC visibility push(hidden)
   51: 
   52: int www_Errno;
   53: char www_Error[STRSIZ];
   54: 
   55: #pragma GCC visibility pop
   56: 
   57: // www_GetErrno() Get error code of last operation
   58: int
   59: www_GetErrno()
   60: {
   61: 	return www_Errno;
   62: }
   63: 
   64: // www_GetError() Get error text of last operation
   65: const char *
   66: www_GetError()
   67: {
   68: 	return www_Error;
   69: }
   70: 
   71: // www_SetErr() Set error to variables for internal use!!!
   72: void
   73: www_SetErr(int eno, char *estr, ...)
   74: {
   75: 	va_list lst;
   76: 
   77: 	www_Errno = eno;
   78: 	memset(www_Error, 0, sizeof www_Errno);
   79: 	va_start(lst, estr);
   80: 	vsnprintf(www_Error, sizeof www_Errno, estr, lst);
   81: 	va_end(lst);
   82: }
   83: 
   84: /* -------------------------------------------------------------- */
   85: 
   86: /*
   87:  * www_initCGI() - Init CGI program
   88:  *
   89:  * return: NULL error or allocated cgi session
   90:  */
   91: cgi_t *
   92: www_initCGI(void)
   93: {
   94: 	char *s, *str;
   95: 	int ctlen, rlen;
   96: 	register int i;
   97: 	cgi_t *cgi = NULL;
   98: 
   99: 	str = getenv("REQUEST_METHOD");
  100: 	if (!str) {
  101: 		www_SetErr(EFAULT, "Request method not found");
  102: 		return NULL;
  103: 	}
  104: 	if (!strcmp(str, "GET") || !strcmp(str, "HEAD")) {
  105: 		/* GET | HEAD */
  106: 		str = getenv("QUERY_STRING");
  107: 		if (!str) {
  108: 			www_SetErr(EFAULT, "Query string not found");
  109: 			return NULL;
  110: 		}
  111: 		cgi = www_parseQuery(str);
  112: 	} else if (!strcmp(str, "POST")) {
  113: 		/* POST */
  114: 		str = getenv("CONTENT_LENGTH");
  115: 		if (!str) {
  116: 			www_SetErr(EFAULT, "Content length not found");
  117: 			return NULL;
  118: 		} else
  119: 			ctlen = strtol(str, NULL, 0);
  120: 
  121: 		s = getenv("CONTENT_TYPE");
  122: 		if (!s) {
  123: 			www_SetErr(EFAULT, "Content type not found");
  124: 			return NULL;
  125: 		}
  126: 		if (www_cmp(s, "multipart/form-data") && 
  127: 				www_cmp(s, "application/x-www-form-urlencoded")) {
  128: 			www_SetErr(EFAULT, "MIME parts are broken");
  129: 			return NULL;
  130: 		}
  131: 
  132: 		/* allocated space for post data */
  133: 		str = e_malloc(ctlen + 1);
  134: 		if (!str) {
  135: 			www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  136: 			return NULL;
  137: 		} else
  138: 			memset(str, 0, ctlen + 1);
  139: 		for (i = 0; i < ctlen && (rlen = 
  140: 					read(STDIN_FILENO, (void*) str + i, ctlen - i)) > 0; i += rlen);
  141: 		str[ctlen] = 0;
  142: 
  143: 		if (!www_cmp(s, "application/x-www-form-urlencoded"))
  144: 			cgi = www_parseQuery(str);
  145: 		else if (!www_cmp(s, "multipart/form-data"))
  146: 			cgi = www_parseMultiPart(str, ctlen, s);
  147: 
  148: 		e_free(str);
  149: 	} else {
  150: 		/* Unknown method */
  151: 		www_SetErr(EFAULT, "Unknown request method");
  152: 		return NULL;
  153: 	}
  154: 
  155: 	return cgi;
  156: }
  157: 
  158: /*
  159:  * www_closeCGI() - Close and free all CGI resources
  160:  *
  161:  * @cgi = Inited cgi session
  162:  * return: none
  163:  */
  164: void
  165: www_closeCGI(cgi_t ** __restrict cgi)
  166: {
  167: 	struct tagCGI *t;
  168: 
  169: 	if (!cgi || !*cgi)
  170: 		return;
  171: 
  172: 	while ((t = SLIST_FIRST(*cgi))) {
  173: 		ait_freeVar(&t->cgi_name);
  174: 		ait_freeVar(&t->cgi_value);
  175: 
  176: 		SLIST_REMOVE_HEAD(*cgi, cgi_node);
  177: 		e_free(t);
  178: 	}
  179: 
  180: 	e_free(*cgi);
  181: 	*cgi = NULL;
  182: }
  183: 
  184: /*
  185:  * www_parseQuery() - Parse CGI query string
  186:  *
  187:  * @str = String
  188:  * return: NULL error or allocated cgi session
  189:  */
  190: cgi_t *
  191: www_parseQuery(const char *str)
  192: {
  193: 	char *base, *wrk;
  194: 	cgi_t *cgi;
  195: 	struct tagCGI *t, *old = NULL;
  196: 
  197: 	if (!str) {
  198: 		www_SetErr(EINVAL, "String is NULL");
  199: 		return NULL;
  200: 	}
  201: 
  202: 	cgi = e_malloc(sizeof(cgi_t));
  203: 	if (!cgi) {
  204: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  205: 		return NULL;
  206: 	} else {
  207: 		memset(cgi, 0, sizeof(cgi_t));
  208: 		SLIST_INIT(cgi);
  209: 	}
  210: 
  211: 	base = wrk = e_strdup(str);
  212: 
  213: 	while (*wrk) {
  214: 		t = e_malloc(sizeof(struct tagCGI));
  215: 		if (!t) {
  216: 			www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  217: 			www_closeCGI(&cgi);
  218: 			return NULL;
  219: 		} else
  220: 			memset(t, 0, sizeof(struct tagCGI));
  221: 
  222: 		t->cgi_name = www_getpair(&wrk, "=");
  223: 		www_unescape(AIT_GET_STR(t->cgi_name));
  224: 
  225: 		t->cgi_value = www_getpair(&wrk, "&;");
  226: 		www_unescape(AIT_GET_STR(t->cgi_value));
  227: 
  228: 		if (!old)
  229: 			SLIST_INSERT_HEAD(cgi, t, cgi_node);
  230: 		else
  231: 			SLIST_INSERT_AFTER(old, t, cgi_node);
  232: 		old = t;
  233: 	}
  234: 
  235: 	e_free(base);
  236: 	return cgi;
  237: }
  238: 
  239: /*
  240:  * www_getValue() - Get Value from CGI session
  241:  *
  242:  * @cgi = Inited cgi session
  243:  * @name = Name of cgi variable
  244:  * return: NULL not found or !=NULL value
  245:  */
  246: const char *
  247: www_getValue(cgi_t * __restrict cgi, const char *name)
  248: {
  249: 	struct tagCGI *t;
  250: 
  251: 	if (!cgi || !name) {
  252: 		www_SetErr(EINVAL, "Invalid argument(s)");
  253: 		return NULL;
  254: 	}
  255: 
  256: 	SLIST_FOREACH(t, cgi, cgi_node)
  257: 		if (t->cgi_name && !strcmp(name, AIT_GET_STR(t->cgi_name)))
  258: 			return AIT_GET_STR(t->cgi_value);
  259: 
  260: 	return NULL;
  261: }
  262: 
  263: /*
  264:  * www_addValue() - Add new or update if exists CGI variable
  265:  *
  266:  * @cgi = Inited cgi session
  267:  * @name = Name of cgi variable
  268:  * @value = Value of cgi variable
  269:  * return: -1 error, 0 add new one or 1 updated variable
  270:  */
  271: int
  272: www_addValue(cgi_t * __restrict cgi, const char *name, const char *value)
  273: {
  274: 	struct tagCGI *t, *tmp;
  275: 
  276: 	if (!cgi || !name) {
  277: 		www_SetErr(EINVAL, "Invalid argument(s)");
  278: 		return -1;
  279: 	}
  280: 
  281: 	/* search for update */
  282: 	SLIST_FOREACH_SAFE(t, cgi, cgi_node, tmp) {
  283: 		if (t->cgi_name && !strcmp(name, AIT_GET_STR(t->cgi_name))) {
  284: 			AIT_FREE_VAL(t->cgi_value);
  285: 			AIT_SET_STR(t->cgi_value, value);
  286: 			/* update */
  287: 			return 1;
  288: 		}
  289: 		/* save last cgi pair */
  290: 		if (!tmp)
  291: 			break;
  292: 	}
  293: 
  294: 	/* add new one */
  295: 	tmp = e_malloc(sizeof(struct tagCGI));
  296: 	if (!tmp) {
  297: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  298: 		return -1;
  299: 	} else
  300: 		memset(tmp, 0, sizeof(struct tagCGI));
  301: 
  302: 	tmp->cgi_name = ait_allocVar();
  303: 	if (!tmp->cgi_name) {
  304: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  305: 		e_free(tmp);
  306: 		return -1;
  307: 	} else
  308: 		AIT_SET_STR(tmp->cgi_name, name);
  309: 	tmp->cgi_value = ait_allocVar();
  310: 	if (!tmp->cgi_name) {
  311: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  312: 		ait_freeVar(&tmp->cgi_name);
  313: 		e_free(tmp);
  314: 		return -1;
  315: 	} else
  316: 		AIT_SET_STR(tmp->cgi_value, value);
  317: 
  318: 	if (!t)
  319: 		SLIST_INSERT_HEAD(cgi, tmp, cgi_node);
  320: 	else
  321: 		SLIST_INSERT_AFTER(t, tmp, cgi_node);
  322: 	return 0;
  323: }
  324: 
  325: /*
  326:  * www_delPair() - Delete CGI variable from session
  327:  *
  328:  * @cgi = Inited cgi session
  329:  * @name = Name of cgi variable
  330:  * return: -1 error, 0 not found or 1 deleted ok
  331:  */
  332: int
  333: www_delPair(cgi_t * __restrict cgi, const char *name)
  334: {
  335: 	struct tagCGI *t, *tmp;
  336: 
  337: 	if (!cgi || !name) {
  338: 		www_SetErr(EINVAL, "Invalid argument(s)");
  339: 		return -1;
  340: 	}
  341: 
  342: 	/* search for delete */
  343: 	SLIST_FOREACH_SAFE(t, cgi, cgi_node, tmp)
  344: 		if (t->cgi_name && !strcmp(name, AIT_GET_STR(t->cgi_name))) {
  345: 			SLIST_REMOVE(cgi, t, tagCGI, cgi_node);
  346: 
  347: 			ait_freeVar(&t->cgi_name);
  348: 			ait_freeVar(&t->cgi_value);
  349: 			e_free(t);
  350: 			return 1;
  351: 		}
  352: 
  353: 	return 0;
  354: }
  355: 
  356: /*
  357:  * www_listPairs() - Walk over CGI session variables
  358:  *
  359:  * @cgi = Cgi session
  360:  * @func = If !=NULL call function for each element
  361:  * @arg = Optional argument pass through callback
  362:  * return: -1 error or >-1 number of elements
  363:  */
  364: int
  365: www_listPairs(cgi_t * __restrict cgi, list_cb_t func, void *arg)
  366: {
  367: 	register int ret = 0;
  368: 	struct tagCGI *t;
  369: 
  370: 	if (!cgi) {
  371: 		www_SetErr(EINVAL, "Invalid CGI session argument");
  372: 		return -1;
  373: 	}
  374: 
  375: 	SLIST_FOREACH(t, cgi, cgi_node) {
  376: 		ret++;
  377: 
  378: 		if (func)
  379: 			func(t, arg);
  380: 	}
  381: 
  382: 	return ret;
  383: }
  384: 
  385: /*
  386:  * www_header() - Output initial html header
  387:  *
  388:  * @output = file handle
  389:  * return: <1 error or >0 writed bytes
  390:  */
  391: int
  392: www_header(FILE *output)
  393: {
  394: 	FILE *f = output ? output : stdout;
  395: 
  396: 	return fputs("Content-type: text/html\n\n", f);
  397: }
  398: 
  399: static ait_val_t *
  400: quotStr(const char *str, const char **end)
  401: {
  402: 	char *e;
  403: 	int n, len = 0;
  404: 	register int i;
  405: 	ait_val_t *s;
  406: 
  407: 	/* get str w/o " */
  408: 	if (*str != '"') {
  409: 		n = strspn(str, "!#$%&'*+-.0123456789?ABCDEFGHIJKLMNOPQRSTUVWXYZ"
  410: 				"^_`abcdefghijklmnopqrstuvwxyz{|}~");
  411: 		s = ait_allocVar();
  412: 		if (!s) {
  413: 			www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  414: 			return NULL;
  415: 		} else {
  416: 			AIT_SET_STRSIZ(s, n + 1);
  417: 			strlcpy(AIT_GET_STR(s), str, AIT_LEN(s));
  418: 			*end = str + n;
  419: 			return s;
  420: 		}
  421: 	} else
  422: 		str++;
  423: 	/* get quoted string */
  424: 	if (!(e = strchr(str, '"')))
  425: 		return NULL;
  426: 	else
  427: 		len = e - str;
  428: 
  429: 	s = ait_allocVar();
  430: 	if (!s) {
  431: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  432: 		return NULL;
  433: 	} else {
  434: 		AIT_SET_STRSIZ(s, len + 1);
  435: 		e = AIT_GET_STR(s);
  436: 	}
  437: 
  438: 	for (i = 0; i < len; i++, str++) {
  439: 		if (*str == '\\' || *str == '\n')
  440: 			e[i] = *++str;
  441: 		else if (*str == '"')
  442: 			break;
  443: 		else
  444: 			e[i] = *str;
  445: 	}
  446: 	e[i] = 0;
  447: 
  448: 	*end = ++str;
  449: 	return s;
  450: }
  451: 
  452: static struct tagCGI *
  453: addAttr(const char **ct)
  454: {
  455: 	struct tagCGI *a;
  456: 	const char *c;
  457: 	char *eq;
  458: 
  459: 	if (!*ct || !(c = strchr(*ct, ';')))
  460: 		return NULL;
  461: 	else
  462: 		c++;
  463: 	while (isspace((int) *c))
  464: 		c++;
  465: 
  466: 	if (!(eq = strchr(c, '=')))
  467: 		return NULL;
  468: 
  469: 	a = e_malloc(sizeof(struct tagCGI));
  470: 	if (!a) {
  471: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  472: 		return NULL;
  473: 	}
  474: 	a->cgi_name = ait_allocVar();
  475: 	if (!a->cgi_name) {
  476: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  477: 		e_free(a);
  478: 		return NULL;
  479: 	}
  480: 
  481: 	*eq++ = 0;
  482: 	AIT_SET_STR(a->cgi_name, c);
  483: 	a->cgi_value = quotStr(eq, &c);
  484: 	if (!a->cgi_value) {
  485: 		ait_freeVar(&a->cgi_name);
  486: 		e_free(a);
  487: 		return NULL;
  488: 	}
  489: 
  490: 	*ct = c;
  491: 	return a;
  492: }
  493: 
  494: /*
  495:  * www_parseMultiPart() - Parse Multi part POST CGI query string
  496:  *
  497:  * @str = String
  498:  * @ctlen = Content length
  499:  * @ct = Content type
  500:  * return: NULL error or allocated cgi session
  501:  */
  502: cgi_t *
  503: www_parseMultiPart(const char *str, int ctlen, const char *ct)
  504: {
  505: 	cgi_t *cgi, *attr;
  506: 	mime_t *mime = NULL;
  507: 	struct tagMIME *m;
  508: 	struct tagCGI *t, *old = NULL;
  509: 	const char *s;
  510: 	int len;
  511: 	ait_val_t *v;
  512: 
  513: 	if (!str) {
  514: 		www_SetErr(EINVAL, "String is NULL");
  515: 		return NULL;
  516: 	}
  517: 
  518: 	cgi = e_malloc(sizeof(cgi_t));
  519: 	if (!cgi) {
  520: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  521: 		return NULL;
  522: 	} else {
  523: 		memset(cgi, 0, sizeof(cgi_t));
  524: 		SLIST_INIT(cgi);
  525: 	}
  526: 
  527: 	/* parse MIME messages */
  528: 	attr = www_parseAttributes(&ct);
  529: 	if (!attr) {
  530: 		www_closeCGI(&cgi);
  531: 		return NULL;
  532: 	}
  533: 	v = www_getAttribute(attr, "boundary");
  534: 	mime = mime_parseMultiPart(str, ctlen, AIT_GET_STR(v), NULL);
  535: 	www_freeAttributes(&attr);
  536: 	if (!mime) {
  537: 		www_closeCGI(&cgi);
  538: 		return NULL;
  539: 	}
  540: 
  541: 	SLIST_FOREACH(m, mime, mime_node) {
  542: 		s = mime_getValue(m, "content-disposition");
  543: 		attr = www_parseAttributes(&s);
  544: 		if (!www_getAttribute(attr, "name")) {
  545: 			www_freeAttributes(&attr);
  546: 			continue;
  547: 		}
  548: 
  549: 		t = e_malloc(sizeof(struct tagCGI));
  550: 		if (!t) {
  551: 			www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  552: 			mime_close(&mime);
  553: 			www_closeCGI(&cgi);
  554: 			return NULL;
  555: 		} else
  556: 			memset(t, 0, sizeof(struct tagCGI));
  557: 
  558: 		AIT_COPY_VAL(t->cgi_name, www_getAttribute(attr, "name"));
  559: 		len = mime_calcRawSize(m);
  560: 		t->cgi_value = ait_allocVar();
  561: 		if (!t->cgi_value) {
  562: 			www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  563: 			ait_freeVar(&t->cgi_name);
  564: 			e_free(t);
  565: 			mime_close(&mime);
  566: 			www_closeCGI(&cgi);
  567: 			return NULL;
  568: 		} else {
  569: 			AIT_SET_STRSIZ(t->cgi_value, len + 1);
  570: 			len = mime_getRawData(m, AIT_GET_STR(t->cgi_value), AIT_LEN(t->cgi_value));
  571: 		}
  572: 
  573: 		www_freeAttributes(&attr);
  574: 
  575: 		if (!old)
  576: 			SLIST_INSERT_HEAD(cgi, t, cgi_node);
  577: 		else
  578: 			SLIST_INSERT_AFTER(old, t, cgi_node);
  579: 		old = t;
  580: 	}
  581: 
  582: 	mime_close(&mime);
  583: 	return cgi;
  584: }
  585: 
  586: /*
  587:  * www_parseAttributes() - Parse attributes
  588:  *
  589:  * @ct = Content type
  590:  * return: NULL error or !=NULL attributes
  591:  */
  592: cgi_t *
  593: www_parseAttributes(const char **ct)
  594: {
  595: 	struct tagCGI *t, *old = NULL;
  596: 	cgi_t *attr = NULL;
  597: 
  598: 	if (!ct) {
  599: 		www_SetErr(EINVAL, "String is NULL");
  600: 		return NULL;
  601: 	}
  602: 
  603: 	attr = e_malloc(sizeof(cgi_t));
  604: 	if (!attr) {
  605: 		www_SetErr(elwix_GetErrno(), "%s", elwix_GetError());
  606: 		return NULL;
  607: 	} else {
  608: 		memset(attr, 0, sizeof(cgi_t));
  609: 		SLIST_INIT(attr);
  610: 	}
  611: 
  612: 	/* get mime attributes */
  613: 	while ((t = addAttr(ct))) {
  614: 		if (!old)
  615: 			SLIST_INSERT_HEAD(attr, t, cgi_node);
  616: 		else
  617: 			SLIST_INSERT_AFTER(old, t, cgi_node);
  618: 		old = t;
  619: 	}
  620: 
  621: 	return attr;
  622: }
  623: 
  624: /*
  625:  * www_getAttribute() - Get Attribute from attribute session
  626:  *
  627:  * @cgi = Inited attribute session
  628:  * @name = Name of attribute variable
  629:  * return: NULL not found or !=NULL value
  630:  */
  631: ait_val_t *
  632: www_getAttribute(cgi_t * __restrict cgi, const char *name)
  633: {
  634: 	struct tagCGI *t;
  635: 
  636: 	if (!cgi || !name) {
  637: 		www_SetErr(EINVAL, "Invalid argument(s)");
  638: 		return NULL;
  639: 	}
  640: 
  641: 	SLIST_FOREACH(t, cgi, cgi_node)
  642: 		if (t->cgi_name && !strcmp(name, AIT_GET_STR(t->cgi_name)))
  643: 			return t->cgi_value;
  644: 
  645: 	return NULL;
  646: }
  647: 

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