From 96fd568238bc287ca26051bd93426b9ae760b5f6 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Thu, 12 Nov 2026 10:20:08 +0100 Subject: [PATCH] ext/curl: Add CURLOPT_OPENSOCKETFUNCTION The callback is called after DNS resolution and before connect(), with the address and the port libcurl is about to connect to. Returning true lets libcurl create the socket, returning false refuses the address. This makes it possible to block requests to internal addresses before any connection is made, which CURLOPT_PREREQFUNCTION can't do. --- NEWS | 2 + UPGRADING | 8 + ext/curl/curl.stub.php | 5 + ext/curl/curl_arginfo.h | 3 +- ext/curl/curl_private.h | 1 + ext/curl/interface.c | 132 ++++++++++ ...ensocketfunction_abstract_unix_socket.phpt | 30 +++ .../curl_opensocketfunction_address.phpt | 37 +++ .../curl_opensocketfunction_file_scheme.phpt | 45 ++++ .../tests/curl_opensocketfunction_gc.phpt | 26 ++ .../tests/curl_opensocketfunction_multi.phpt | 59 +++++ .../curl_opensocketfunction_next_address.phpt | 46 ++++ .../curl_opensocketfunction_redirect.phpt | 61 +++++ .../curl_opensocketfunction_share_free.phpt | 57 +++++ .../curl_opensocketfunction_unix_socket.phpt | 29 +++ ...url_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt | 229 ++++++++++++++++++ 16 files changed, 769 insertions(+), 1 deletion(-) create mode 100644 ext/curl/tests/curl_opensocketfunction_abstract_unix_socket.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_address.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_file_scheme.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_gc.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_multi.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_next_address.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_redirect.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_share_free.phpt create mode 100644 ext/curl/tests/curl_opensocketfunction_unix_socket.phpt create mode 100644 ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt 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