diff --git a/NEWS b/NEWS index 4efc8258fc2d..57d4652df7be 100644 --- a/NEWS +++ b/NEWS @@ -151,6 +151,8 @@ PHP NEWS . Set content length using CURLOPT_POSTFIELDSIZE_LARGE instead of CURLOPT_POSTFIELDSIZE. This makes it possible to post strings larger than 2GB on some platforms, e.g. Windows. (Sjoerd Langkemper) + . Added CURLOPT_PRECONNECTFUNCTION to allow or refuse each connection libcurl + is about to make. (Xavier Leune) - DOM: . Fixed a typo in the DOMException message for INUSE_ATTRIBUTE_ERR. diff --git a/UPGRADING b/UPGRADING index 14dbf5a96f02..e89d3f74412d 100644 --- a/UPGRADING +++ b/UPGRADING @@ -366,6 +366,24 @@ PHP 8.6 UPGRADE NOTES failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle, offset and origin, and must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK. + . Added CURLOPT_PRECONNECTFUNCTION to register a callback that allows or + refuses each socket libcurl is about to create, after DNS resolution and + before connect(): + + function (CurlHandle $handle, ?string $ip, int $port, + CurlAddressFamily $family): bool + + Returning false refuses the connection, and the transfer fails with + CURLE_COULDNT_CONNECT; any other return type raises a TypeError. $ip is + null for a UNIX domain socket. null as the option value restores libcurl's + own socket creation. + + It is not on its own a complete SSRF defence: the callback is invoked per + socket created, so a connection reused from the pool is not seen by it; + with a proxy it sees the proxy, not the target; CURLOPT_DOH_URL connections + and schemes that open no socket (file:// in particular) bypass it entirely. + Combine it with CURLOPT_PROTOCOLS_STR and CURLOPT_REDIR_PROTOCOLS_STR, and + do not share CURL_LOCK_DATA_CONNECT between handles vetted differently. - Date: . Added a new Time\Duration class. @@ -810,6 +828,9 @@ PHP 8.6 UPGRADE NOTES 7. New Classes and Interfaces ======================================== +- Curl: + . enum CurlAddressFamily + - Date: . Time\Duration RFC: https://wiki.php.net/rfc/duration_class @@ -886,6 +907,7 @@ PHP 8.6 UPGRADE NOTES - Curl: . CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). . CURLOPT_SEEKFUNCTION. + . CURLOPT_PRECONNECTFUNCTION. . CURL_SEEKFUNC_OK. . CURL_SEEKFUNC_FAIL. . CURL_SEEKFUNC_CANTSEEK. diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index 6953f0e97cbc..f2d08341c7d0 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -3739,6 +3739,23 @@ */ const CURLOPT_SAFE_UPLOAD = UNKNOWN; +/** + * @var int + * @cvalue CURLOPT_PRECONNECTFUNCTION + */ +const CURLOPT_PRECONNECTFUNCTION = UNKNOWN; + +/** + * The address family of the endpoint libcurl is about to connect to, as reported + * to the CURLOPT_PRECONNECTFUNCTION callback. + */ +enum CurlAddressFamily +{ + case Inet; + case Inet6; + case Unix; +} + /** * @strict-properties * @not-serializable diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index c38d581cb3cd..6331462f51fa 100644 --- a/ext/curl/curl_arginfo.h +++ b/ext/curl/curl_arginfo.h @@ -1,8 +1,9 @@ /* This is a generated file, edit curl.stub.php instead. - * Stub hash: 5da31d6790f9db408cac4aed3f81f7affb2849a6 */ + * Stub hash: e695473e884d8ce44ac8f4e760bf9cef25053f75 */ #include "zend_attributes.h" #include "zend_constants.h" +#include "zend_enum.h" ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) @@ -995,6 +996,7 @@ static void register_curl_symbols(int module_number) REGISTER_LONG_CONSTANT("CURL_HTTP_VERSION_3ONLY", CURL_HTTP_VERSION_3ONLY, CONST_PERSISTENT); #endif REGISTER_LONG_CONSTANT("CURLOPT_SAFE_UPLOAD", CURLOPT_SAFE_UPLOAD, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLOPT_PRECONNECTFUNCTION", CURLOPT_PRECONNECTFUNCTION, CONST_PERSISTENT); zend_attribute *attribute_Deprecated_func_curl_close_0 = zend_add_function_attribute(zend_hash_str_find_ptr(CG(function_table), "curl_close", sizeof("curl_close") - 1), ZSTR_KNOWN(ZEND_STR_DEPRECATED_CAPITALIZED), 2); @@ -1018,6 +1020,19 @@ static void register_curl_symbols(int module_number) attribute_Deprecated_const_CURLOPT_BINARYTRANSFER_0->args[1].name = ZSTR_KNOWN(ZEND_STR_MESSAGE); } +static zend_class_entry *register_class_CurlAddressFamily(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("CurlAddressFamily", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Inet", NULL); + + zend_enum_add_case_cstr(class_entry, "Inet6", NULL); + + zend_enum_add_case_cstr(class_entry, "Unix", NULL); + + return class_entry; +} + static zend_class_entry *register_class_CurlHandle(void) { zend_class_entry ce, *class_entry; diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 25cc37b6e458..0b9affe1b9a4 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -28,6 +28,9 @@ #define CURLOPT_RETURNTRANSFER 19913 #define CURLOPT_BINARYTRANSFER 19914 /* For Backward compatibility */ +/* PHP-specific option: the callback returns a bool rather than libcurl's socket, + * so it does not share CURLOPT_OPENSOCKETFUNCTION's name or number. */ +#define CURLOPT_PRECONNECTFUNCTION 19915 #define PHP_CURL_STDOUT 0 #define PHP_CURL_FILE 1 #define PHP_CURL_USER 2 @@ -79,6 +82,7 @@ typedef struct { zend_fcall_info_cache xferinfo; zend_fcall_info_cache fnmatch; zend_fcall_info_cache debug; + zend_fcall_info_cache preconnect; #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 55bcebbe7113..1570926be3a9 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -26,7 +26,12 @@ #ifdef PHP_WIN32 #include +#include #include +#else +#include +#include +#include #endif #include @@ -36,6 +41,8 @@ #include "ext/standard/info.h" #include "ext/standard/file.h" #include "ext/standard/url.h" +#include "ext/standard/php_net.h" +#include "Zend/zend_enum.h" #include "curl_private.h" #ifdef __GNUC__ @@ -232,6 +239,7 @@ PHP_GSHUTDOWN_FUNCTION(curl) zend_class_entry *curl_ce; zend_class_entry *curl_share_ce; zend_class_entry *curl_share_persistent_ce; +static zend_class_entry *curl_address_family_ce; static zend_object_handlers curl_object_handlers; static zend_object *curl_create_object(zend_class_entry *class_type); @@ -397,6 +405,8 @@ PHP_MINIT_FUNCTION(curl) curl_share_persistent_ce = register_class_CurlSharePersistentHandle(); curl_share_persistent_register_handlers(); + curl_address_family_ce = register_class_CurlAddressFamily(); + curlfile_register_class(); return SUCCESS; @@ -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.preconnect)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.preconnect); + } + #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); @@ -811,6 +825,124 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key, } #endif +/* {{{ curl_sockaddr_port + The port libcurl is about to connect to, in host byte order. */ +static zend_long curl_sockaddr_port(const struct sockaddr *sa) +{ + switch (sa->sa_family) { + case AF_INET: + return ntohs(((const struct sockaddr_in *) sa)->sin_port); +#ifdef AF_INET6 + case AF_INET6: + return ntohs(((const struct sockaddr_in6 *) sa)->sin6_port); +#endif + default: + return 0; + } +} +/* }}} */ + +/* {{{ curl_preconnectfunction + Registered as libcurl's CURLOPT_OPENSOCKETFUNCTION, but the PHP callback only + allows or refuses the connection: no descriptor is ever exposed to userland. */ +static curl_socket_t curl_preconnectfunction(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.preconnect))) { + return CURL_SOCKET_BAD; + } + +#if PHP_CURL_DEBUG + fprintf(stderr, "curl_preconnectfunction() 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; + } + + const char *address_family; + bool numeric_address; + + switch (address->family) { + case AF_INET: + address_family = "Inet"; + numeric_address = true; + break; +#ifdef AF_INET6 + case AF_INET6: + address_family = "Inet6"; + numeric_address = true; + break; +#endif +#ifdef AF_UNIX + case AF_UNIX: + address_family = "Unix"; + numeric_address = false; + break; +#endif + default: + return CURL_SOCKET_BAD; + } + + zval args[4]; + zval retval; + curl_socket_t rval = CURL_SOCKET_BAD; + + if (numeric_address) { + zend_string *ip = php_inet_ntop(&address->addr); + if (UNEXPECTED(ip == NULL)) { + return CURL_SOCKET_BAD; + } + ZVAL_STR(&args[1], ip); + ZVAL_LONG(&args[2], curl_sockaddr_port(&address->addr)); + } else { + /* A UNIX domain socket (CURLOPT_UNIX_SOCKET_PATH or + * CURLOPT_ABSTRACT_UNIX_SOCKET) has no address to report: null rather + * than an empty string, so a policy has to handle the case. */ + ZVAL_NULL(&args[1]); + ZVAL_LONG(&args[2], 0); + } + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + ZVAL_OBJ_COPY(&args[3], zend_enum_get_case_cstr(curl_address_family_ce, address_family)); + + ch->in_callback = true; + zend_call_known_fcc(&ch->handlers.preconnect, &retval, /* param_count */ 4, args, /* named_params */ NULL); + ch->in_callback = false; + + /* retval is undefined when the callback threw: the connection is refused and + * the exception propagates out of curl_exec(). */ + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + 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); + } + } else { + zend_type_error("The CURLOPT_PRECONNECTFUNCTION callback must return a bool"); + } + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + zval_ptr_dtor(&args[3]); + + return rval; +} +/* }}} */ + /* {{{ curl_read */ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx) { @@ -1134,6 +1266,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.preconnect = 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; @@ -1306,6 +1439,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.preconnect, &source->handlers.preconnect); #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 @@ -1677,6 +1811,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_PRECONNECTFUNCTION: { + bool installed = php_curl_set_callable_handler(&ch->handlers.preconnect, zvalue, is_array_config, "CURLOPT_PRECONNECTFUNCTION"); + if (!installed || !ZEND_FCC_INITIALIZED(ch->handlers.preconnect)) { + /* 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_preconnectfunction); + 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 @@ -2870,6 +3025,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.preconnect)) { + zend_fcc_dtor(&ch->handlers.preconnect); + } #if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */ if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) { zend_fcc_dtor(&ch->handlers.prereq); @@ -2962,6 +3120,10 @@ static void _php_curl_reset_handlers(php_curl *ch) zend_fcc_dtor(&ch->handlers.debug); } + if (ZEND_FCC_INITIALIZED(ch->handlers.preconnect)) { + zend_fcc_dtor(&ch->handlers.preconnect); + } + #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/sync-constants.php b/ext/curl/sync-constants.php index e24c773a5209..3cc80053e8d1 100755 --- a/ext/curl/sync-constants.php +++ b/ext/curl/sync-constants.php @@ -47,6 +47,7 @@ const IGNORED_PHP_CONSTANTS = [ 'CURLOPT_BINARYTRANSFER', + 'CURLOPT_PRECONNECTFUNCTION', 'CURLOPT_RETURNTRANSFER', 'CURLOPT_SAFE_UPLOAD', ]; diff --git a/ext/curl/tests/curl_address_family.phpt b/ext/curl/tests/curl_address_family.phpt new file mode 100644 index 000000000000..0f1a035e89b9 --- /dev/null +++ b/ext/curl/tests/curl_address_family.phpt @@ -0,0 +1,30 @@ +--TEST-- +CurlAddressFamily enum +--EXTENSIONS-- +curl +--FILE-- +isEnum(), $re->getBackingType()); + +foreach (CurlAddressFamily::cases() as $case) { + echo $case->name, PHP_EOL; +} + +var_dump(unserialize(serialize(CurlAddressFamily::Inet)) === CurlAddressFamily::Inet); + +echo "Done"; +?> +--EXPECT-- +enum(CurlAddressFamily::Inet) +enum(CurlAddressFamily::Inet6) +enum(CurlAddressFamily::Unix) +bool(true) +NULL +Inet +Inet6 +Unix +bool(true) +Done diff --git a/ext/curl/tests/curl_preconnectfunction_address_family.phpt b/ext/curl/tests/curl_preconnectfunction_address_family.phpt new file mode 100644 index 000000000000..e673e598ad7b --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_address_family.phpt @@ -0,0 +1,64 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION reports the address family of the endpoint +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +IPv4 literal +string(9) "127.0.0.1" +int(1) +enum(CurlAddressFamily::Inet) + +IPv6 literal +string(3) "::1" +int(1) +enum(CurlAddressFamily::Inet6) + +IPv4-mapped IPv6 address +string(16) "::ffff:127.0.0.1" +int(1) +enum(CurlAddressFamily::Inet6) + +IPv4-mapped IPv6 address, hexadecimal form +string(16) "::ffff:127.0.0.1" +int(1) +enum(CurlAddressFamily::Inet6) +Done diff --git a/ext/curl/tests/curl_preconnectfunction_file_scheme.phpt b/ext/curl/tests/curl_preconnectfunction_file_scheme.phpt new file mode 100644 index 000000000000..a9da957a6f39 --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_file_scheme.phpt @@ -0,0 +1,45 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION 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_preconnectfunction_gc.phpt b/ext/curl/tests/curl_preconnectfunction_gc.phpt new file mode 100644 index 000000000000..b537945870ac --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_gc.phpt @@ -0,0 +1,27 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION: 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_preconnectfunction_multi.phpt b/ext/curl/tests/curl_preconnectfunction_multi.phpt new file mode 100644 index 000000000000..0e01b49ca3f6 --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_multi.phpt @@ -0,0 +1,59 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION 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_PRECONNECTFUNCTION, 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_preconnectfunction_redirect.phpt b/ext/curl/tests/curl_preconnectfunction_redirect.phpt new file mode 100644 index 000000000000..7154500d4028 --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_redirect.phpt @@ -0,0 +1,61 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION 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_preconnectfunction_share_free.phpt b/ext/curl/tests/curl_preconnectfunction_share_free.phpt new file mode 100644 index 000000000000..01cf1995d471 --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_share_free.phpt @@ -0,0 +1,59 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION: 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_preconnectfunction_unix_socket.phpt b/ext/curl/tests/curl_preconnectfunction_unix_socket.phpt new file mode 100644 index 000000000000..b04f44ffdafb --- /dev/null +++ b/ext/curl/tests/curl_preconnectfunction_unix_socket.phpt @@ -0,0 +1,33 @@ +--TEST-- +CURLOPT_PRECONNECTFUNCTION reports a null address for a UNIX socket +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +NULL +int(0) +enum(CurlAddressFamily::Unix) +bool(false) +bool(true) +Done diff --git a/ext/curl/tests/curl_setopt_CURLOPT_PRECONNECTFUNCTION.phpt b/ext/curl/tests/curl_setopt_CURLOPT_PRECONNECTFUNCTION.phpt new file mode 100644 index 000000000000..6124bcf34bdc --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_PRECONNECTFUNCTION.phpt @@ -0,0 +1,234 @@ +--TEST-- +Curl option CURLOPT_PRECONNECTFUNCTION +--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_PRECONNECTFUNCTION, 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_PRECONNECTFUNCTION, 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_PRECONNECTFUNCTION, 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_PRECONNECTFUNCTION, 42); +} catch (\TypeError $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; +} + +echo "\nTesting with an invalid option callback\n"; +try { + curl_setopt($ch, CURLOPT_PRECONNECTFUNCTION, '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_PRECONNECTFUNCTION => 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_PRECONNECTFUNCTION, $refuse); +try { + curl_setopt($src, CURLOPT_PRECONNECTFUNCTION, '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_PRECONNECTFUNCTION, $cbn); +$weakNull = WeakReference::create($cbn); +unset($cbn); +var_dump(curl_exec($ch)); +var_dump(curl_setopt($ch, CURLOPT_PRECONNECTFUNCTION, 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(19915) + +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_PRECONNECTFUNCTION callback must return a bool + +Testing with an invalid return type +TypeError: The CURLOPT_PRECONNECTFUNCTION callback must return a bool + +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_PRECONNECTFUNCTION, 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_PRECONNECTFUNCTION, 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_PRECONNECTFUNCTION, 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