Annotation of embedaddon/curl/lib/vtls/mesalink.c, revision 1.1.1.1
1.1 misho 1: /***************************************************************************
2: * _ _ ____ _
3: * Project ___| | | | _ \| |
4: * / __| | | | |_) | |
5: * | (__| |_| | _ <| |___
6: * \___|\___/|_| \_\_____|
7: *
8: * Copyright (C) 2017 - 2018, Yiming Jing, <jingyiming@baidu.com>
9: * Copyright (C) 1998 - 2019, Daniel Stenberg, <daniel@haxx.se>, et al.
10: *
11: * This software is licensed as described in the file COPYING, which
12: * you should have received as part of this distribution. The terms
13: * are also available at https://curl.haxx.se/docs/copyright.html.
14: *
15: * You may opt to use, copy, modify, merge, publish, distribute and/or sell
16: * copies of the Software, and permit persons to whom the Software is
17: * furnished to do so, under the terms of the COPYING file.
18: *
19: * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20: * KIND, either express or implied.
21: *
22: ***************************************************************************/
23:
24: /*
25: * Source file for all MesaLink-specific code for the TLS/SSL layer. No code
26: * but vtls.c should ever call or use these functions.
27: *
28: */
29:
30: /*
31: * Based upon the CyaSSL implementation in cyassl.c and cyassl.h:
32: * Copyright (C) 1998 - 2017, Daniel Stenberg, <daniel@haxx.se>, et al.
33: *
34: * Thanks for code and inspiration!
35: */
36:
37: #include "curl_setup.h"
38:
39: #ifdef USE_MESALINK
40:
41: #include <mesalink/options.h>
42: #include <mesalink/version.h>
43:
44: #include "urldata.h"
45: #include "sendf.h"
46: #include "inet_pton.h"
47: #include "vtls.h"
48: #include "parsedate.h"
49: #include "connect.h" /* for the connect timeout */
50: #include "select.h"
51: #include "strcase.h"
52: #include "x509asn1.h"
53: #include "curl_printf.h"
54:
55: #include "mesalink.h"
56: #include <mesalink/openssl/ssl.h>
57: #include <mesalink/openssl/err.h>
58:
59: /* The last #include files should be: */
60: #include "curl_memory.h"
61: #include "memdebug.h"
62:
63: #define MESALINK_MAX_ERROR_SZ 80
64:
65: struct ssl_backend_data
66: {
67: SSL_CTX *ctx;
68: SSL *handle;
69: };
70:
71: #define BACKEND connssl->backend
72:
73: static Curl_recv mesalink_recv;
74: static Curl_send mesalink_send;
75:
76: static int do_file_type(const char *type)
77: {
78: if(!type || !type[0])
79: return SSL_FILETYPE_PEM;
80: if(strcasecompare(type, "PEM"))
81: return SSL_FILETYPE_PEM;
82: if(strcasecompare(type, "DER"))
83: return SSL_FILETYPE_ASN1;
84: return -1;
85: }
86:
87: /*
88: * This function loads all the client/CA certificates and CRLs. Setup the TLS
89: * layer and do all necessary magic.
90: */
91: static CURLcode
92: mesalink_connect_step1(struct connectdata *conn, int sockindex)
93: {
94: char *ciphers;
95: struct Curl_easy *data = conn->data;
96: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
97: struct in_addr addr4;
98: #ifdef ENABLE_IPV6
99: struct in6_addr addr6;
100: #endif
101: const char *const hostname =
102: SSL_IS_PROXY() ? conn->http_proxy.host.name : conn->host.name;
103: size_t hostname_len = strlen(hostname);
104:
105: SSL_METHOD *req_method = NULL;
106: curl_socket_t sockfd = conn->sock[sockindex];
107:
108: if(connssl->state == ssl_connection_complete)
109: return CURLE_OK;
110:
111: if(SSL_CONN_CONFIG(version_max) != CURL_SSLVERSION_MAX_NONE) {
112: failf(data, "MesaLink does not support to set maximum SSL/TLS version");
113: return CURLE_SSL_CONNECT_ERROR;
114: }
115:
116: switch(SSL_CONN_CONFIG(version)) {
117: case CURL_SSLVERSION_SSLv3:
118: case CURL_SSLVERSION_TLSv1:
119: case CURL_SSLVERSION_TLSv1_0:
120: case CURL_SSLVERSION_TLSv1_1:
121: failf(data, "MesaLink does not support SSL 3.0, TLS 1.0, or TLS 1.1");
122: return CURLE_NOT_BUILT_IN;
123: case CURL_SSLVERSION_DEFAULT:
124: case CURL_SSLVERSION_TLSv1_2:
125: req_method = TLSv1_2_client_method();
126: break;
127: case CURL_SSLVERSION_TLSv1_3:
128: req_method = TLSv1_3_client_method();
129: break;
130: case CURL_SSLVERSION_SSLv2:
131: failf(data, "MesaLink does not support SSLv2");
132: return CURLE_SSL_CONNECT_ERROR;
133: default:
134: failf(data, "Unrecognized parameter passed via CURLOPT_SSLVERSION");
135: return CURLE_SSL_CONNECT_ERROR;
136: }
137:
138: if(!req_method) {
139: failf(data, "SSL: couldn't create a method!");
140: return CURLE_OUT_OF_MEMORY;
141: }
142:
143: if(BACKEND->ctx)
144: SSL_CTX_free(BACKEND->ctx);
145: BACKEND->ctx = SSL_CTX_new(req_method);
146:
147: if(!BACKEND->ctx) {
148: failf(data, "SSL: couldn't create a context!");
149: return CURLE_OUT_OF_MEMORY;
150: }
151:
152: SSL_CTX_set_verify(
153: BACKEND->ctx, SSL_CONN_CONFIG(verifypeer) ?
154: SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL);
155:
156: if(SSL_CONN_CONFIG(CAfile) || SSL_CONN_CONFIG(CApath)) {
157: if(!SSL_CTX_load_verify_locations(BACKEND->ctx, SSL_CONN_CONFIG(CAfile),
158: SSL_CONN_CONFIG(CApath))) {
159: if(SSL_CONN_CONFIG(verifypeer)) {
160: failf(data,
161: "error setting certificate verify locations:\n"
162: " CAfile: %s\n CApath: %s",
163: SSL_CONN_CONFIG(CAfile) ?
164: SSL_CONN_CONFIG(CAfile) : "none",
165: SSL_CONN_CONFIG(CApath) ?
166: SSL_CONN_CONFIG(CApath) : "none");
167: return CURLE_SSL_CACERT_BADFILE;
168: }
169: infof(data,
170: "error setting certificate verify locations,"
171: " continuing anyway:\n");
172: }
173: else {
174: infof(data, "successfully set certificate verify locations:\n");
175: }
176: infof(data,
177: " CAfile: %s\n"
178: " CApath: %s\n",
179: SSL_CONN_CONFIG(CAfile)?
180: SSL_CONN_CONFIG(CAfile): "none",
181: SSL_CONN_CONFIG(CApath)?
182: SSL_CONN_CONFIG(CApath): "none");
183: }
184:
185: if(SSL_SET_OPTION(cert) && SSL_SET_OPTION(key)) {
186: int file_type = do_file_type(SSL_SET_OPTION(cert_type));
187:
188: if(SSL_CTX_use_certificate_chain_file(BACKEND->ctx, SSL_SET_OPTION(cert),
189: file_type) != 1) {
190: failf(data, "unable to use client certificate (no key or wrong pass"
191: " phrase?)");
192: return CURLE_SSL_CONNECT_ERROR;
193: }
194:
195: file_type = do_file_type(SSL_SET_OPTION(key_type));
196: if(SSL_CTX_use_PrivateKey_file(BACKEND->ctx, SSL_SET_OPTION(key),
197: file_type) != 1) {
198: failf(data, "unable to set private key");
199: return CURLE_SSL_CONNECT_ERROR;
200: }
201: infof(data,
202: "client cert: %s\n",
203: SSL_CONN_CONFIG(clientcert)?
204: SSL_CONN_CONFIG(clientcert): "none");
205: }
206:
207: ciphers = SSL_CONN_CONFIG(cipher_list);
208: if(ciphers) {
209: #ifdef MESALINK_HAVE_CIPHER
210: if(!SSL_CTX_set_cipher_list(BACKEND->ctx, ciphers)) {
211: failf(data, "failed setting cipher list: %s", ciphers);
212: return CURLE_SSL_CIPHER;
213: }
214: #endif
215: infof(data, "Cipher selection: %s\n", ciphers);
216: }
217:
218: if(BACKEND->handle)
219: SSL_free(BACKEND->handle);
220: BACKEND->handle = SSL_new(BACKEND->ctx);
221: if(!BACKEND->handle) {
222: failf(data, "SSL: couldn't create a context (handle)!");
223: return CURLE_OUT_OF_MEMORY;
224: }
225:
226: if((hostname_len < USHRT_MAX) &&
227: (0 == Curl_inet_pton(AF_INET, hostname, &addr4))
228: #ifdef ENABLE_IPV6
229: && (0 == Curl_inet_pton(AF_INET6, hostname, &addr6))
230: #endif
231: ) {
232: /* hostname is not a valid IP address */
233: if(SSL_set_tlsext_host_name(BACKEND->handle, hostname) != SSL_SUCCESS) {
234: failf(data,
235: "WARNING: failed to configure server name indication (SNI) "
236: "TLS extension\n");
237: return CURLE_SSL_CONNECT_ERROR;
238: }
239: }
240: else {
241: #ifdef CURLDEBUG
242: /* Check if the hostname is 127.0.0.1 or [::1];
243: * otherwise reject because MesaLink always wants a valid DNS Name
244: * specified in RFC 5280 Section 7.2 */
245: if(strncmp(hostname, "127.0.0.1", 9) == 0
246: #ifdef ENABLE_IPV6
247: || strncmp(hostname, "[::1]", 5) == 0
248: #endif
249: ) {
250: SSL_set_tlsext_host_name(BACKEND->handle, "localhost");
251: }
252: else
253: #endif
254: {
255: failf(data,
256: "ERROR: MesaLink does not accept an IP address as a hostname\n");
257: return CURLE_SSL_CONNECT_ERROR;
258: }
259: }
260:
261: #ifdef MESALINK_HAVE_SESSION
262: if(SSL_SET_OPTION(primary.sessionid)) {
263: void *ssl_sessionid = NULL;
264:
265: Curl_ssl_sessionid_lock(conn);
266: if(!Curl_ssl_getsessionid(conn, &ssl_sessionid, NULL, sockindex)) {
267: /* we got a session id, use it! */
268: if(!SSL_set_session(BACKEND->handle, ssl_sessionid)) {
269: Curl_ssl_sessionid_unlock(conn);
270: failf(
271: data,
272: "SSL: SSL_set_session failed: %s",
273: ERR_error_string(SSL_get_error(BACKEND->handle, 0), error_buffer));
274: return CURLE_SSL_CONNECT_ERROR;
275: }
276: /* Informational message */
277: infof(data, "SSL re-using session ID\n");
278: }
279: Curl_ssl_sessionid_unlock(conn);
280: }
281: #endif /* MESALINK_HAVE_SESSION */
282:
283: if(SSL_set_fd(BACKEND->handle, (int)sockfd) != SSL_SUCCESS) {
284: failf(data, "SSL: SSL_set_fd failed");
285: return CURLE_SSL_CONNECT_ERROR;
286: }
287:
288: connssl->connecting_state = ssl_connect_2;
289: return CURLE_OK;
290: }
291:
292: static CURLcode
293: mesalink_connect_step2(struct connectdata *conn, int sockindex)
294: {
295: int ret = -1;
296: struct Curl_easy *data = conn->data;
297: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
298:
299: conn->recv[sockindex] = mesalink_recv;
300: conn->send[sockindex] = mesalink_send;
301:
302: ret = SSL_connect(BACKEND->handle);
303: if(ret != SSL_SUCCESS) {
304: int detail = SSL_get_error(BACKEND->handle, ret);
305:
306: if(SSL_ERROR_WANT_CONNECT == detail || SSL_ERROR_WANT_READ == detail) {
307: connssl->connecting_state = ssl_connect_2_reading;
308: return CURLE_OK;
309: }
310: else {
311: char error_buffer[MESALINK_MAX_ERROR_SZ];
312: failf(data,
313: "SSL_connect failed with error %d: %s",
314: detail,
315: ERR_error_string_n(detail, error_buffer, sizeof(error_buffer)));
316: ERR_print_errors_fp(stderr);
317: if(detail && SSL_CONN_CONFIG(verifypeer)) {
318: detail &= ~0xFF;
319: if(detail == TLS_ERROR_WEBPKI_ERRORS) {
320: failf(data, "Cert verify failed");
321: return CURLE_PEER_FAILED_VERIFICATION;
322: }
323: }
324: return CURLE_SSL_CONNECT_ERROR;
325: }
326: }
327:
328: connssl->connecting_state = ssl_connect_3;
329: infof(data,
330: "SSL connection using %s / %s\n",
331: SSL_get_version(BACKEND->handle),
332: SSL_get_cipher_name(BACKEND->handle));
333:
334: return CURLE_OK;
335: }
336:
337: static CURLcode
338: mesalink_connect_step3(struct connectdata *conn, int sockindex)
339: {
340: CURLcode result = CURLE_OK;
341: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
342:
343: DEBUGASSERT(ssl_connect_3 == connssl->connecting_state);
344:
345: #ifdef MESALINK_HAVE_SESSION
346: if(SSL_SET_OPTION(primary.sessionid)) {
347: bool incache;
348: SSL_SESSION *our_ssl_sessionid;
349: void *old_ssl_sessionid = NULL;
350:
351: our_ssl_sessionid = SSL_get_session(BACKEND->handle);
352:
353: Curl_ssl_sessionid_lock(conn);
354: incache =
355: !(Curl_ssl_getsessionid(conn, &old_ssl_sessionid, NULL, sockindex));
356: if(incache) {
357: if(old_ssl_sessionid != our_ssl_sessionid) {
358: infof(data, "old SSL session ID is stale, removing\n");
359: Curl_ssl_delsessionid(conn, old_ssl_sessionid);
360: incache = FALSE;
361: }
362: }
363:
364: if(!incache) {
365: result = Curl_ssl_addsessionid(
366: conn, our_ssl_sessionid, 0 /* unknown size */, sockindex);
367: if(result) {
368: Curl_ssl_sessionid_unlock(conn);
369: failf(data, "failed to store ssl session");
370: return result;
371: }
372: }
373: Curl_ssl_sessionid_unlock(conn);
374: }
375: #endif /* MESALINK_HAVE_SESSION */
376:
377: connssl->connecting_state = ssl_connect_done;
378:
379: return result;
380: }
381:
382: static ssize_t
383: mesalink_send(struct connectdata *conn, int sockindex, const void *mem,
384: size_t len, CURLcode *curlcode)
385: {
386: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
387: char error_buffer[MESALINK_MAX_ERROR_SZ];
388: int memlen = (len > (size_t)INT_MAX) ? INT_MAX : (int)len;
389: int rc = SSL_write(BACKEND->handle, mem, memlen);
390:
391: if(rc < 0) {
392: int err = SSL_get_error(BACKEND->handle, rc);
393: switch(err) {
394: case SSL_ERROR_WANT_READ:
395: case SSL_ERROR_WANT_WRITE:
396: /* there's data pending, re-invoke SSL_write() */
397: *curlcode = CURLE_AGAIN;
398: return -1;
399: default:
400: failf(conn->data,
401: "SSL write: %s, errno %d",
402: ERR_error_string_n(err, error_buffer, sizeof(error_buffer)),
403: SOCKERRNO);
404: *curlcode = CURLE_SEND_ERROR;
405: return -1;
406: }
407: }
408: return rc;
409: }
410:
411: static void
412: Curl_mesalink_close(struct connectdata *conn, int sockindex)
413: {
414: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
415:
416: if(BACKEND->handle) {
417: (void)SSL_shutdown(BACKEND->handle);
418: SSL_free(BACKEND->handle);
419: BACKEND->handle = NULL;
420: }
421: if(BACKEND->ctx) {
422: SSL_CTX_free(BACKEND->ctx);
423: BACKEND->ctx = NULL;
424: }
425: }
426:
427: static ssize_t
428: mesalink_recv(struct connectdata *conn, int num, char *buf, size_t buffersize,
429: CURLcode *curlcode)
430: {
431: struct ssl_connect_data *connssl = &conn->ssl[num];
432: char error_buffer[MESALINK_MAX_ERROR_SZ];
433: int buffsize = (buffersize > (size_t)INT_MAX) ? INT_MAX : (int)buffersize;
434: int nread = SSL_read(BACKEND->handle, buf, buffsize);
435:
436: if(nread <= 0) {
437: int err = SSL_get_error(BACKEND->handle, nread);
438:
439: switch(err) {
440: case SSL_ERROR_ZERO_RETURN: /* no more data */
441: case IO_ERROR_CONNECTION_ABORTED:
442: break;
443: case SSL_ERROR_WANT_READ:
444: case SSL_ERROR_WANT_WRITE:
445: /* there's data pending, re-invoke SSL_read() */
446: *curlcode = CURLE_AGAIN;
447: return -1;
448: default:
449: failf(conn->data,
450: "SSL read: %s, errno %d",
451: ERR_error_string_n(err, error_buffer, sizeof(error_buffer)),
452: SOCKERRNO);
453: *curlcode = CURLE_RECV_ERROR;
454: return -1;
455: }
456: }
457: return nread;
458: }
459:
460: static size_t
461: Curl_mesalink_version(char *buffer, size_t size)
462: {
463: return msnprintf(buffer, size, "MesaLink/%s", MESALINK_VERSION_STRING);
464: }
465:
466: static int
467: Curl_mesalink_init(void)
468: {
469: return (SSL_library_init() == SSL_SUCCESS);
470: }
471:
472: /*
473: * This function is called to shut down the SSL layer but keep the
474: * socket open (CCC - Clear Command Channel)
475: */
476: static int
477: Curl_mesalink_shutdown(struct connectdata *conn, int sockindex)
478: {
479: int retval = 0;
480: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
481:
482: if(BACKEND->handle) {
483: SSL_free(BACKEND->handle);
484: BACKEND->handle = NULL;
485: }
486: return retval;
487: }
488:
489: static CURLcode
490: mesalink_connect_common(struct connectdata *conn, int sockindex,
491: bool nonblocking, bool *done)
492: {
493: CURLcode result;
494: struct Curl_easy *data = conn->data;
495: struct ssl_connect_data *connssl = &conn->ssl[sockindex];
496: curl_socket_t sockfd = conn->sock[sockindex];
497: timediff_t timeout_ms;
498: int what;
499:
500: /* check if the connection has already been established */
501: if(ssl_connection_complete == connssl->state) {
502: *done = TRUE;
503: return CURLE_OK;
504: }
505:
506: if(ssl_connect_1 == connssl->connecting_state) {
507: /* Find out how much more time we're allowed */
508: timeout_ms = Curl_timeleft(data, NULL, TRUE);
509:
510: if(timeout_ms < 0) {
511: /* no need to continue if time already is up */
512: failf(data, "SSL connection timeout");
513: return CURLE_OPERATION_TIMEDOUT;
514: }
515:
516: result = mesalink_connect_step1(conn, sockindex);
517: if(result)
518: return result;
519: }
520:
521: while(ssl_connect_2 == connssl->connecting_state ||
522: ssl_connect_2_reading == connssl->connecting_state ||
523: ssl_connect_2_writing == connssl->connecting_state) {
524:
525: /* check allowed time left */
526: timeout_ms = Curl_timeleft(data, NULL, TRUE);
527:
528: if(timeout_ms < 0) {
529: /* no need to continue if time already is up */
530: failf(data, "SSL connection timeout");
531: return CURLE_OPERATION_TIMEDOUT;
532: }
533:
534: /* if ssl is expecting something, check if it's available. */
535: if(connssl->connecting_state == ssl_connect_2_reading ||
536: connssl->connecting_state == ssl_connect_2_writing) {
537:
538: curl_socket_t writefd =
539: ssl_connect_2_writing == connssl->connecting_state ? sockfd
540: : CURL_SOCKET_BAD;
541: curl_socket_t readfd = ssl_connect_2_reading == connssl->connecting_state
542: ? sockfd
543: : CURL_SOCKET_BAD;
544:
545: what = Curl_socket_check(
546: readfd, CURL_SOCKET_BAD, writefd,
547: nonblocking ? 0 : (time_t)timeout_ms);
548: if(what < 0) {
549: /* fatal error */
550: failf(data, "select/poll on SSL socket, errno: %d", SOCKERRNO);
551: return CURLE_SSL_CONNECT_ERROR;
552: }
553: else if(0 == what) {
554: if(nonblocking) {
555: *done = FALSE;
556: return CURLE_OK;
557: }
558: else {
559: /* timeout */
560: failf(data, "SSL connection timeout");
561: return CURLE_OPERATION_TIMEDOUT;
562: }
563: }
564: /* socket is readable or writable */
565: }
566:
567: /* Run transaction, and return to the caller if it failed or if
568: * this connection is part of a multi handle and this loop would
569: * execute again. This permits the owner of a multi handle to
570: * abort a connection attempt before step2 has completed while
571: * ensuring that a client using select() or epoll() will always
572: * have a valid fdset to wait on.
573: */
574: result = mesalink_connect_step2(conn, sockindex);
575:
576: if(result ||
577: (nonblocking && (ssl_connect_2 == connssl->connecting_state ||
578: ssl_connect_2_reading == connssl->connecting_state ||
579: ssl_connect_2_writing == connssl->connecting_state))) {
580: return result;
581: }
582: } /* repeat step2 until all transactions are done. */
583:
584: if(ssl_connect_3 == connssl->connecting_state) {
585: result = mesalink_connect_step3(conn, sockindex);
586: if(result)
587: return result;
588: }
589:
590: if(ssl_connect_done == connssl->connecting_state) {
591: connssl->state = ssl_connection_complete;
592: conn->recv[sockindex] = mesalink_recv;
593: conn->send[sockindex] = mesalink_send;
594: *done = TRUE;
595: }
596: else
597: *done = FALSE;
598:
599: /* Reset our connect state machine */
600: connssl->connecting_state = ssl_connect_1;
601:
602: return CURLE_OK;
603: }
604:
605: static CURLcode
606: Curl_mesalink_connect_nonblocking(struct connectdata *conn, int sockindex,
607: bool *done)
608: {
609: return mesalink_connect_common(conn, sockindex, TRUE, done);
610: }
611:
612: static CURLcode
613: Curl_mesalink_connect(struct connectdata *conn, int sockindex)
614: {
615: CURLcode result;
616: bool done = FALSE;
617:
618: result = mesalink_connect_common(conn, sockindex, FALSE, &done);
619: if(result)
620: return result;
621:
622: DEBUGASSERT(done);
623:
624: return CURLE_OK;
625: }
626:
627: static void *
628: Curl_mesalink_get_internals(struct ssl_connect_data *connssl,
629: CURLINFO info UNUSED_PARAM)
630: {
631: (void)info;
632: return BACKEND->handle;
633: }
634:
635: const struct Curl_ssl Curl_ssl_mesalink = {
636: { CURLSSLBACKEND_MESALINK, "MesaLink" }, /* info */
637:
638: SSLSUPP_SSL_CTX,
639:
640: sizeof(struct ssl_backend_data),
641:
642: Curl_mesalink_init, /* init */
643: Curl_none_cleanup, /* cleanup */
644: Curl_mesalink_version, /* version */
645: Curl_none_check_cxn, /* check_cxn */
646: Curl_mesalink_shutdown, /* shutdown */
647: Curl_none_data_pending, /* data_pending */
648: Curl_none_random, /* random */
649: Curl_none_cert_status_request, /* cert_status_request */
650: Curl_mesalink_connect, /* connect */
651: Curl_mesalink_connect_nonblocking, /* connect_nonblocking */
652: Curl_mesalink_get_internals, /* get_internals */
653: Curl_mesalink_close, /* close_one */
654: Curl_none_close_all, /* close_all */
655: Curl_none_session_free, /* session_free */
656: Curl_none_set_engine, /* set_engine */
657: Curl_none_set_engine_default, /* set_engine_default */
658: Curl_none_engines_list, /* engines_list */
659: Curl_none_false_start, /* false_start */
660: Curl_none_md5sum, /* md5sum */
661: NULL /* sha256sum */
662: };
663:
664: #endif
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>