diff --git a/NEWS b/NEWS index 5acaf162608b..3aefec858702 100644 --- a/NEWS +++ b/NEWS @@ -15,6 +15,8 @@ PHP NEWS . Uploading multiple files with the same field name is now possible by passing an array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS. (Sjoerd Langkemper) + . Added CURLOPT_OPENSOCKETFUNCTION to allow or refuse each connection libcurl + is about to make. (Xavier Leune) - FPM: . Use recommended TYPE, HELP order for OpenMetrics metadata. diff --git a/UPGRADING b/UPGRADING index 3fc692ac9ac0..465e20661a5c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -76,6 +76,14 @@ PHP 8.7 UPGRADE NOTES array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS. . Support for upstream options CURLOPT_ECH, CURLOPT_HAPROXY_CLIENT_IP, and CURLOPT_SERVER_RESPONSE_TIMEOUT_MS. + . Added CURLOPT_OPENSOCKETFUNCTION. Its callback has the signature + function(CurlHandle $handle, string $address, int $port): bool and is + invoked after DNS resolution and before connect(), for each socket libcurl + is about to create. $address is the IP address, or the path of a UNIX + socket, whose $port is 0. Returning false refuses the connection, and + libcurl tries the next address, if any. A connection reused from a pool is + not vetted again; CURLOPT_PREREQFUNCTION runs before every request. + RFC: https://wiki.php.net/rfc/curl_opensocketfunction - Intl: . Added IntlRelativeDateTimeFormatter to format relative date and time offsets @@ -172,6 +180,7 @@ PHP 8.7 UPGRADE NOTES CURLOPT_SERVER_RESPONSE_TIMEOUT_MS. . CURLOPT_MAIL_RCPT_ALLOWFAILS is the correctly spelled alternative to CURLOPT_MAIL_RCPT_ALLLOWFAILS. + . CURLOPT_OPENSOCKETFUNCTION. ======================================== 11. Changes to INI File Handling diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index bac8694da631..13873a98c4d1 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -257,6 +257,11 @@ * @cvalue CURLOPT_NOSIGNAL */ const CURLOPT_NOSIGNAL = UNKNOWN; +/** + * @var int + * @cvalue CURLOPT_OPENSOCKETFUNCTION + */ +const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN; /** * @var int * @cvalue CURLOPT_PORT diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index bf47f682b1b2..d68ce84542e9 100644 --- a/ext/curl/curl_arginfo.h +++ b/ext/curl/curl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit curl.stub.php instead. - * Stub hash: 74051be5006c0e0d66dced8f60f2fa4128ecad48 */ + * Stub hash: 9703f6327501da9ceb4a65550df9ad498c4d4069 */ #include "zend_attributes.h" #include "zend_constants.h" @@ -283,6 +283,7 @@ static void register_curl_symbols(int module_number) REGISTER_LONG_CONSTANT("CURLOPT_NOBODY", CURLOPT_NOBODY, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_NOPROGRESS", CURLOPT_NOPROGRESS, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_NOSIGNAL", CURLOPT_NOSIGNAL, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLOPT_OPENSOCKETFUNCTION", CURLOPT_OPENSOCKETFUNCTION, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_PORT", CURLOPT_PORT, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_POST", CURLOPT_POST, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_POSTFIELDS", CURLOPT_POSTFIELDS, CONST_PERSISTENT); diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index f8550dc3f740..9fec873dbe02 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -83,6 +83,7 @@ typedef struct { zend_fcall_info_cache xferinfo; zend_fcall_info_cache fnmatch; zend_fcall_info_cache debug; + zend_fcall_info_cache opensocket; #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ zend_fcall_info_cache prereq; #endif diff --git a/ext/curl/interface.c b/ext/curl/interface.c index b94949b784c2..861f48460626 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -26,7 +26,15 @@ #ifdef PHP_WIN32 #include +#include #include +#else +#include +#include +#include +# ifdef AF_UNIX +# include +# endif #endif #include @@ -36,6 +44,8 @@ #include "ext/standard/info.h" #include "ext/standard/file.h" #include "ext/standard/url.h" +#include "ext/standard/php_net.h" +#include "php_network.h" #include "curl_private.h" #ifdef __GNUC__ @@ -499,6 +509,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n) zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.debug); } + if (ZEND_FCC_INITIALIZED(curl->handlers.opensocket)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.opensocket); + } + #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(curl->handlers.prereq)) { zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.prereq); @@ -837,6 +851,133 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key, } #endif +/* {{{ curl_opensocket_unix_path + The path of a UNIX domain socket, as libcurl built it. A Linux abstract socket + name starts with a NUL byte, which is kept: it is part of the name. */ +#if defined(AF_UNIX) && !defined(PHP_WIN32) +static zend_string *curl_opensocket_unix_path(const struct curl_sockaddr *address) +{ + const struct sockaddr_un *sa_un = (const struct sockaddr_un *) &address->addr; + size_t offset = offsetof(struct sockaddr_un, sun_path); + size_t len = address->addrlen > offset ? address->addrlen - offset : 0; + + if (len > sizeof(sa_un->sun_path)) { + len = sizeof(sa_un->sun_path); + } + if (len > 0 && sa_un->sun_path[0] != '\0') { + /* A pathname: libcurl may count the terminating NUL in addrlen. */ + len = zend_strnlen(sa_un->sun_path, len); + } + + return zend_string_init(sa_un->sun_path, len, 0); +} +#endif +/* }}} */ + +/* {{{ curl_opensocketfunction + The PHP callback only allows or refuses the connection: no descriptor is ever + exposed to userland. */ +static curl_socket_t curl_opensocketfunction(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) +{ + php_curl *ch = (php_curl *) clientp; + + /* Unreachable in normal operation: libcurl's own socket creation is restored + * whenever no callback is installed, so the trampoline is only registered + * while the FCC is initialized. Refuse rather than connect, so that an + * unexpected state can never silently bypass the filter. */ + if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.opensocket))) { + return CURL_SOCKET_BAD; + } + +#if PHP_CURL_DEBUG + fprintf(stderr, "curl_opensocketfunction() called\n"); + fprintf(stderr, "purpose = %d, family = %d, socktype = %d, protocol = %d\n", purpose, address->family, address->socktype, address->protocol); +#endif + + /* Whatever cannot be described to the callback is refused rather than + * connected: a policy must never be bypassed by an endpoint it was not + * shown. CURLSOCKTYPE_IPCXN is the only purpose libcurl currently uses; a + * future one may well not be a destination address at all. */ + if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN)) { + return CURL_SOCKET_BAD; + } + + zval args[3]; + zval retval; + curl_socket_t rval = CURL_SOCKET_BAD; + + switch (address->family) { + case AF_INET: + ZVAL_LONG(&args[2], ntohs(((const struct sockaddr_in *) &address->addr)->sin_port)); + break; +#ifdef AF_INET6 + case AF_INET6: + ZVAL_LONG(&args[2], ntohs(((const struct sockaddr_in6 *) &address->addr)->sin6_port)); + break; +#endif +#if defined(AF_UNIX) && !defined(PHP_WIN32) + case AF_UNIX: + /* CURLOPT_UNIX_SOCKET_PATH or CURLOPT_ABSTRACT_UNIX_SOCKET: a path + * has no port, reported as 0 like CURLINFO_PRIMARY_PORT does. */ + ZVAL_LONG(&args[2], 0); + break; +#endif + default: + return CURL_SOCKET_BAD; + } + +#if defined(AF_UNIX) && !defined(PHP_WIN32) + if (address->family == AF_UNIX) { + ZVAL_STR(&args[1], curl_opensocket_unix_path(address)); + } else +#endif + { + zend_string *ip = php_inet_ntop(&address->addr); + if (UNEXPECTED(ip == NULL)) { + return CURL_SOCKET_BAD; + } + ZVAL_STR(&args[1], ip); + } + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + + php_curl_call_callback(ch, &ch->handlers.opensocket, &retval, /* argc */ 3, args); + + /* retval is undefined when the callback threw, and must not be trusted when + * an exception was thrown after it was set, e.g. by a destructor: either + * way the connection is refused and the exception propagates out of + * curl_exec(). */ + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + if (UNEXPECTED(EG(exception))) { + /* Refused. */ + } else if (EXPECTED(Z_TYPE(retval) == IS_TRUE || Z_TYPE(retval) == IS_FALSE)) { + if (Z_TYPE(retval) == IS_TRUE) { + rval = socket(address->family, address->socktype, address->protocol); + /* Where socket() supports SOCK_NONBLOCK, libcurl only makes a + * socket non-blocking if it came from this callback, which it + * checks after the call: if the callback removed the option, + * the socket would stay blocking and ignore the connect + * timeout. Doing it here is harmless otherwise. */ + if (rval != CURL_SOCKET_BAD && php_set_sock_blocking(rval, false) != SUCCESS) { + closesocket(rval); + rval = CURL_SOCKET_BAD; + } + } + } else { + zend_type_error("The CURLOPT_OPENSOCKETFUNCTION callback must return a bool"); + } + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + + return rval; +} +/* }}} */ + /* {{{ curl_read */ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx) { @@ -1154,6 +1295,7 @@ void init_curl_handle(php_curl *ch) ch->handlers.progress = empty_fcall_info_cache; ch->handlers.xferinfo = empty_fcall_info_cache; ch->handlers.fnmatch = empty_fcall_info_cache; + ch->handlers.opensocket = empty_fcall_info_cache; ch->handlers.debug = empty_fcall_info_cache; #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ ch->handlers.prereq = empty_fcall_info_cache; @@ -1332,6 +1474,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source) php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo); php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch); php_curl_copy_fcc_with_option(ch, CURLOPT_DEBUGDATA, &ch->handlers.debug, &source->handlers.debug); + php_curl_copy_fcc_with_option(ch, CURLOPT_OPENSOCKETDATA, &ch->handlers.opensocket, &source->handlers.opensocket); #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ php_curl_copy_fcc_with_option(ch, CURLOPT_PREREQDATA, &ch->handlers.prereq, &source->handlers.prereq); #endif @@ -1727,6 +1870,27 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch); HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_DEBUG, handlers.debug, curl_debug); + case CURLOPT_OPENSOCKETFUNCTION: { + bool installed = php_curl_set_callable_handler(&ch->handlers.opensocket, zvalue, is_array_config, "CURLOPT_OPENSOCKETFUNCTION"); + if (!installed || !ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + /* Restore libcurl's own socket creation, so that the trampoline + * is registered if and only if the FCC is initialized. A + * rejected callable also releases the previous handler, so + * leaving the option armed would point CURLOPT_OPENSOCKETDATA + * at a handle whose FCC is gone, and curl_copy_handle() would + * then carry that pointer into the copy. */ + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, NULL); + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, NULL); + if (!installed) { + return FAILURE; + } + break; + } + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, curl_opensocketfunction); + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, ch); + break; + } + #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PREREQ, handlers.prereq, curl_prereqfunction); #endif @@ -2925,6 +3089,9 @@ static void curl_free_obj(zend_object *object) if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) { zend_fcc_dtor(&ch->handlers.debug); } + if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + zend_fcc_dtor(&ch->handlers.opensocket); + } #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) { zend_fcc_dtor(&ch->handlers.prereq); @@ -3017,6 +3184,10 @@ static void _php_curl_reset_handlers(php_curl *ch) zend_fcc_dtor(&ch->handlers.debug); } + if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + zend_fcc_dtor(&ch->handlers.opensocket); + } + #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) { zend_fcc_dtor(&ch->handlers.prereq); diff --git a/ext/curl/tests/curl_opensocketfunction_abstract_unix_socket.phpt b/ext/curl/tests/curl_opensocketfunction_abstract_unix_socket.phpt new file mode 100644 index 000000000000..f04a9e950c09 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_abstract_unix_socket.phpt @@ -0,0 +1,30 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION reports an abstract UNIX socket name with its leading NUL byte +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +int(0) +bool(false) +bool(true) diff --git a/ext/curl/tests/curl_opensocketfunction_address_family.phpt b/ext/curl/tests/curl_opensocketfunction_address_family.phpt new file mode 100644 index 000000000000..bcdddc1ca216 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_address_family.phpt @@ -0,0 +1,62 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION reports the address and port of IPv4 and IPv6 endpoints +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +IPv4 literal +string(9) "127.0.0.1" +int(1) + +IPv6 literal +string(3) "::1" +int(1) + +IPv4-mapped IPv6 address +string(16) "::ffff:127.0.0.1" +int(1) + +IPv4-mapped IPv6 address, hexadecimal form +string(16) "::ffff:127.0.0.1" +int(1) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_clone.phpt b/ext/curl/tests/curl_opensocketfunction_clone.phpt new file mode 100644 index 000000000000..47cfcbe051c6 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_clone.phpt @@ -0,0 +1,59 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: clone, and changing the option from inside the callback +--EXTENSIONS-- +curl +--FILE-- + $h !== $clone) === []); + +echo "\nThe clone keeps refusing after the original's callback is removed\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, null); +curl_setopt($clone, CURLOPT_OPENSOCKETFUNCTION, fn (): bool => false); +var_dump(curl_exec($clone), curl_errno($clone) === CURLE_COULDNT_CONNECT); +var_dump(curl_exec($ch)); + +echo "\nThe callback removes itself and allows the connection\n"; +$calls = 0; +$ch = curl_init("{$host}/get.inc?test=method"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle) use (&$calls): bool { + $calls++; + curl_setopt($handle, CURLOPT_OPENSOCKETFUNCTION, null); + return true; +}); +var_dump(curl_exec($ch), $calls); +var_dump(curl_exec($ch), $calls); + +echo "Done\n"; +?> +--EXPECT-- +clone carries the callback, which is then invoked with the clone +string(3) "GET" +bool(true) + +The clone keeps refusing after the original's callback is removed +bool(false) +bool(true) +string(3) "GET" + +The callback removes itself and allows the connection +string(3) "GET" +int(1) +string(3) "GET" +int(1) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_file_scheme.phpt b/ext/curl/tests/curl_opensocketfunction_file_scheme.phpt new file mode 100644 index 000000000000..5c7e09315ddf --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_file_scheme.phpt @@ -0,0 +1,45 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION is not called for file://, which creates no socket +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +string(13) "not a socket +" +int(0) +bool(false) +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_gc.phpt b/ext/curl/tests/curl_opensocketfunction_gc.phpt new file mode 100644 index 000000000000..2a7f5f0842ad --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_gc.phpt @@ -0,0 +1,27 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: the handle/callback cycle is collectable +--EXTENSIONS-- +curl +--FILE-- +get() !== null); +gc_collect_cycles(); +var_dump($weak->get() === null); + +echo "Done"; +?> +--EXPECT-- +bool(true) +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_multi.phpt b/ext/curl/tests/curl_opensocketfunction_multi.phpt new file mode 100644 index 000000000000..80b744aedae7 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_multi.phpt @@ -0,0 +1,59 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION under curl_multi +--EXTENSIONS-- +curl +--FILE-- + $allow) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=method"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FORBID_REUSE, true); + $calls[$i] = 0; + curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function () use ($allow, $i, &$calls): bool { + $calls[$i]++; + return $allow; + }); + curl_multi_add_handle($mh, $ch); + $handles[$i] = $ch; +} + +do { + curl_multi_exec($mh, $active); + if ($active) { + curl_multi_select($mh); + } +} while ($active); + +$results = []; +while ($info = curl_multi_info_read($mh)) { + $results[array_search($info['handle'], $handles, true)] = $info['result']; +} +ksort($results); + +var_dump($calls[0] >= 1, $calls[1] >= 1); +var_dump(curl_multi_getcontent($handles[0])); +var_dump($results[0] === CURLE_OK); +var_dump($results[1] === CURLE_COULDNT_CONNECT); + +foreach ($handles as $ch) { + curl_multi_remove_handle($mh, $ch); +} + +echo "Done"; +?> +--EXPECT-- +bool(true) +bool(true) +string(3) "GET" +bool(true) +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_next_address.phpt b/ext/curl/tests/curl_opensocketfunction_next_address.phpt new file mode 100644 index 000000000000..7c991fb59d96 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_next_address.phpt @@ -0,0 +1,46 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: refusing an address makes libcurl try the next one +--EXTENSIONS-- +curl +--FILE-- +getMessage(), PHP_EOL; + } + var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); +} + +echo "Refusing every address\n"; +probe(function (CurlHandle $handle, string $address, int $port): bool { + echo "$address:$port\n"; + return false; +}); + +echo "\nThrowing on the first address\n"; +probe(function (CurlHandle $handle, string $address, int $port): bool { + echo "$address:$port\n"; + throw new Exception('refused'); +}); + +echo "Done"; +?> +--EXPECT-- +Refusing every address +127.0.0.2:1 +127.0.0.3:1 +bool(false) +bool(true) + +Throwing on the first address +127.0.0.2:1 +Exception: refused +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_redirect.phpt b/ext/curl/tests/curl_opensocketfunction_redirect.phpt new file mode 100644 index 000000000000..aec98662d042 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_redirect.phpt @@ -0,0 +1,61 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION can refuse a redirect hop +--EXTENSIONS-- +curl +--FILE-- + +--EXPECTF-- +Allowing every hop +string(3) "GET" +Array +( + [0] => 0 http://%s/get.inc?test=redirect&target=method&code=302 + [1] => 1 http://%s/get.inc?test=method +) + +Refusing the redirect hop +bool(false) +Array +( + [0] => 0 http://%s/get.inc?test=redirect&target=method&code=302 + [1] => 1 http://%s/get.inc?test=method +) +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_reuse.phpt b/ext/curl/tests/curl_opensocketfunction_reuse.phpt new file mode 100644 index 000000000000..10452d035550 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_reuse.phpt @@ -0,0 +1,67 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION is not invoked for a connection reused from the pool +--EXTENSIONS-- +curl +--FILE-- + true, CURLOPT_PROXY => '']); +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, $count); +var_dump(curl_exec($ch), curl_exec($ch), $calls); + +echo "\nSet after a transfer: the pooled connection is reused without the callback\n"; +$calls = 0; +$ch = curl_init($url); +curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_PROXY => '']); +var_dump(curl_exec($ch)); +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, $count); +var_dump(curl_exec($ch), $calls); + +echo "\nCURLOPT_FRESH_CONNECT opens a new connection, which is vetted\n"; +curl_setopt($ch, CURLOPT_FRESH_CONNECT, true); +var_dump(curl_exec($ch), $calls); + +echo "\nCURLOPT_PREREQFUNCTION runs before every request, on a reused connection too\n"; +curl_setopt($ch, CURLOPT_FRESH_CONNECT, false); +curl_setopt($ch, CURLOPT_PREREQFUNCTION, function (CurlHandle $handle, string $ip, string $localIp, int $port, int $localPort): int { + var_dump($ip); + return CURL_PREREQFUNC_OK; +}); +var_dump(curl_exec($ch), $calls); + +echo "Done\n"; +?> +--EXPECT-- +Set before the first transfer: only the new connection is vetted +string(1) "1" +string(1) "1" +int(1) + +Set after a transfer: the pooled connection is reused without the callback +string(1) "2" +string(1) "2" +int(0) + +CURLOPT_FRESH_CONNECT opens a new connection, which is vetted +string(1) "3" +int(1) + +CURLOPT_PREREQFUNCTION runs before every request, on a reused connection too +string(9) "127.0.0.1" +string(1) "3" +int(1) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_share_free.phpt b/ext/curl/tests/curl_opensocketfunction_share_free.phpt new file mode 100644 index 000000000000..cdaaebeb1b4d --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_share_free.phpt @@ -0,0 +1,63 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: freeing the handle while its connection stays pooled +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +string(1) "1" +string(1) "1" +string(1) "1" +string(1) "2" +Done diff --git a/ext/curl/tests/curl_opensocketfunction_unix_socket.phpt b/ext/curl/tests/curl_opensocketfunction_unix_socket.phpt new file mode 100644 index 000000000000..143e8edeba9a --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_unix_socket.phpt @@ -0,0 +1,38 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION reports the path and port 0 for a UNIX socket +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + 100) { + $path = '/tmp/php-curl-' . getmypid() . '.sock'; +} + +$ch = curl_init(); +curl_setopt($ch, CURLOPT_URL, 'http://localhost/'); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, $path); +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle, string $address, int $port) use ($path): bool { + var_dump($address === $path, $port); + return false; +}); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); + +echo "Done"; +?> +--EXPECT-- +bool(true) +int(0) +bool(false) +bool(true) +Done diff --git a/ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt b/ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt new file mode 100644 index 000000000000..ec8c7397d30e --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt @@ -0,0 +1,249 @@ +--TEST-- +Curl option CURLOPT_OPENSOCKETFUNCTION +--EXTENSIONS-- +curl +--FILE-- +get() === null); +curl_setopt($ch3, CURLOPT_URL, "{$host}/get.inc"); +curl_setopt($ch3, CURLOPT_RETURNTRANSFER, true); +var_dump(curl_exec($ch3)); +unset($ch3); + +echo "\nTesting with no return value\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function () {}); +try { + curl_exec($ch); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting with an invalid return type\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function () { return 1; }); +try { + curl_exec($ch); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting with an exception thrown from the callback\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function () { + throw new \Exception('refused'); +}); +try { + curl_exec($ch); +} catch (\Exception $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); + +echo "\nTesting with an exception thrown by a destructor after returning true\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (): bool { + $o = new class { function __destruct() { throw new \Exception('refused by destructor'); } }; + return true; +}); +try { + curl_exec($ch); +} catch (\Exception $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} +var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); + +echo "\nTesting with an invalid option value\n"; +try { + curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, 42); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting with an invalid option callback\n"; +try { + curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, 'function_does_not_exist'); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting through curl_setopt_array()\n"; +try { + curl_setopt_array($ch, [CURLOPT_OPENSOCKETFUNCTION => 42]); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting a rejected callable followed by curl_copy_handle()\n"; +$src = curl_init(); +curl_setopt($src, CURLOPT_URL, "{$host}/get.inc"); +curl_setopt($src, CURLOPT_RETURNTRANSFER, true); +curl_setopt($src, CURLOPT_OPENSOCKETFUNCTION, $refuse); +try { + curl_setopt($src, CURLOPT_OPENSOCKETFUNCTION, 'function_does_not_exist'); +} catch (\TypeError $e) { + echo $e::class, PHP_EOL; +} +/* A rejected callable also releases the previously installed handler, so the + * option is left disarmed and libcurl creates the socket itself again. The copy + * must not carry a CURLOPT_OPENSOCKETDATA pointer to the source handle, which + * would dangle as soon as the source is released. */ +$copy = curl_copy_handle($src); +unset($src); +var_dump(curl_exec($copy)); +unset($copy); + +echo "\nTesting with null as the callback\n"; +$cbn = function (): bool { return false; }; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, $cbn); +$weakNull = WeakReference::create($cbn); +unset($cbn); +var_dump(curl_exec($ch)); +var_dump(curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, null)); +/* null must release the callback and hand socket creation back to libcurl. */ +var_dump($weakNull->get() === null); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); + +echo "\nDone"; +?> +--EXPECT-- +int(20163) + +Allowing the connection +string(25) "Hello World! +Hello World!" +int(0) +bool(true) + +Refusing the connection +bool(false) +bool(true) + +Testing with curl_copy_handle +string(25) "Hello World! +Hello World!" +bool(true) +string(25) "Hello World! +Hello World!" +bool(true) +int(0) +bool(false) +bool(true) + +Testing with curl_reset +bool(true) +string(25) "Hello World! +Hello World!" + +Testing with no return value +TypeError: The CURLOPT_OPENSOCKETFUNCTION callback must return a bool + +Testing with an invalid return type +TypeError: The CURLOPT_OPENSOCKETFUNCTION callback must return a bool + +Testing with an exception thrown from the callback +Exception: refused +bool(true) + +Testing with an exception thrown by a destructor after returning true +Exception: refused by destructor +bool(true) + +Testing with an invalid option value +TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_OPENSOCKETFUNCTION, no array or string given + +Testing with an invalid option callback +TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_OPENSOCKETFUNCTION, function "function_does_not_exist" not found or invalid function name + +Testing through curl_setopt_array() +TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_OPENSOCKETFUNCTION, no array or string given + +Testing a rejected callable followed by curl_copy_handle() +TypeError +string(25) "Hello World! +Hello World!" + +Testing with null as the callback +bool(false) +bool(true) +bool(true) +string(25) "Hello World! +Hello World!" +int(0) + +Done diff --git a/ext/curl/tests/keepalive_server.inc b/ext/curl/tests/keepalive_server.inc new file mode 100644 index 000000000000..d81dc57e4df7 --- /dev/null +++ b/ext/curl/tests/keepalive_server.inc @@ -0,0 +1,63 @@ + ['pipe', 'w']], $pipes); + $address = trim((string) fgets($pipes[1])); + if (!preg_match('/^127\.0\.0\.1:\d+$/', $address)) { + proc_terminate($process); + die("Failed to start the keep-alive server: $address"); + } + + register_shutdown_function(function () use ($process, $pipes): void { + proc_terminate($process); + fclose($pipes[1]); + proc_close($process); + }); + + return "http://$address"; +}