Annotation of embedaddon/php/ext/mysqlnd/mysqlnd_net.c, revision 1.1
1.1 ! misho 1: /*
! 2: +----------------------------------------------------------------------+
! 3: | PHP Version 5 |
! 4: +----------------------------------------------------------------------+
! 5: | Copyright (c) 2006-2012 The PHP Group |
! 6: +----------------------------------------------------------------------+
! 7: | This source file is subject to version 3.01 of the PHP license, |
! 8: | that is bundled with this package in the file LICENSE, and is |
! 9: | available through the world-wide-web at the following url: |
! 10: | http://www.php.net/license/3_01.txt |
! 11: | If you did not receive a copy of the PHP license and are unable to |
! 12: | obtain it through the world-wide-web, please send a note to |
! 13: | license@php.net so we can mail you a copy immediately. |
! 14: +----------------------------------------------------------------------+
! 15: | Authors: Georg Richter <georg@mysql.com> |
! 16: | Andrey Hristov <andrey@mysql.com> |
! 17: | Ulf Wendel <uwendel@mysql.com> |
! 18: +----------------------------------------------------------------------+
! 19: */
! 20: #include "php.h"
! 21: #include "php_globals.h"
! 22: #include "mysqlnd.h"
! 23: #include "mysqlnd_priv.h"
! 24: #include "mysqlnd_wireprotocol.h"
! 25: #include "mysqlnd_statistics.h"
! 26: #include "mysqlnd_debug.h"
! 27: #include "ext/standard/sha1.h"
! 28: #include "php_network.h"
! 29: #include "zend_ini.h"
! 30: #ifdef MYSQLND_COMPRESSION_ENABLED
! 31: #include <zlib.h>
! 32: #endif
! 33:
! 34: #ifndef PHP_WIN32
! 35: #include <netinet/tcp.h>
! 36: #else
! 37: #include <winsock.h>
! 38: #endif
! 39:
! 40:
! 41: /* {{{ mysqlnd_set_sock_no_delay */
! 42: static int
! 43: mysqlnd_set_sock_no_delay(php_stream * stream TSRMLS_DC)
! 44: {
! 45:
! 46: int socketd = ((php_netstream_data_t*)stream->abstract)->socket;
! 47: int ret = SUCCESS;
! 48: int flag = 1;
! 49: int result = setsockopt(socketd, IPPROTO_TCP, TCP_NODELAY, (char *) &flag, sizeof(int));
! 50:
! 51: DBG_ENTER("mysqlnd_set_sock_no_delay");
! 52:
! 53: if (result == -1) {
! 54: ret = FAILURE;
! 55: }
! 56:
! 57: DBG_RETURN(ret);
! 58: }
! 59: /* }}} */
! 60:
! 61:
! 62: /* {{{ mysqlnd_net::network_read */
! 63: static enum_func_status
! 64: MYSQLND_METHOD(mysqlnd_net, network_read)(MYSQLND * conn, zend_uchar * buffer, size_t count TSRMLS_DC)
! 65: {
! 66: enum_func_status return_value = PASS;
! 67: size_t to_read = count, ret;
! 68: size_t old_chunk_size = conn->net->stream->chunk_size;
! 69: DBG_ENTER("mysqlnd_net::network_read");
! 70: DBG_INF_FMT("count=%u", count);
! 71: conn->net->stream->chunk_size = MIN(to_read, conn->net->options.net_read_buffer_size);
! 72: while (to_read) {
! 73: if (!(ret = php_stream_read(conn->net->stream, (char *) buffer, to_read))) {
! 74: DBG_ERR_FMT("Error while reading header from socket");
! 75: return_value = FAIL;
! 76: break;
! 77: }
! 78: buffer += ret;
! 79: to_read -= ret;
! 80: }
! 81: MYSQLND_INC_CONN_STATISTIC_W_VALUE(conn->stats, STAT_BYTES_RECEIVED, count - to_read);
! 82: conn->net->stream->chunk_size = old_chunk_size;
! 83: DBG_RETURN(return_value);
! 84: }
! 85: /* }}} */
! 86:
! 87:
! 88: /* {{{ mysqlnd_net::network_write */
! 89: static size_t
! 90: MYSQLND_METHOD(mysqlnd_net, network_write)(MYSQLND * const conn, const zend_uchar * const buf, size_t count TSRMLS_DC)
! 91: {
! 92: size_t ret;
! 93: DBG_ENTER("mysqlnd_net::network_write");
! 94: ret = php_stream_write(conn->net->stream, (char *)buf, count);
! 95: DBG_RETURN(ret);
! 96: }
! 97: /* }}} */
! 98:
! 99:
! 100:
! 101: /* {{{ mysqlnd_net::connect */
! 102: static enum_func_status
! 103: MYSQLND_METHOD(mysqlnd_net, connect)(MYSQLND_NET * net, const char * const scheme, size_t scheme_len, zend_bool persistent, char **errstr, int * errcode TSRMLS_DC)
! 104: {
! 105: #if PHP_API_VERSION < 20100412
! 106: unsigned int streams_options = ENFORCE_SAFE_MODE;
! 107: #else
! 108: unsigned int streams_options = 0;
! 109: #endif
! 110: unsigned int streams_flags = STREAM_XPORT_CLIENT | STREAM_XPORT_CONNECT;
! 111: char * hashed_details = NULL;
! 112: int hashed_details_len = 0;
! 113: struct timeval tv;
! 114: DBG_ENTER("mysqlnd_net::connect");
! 115:
! 116: if (persistent) {
! 117: hashed_details_len = spprintf(&hashed_details, 0, "%p", net);
! 118: DBG_INF_FMT("hashed_details=%s", hashed_details);
! 119: }
! 120:
! 121: net->packet_no = net->compressed_envelope_packet_no = 0;
! 122:
! 123: if (net->stream) {
! 124: /* close before opening a new one */
! 125: DBG_INF_FMT("Freeing stream. abstract=%p", net->stream->abstract);
! 126: if (net->persistent) {
! 127: php_stream_free(net->stream, PHP_STREAM_FREE_CLOSE_PERSISTENT | PHP_STREAM_FREE_RSRC_DTOR);
! 128: } else {
! 129: php_stream_free(net->stream, PHP_STREAM_FREE_CLOSE);
! 130: }
! 131: net->stream = NULL;
! 132: }
! 133:
! 134: if (net->options.timeout_connect) {
! 135: tv.tv_sec = net->options.timeout_connect;
! 136: tv.tv_usec = 0;
! 137: }
! 138:
! 139: DBG_INF_FMT("calling php_stream_xport_create");
! 140: net->stream = php_stream_xport_create(scheme, scheme_len, streams_options, streams_flags,
! 141: hashed_details, (net->options.timeout_connect) ? &tv : NULL,
! 142: NULL /*ctx*/, errstr, errcode);
! 143:
! 144: if (*errstr || !net->stream) {
! 145: if (hashed_details) {
! 146: efree(hashed_details); /* allocated by spprintf */
! 147: }
! 148: *errcode = CR_CONNECTION_ERROR;
! 149: DBG_RETURN(FAIL);
! 150: }
! 151:
! 152: if (hashed_details) {
! 153: /*
! 154: If persistent, the streams register it in EG(persistent_list).
! 155: This is unwanted. ext/mysql or ext/mysqli are responsible to clean,
! 156: whatever they have to.
! 157: */
! 158: zend_rsrc_list_entry *le;
! 159:
! 160: if (zend_hash_find(&EG(persistent_list), hashed_details, hashed_details_len + 1, (void*) &le) == SUCCESS) {
! 161: /*
! 162: in_free will let streams code skip destructing - big HACK,
! 163: but STREAMS suck big time regarding persistent streams.
! 164: Just not compatible for extensions that need persistency.
! 165: */
! 166: net->stream->in_free = 1;
! 167: zend_hash_del(&EG(persistent_list), hashed_details, hashed_details_len + 1);
! 168: net->stream->in_free = 0;
! 169: }
! 170: #if ZEND_DEBUG
! 171: /* Shut-up the streams, they don't know what they are doing */
! 172: net->stream->__exposed = 1;
! 173: #endif
! 174: efree(hashed_details);
! 175: }
! 176: /*
! 177: Streams are not meant for C extensions! Thus we need a hack. Every connected stream will
! 178: be registered as resource (in EG(regular_list). So far, so good. However, it won't be
! 179: unregistered till the script ends. So, we need to take care of that.
! 180: */
! 181: net->stream->in_free = 1;
! 182: zend_hash_index_del(&EG(regular_list), net->stream->rsrc_id);
! 183: net->stream->in_free = 0;
! 184:
! 185: if (!net->options.timeout_read) {
! 186: /* should always happen because read_timeout cannot be set via API */
! 187: net->options.timeout_read = (unsigned int) MYSQLND_G(net_read_timeout);
! 188: }
! 189: if (net->options.timeout_read) {
! 190: DBG_INF_FMT("setting %u as PHP_STREAM_OPTION_READ_TIMEOUT", net->options.timeout_read);
! 191: tv.tv_sec = net->options.timeout_read;
! 192: tv.tv_usec = 0;
! 193: php_stream_set_option(net->stream, PHP_STREAM_OPTION_READ_TIMEOUT, 0, &tv);
! 194: }
! 195:
! 196: if (!memcmp(scheme, "tcp://", sizeof("tcp://") - 1)) {
! 197: /* TCP -> Set TCP_NODELAY */
! 198: mysqlnd_set_sock_no_delay(net->stream TSRMLS_CC);
! 199: }
! 200:
! 201: {
! 202: unsigned int buf_size = MYSQLND_G(net_read_buffer_size); /* this is long, cast to unsigned int*/
! 203: net->m.set_client_option(net, MYSQLND_OPT_NET_READ_BUFFER_SIZE, (char *)&buf_size TSRMLS_CC);
! 204: }
! 205:
! 206:
! 207: DBG_RETURN(PASS);
! 208: }
! 209: /* }}} */
! 210:
! 211:
! 212: /* We assume that MYSQLND_HEADER_SIZE is 4 bytes !! */
! 213: #define COPY_HEADER(T,A) do { \
! 214: *(((char *)(T))) = *(((char *)(A)));\
! 215: *(((char *)(T))+1) = *(((char *)(A))+1);\
! 216: *(((char *)(T))+2) = *(((char *)(A))+2);\
! 217: *(((char *)(T))+3) = *(((char *)(A))+3); } while (0)
! 218: #define STORE_HEADER_SIZE(safe_storage, buffer) COPY_HEADER((safe_storage), (buffer))
! 219: #define RESTORE_HEADER_SIZE(buffer, safe_storage) STORE_HEADER_SIZE((safe_storage), (buffer))
! 220:
! 221: /* {{{ mysqlnd_net::send */
! 222: /*
! 223: IMPORTANT : It's expected that buf has place in the beginning for MYSQLND_HEADER_SIZE !!!!
! 224: This is done for performance reasons in the caller of this function.
! 225: Otherwise we will have to do send two TCP packets, or do new alloc and memcpy.
! 226: Neither are quick, thus the clients of this function are obligated to do
! 227: what they are asked for.
! 228:
! 229: `count` is actually the length of the payload data. Thus :
! 230: count + MYSQLND_HEADER_SIZE = sizeof(buf) (not the pointer but the actual buffer)
! 231: */
! 232: size_t
! 233: MYSQLND_METHOD(mysqlnd_net, send)(MYSQLND * const conn, char * const buf, size_t count TSRMLS_DC)
! 234: {
! 235: zend_uchar safe_buf[((MYSQLND_HEADER_SIZE) + (sizeof(zend_uchar)) - 1) / (sizeof(zend_uchar))];
! 236: zend_uchar *safe_storage = safe_buf;
! 237: MYSQLND_NET *net = conn->net;
! 238: size_t old_chunk_size = net->stream->chunk_size;
! 239: size_t ret, packets_sent = 1;
! 240: size_t left = count;
! 241: zend_uchar *p = (zend_uchar *) buf;
! 242: zend_uchar * compress_buf = NULL;
! 243: size_t to_be_sent;
! 244:
! 245: DBG_ENTER("mysqlnd_net::send");
! 246: DBG_INF_FMT("conn=%llu count=%lu compression=%u", conn->thread_id, count, net->compressed);
! 247:
! 248: net->stream->chunk_size = MYSQLND_MAX_PACKET_SIZE;
! 249:
! 250: if (net->compressed == TRUE) {
! 251: size_t comp_buf_size = MYSQLND_HEADER_SIZE + COMPRESSED_HEADER_SIZE + MYSQLND_HEADER_SIZE + MIN(left, MYSQLND_MAX_PACKET_SIZE);
! 252: DBG_INF_FMT("compress_buf_size="MYSQLND_SZ_T_SPEC, comp_buf_size);
! 253: compress_buf = mnd_emalloc(comp_buf_size);
! 254: }
! 255:
! 256: do {
! 257: to_be_sent = MIN(left, MYSQLND_MAX_PACKET_SIZE);
! 258: #ifdef MYSQLND_COMPRESSION_ENABLED
! 259: if (net->compressed == TRUE) {
! 260: /* here we need to compress the data and then write it, first comes the compressed header */
! 261: size_t tmp_complen = to_be_sent;
! 262: size_t payload_size;
! 263: zend_uchar * uncompressed_payload = p; /* should include the header */
! 264:
! 265: STORE_HEADER_SIZE(safe_storage, uncompressed_payload);
! 266: int3store(uncompressed_payload, to_be_sent);
! 267: int1store(uncompressed_payload + 3, net->packet_no);
! 268: if (PASS == net->m.encode((compress_buf + COMPRESSED_HEADER_SIZE + MYSQLND_HEADER_SIZE), &tmp_complen,
! 269: uncompressed_payload, to_be_sent + MYSQLND_HEADER_SIZE TSRMLS_CC))
! 270: {
! 271: int3store(compress_buf + MYSQLND_HEADER_SIZE, to_be_sent + MYSQLND_HEADER_SIZE);
! 272: payload_size = tmp_complen;
! 273: } else {
! 274: int3store(compress_buf + MYSQLND_HEADER_SIZE, 0);
! 275: memcpy(compress_buf + MYSQLND_HEADER_SIZE + COMPRESSED_HEADER_SIZE, uncompressed_payload, to_be_sent + MYSQLND_HEADER_SIZE);
! 276: payload_size = to_be_sent + MYSQLND_HEADER_SIZE;
! 277: }
! 278: RESTORE_HEADER_SIZE(uncompressed_payload, safe_storage);
! 279:
! 280: int3store(compress_buf, payload_size);
! 281: int1store(compress_buf + 3, net->packet_no);
! 282: DBG_INF_FMT("writing "MYSQLND_SZ_T_SPEC" bytes to the network", payload_size + MYSQLND_HEADER_SIZE + COMPRESSED_HEADER_SIZE);
! 283: ret = conn->net->m.network_write(conn, compress_buf, payload_size + MYSQLND_HEADER_SIZE + COMPRESSED_HEADER_SIZE TSRMLS_CC);
! 284: net->compressed_envelope_packet_no++;
! 285: #if WHEN_WE_NEED_TO_CHECK_WHETHER_COMPRESSION_WORKS_CORRECTLY
! 286: if (res == Z_OK) {
! 287: size_t decompressed_size = left + MYSQLND_HEADER_SIZE;
! 288: zend_uchar * decompressed_data = mnd_malloc(decompressed_size);
! 289: int error = net->m.decode(decompressed_data, decompressed_size,
! 290: compress_buf + MYSQLND_HEADER_SIZE + COMPRESSED_HEADER_SIZE, payload_size);
! 291: if (error == Z_OK) {
! 292: int i;
! 293: DBG_INF("success decompressing");
! 294: for (i = 0 ; i < decompressed_size; i++) {
! 295: if (i && (i % 30 == 0)) {
! 296: printf("\n\t\t");
! 297: }
! 298: printf("%.2X ", (int)*((char*)&(decompressed_data[i])));
! 299: DBG_INF_FMT("%.2X ", (int)*((char*)&(decompressed_data[i])));
! 300: }
! 301: } else {
! 302: DBG_INF("error decompressing");
! 303: }
! 304: mnd_free(decompressed_data);
! 305: }
! 306: #endif /* WHEN_WE_NEED_TO_CHECK_WHETHER_COMPRESSION_WORKS_CORRECTLY */
! 307: } else
! 308: #endif /* MYSQLND_COMPRESSION_ENABLED */
! 309: {
! 310: DBG_INF("no compression");
! 311: STORE_HEADER_SIZE(safe_storage, p);
! 312: int3store(p, to_be_sent);
! 313: int1store(p + 3, net->packet_no);
! 314: ret = conn->net->m.network_write(conn, p, to_be_sent + MYSQLND_HEADER_SIZE TSRMLS_CC);
! 315: RESTORE_HEADER_SIZE(p, safe_storage);
! 316: net->compressed_envelope_packet_no++;
! 317: }
! 318: net->packet_no++;
! 319:
! 320: p += to_be_sent;
! 321: left -= to_be_sent;
! 322: packets_sent++;
! 323: /*
! 324: if left is 0 then there is nothing more to send, but if the last packet was exactly
! 325: with the size MYSQLND_MAX_PACKET_SIZE we need to send additional packet, which has
! 326: empty payload. Thus if left == 0 we check for to_be_sent being the max size. If it is
! 327: indeed it then loop once more, then to_be_sent will become 0, left will stay 0. Empty
! 328: packet will be sent and this loop will end.
! 329: */
! 330: } while (ret && (left > 0 || to_be_sent == MYSQLND_MAX_PACKET_SIZE));
! 331:
! 332: DBG_INF_FMT("packet_size="MYSQLND_SZ_T_SPEC" packet_no=%u", left, net->packet_no);
! 333: /* Even for zero size payload we have to send a packet */
! 334: if (!ret) {
! 335: DBG_ERR_FMT("Can't %u send bytes", count);
! 336: conn->state = CONN_QUIT_SENT;
! 337: SET_CLIENT_ERROR(conn->error_info, CR_SERVER_GONE_ERROR, UNKNOWN_SQLSTATE, mysqlnd_server_gone);
! 338: }
! 339:
! 340: MYSQLND_INC_CONN_STATISTIC_W_VALUE3(conn->stats,
! 341: STAT_BYTES_SENT, count + packets_sent * MYSQLND_HEADER_SIZE,
! 342: STAT_PROTOCOL_OVERHEAD_OUT, packets_sent * MYSQLND_HEADER_SIZE,
! 343: STAT_PACKETS_SENT, packets_sent);
! 344:
! 345: net->stream->chunk_size = old_chunk_size;
! 346: if (compress_buf) {
! 347: mnd_efree(compress_buf);
! 348: }
! 349: DBG_RETURN(ret);
! 350: }
! 351: /* }}} */
! 352:
! 353:
! 354: #ifdef MYSQLND_COMPRESSION_ENABLED
! 355: /* {{{ php_mysqlnd_read_buffer_is_empty */
! 356: static zend_bool
! 357: php_mysqlnd_read_buffer_is_empty(MYSQLND_READ_BUFFER * buffer)
! 358: {
! 359: return buffer->len? FALSE:TRUE;
! 360: }
! 361: /* }}} */
! 362:
! 363:
! 364: /* {{{ php_mysqlnd_read_buffer_read */
! 365: static void
! 366: php_mysqlnd_read_buffer_read(MYSQLND_READ_BUFFER * buffer, size_t count, zend_uchar * dest)
! 367: {
! 368: if (buffer->len >= count) {
! 369: memcpy(dest, buffer->data + buffer->offset, count);
! 370: buffer->offset += count;
! 371: buffer->len -= count;
! 372: }
! 373: }
! 374: /* }}} */
! 375:
! 376:
! 377: /* {{{ php_mysqlnd_read_buffer_bytes_left */
! 378: static size_t
! 379: php_mysqlnd_read_buffer_bytes_left(MYSQLND_READ_BUFFER * buffer)
! 380: {
! 381: return buffer->len;
! 382: }
! 383: /* }}} */
! 384:
! 385:
! 386: /* {{{ php_mysqlnd_read_buffer_free */
! 387: static void
! 388: php_mysqlnd_read_buffer_free(MYSQLND_READ_BUFFER ** buffer TSRMLS_DC)
! 389: {
! 390: DBG_ENTER("php_mysqlnd_read_buffer_free");
! 391: if (*buffer) {
! 392: mnd_efree((*buffer)->data);
! 393: mnd_efree(*buffer);
! 394: *buffer = NULL;
! 395: }
! 396: DBG_VOID_RETURN;
! 397: }
! 398: /* }}} */
! 399:
! 400:
! 401: /* {{{ php_mysqlnd_create_read_buffer */
! 402: static MYSQLND_READ_BUFFER *
! 403: mysqlnd_create_read_buffer(size_t count TSRMLS_DC)
! 404: {
! 405: MYSQLND_READ_BUFFER * ret = mnd_emalloc(sizeof(MYSQLND_READ_BUFFER));
! 406: DBG_ENTER("mysqlnd_create_read_buffer");
! 407: ret->is_empty = php_mysqlnd_read_buffer_is_empty;
! 408: ret->read = php_mysqlnd_read_buffer_read;
! 409: ret->bytes_left = php_mysqlnd_read_buffer_bytes_left;
! 410: ret->free_buffer = php_mysqlnd_read_buffer_free;
! 411: ret->data = mnd_emalloc(count);
! 412: ret->size = ret->len = count;
! 413: ret->offset = 0;
! 414: DBG_RETURN(ret);
! 415: }
! 416: /* }}} */
! 417:
! 418:
! 419: /* {{{ mysqlnd_read_compressed_packet_from_stream_and_fill_read_buffer */
! 420: static enum_func_status
! 421: mysqlnd_read_compressed_packet_from_stream_and_fill_read_buffer(MYSQLND * conn, size_t net_payload_size TSRMLS_DC)
! 422: {
! 423: MYSQLND_NET * net = conn->net;
! 424: size_t decompressed_size;
! 425: enum_func_status ret = PASS;
! 426: zend_uchar * compressed_data = NULL;
! 427: zend_uchar comp_header[COMPRESSED_HEADER_SIZE];
! 428: DBG_ENTER("mysqlnd_read_compressed_packet_from_stream_and_fill_read_buffer");
! 429:
! 430: /* Read the compressed header */
! 431: if (FAIL == conn->net->m.network_read(conn, comp_header, COMPRESSED_HEADER_SIZE TSRMLS_CC)) {
! 432: DBG_RETURN(FAIL);
! 433: }
! 434: decompressed_size = uint3korr(comp_header);
! 435:
! 436: /* When decompressed_size is 0, then the data is not compressed, and we have wasted 3 bytes */
! 437: /* we need to decompress the data */
! 438:
! 439: if (decompressed_size) {
! 440: compressed_data = mnd_emalloc(net_payload_size);
! 441: if (FAIL == conn->net->m.network_read(conn, compressed_data, net_payload_size TSRMLS_CC)) {
! 442: ret = FAIL;
! 443: goto end;
! 444: }
! 445: net->uncompressed_data = mysqlnd_create_read_buffer(decompressed_size TSRMLS_CC);
! 446: ret = net->m.decode(net->uncompressed_data->data, decompressed_size, compressed_data, net_payload_size TSRMLS_CC);
! 447: if (ret == FAIL) {
! 448: goto end;
! 449: }
! 450: } else {
! 451: DBG_INF_FMT("The server decided not to compress the data. Our job is easy. Copying %u bytes", net_payload_size);
! 452: net->uncompressed_data = mysqlnd_create_read_buffer(net_payload_size TSRMLS_CC);
! 453: if (FAIL == conn->net->m.network_read(conn, net->uncompressed_data->data, net_payload_size TSRMLS_CC)) {
! 454: ret = FAIL;
! 455: goto end;
! 456: }
! 457: }
! 458: end:
! 459: if (compressed_data) {
! 460: mnd_efree(compressed_data);
! 461: }
! 462: DBG_RETURN(ret);
! 463: }
! 464: /* }}} */
! 465: #endif /* MYSQLND_COMPRESSION_ENABLED */
! 466:
! 467:
! 468: /* {{{ mysqlnd_net::decode */
! 469: static enum_func_status
! 470: MYSQLND_METHOD(mysqlnd_net, decode)(zend_uchar * uncompressed_data, size_t uncompressed_data_len,
! 471: const zend_uchar * const compressed_data, size_t compressed_data_len TSRMLS_DC)
! 472: {
! 473: #ifdef MYSQLND_COMPRESSION_ENABLED
! 474: int error;
! 475: uLongf tmp_complen = uncompressed_data_len;
! 476: DBG_ENTER("mysqlnd_net::decode");
! 477: error = uncompress(uncompressed_data, &tmp_complen, compressed_data, compressed_data_len);
! 478:
! 479: DBG_INF_FMT("compressed data: decomp_len=%lu compressed_size="MYSQLND_SZ_T_SPEC, tmp_complen, compressed_data_len);
! 480: if (error != Z_OK) {
! 481: DBG_INF_FMT("decompression NOT successful. error=%d Z_OK=%d Z_BUF_ERROR=%d Z_MEM_ERROR=%d", error, Z_OK, Z_BUF_ERROR, Z_MEM_ERROR);
! 482: }
! 483: DBG_RETURN(error == Z_OK? PASS:FAIL);
! 484: #else
! 485: DBG_ENTER("mysqlnd_net::decode");
! 486: DBG_RETURN(FAIL);
! 487: #endif
! 488: }
! 489: /* }}} */
! 490:
! 491:
! 492: /* {{{ mysqlnd_net::encode */
! 493: static enum_func_status
! 494: MYSQLND_METHOD(mysqlnd_net, encode)(zend_uchar * compress_buffer, size_t * compress_buffer_len,
! 495: const zend_uchar * const uncompressed_data, size_t uncompressed_data_len TSRMLS_DC)
! 496: {
! 497: #ifdef MYSQLND_COMPRESSION_ENABLED
! 498: int error;
! 499: uLongf tmp_complen = *compress_buffer_len;
! 500: DBG_ENTER("mysqlnd_net::encode");
! 501: error = compress(compress_buffer, &tmp_complen, uncompressed_data, uncompressed_data_len);
! 502:
! 503: if (error != Z_OK) {
! 504: DBG_INF_FMT("compression NOT successful. error=%d Z_OK=%d Z_BUF_ERROR=%d Z_MEM_ERROR=%d", error, Z_OK, Z_BUF_ERROR, Z_MEM_ERROR);
! 505: } else {
! 506: *compress_buffer_len = tmp_complen;
! 507: DBG_INF_FMT("compression successful. compressed size=%lu", tmp_complen);
! 508: }
! 509:
! 510: DBG_RETURN(error == Z_OK? PASS:FAIL);
! 511: #else
! 512: DBG_ENTER("mysqlnd_net::encode");
! 513: DBG_RETURN(FAIL);
! 514: #endif
! 515: }
! 516: /* }}} */
! 517:
! 518:
! 519: /* {{{ mysqlnd_net::receive */
! 520: static enum_func_status
! 521: MYSQLND_METHOD(mysqlnd_net, receive)(MYSQLND * conn, zend_uchar * buffer, size_t count TSRMLS_DC)
! 522: {
! 523: size_t to_read = count;
! 524: zend_uchar * p = buffer;
! 525: MYSQLND_NET * net = conn->net;
! 526:
! 527: DBG_ENTER("mysqlnd_net::receive");
! 528: #ifdef MYSQLND_COMPRESSION_ENABLED
! 529: if (net->compressed) {
! 530: if (net->uncompressed_data) {
! 531: size_t to_read_from_buffer = MIN(net->uncompressed_data->bytes_left(net->uncompressed_data), to_read);
! 532: DBG_INF_FMT("reading %u from uncompressed_data buffer", to_read_from_buffer);
! 533: if (to_read_from_buffer) {
! 534: net->uncompressed_data->read(net->uncompressed_data, to_read_from_buffer, (zend_uchar *) p);
! 535: p += to_read_from_buffer;
! 536: to_read -= to_read_from_buffer;
! 537: }
! 538: DBG_INF_FMT("left %u to read", to_read);
! 539: if (TRUE == net->uncompressed_data->is_empty(net->uncompressed_data)) {
! 540: /* Everything was consumed. This should never happen here, but for security */
! 541: net->uncompressed_data->free_buffer(&net->uncompressed_data TSRMLS_CC);
! 542: }
! 543: }
! 544: if (to_read) {
! 545: zend_uchar net_header[MYSQLND_HEADER_SIZE];
! 546: size_t net_payload_size;
! 547: zend_uchar packet_no;
! 548:
! 549: if (FAIL == net->m.network_read(conn, net_header, MYSQLND_HEADER_SIZE TSRMLS_CC)) {
! 550: DBG_RETURN(FAIL);
! 551: }
! 552: net_payload_size = uint3korr(net_header);
! 553: packet_no = uint1korr(net_header + 3);
! 554: if (net->compressed_envelope_packet_no != packet_no) {
! 555: DBG_ERR_FMT("Transport level: packets out of order. Expected %u received %u. Packet size="MYSQLND_SZ_T_SPEC,
! 556: net->compressed_envelope_packet_no, packet_no, net_payload_size);
! 557:
! 558: php_error(E_WARNING, "Packets out of order. Expected %u received %u. Packet size="MYSQLND_SZ_T_SPEC,
! 559: net->compressed_envelope_packet_no, packet_no, net_payload_size);
! 560: DBG_RETURN(FAIL);
! 561: }
! 562: net->compressed_envelope_packet_no++;
! 563: #ifdef MYSQLND_DUMP_HEADER_N_BODY
! 564: DBG_INF_FMT("HEADER: hwd_packet_no=%u size=%3u", packet_no, (unsigned long) net_payload_size);
! 565: #endif
! 566: /* Now let's read from the wire, decompress it and fill the read buffer */
! 567: mysqlnd_read_compressed_packet_from_stream_and_fill_read_buffer(conn, net_payload_size TSRMLS_CC);
! 568:
! 569: /*
! 570: Now a bit of recursion - read from the read buffer,
! 571: if the data which we have just read from the wire
! 572: is not enough, then the recursive call will try to
! 573: satisfy it until it is satisfied.
! 574: */
! 575: DBG_RETURN(net->m.receive(conn, p, to_read TSRMLS_CC));
! 576: }
! 577: DBG_RETURN(PASS);
! 578: }
! 579: #endif /* MYSQLND_COMPRESSION_ENABLED */
! 580: DBG_RETURN(net->m.network_read(conn, p, to_read TSRMLS_CC));
! 581: }
! 582: /* }}} */
! 583:
! 584:
! 585: /* {{{ mysqlnd_net::set_client_option */
! 586: static enum_func_status
! 587: MYSQLND_METHOD(mysqlnd_net, set_client_option)(MYSQLND_NET * const net, enum mysqlnd_option option, const char * const value TSRMLS_DC)
! 588: {
! 589: DBG_ENTER("mysqlnd_net::set_client_option");
! 590: DBG_INF_FMT("option=%u", option);
! 591: switch (option) {
! 592: case MYSQLND_OPT_NET_CMD_BUFFER_SIZE:
! 593: DBG_INF("MYSQLND_OPT_NET_CMD_BUFFER_SIZE");
! 594: if (*(unsigned int*) value < MYSQLND_NET_CMD_BUFFER_MIN_SIZE) {
! 595: DBG_RETURN(FAIL);
! 596: }
! 597: net->cmd_buffer.length = *(unsigned int*) value;
! 598: DBG_INF_FMT("new_length=%u", net->cmd_buffer.length);
! 599: if (!net->cmd_buffer.buffer) {
! 600: net->cmd_buffer.buffer = mnd_pemalloc(net->cmd_buffer.length, net->persistent);
! 601: } else {
! 602: net->cmd_buffer.buffer = mnd_perealloc(net->cmd_buffer.buffer, net->cmd_buffer.length, net->persistent);
! 603: }
! 604: break;
! 605: case MYSQLND_OPT_NET_READ_BUFFER_SIZE:
! 606: DBG_INF("MYSQLND_OPT_NET_READ_BUFFER_SIZE");
! 607: net->options.net_read_buffer_size = *(unsigned int*) value;
! 608: DBG_INF_FMT("new_length=%u", net->options.net_read_buffer_size);
! 609: break;
! 610: case MYSQL_OPT_CONNECT_TIMEOUT:
! 611: DBG_INF("MYSQL_OPT_CONNECT_TIMEOUT");
! 612: net->options.timeout_connect = *(unsigned int*) value;
! 613: break;
! 614: case MYSQLND_OPT_SSL_KEY:
! 615: {
! 616: zend_bool pers = net->persistent;
! 617: if (net->options.ssl_key) {
! 618: mnd_pefree(net->options.ssl_key, pers);
! 619: }
! 620: net->options.ssl_key = value? mnd_pestrdup(value, pers) : NULL;
! 621: break;
! 622: }
! 623: case MYSQLND_OPT_SSL_CERT:
! 624: {
! 625: zend_bool pers = net->persistent;
! 626: if (net->options.ssl_cert) {
! 627: mnd_pefree(net->options.ssl_cert, pers);
! 628: }
! 629: net->options.ssl_cert = value? mnd_pestrdup(value, pers) : NULL;
! 630: break;
! 631: }
! 632: case MYSQLND_OPT_SSL_CA:
! 633: {
! 634: zend_bool pers = net->persistent;
! 635: if (net->options.ssl_ca) {
! 636: mnd_pefree(net->options.ssl_ca, pers);
! 637: }
! 638: net->options.ssl_ca = value? mnd_pestrdup(value, pers) : NULL;
! 639: break;
! 640: }
! 641: case MYSQLND_OPT_SSL_CAPATH:
! 642: {
! 643: zend_bool pers = net->persistent;
! 644: if (net->options.ssl_capath) {
! 645: mnd_pefree(net->options.ssl_capath, pers);
! 646: }
! 647: net->options.ssl_capath = value? mnd_pestrdup(value, pers) : NULL;
! 648: break;
! 649: }
! 650: case MYSQLND_OPT_SSL_CIPHER:
! 651: {
! 652: zend_bool pers = net->persistent;
! 653: if (net->options.ssl_cipher) {
! 654: mnd_pefree(net->options.ssl_cipher, pers);
! 655: }
! 656: net->options.ssl_cipher = value? mnd_pestrdup(value, pers) : NULL;
! 657: break;
! 658: }
! 659: case MYSQLND_OPT_SSL_PASSPHRASE:
! 660: {
! 661: zend_bool pers = net->persistent;
! 662: if (net->options.ssl_passphrase) {
! 663: mnd_pefree(net->options.ssl_passphrase, pers);
! 664: }
! 665: net->options.ssl_passphrase = value? mnd_pestrdup(value, pers) : NULL;
! 666: break;
! 667: }
! 668: case MYSQL_OPT_SSL_VERIFY_SERVER_CERT:
! 669: net->options.ssl_verify_peer = value? ((*(zend_bool *)value)? TRUE:FALSE): FALSE;
! 670: break;
! 671: #ifdef WHEN_SUPPORTED_BY_MYSQLI
! 672: case MYSQL_OPT_READ_TIMEOUT:
! 673: DBG_INF("MYSQL_OPT_READ_TIMEOUT");
! 674: net->options.timeout_read = *(unsigned int*) value;
! 675: break;
! 676: case MYSQL_OPT_WRITE_TIMEOUT:
! 677: DBG_INF("MYSQL_OPT_WRITE_TIMEOUT");
! 678: net->options.timeout_write = *(unsigned int*) value;
! 679: break;
! 680: #endif
! 681: case MYSQL_OPT_COMPRESS:
! 682: net->options.flags |= MYSQLND_NET_FLAG_USE_COMPRESSION;
! 683: break;
! 684: default:
! 685: DBG_RETURN(FAIL);
! 686: }
! 687: DBG_RETURN(PASS);
! 688: }
! 689: /* }}} */
! 690:
! 691: /* {{{ mysqlnd_net::consume_uneaten_data */
! 692: size_t
! 693: MYSQLND_METHOD(mysqlnd_net, consume_uneaten_data)(MYSQLND_NET * const net, enum php_mysqlnd_server_command cmd TSRMLS_DC)
! 694: {
! 695: #ifdef MYSQLND_DO_WIRE_CHECK_BEFORE_COMMAND
! 696: /*
! 697: Switch to non-blocking mode and try to consume something from
! 698: the line, if possible, then continue. This saves us from looking for
! 699: the actuall place where out-of-order packets have been sent.
! 700: If someone is completely sure that everything is fine, he can switch it
! 701: off.
! 702: */
! 703: char tmp_buf[256];
! 704: size_t skipped_bytes = 0;
! 705: int opt = PHP_STREAM_OPTION_BLOCKING;
! 706: int was_blocked = net->stream->ops->set_option(net->stream, opt, 0, NULL TSRMLS_CC);
! 707:
! 708: DBG_ENTER("mysqlnd_net::consume_uneaten_data");
! 709:
! 710: if (PHP_STREAM_OPTION_RETURN_ERR != was_blocked) {
! 711: /* Do a read of 1 byte */
! 712: int bytes_consumed;
! 713:
! 714: do {
! 715: skipped_bytes += (bytes_consumed = php_stream_read(net->stream, tmp_buf, sizeof(tmp_buf)));
! 716: } while (bytes_consumed == sizeof(tmp_buf));
! 717:
! 718: if (was_blocked) {
! 719: net->stream->ops->set_option(net->stream, opt, 1, NULL TSRMLS_CC);
! 720: }
! 721:
! 722: if (bytes_consumed) {
! 723: DBG_ERR_FMT("Skipped %u bytes. Last command %s hasn't consumed all the output from the server",
! 724: bytes_consumed, mysqlnd_command_to_text[net->last_command]);
! 725: php_error_docref(NULL TSRMLS_CC, E_WARNING, "Skipped %u bytes. Last command %s hasn't "
! 726: "consumed all the output from the server",
! 727: bytes_consumed, mysqlnd_command_to_text[net->last_command]);
! 728: }
! 729: }
! 730: net->last_command = cmd;
! 731:
! 732: DBG_RETURN(skipped_bytes);
! 733: #else
! 734: return 0;
! 735: #endif
! 736: }
! 737: /* }}} */
! 738:
! 739: /*
! 740: in libmyusql, if cert and !key then key=cert
! 741: */
! 742: /* {{{ mysqlnd_net::enable_ssl */
! 743: static enum_func_status
! 744: MYSQLND_METHOD(mysqlnd_net, enable_ssl)(MYSQLND_NET * const net TSRMLS_DC)
! 745: {
! 746: #ifdef MYSQLND_SSL_SUPPORTED
! 747: php_stream_context *context = php_stream_context_alloc();
! 748: DBG_ENTER("mysqlnd_net::enable_ssl");
! 749: if (!context) {
! 750: DBG_RETURN(FAIL);
! 751: }
! 752:
! 753: if (net->options.ssl_key) {
! 754: zval key_zval;
! 755: ZVAL_STRING(&key_zval, net->options.ssl_key, 0);
! 756: DBG_INF("key");
! 757: php_stream_context_set_option(context, "ssl", "local_pk", &key_zval);
! 758: }
! 759: if (net->options.ssl_verify_peer) {
! 760: zval verify_peer_zval;
! 761: ZVAL_TRUE(&verify_peer_zval);
! 762: DBG_INF("verify peer");
! 763: php_stream_context_set_option(context, "ssl", "verify_peer", &verify_peer_zval);
! 764: }
! 765: if (net->options.ssl_cert) {
! 766: zval cert_zval;
! 767: ZVAL_STRING(&cert_zval, net->options.ssl_cert, 0);
! 768: DBG_INF_FMT("local_cert=%s", net->options.ssl_cert);
! 769: php_stream_context_set_option(context, "ssl", "local_cert", &cert_zval);
! 770: if (!net->options.ssl_key) {
! 771: php_stream_context_set_option(context, "ssl", "local_pk", &cert_zval);
! 772: }
! 773: }
! 774: if (net->options.ssl_ca) {
! 775: zval cafile_zval;
! 776: ZVAL_STRING(&cafile_zval, net->options.ssl_ca, 0);
! 777: DBG_INF_FMT("cafile=%s", net->options.ssl_ca);
! 778: php_stream_context_set_option(context, "ssl", "cafile", &cafile_zval);
! 779: }
! 780: if (net->options.ssl_capath) {
! 781: zval capath_zval;
! 782: ZVAL_STRING(&capath_zval, net->options.ssl_capath, 0);
! 783: DBG_INF_FMT("capath=%s", net->options.ssl_capath);
! 784: php_stream_context_set_option(context, "ssl", "cafile", &capath_zval);
! 785: }
! 786: if (net->options.ssl_passphrase) {
! 787: zval passphrase_zval;
! 788: ZVAL_STRING(&passphrase_zval, net->options.ssl_passphrase, 0);
! 789: php_stream_context_set_option(context, "ssl", "passphrase", &passphrase_zval);
! 790: }
! 791: if (net->options.ssl_cipher) {
! 792: zval cipher_zval;
! 793: ZVAL_STRING(&cipher_zval, net->options.ssl_cipher, 0);
! 794: DBG_INF_FMT("ciphers=%s", net->options.ssl_cipher);
! 795: php_stream_context_set_option(context, "ssl", "ciphers", &cipher_zval);
! 796: }
! 797: php_stream_context_set(net->stream, context);
! 798: if (php_stream_xport_crypto_setup(net->stream, STREAM_CRYPTO_METHOD_TLS_CLIENT, NULL TSRMLS_CC) < 0 ||
! 799: php_stream_xport_crypto_enable(net->stream, 1 TSRMLS_CC) < 0)
! 800: {
! 801: DBG_ERR("Cannot connect to MySQL by using SSL");
! 802: php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot connect to MySQL by using SSL");
! 803: DBG_RETURN(FAIL);
! 804: }
! 805: /*
! 806: get rid of the context. we are persistent and if this is a real pconn used by mysql/mysqli,
! 807: then the context would not survive cleaning of EG(regular_list), where it is registered, as a
! 808: resource. What happens is that after this destruction any use of the network will mean usage
! 809: of the context, which means usage of already freed memory, bad. Actually we don't need this
! 810: context anymore after we have enabled SSL on the connection. Thus it is very simple, we remove it.
! 811: */
! 812: php_stream_context_set(net->stream, NULL);
! 813:
! 814: if (net->options.timeout_read) {
! 815: struct timeval tv;
! 816: DBG_INF_FMT("setting %u as PHP_STREAM_OPTION_READ_TIMEOUT", net->options.timeout_read);
! 817: tv.tv_sec = net->options.timeout_read;
! 818: tv.tv_usec = 0;
! 819: php_stream_set_option(net->stream, PHP_STREAM_OPTION_READ_TIMEOUT, 0, &tv);
! 820: }
! 821:
! 822: DBG_RETURN(PASS);
! 823: #else
! 824: DBG_ENTER("mysqlnd_net::enable_ssl");
! 825: DBG_RETURN(PASS);
! 826: #endif
! 827: }
! 828: /* }}} */
! 829:
! 830:
! 831: /* {{{ mysqlnd_net::disable_ssl */
! 832: static enum_func_status
! 833: MYSQLND_METHOD(mysqlnd_net, disable_ssl)(MYSQLND_NET * const net TSRMLS_DC)
! 834: {
! 835: DBG_ENTER("mysqlnd_net::disable_ssl");
! 836: DBG_RETURN(PASS);
! 837: }
! 838: /* }}} */
! 839:
! 840:
! 841: /* {{{ mysqlnd_net::set_client_option */
! 842: static void
! 843: MYSQLND_METHOD(mysqlnd_net, free_contents)(MYSQLND_NET * net TSRMLS_DC)
! 844: {
! 845: zend_bool pers = net->persistent;
! 846: DBG_ENTER("mysqlnd_net::free_contents");
! 847:
! 848: #ifdef MYSQLND_COMPRESSION_ENABLED
! 849: if (net->uncompressed_data) {
! 850: net->uncompressed_data->free_buffer(&net->uncompressed_data TSRMLS_CC);
! 851: }
! 852: #endif
! 853: if (net->options.ssl_key) {
! 854: mnd_pefree(net->options.ssl_key, pers);
! 855: net->options.ssl_key = NULL;
! 856: }
! 857: if (net->options.ssl_cert) {
! 858: mnd_pefree(net->options.ssl_cert, pers);
! 859: net->options.ssl_cert = NULL;
! 860: }
! 861: if (net->options.ssl_ca) {
! 862: mnd_pefree(net->options.ssl_ca, pers);
! 863: net->options.ssl_ca = NULL;
! 864: }
! 865: if (net->options.ssl_capath) {
! 866: mnd_pefree(net->options.ssl_capath, pers);
! 867: net->options.ssl_capath = NULL;
! 868: }
! 869: if (net->options.ssl_cipher) {
! 870: mnd_pefree(net->options.ssl_cipher, pers);
! 871: net->options.ssl_cipher = NULL;
! 872: }
! 873:
! 874: DBG_VOID_RETURN;
! 875: }
! 876: /* }}} */
! 877:
! 878: static
! 879: MYSQLND_CLASS_METHODS_START(mysqlnd_net)
! 880: MYSQLND_METHOD(mysqlnd_net, connect),
! 881: MYSQLND_METHOD(mysqlnd_net, send),
! 882: MYSQLND_METHOD(mysqlnd_net, receive),
! 883: MYSQLND_METHOD(mysqlnd_net, set_client_option),
! 884: MYSQLND_METHOD(mysqlnd_net, network_read),
! 885: MYSQLND_METHOD(mysqlnd_net, network_write),
! 886: MYSQLND_METHOD(mysqlnd_net, decode),
! 887: MYSQLND_METHOD(mysqlnd_net, encode),
! 888: MYSQLND_METHOD(mysqlnd_net, consume_uneaten_data),
! 889: MYSQLND_METHOD(mysqlnd_net, free_contents),
! 890: MYSQLND_METHOD(mysqlnd_net, enable_ssl),
! 891: MYSQLND_METHOD(mysqlnd_net, disable_ssl)
! 892: MYSQLND_CLASS_METHODS_END;
! 893:
! 894:
! 895: /* {{{ mysqlnd_net_init */
! 896: PHPAPI MYSQLND_NET *
! 897: mysqlnd_net_init(zend_bool persistent TSRMLS_DC)
! 898: {
! 899: size_t alloc_size = sizeof(MYSQLND_NET) + mysqlnd_plugin_count() * sizeof(void *);
! 900: MYSQLND_NET * net = mnd_pecalloc(1, alloc_size, persistent);
! 901:
! 902: DBG_ENTER("mysqlnd_net_init");
! 903: DBG_INF_FMT("persistent=%u", persistent);
! 904: if (net) {
! 905: net->persistent = persistent;
! 906: net->m = mysqlnd_mysqlnd_net_methods;
! 907:
! 908: {
! 909: unsigned int buf_size = MYSQLND_G(net_cmd_buffer_size); /* this is long, cast to unsigned int*/
! 910: net->m.set_client_option(net, MYSQLND_OPT_NET_CMD_BUFFER_SIZE, (char *) &buf_size TSRMLS_CC);
! 911: }
! 912: }
! 913: DBG_RETURN(net);
! 914: }
! 915: /* }}} */
! 916:
! 917:
! 918: /* {{{ mysqlnd_net_free */
! 919: PHPAPI void
! 920: mysqlnd_net_free(MYSQLND_NET * const net TSRMLS_DC)
! 921: {
! 922: DBG_ENTER("mysqlnd_net_free");
! 923:
! 924: if (net) {
! 925: zend_bool pers = net->persistent;
! 926:
! 927: net->m.free_contents(net TSRMLS_CC);
! 928: if (net->cmd_buffer.buffer) {
! 929: DBG_INF("Freeing cmd buffer");
! 930: mnd_pefree(net->cmd_buffer.buffer, pers);
! 931: net->cmd_buffer.buffer = NULL;
! 932: }
! 933: if (net->stream) {
! 934: DBG_INF_FMT("Freeing stream. abstract=%p", net->stream->abstract);
! 935: if (pers) {
! 936: php_stream_free(net->stream, PHP_STREAM_FREE_CLOSE_PERSISTENT | PHP_STREAM_FREE_RSRC_DTOR);
! 937: } else {
! 938: php_stream_free(net->stream, PHP_STREAM_FREE_CLOSE);
! 939: }
! 940: net->stream = NULL;
! 941: }
! 942: mnd_pefree(net, pers);
! 943: }
! 944: DBG_VOID_RETURN;
! 945: }
! 946: /* }}} */
! 947:
! 948:
! 949: /* {{{ _mysqlnd_plugin_get_plugin_net_data */
! 950: PHPAPI void ** _mysqlnd_plugin_get_plugin_net_data(const MYSQLND_NET * net, unsigned int plugin_id TSRMLS_DC)
! 951: {
! 952: DBG_ENTER("_mysqlnd_plugin_get_plugin_net_data");
! 953: DBG_INF_FMT("plugin_id=%u", plugin_id);
! 954: if (!net || plugin_id >= mysqlnd_plugin_count()) {
! 955: return NULL;
! 956: }
! 957: DBG_RETURN((void *)((char *)net + sizeof(MYSQLND_NET) + plugin_id * sizeof(void *)));
! 958: }
! 959: /* }}} */
! 960:
! 961:
! 962: /* {{{ mysqlnd_net_get_methods */
! 963: PHPAPI struct st_mysqlnd_net_methods *
! 964: mysqlnd_net_get_methods()
! 965: {
! 966: return &mysqlnd_mysqlnd_net_methods;
! 967: }
! 968: /* }}} */
! 969:
! 970:
! 971: /*
! 972: * Local variables:
! 973: * tab-width: 4
! 974: * c-basic-offset: 4
! 975: * End:
! 976: * vim600: noet sw=4 ts=4 fdm=marker
! 977: * vim<600: noet sw=4 ts=4
! 978: */
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>