From 413260920b53e104e4b4d9b3ab0359941f98a29c Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Fri, 11 Sep 2026 09:43:20 +0200 Subject: [PATCH] ext/curl: add CURLOPT_PRECONNECTFUNCTION to vet outgoing connections Registers a userland callback that allows or refuses each socket libcurl is about to create: curl_setopt($ch, CURLOPT_PRECONNECTFUNCTION, function (CurlHandle $handle, ?string $ip, int $port, CurlAddressFamily $family): bool { /* ... */ }); The main use case is SSRF filtering: the callback runs after DNS resolution, with the address libcurl is about to connect to, and before connect(). Returning false refuses the connection and the transfer fails with CURLE_COULDNT_CONNECT. The callback returns a bool rather than a socket, so no descriptor is ever exposed to userland. Because these PHP semantics deliberately differ from the libcurl option of the same name, the option carries its own name and a PHP-private constant value, the way CURLOPT_RETURNTRANSFER already does, which leaves CURLOPT_OPENSOCKETFUNCTION available should a socket-returning binding ever be wanted. Whatever cannot be described to the callback is refused without invoking it, so a policy can never be bypassed by an endpoint it was not shown: address families other than AF_INET, AF_INET6 and AF_UNIX, and purposes other than CURLSOCKTYPE_IPCXN, which is the only one libcurl currently uses. A UNIX domain socket has no address to report and is passed a null $ip. When the callback allows the connection, ext/curl creates the socket with socket(family, socktype, protocol), which is what libcurl documents as the default behaviour of the hook. Everything that follows -- non-blocking mode, CURLOPT_INTERFACE and CURLOPT_LOCALPORT binding, CURLOPT_SOCKOPTFUNCTION, the IPv6 scope id -- is still applied by libcurl on the returned descriptor, so an allowed connection is established exactly as it would have been without the option. UPGRADING documents the cases the hook does not cover, since it is not on its own a complete SSRF defence: schemes that open no socket (file:// in particular), reused pooled connections, proxies, and CURLOPT_DOH_URL. CURLOPT_SOCKOPTFUNCTION and CURLOPT_CLOSESOCKETFUNCTION are deliberately out of scope, as is any exposure of a Socket object. --- NEWS | 2 + UPGRADING | 22 ++ ext/curl/curl.stub.php | 17 ++ ext/curl/curl_arginfo.h | 17 +- ext/curl/curl_private.h | 4 + ext/curl/interface.c | 162 ++++++++++++ ext/curl/sync-constants.php | 1 + ext/curl/tests/curl_address_family.phpt | 30 +++ ...url_preconnectfunction_address_family.phpt | 64 +++++ .../curl_preconnectfunction_file_scheme.phpt | 45 ++++ .../tests/curl_preconnectfunction_gc.phpt | 27 ++ .../tests/curl_preconnectfunction_multi.phpt | 59 +++++ .../curl_preconnectfunction_redirect.phpt | 61 +++++ .../curl_preconnectfunction_share_free.phpt | 59 +++++ .../curl_preconnectfunction_unix_socket.phpt | 33 +++ ...url_setopt_CURLOPT_PRECONNECTFUNCTION.phpt | 234 ++++++++++++++++++ 16 files changed, 836 insertions(+), 1 deletion(-) create mode 100644 ext/curl/tests/curl_address_family.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_address_family.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_file_scheme.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_gc.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_multi.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_redirect.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_share_free.phpt create mode 100644 ext/curl/tests/curl_preconnectfunction_unix_socket.phpt create mode 100644 ext/curl/tests/curl_setopt_CURLOPT_PRECONNECTFUNCTION.phpt 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