diff --git a/NEWS b/NEWS index 460051eed719..87c17744d5fb 100644 --- a/NEWS +++ b/NEWS @@ -596,7 +596,8 @@ PHP NEWS . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream socket context options. (Jakub Zelenka) . Added so_reuseaddr streams context socket option that allows disabling - address reuse. (Jakub Zelenka) + address resuse. (Jakub Zelenka) + . Added so_linger stream socket context option. (Jakub Zelenka) . Fixed bug GH-20370 (User stream filters could violate typed property constraints). (alexandre-daubois) . Allowed filtered streams to be casted as fd for select. (Jakub Zelenka) diff --git a/UPGRADING b/UPGRADING index b988847c8f53..012557dc5b63 100644 --- a/UPGRADING +++ b/UPGRADING @@ -378,6 +378,10 @@ PHP 8.6 UPGRADE NOTES . Added stream socket context options so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt that allow setting socket keepalive options. + . Added stream socket context option so_linger that sets SO_LINGER on TCP + 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. . Allowed casting 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_linger.phpt b/ext/standard/tests/network/so_linger.phpt new file mode 100644 index 000000000000..ee0561b77381 --- /dev/null +++ b/ext/standard/tests/network/so_linger.phpt @@ -0,0 +1,82 @@ +--TEST-- +stream_socket_server() and stream_socket_client() SO_LINGER context option test +--EXTENSIONS-- +sockets +--FILE-- + [ + 'so_linger' => 10, + ] +]); + +$server = stream_socket_server("tcp://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $server_context); + +if (!$server) { + die('Unable to create server'); +} + +$addr = stream_socket_get_name($server, false); +$port = (int)substr(strrchr($addr, ':'), 1); + +// Test client with SO_LINGER enabled +$client_context = stream_context_create([ + 'socket' => [ + 'so_linger' => 8, + ] +]); + +$client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, 30, + STREAM_CLIENT_CONNECT, $client_context); + +if (!$client) { + die('Unable to create client'); +} + +$accepted = stream_socket_accept($server, 1); + +if (!$accepted) { + die('Unable to accept connection'); +} + +// macOS expresses SO_LINGER in ticks, SO_LINGER_SEC in seconds. +$so_linger = defined('SO_LINGER_SEC') ? SO_LINGER_SEC : SO_LINGER; + +// Verify the listening socket +$listen_sock = socket_import_stream($server); +$listen_linger = socket_get_option($listen_sock, SOL_SOCKET, $so_linger); +echo "Listen SO_LINGER\n"; +var_dump($listen_linger['l_onoff'] > 0); +var_dump($listen_linger['l_linger']); + +// Verify server side (accepted connection, inherits from the listening socket) +$server_sock = socket_import_stream($accepted); +$server_linger = socket_get_option($server_sock, SOL_SOCKET, $so_linger); +echo "Server SO_LINGER\n"; +var_dump($server_linger['l_onoff'] > 0); +var_dump($server_linger['l_linger']); + +// Verify client side +$client_sock = socket_import_stream($client); +$client_linger = socket_get_option($client_sock, SOL_SOCKET, $so_linger); +echo "Client SO_LINGER\n"; +var_dump($client_linger['l_onoff'] > 0); +var_dump($client_linger['l_linger']); + +fclose($accepted); +fclose($client); +fclose($server); + +?> +--EXPECT-- +Listen SO_LINGER +bool(true) +int(10) +Server SO_LINGER +bool(true) +int(10) +Client SO_LINGER +bool(true) +int(8) diff --git a/main/network.c b/main/network.c index b4d3ded15196..836abaeac7dd 100644 --- a/main/network.c +++ b/main/network.c @@ -535,6 +535,24 @@ php_socket_t php_network_bind_socket_to_local_addr_ex(const char *host, unsigned /* Set socket values if provided */ if (sockvals != NULL) { +#ifdef SO_LINGER + if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) { + /* l_linger is an unsigned short on Windows, so clamp rather than + * truncate: a truncated value may still be in range and would then + * be applied silently (e.g. 65536 becoming 0, an abortive close). */ + unsigned short secs = sockvals->linger > USHRT_MAX + ? USHRT_MAX : (unsigned short)sockvals->linger; + struct linger linger_val = { + .l_onoff = (sockvals->linger > 0), + .l_linger = sockvals->linger > 0 ? secs : 0 + }; +#ifdef SO_LINGER_SEC + setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val)); +#else + setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); +#endif + } +#endif #if defined(TCP_KEEPIDLE) if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); @@ -1021,6 +1039,24 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh /* Set socket values if provided */ if (sockvals != NULL) { +#ifdef SO_LINGER + if (sockvals->mask & PHP_SOCKVAL_SO_LINGER) { + /* l_linger is an unsigned short on Windows, so clamp rather than + * truncate: a truncated value may still be in range and would then + * be applied silently (e.g. 65536 becoming 0, an abortive close). */ + unsigned short secs = sockvals->linger > USHRT_MAX + ? USHRT_MAX : (unsigned short)sockvals->linger; + struct linger linger_val = { + .l_onoff = (sockvals->linger > 0), + .l_linger = sockvals->linger > 0 ? secs : 0 + }; +#ifdef SO_LINGER_SEC + setsockopt(sock, SOL_SOCKET, SO_LINGER_SEC, (char*)&linger_val, sizeof(linger_val)); +#else + setsockopt(sock, SOL_SOCKET, SO_LINGER, (char*)&linger_val, sizeof(linger_val)); +#endif + } +#endif #if defined(TCP_KEEPIDLE) if (sockvals->mask & PHP_SOCKVAL_TCP_KEEPIDLE) { setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); diff --git a/main/php_network.h b/main/php_network.h index e6d3009a6c82..c966b7af0053 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -269,12 +269,14 @@ typedef struct { #define PHP_SOCKVAL_TCP_KEEPIDLE (1 << 1) #define PHP_SOCKVAL_TCP_KEEPCNT (1 << 2) #define PHP_SOCKVAL_TCP_KEEPINTVL (1 << 3) +#define PHP_SOCKVAL_SO_LINGER (1 << 4) #define PHP_SOCKVAL_IS_SET(sockvals, opt) ((sockvals)->mask & (opt)) typedef struct { unsigned int mask; int tcp_nodelay; + int linger; struct { int keepidle; int keepcnt; diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3eef731544de..a9e2fa2b1d05 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -759,6 +759,16 @@ static inline int php_tcp_sockop_bind(php_stream *stream, php_netstream_data_t * } #endif +#ifdef SO_LINGER + if (PHP_STREAM_XPORT_IS_TCP(stream) + && PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL + ) { + sockvals.mask |= PHP_SOCKVAL_SO_LINGER; + sockvals.linger = (int)zval_get_long(tmpzval); + } +#endif + #ifdef SO_KEEPALIVE if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ && PHP_STREAM_CONTEXT(stream) @@ -887,6 +897,16 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ sockopts |= STREAM_SOCKOP_TCP_NODELAY; } +#ifdef SO_LINGER + if (PHP_STREAM_XPORT_IS_TCP(stream) + && PHP_STREAM_CONTEXT(stream) + && (tmpzval = php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "so_linger")) != NULL + ) { + sockvals.mask |= PHP_SOCKVAL_SO_LINGER; + sockvals.linger = (int)zval_get_long(tmpzval); + } +#endif + #ifdef SO_KEEPALIVE if (PHP_STREAM_XPORT_IS_TCP(stream) /* SO_KEEPALIVE is only applicable for TCP */ && PHP_STREAM_CONTEXT(stream)