diff --git a/NEWS b/NEWS index 5acaf162608b..4042a8ed1abe 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 address libcurl + is about to connect to. (Xavier Leune, Nicolas Grekas) - FPM: . Use recommended TYPE, HELP order for OpenMetrics metadata. diff --git a/UPGRADING b/UPGRADING index 3fc692ac9ac0..3700ecba896a 100644 --- a/UPGRADING +++ b/UPGRADING @@ -76,6 +76,13 @@ 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 to register a callback that allows or + refuses each address libcurl is about to connect to, after DNS resolution + and before connect(). The callback receives the CurlHandle, the address + and the port. It must return true to let libcurl create the socket, or + false to make libcurl try the next address. The address is an IP address, + or the path of a UNIX domain socket with port 0. The callback isn't called + when a connection is reused, which CURLOPT_PREREQFUNCTION can check. - Intl: . Added IntlRelativeDateTimeFormatter to format relative date and time offsets @@ -172,6 +179,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..e726ec114c6c 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -26,7 +26,15 @@ #ifdef PHP_WIN32 #include +#include #include +#else +#include +#include +# ifdef AF_UNIX +# include +# define PHP_CURL_HAVE_AF_UNIX 1 +# 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,100 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key, } #endif +/* {{{ curl_opensocket */ +static curl_socket_t curl_opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) +{ + php_curl *ch = (php_curl *) clientp; + curl_socket_t sock = CURL_SOCKET_BAD; + +#if PHP_CURL_DEBUG + fprintf(stderr, "curl_opensocket() called\n"); + fprintf(stderr, "clientp = %p, purpose = %d, family = %d, socktype = %d, protocol = %d\n", clientp, purpose, address->family, address->socktype, address->protocol); +#endif + + /* Refuse what can't be passed to the callback. Once the callback threw, refuse + * the remaining addresses too. */ + if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN || !ZEND_FCC_INITIALIZED(ch->handlers.opensocket) || EG(exception))) { + return sock; + } + + zval args[3]; + zend_long port = 0; + bool is_ip = true; + + switch (address->family) { + case AF_INET: + port = ntohs(((const struct sockaddr_in *) &address->addr)->sin_port); + break; +#ifdef AF_INET6 + case AF_INET6: + port = ntohs(((const struct sockaddr_in6 *) &address->addr)->sin6_port); + break; +#endif +#ifdef PHP_CURL_HAVE_AF_UNIX + case AF_UNIX: { + /* The path of the socket, with its leading NUL byte for an abstract one */ + const struct sockaddr_un *sa_un = (const struct sockaddr_un *) &address->addr; + size_t len = 0; + if (address->addrlen > offsetof(struct sockaddr_un, sun_path)) { + len = MIN(address->addrlen - offsetof(struct sockaddr_un, sun_path), sizeof(sa_un->sun_path)); + if (len && sa_un->sun_path[0] != '\0') { + const char *nul = memchr(sa_un->sun_path, '\0', len); + if (nul) { + len = nul - sa_un->sun_path; + } + } + } + ZVAL_STRINGL(&args[1], sa_un->sun_path, len); + is_ip = false; + break; + } +#endif + default: + return sock; + } + + if (is_ip) { + zend_string *ip = php_inet_ntop(&address->addr); + if (UNEXPECTED(!ip)) { + return sock; + } + ZVAL_STR(&args[1], ip); + } + + zval retval; + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + ZVAL_LONG(&args[2], port); + + php_curl_call_callback(ch, &ch->handlers.opensocket, &retval, /* argc */ 3, args); + + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + if (Z_TYPE(retval) == IS_TRUE) { + if (!EG(exception)) { + /* What libcurl does when no callback is set */ + sock = socket(address->family, address->socktype, address->protocol); + /* libcurl doesn't make the socket non-blocking if the callback removed itself */ + if (sock != CURL_SOCKET_BAD && php_set_sock_blocking(sock, false) != SUCCESS) { + closesocket(sock); + sock = CURL_SOCKET_BAD; + } + } + } else if (Z_TYPE(retval) != IS_FALSE) { + 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 sock; +} +/* }}} */ + /* {{{ curl_read */ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx) { @@ -1155,6 +1263,7 @@ void init_curl_handle(php_curl *ch) ch->handlers.xferinfo = empty_fcall_info_cache; ch->handlers.fnmatch = empty_fcall_info_cache; ch->handlers.debug = empty_fcall_info_cache; + ch->handlers.opensocket = empty_fcall_info_cache; #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ ch->handlers.prereq = empty_fcall_info_cache; #endif @@ -1332,6 +1441,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 +1837,21 @@ 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 result = php_curl_set_callable_handler(&ch->handlers.opensocket, zvalue, is_array_config, "CURLOPT_OPENSOCKETFUNCTION"); + /* Without a callback, libcurl creates the sockets itself */ + if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, curl_opensocket); + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, ch); + } else { + curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, NULL); + } + if (!result) { + return FAILURE; + } + 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 +3050,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 +3145,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..c6eadd4e6887 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_abstract_unix_socket.phpt @@ -0,0 +1,30 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: abstract UNIX domain socket +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +int(0) +bool(false) +bool(true) +Done diff --git a/ext/curl/tests/curl_opensocketfunction_address.phpt b/ext/curl/tests/curl_opensocketfunction_address.phpt new file mode 100644 index 000000000000..0ef1429fedd1 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_address.phpt @@ -0,0 +1,37 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: IPv6 addresses +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +string(3) "::1" +int(1) +string(16) "::ffff:127.0.0.1" +int(2) +string(16) "::ffff:127.0.0.1" +int(3) +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..d9ccd05f3593 --- /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..87659b69839a --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_gc.phpt @@ -0,0 +1,26 @@ +--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..1924d98cacea --- /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_share_free.phpt b/ext/curl/tests/curl_opensocketfunction_share_free.phpt new file mode 100644 index 000000000000..775819da323e --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_share_free.phpt @@ -0,0 +1,57 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: freeing the handle while its connection stays pooled +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +string(3) "GET" +string(3) "GET" +string(3) "GET" +string(3) "GET" +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..d39b577ea948 --- /dev/null +++ b/ext/curl/tests/curl_opensocketfunction_unix_socket.phpt @@ -0,0 +1,29 @@ +--TEST-- +CURLOPT_OPENSOCKETFUNCTION: UNIX domain socket +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--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..b30c76d637cf --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt @@ -0,0 +1,229 @@ +--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; +} +var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); + +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 invalid option value\n"; +try { + curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, 42); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting through curl_setopt_array()\n"; +try { + curl_setopt_array($ch, [CURLOPT_OPENSOCKETFUNCTION => 'function_does_not_exist']); +} 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 releases the previous one, like for other callback options */ +$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)); +var_dump($weakNull->get() === null); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); + +echo "\nTesting a callback that removes itself\n"; +$calls = 0; +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)); +var_dump(curl_exec($ch)); +var_dump($calls); + +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) +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 +bool(true) + +Testing with an exception thrown from the callback +Exception: refused +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 through curl_setopt_array() +TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_OPENSOCKETFUNCTION, function "function_does_not_exist" not found or invalid function name + +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) + +Testing a callback that removes itself +string(25) "Hello World! +Hello World!" +string(25) "Hello World! +Hello World!" +int(1) + +Done