diff --git a/UPGRADING b/UPGRADING index b55cb53a2bad..3d8aa09a040e 100644 --- a/UPGRADING +++ b/UPGRADING @@ -435,6 +435,12 @@ PHP 8.6 UPGRADE NOTES sockets. A positive value enables lingering for that many seconds, zero or a negative value disables it. Values above 65535 are clamped as the linger time is limited to an unsigned short on some platforms. + . Added stream socket context options so_rcvbuf and so_sndbuf that set the + socket receive and send buffer sizes in bytes (SO_RCVBUF and SO_SNDBUF) on + TCP and UDP sockets. The value must be an integer between 1 and 2147483647, + any other value throws a ValueError. The operating system may round, cap or + otherwise adjust the requested size, and may stop sizing that buffer + automatically, so the size read back can differ from the one requested. . Allowed casting filtered streams as file descriptor for select. . Added the "write_seek_mode stream" filter parameter for the bz2, iconv, zlib, and string stream filters. This parameter must be set via an diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt new file mode 100644 index 000000000000..fdb76713cb05 --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf.phpt @@ -0,0 +1,94 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_RCVBUF and SO_SNDBUF context options test +--EXTENSIONS-- +sockets +--FILE-- + [ + 'so_rcvbuf' => intdiv($rcvbuf, 4), + 'so_sndbuf' => intdiv($sndbuf, 4), + ]]); +} + +function port($server): int { + return (int)substr(strrchr(stream_socket_get_name($server, false), ':'), 1); +} + +$control = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); + +if (!$control) { + die('Unable to create server'); +} + +[$rcvbuf, $sndbuf] = buffers($control); + +$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, context($rcvbuf, $sndbuf)); + +if (!$server) { + die('Unable to create server'); +} + +echo "Listen buffers\n"; +[$listen_rcvbuf, $listen_sndbuf] = buffers($server); +var_dump($listen_rcvbuf < $rcvbuf); +var_dump($listen_sndbuf < $sndbuf); + +// A connection is compared against another connection: some systems size the +// receive buffer of a connected socket on their own. +$control_client = stream_socket_client("tcp://127.0.0.1:" . port($server), $errno, $errstr, 30); + +if (!$control_client) { + die('Unable to create client'); +} + +$control_accepted = stream_socket_accept($server, 1); + +if (!$control_accepted) { + die('Unable to accept connection'); +} + +[, $client_sndbuf] = buffers($control_client); + +$client = stream_socket_client("tcp://127.0.0.1:" . port($server), $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, context($client_sndbuf, $client_sndbuf)); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +echo "Client buffers\n"; +[, $client_sndbuf2] = buffers($client); +var_dump($client_sndbuf2 < $client_sndbuf); + +fclose($accepted); +fclose($control_accepted); +fclose($client); +fclose($control_client); +fclose($server); +fclose($control); + +?> +--EXPECT-- +Listen buffers +bool(true) +bool(true) +Client buffers +bool(true) diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt new file mode 100644 index 000000000000..5f94b15691de --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_error.phpt @@ -0,0 +1,33 @@ +--TEST-- +SO_RCVBUF and SO_SNDBUF context options reject invalid values +--FILE-- + [$option => $value]]); + try { + @stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); + } catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +} + +$context = stream_context_create(['socket' => ['so_rcvbuf' => 0]]); +try { + @stream_socket_client("tcp://127.0.0.1:1", $errno, $errstr, 1, + STREAM_CLIENT_CONNECT, $context); +} catch (ValueError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_sndbuf' must be between 1 and 2147483647 +ValueError: stream context option 'so_rcvbuf' must be between 1 and 2147483647 diff --git a/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt b/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt new file mode 100644 index 000000000000..82dcb3c83ffe --- /dev/null +++ b/ext/standard/tests/network/so_rcvbuf_sndbuf_udp.phpt @@ -0,0 +1,47 @@ +--TEST-- +stream_socket_server() SO_RCVBUF and SO_SNDBUF context options test with UDP +--EXTENSIONS-- +sockets +--FILE-- + [ + 'so_rcvbuf' => intdiv($rcvbuf, 4), + 'so_sndbuf' => intdiv($sndbuf, 4), +]]); + +$server = stream_socket_server("udp://127.0.0.1:0", $errno, $errstr, STREAM_SERVER_BIND, $context); + +if (!$server) { + die('Unable to create server'); +} + +[$server_rcvbuf, $server_sndbuf] = buffers($server); +echo "Server buffers\n"; +var_dump($server_rcvbuf < $rcvbuf); +var_dump($server_sndbuf < $sndbuf); + +fclose($server); +fclose($control); + +?> +--EXPECT-- +Server buffers +bool(true) +bool(true) diff --git a/main/network.c b/main/network.c index 836abaeac7dd..01e4f2e778d4 100644 --- a/main/network.c +++ b/main/network.c @@ -443,6 +443,20 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, } /* }}} */ +static void php_network_set_socket_buffers(php_socket_t sock, const php_sockvals *sockvals) +{ +#ifdef SO_RCVBUF + if (sockvals->mask & PHP_SOCKVAL_SO_RCVBUF) { + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&sockvals->rcvbuf, sizeof(sockvals->rcvbuf)); + } +#endif +#ifdef SO_SNDBUF + if (sockvals->mask & PHP_SOCKVAL_SO_SNDBUF) { + setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sockvals->sndbuf, sizeof(sockvals->sndbuf)); + } +#endif +} + /* Bind to a local IP address. * Returns the bound socket, or -1 on failure. * */ @@ -573,6 +587,7 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); } #endif + php_network_set_socket_buffers(sock, sockvals); } n = bind(sock, sa, socklen); @@ -1077,6 +1092,7 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, (char*)&sockvals->keepalive.keepcnt, sizeof(sockvals->keepalive.keepcnt)); } #endif + php_network_set_socket_buffers(sock, sockvals); } n = php_network_connect_socket(sock, sa, socklen, asynchronous, diff --git a/main/php_network.h b/main/php_network.h index c966b7af0053..c93a519911f0 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -270,6 +270,8 @@ typedef struct { #define PHP_SOCKVAL_TCP_KEEPCNT (1 << 2) #define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3) #define PHP_SOCKVAL_SO_LINGER (1 << 4) +#define PHP_SOCKVAL_SO_RCVBUF (1 << 5) +#define PHP_SOCKVAL_SO_SNDBUF (1 << 6) #define PHP_SOCKVAL_IS_SET(sockvals, opt) ((sockvals)->mask & (opt)) @@ -277,6 +279,8 @@ typedef struct { unsigned int mask; int tcp_nodelay; int linger; + int rcvbuf; + int sndbuf; struct { int keepidle; int keepcnt; diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index a9e2fa2b1d05..da5c73a02977 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -677,6 +677,45 @@ static inline char *parse_ip_address(php_stream_xport_param *xparam, int *portno return parse_ip_address_ex(xparam->inputs.name, xparam->inputs.namelen, portno, xparam->want_errortext, &xparam->outputs.error_text); } +static int php_sockop_parse_buffer_sizes(php_stream *stream, php_sockvals *sockvals) +{ + zval *tmpzval; + + if (!PHP_STREAM_CONTEXT(stream)) { + return 0; + } + +#ifdef SO_RCVBUF + if ((tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_rcvbuf")) != NULL) { + zend_long bufsize = zval_get_long(tmpzval); + + if (bufsize < 1 || bufsize > INT_MAX) { + zend_value_error("stream context option 'so_rcvbuf' must be between 1 and %d", INT_MAX); + return -1; + } + + sockvals->mask |= PHP_SOCKVAL_SO_RCVBUF; + sockvals->rcvbuf = (int) bufsize; + } +#endif + +#ifdef SO_SNDBUF + if ((tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_sndbuf")) != NULL) { + zend_long bufsize = zval_get_long(tmpzval); + + if (bufsize < 1 || bufsize > INT_MAX) { + zend_value_error("stream context option 'so_sndbuf' must be between 1 and %d", INT_MAX); + return -1; + } + + sockvals->mask |= PHP_SOCKVAL_SO_SNDBUF; + sockvals->sndbuf = (int) bufsize; + } +#endif + + return 0; +} + static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t *sock, php_stream_xport_param *xparam) { @@ -720,6 +759,11 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * return -1; } + if (php_sockop_parse_buffer_sizes(stream, &sockvals) == -1) { + efree(host); + return -1; + } + #ifdef IPV6_V6ONLY if (PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "ipv6_v6only")) != NULL @@ -868,6 +912,11 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ return -1; } + if (php_sockop_parse_buffer_sizes(stream, &sockvals) == -1) { + efree(host); + return -1; + } + if (PHP_STREAM_CONTEXT(stream) && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "bindto")) != NULL) { if (Z_TYPE_P(tmpzval) != IS_STRING) { if (xparam->want_errortext) {