From 3ed7c4615c71cf42202f87ed54d2b56f5652d522 Mon Sep 17 00:00:00 2001 From: Xavier Leune Date: Wed, 27 May 2026 13:21:20 +0200 Subject: [PATCH] ext/curl: add socket callback options bridging to ext/sockets Expose libcurl's CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and CURLOPT_CLOSESOCKETFUNCTION, letting userland hook into socket creation, configuration and teardown. These are useful for application security, in particular SSRF protection (validating the resolved address before connecting) and low-level socket hardening. Following the existing curl_write_header / curl_prereqfunction bridge model, the callbacks exchange ext/sockets Socket objects so they are fully usable in pure PHP (socket_create/socket_bind, socket_set_option, socket_close): - sockopt: fn(CurlHandle $ch, Socket $socket, int $purpose): int returns CURL_SOCKOPT_OK / _ERROR / _ALREADY_CONNECTED - opensocket: fn(CurlHandle $ch, int $purpose, array $address): Socket|false $address = [family, socktype, protocol, ip, port]; returning false aborts the connection (CURL_SOCKET_BAD) - closesocket: fn(CurlHandle $ch, Socket $socket): void This is achieved through the C API exported by ext/sockets (socket_ce, the php_socket struct and socket_import_file_descriptor()), so ext/curl now depends on ext/sockets. The whole feature is guarded by HAVE_SOCKETS; without the sockets extension curl still builds, just without these options. Notable details: - Descriptors owned by libcurl are detached (bsd_socket = -1) before the temporary Socket object is released, to avoid a double close. - The close-socket callback is disabled before curl_easy_cleanup() so libcurl closes pooled sockets natively rather than calling into PHP during handle destruction. - Setting an option to null restores libcurl's native default. New constants: CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION, CURLOPT_CLOSESOCKETFUNCTION, CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR, CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN, CURLSOCKTYPE_ACCEPT. --- NEWS | 5 + UPGRADING | 10 + ext/curl/config.m4 | 4 + ext/curl/config.w32 | 4 + ext/curl/curl.stub.php | 57 ++++ ext/curl/curl_arginfo.h | 12 +- ext/curl/curl_private.h | 5 + ext/curl/interface.c | 301 ++++++++++++++++++ ...rl_setopt_CURLOPT_CLOSESOCKETFUNCTION.phpt | 72 +++++ ...url_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt | 110 +++++++ .../curl_setopt_CURLOPT_SOCKOPTFUNCTION.phpt | 126 ++++++++ ext/curl/tests/curl_sockopt_trampoline.phpt | 34 ++ 12 files changed, 739 insertions(+), 1 deletion(-) create mode 100644 ext/curl/tests/curl_setopt_CURLOPT_CLOSESOCKETFUNCTION.phpt create mode 100644 ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt create mode 100644 ext/curl/tests/curl_setopt_CURLOPT_SOCKOPTFUNCTION.phpt create mode 100644 ext/curl/tests/curl_sockopt_trampoline.phpt diff --git a/NEWS b/NEWS index 23212414d361..a465ec266305 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,11 @@ PHP NEWS - BCMath: . Added NUL-byte validation to BCMath functions. (jorgsowa) +- Curl: + . Added CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and + CURLOPT_CLOSESOCKETFUNCTION options, bridging libcurl's socket callbacks to + ext/sockets Socket objects (requires the sockets extension). + - Date: . Update timelib to 2022.16. (Derick) diff --git a/UPGRADING b/UPGRADING index 3d6c89e90e05..54b384ba43d1 100644 --- a/UPGRADING +++ b/UPGRADING @@ -171,6 +171,16 @@ PHP 8.6 UPGRADE NOTES . It is now possible to define the `__debugInfo()` magic method on enums. RFC: https://wiki.php.net/rfc/debugable-enums +- Curl: + . Added the CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and + CURLOPT_CLOSESOCKETFUNCTION options, which let userland hook into libcurl's + socket creation, configuration and teardown. The callbacks exchange + ext/sockets Socket objects, e.g. to validate the resolved address before + connecting (SSRF protection) or to set low-level socket options. These + options require the sockets extension; the new CURL_SOCKOPT_OK, + CURL_SOCKOPT_ERROR, CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN and + CURLSOCKTYPE_ACCEPT constants are defined alongside them. + - Fileinfo: . finfo_file() now works with remote streams. diff --git a/ext/curl/config.m4 b/ext/curl/config.m4 index bbb41fdde322..e24c422c5c22 100644 --- a/ext/curl/config.m4 +++ b/ext/curl/config.m4 @@ -75,6 +75,10 @@ if test "$PHP_CURL" != "no"; then PHP_NEW_EXTENSION([curl], [interface.c multi.c share.c curl_file.c], [$ext_shared]) + dnl The CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKETFUNCTION callbacks bridge to + dnl ext/sockets Socket objects (guarded by HAVE_SOCKETS). The dependency is + dnl optional: without sockets, curl still builds, just without those options. + PHP_ADD_EXTENSION_DEP(curl, sockets, true) PHP_INSTALL_HEADERS([ext/curl], [php_curl.h]) PHP_SUBST([CURL_SHARED_LIBADD]) fi diff --git a/ext/curl/config.w32 b/ext/curl/config.w32 index 567699f3b748..56b71c4a243a 100644 --- a/ext/curl/config.w32 +++ b/ext/curl/config.w32 @@ -25,6 +25,10 @@ if (PHP_CURL != "no") { WARNING("zstd in curl not enabled; library not found"); } EXTENSION("curl", "interface.c multi.c share.c curl_file.c"); + // The socket callbacks (CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKETFUNCTION) + // bridge to ext/sockets Socket objects, guarded by HAVE_SOCKETS. Optional: + // without sockets, curl still builds, just without those options. + ADD_EXTENSION_DEP('curl', 'sockets', true); AC_DEFINE('HAVE_CURL', 1, "Define to 1 if the PHP extension 'curl' is available."); ADD_FLAG("CFLAGS_CURL", "/D PHP_CURL_EXPORTS=1"); if (curl_location.match(/libcurl_a\.lib$/)) { diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index aadab8cb0b0d..7809e313a0f3 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -2327,6 +2327,63 @@ */ const CURL_FNMATCHFUNC_NOMATCH = UNKNOWN; +#ifdef HAVE_SOCKETS +/** + * Used with CURLOPT_SOCKOPTFUNCTION, which receives a Socket wrapping the + * descriptor libcurl just created and must return one of the CURL_SOCKOPT_* + * constants below. + * @var int + * @cvalue CURLOPT_SOCKOPTFUNCTION + */ +const CURLOPT_SOCKOPTFUNCTION = UNKNOWN; +/** + * Used with CURLOPT_OPENSOCKETFUNCTION, which receives the resolved address and + * must return a Socket to use for the connection, or false to abort it. + * @var int + * @cvalue CURLOPT_OPENSOCKETFUNCTION + */ +const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN; +/** + * Used with CURLOPT_CLOSESOCKETFUNCTION, which is notified when libcurl is done + * with a socket created by the open-socket callback. + * @var int + * @cvalue CURLOPT_CLOSESOCKETFUNCTION + */ +const CURLOPT_CLOSESOCKETFUNCTION = UNKNOWN; +/** + * Return value for the CURLOPT_SOCKOPTFUNCTION callback: proceed normally. + * @var int + * @cvalue CURL_SOCKOPT_OK + */ +const CURL_SOCKOPT_OK = UNKNOWN; +/** + * Return value for the CURLOPT_SOCKOPTFUNCTION callback: abort the connection. + * @var int + * @cvalue CURL_SOCKOPT_ERROR + */ +const CURL_SOCKOPT_ERROR = UNKNOWN; +/** + * Return value for the CURLOPT_SOCKOPTFUNCTION callback: the socket is already + * connected, so libcurl should skip its own connect step. + * @var int + * @cvalue CURL_SOCKOPT_ALREADY_CONNECTED + */ +const CURL_SOCKOPT_ALREADY_CONNECTED = UNKNOWN; +/** + * Purpose passed to the socket callbacks: a socket for a regular IP connection. + * @var int + * @cvalue CURLSOCKTYPE_IPCXN + */ +const CURLSOCKTYPE_IPCXN = UNKNOWN; +/** + * Purpose passed to the socket callbacks: a socket created from accept() (e.g. + * active FTP). + * @var int + * @cvalue CURLSOCKTYPE_ACCEPT + */ +const CURLSOCKTYPE_ACCEPT = UNKNOWN; +#endif + /* Available since 7.21.2 */ /** * @var int diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index 6fb17ed029e3..9ea35394fbba 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: 10ebdc94560ed19ecd6b61a11b3dab5d32989d66 */ + * Stub hash: 7c2cde7dab2d14cefbc319046a70ef39eed3755d */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) @@ -677,6 +677,16 @@ static void register_curl_symbols(int module_number) REGISTER_LONG_CONSTANT("CURL_FNMATCHFUNC_FAIL", CURL_FNMATCHFUNC_FAIL, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURL_FNMATCHFUNC_MATCH", CURL_FNMATCHFUNC_MATCH, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURL_FNMATCHFUNC_NOMATCH", CURL_FNMATCHFUNC_NOMATCH, CONST_PERSISTENT); +#if defined(HAVE_SOCKETS) + REGISTER_LONG_CONSTANT("CURLOPT_SOCKOPTFUNCTION", CURLOPT_SOCKOPTFUNCTION, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLOPT_OPENSOCKETFUNCTION", CURLOPT_OPENSOCKETFUNCTION, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLOPT_CLOSESOCKETFUNCTION", CURLOPT_CLOSESOCKETFUNCTION, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURL_SOCKOPT_OK", CURL_SOCKOPT_OK, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURL_SOCKOPT_ERROR", CURL_SOCKOPT_ERROR, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURL_SOCKOPT_ALREADY_CONNECTED", CURL_SOCKOPT_ALREADY_CONNECTED, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLSOCKTYPE_IPCXN", CURLSOCKTYPE_IPCXN, CONST_PERSISTENT); + REGISTER_LONG_CONSTANT("CURLSOCKTYPE_ACCEPT", CURLSOCKTYPE_ACCEPT, CONST_PERSISTENT); +#endif REGISTER_LONG_CONSTANT("CURLPROTO_GOPHER", CURLPROTO_GOPHER, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLAUTH_ONLY", CURLAUTH_ONLY, CONST_PERSISTENT); REGISTER_LONG_CONSTANT("CURLOPT_RESOLVE", CURLOPT_RESOLVE, CONST_PERSISTENT); diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 2e7b1cf41d30..9e4ee8dae69d 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -84,6 +84,11 @@ typedef struct { #if LIBCURL_VERSION_NUM >= 0x075400 /* Available since 7.84.0 */ zend_fcall_info_cache sshhostkey; #endif +#ifdef HAVE_SOCKETS + zend_fcall_info_cache sockopt; + zend_fcall_info_cache opensocket; + zend_fcall_info_cache closesocket; +#endif } php_curl_handlers; struct _php_curl_error { diff --git a/ext/curl/interface.c b/ext/curl/interface.c index ed544866a886..b8963c15fea3 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -43,6 +43,24 @@ #include "ext/standard/url.h" #include "curl_private.h" +#ifdef HAVE_SOCKETS +/* For the CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKET callbacks, which bridge the + * libcurl socket file descriptor to ext/sockets Socket objects. */ +#include "ext/sockets/php_sockets.h" +# ifdef PHP_WIN32 +# include +# else +# include +# include +# endif +# ifndef NI_MAXHOST +# define NI_MAXHOST 1025 +# endif +# ifndef NI_MAXSERV +# define NI_MAXSERV 32 +# endif +#endif + #ifdef __GNUC__ /* don't complain about deprecated CURLOPT_* we're exposing to PHP; we need to keep using those to avoid breaking PHP API compatibility */ @@ -170,9 +188,24 @@ void _php_curl_verify_handlers(php_curl *ch, bool reporterror) /* {{{ */ } /* }}} */ +#ifdef HAVE_SOCKETS +static const zend_module_dep curl_deps[] = { + /* The socket callbacks (CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKETFUNCTION) hand + * Socket objects to userland, so ext/sockets must be present and initialized + * before curl. */ + ZEND_MOD_REQUIRED("sockets") + ZEND_MOD_END +}; +#endif + /* {{{ curl_module_entry */ zend_module_entry curl_module_entry = { +#ifdef HAVE_SOCKETS + STANDARD_MODULE_HEADER_EX, NULL, + curl_deps, +#else STANDARD_MODULE_HEADER, +#endif "curl", ext_functions, PHP_MINIT(curl), @@ -484,6 +517,18 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n) } #endif +#ifdef HAVE_SOCKETS + if (ZEND_FCC_INITIALIZED(curl->handlers.sockopt)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.sockopt); + } + if (ZEND_FCC_INITIALIZED(curl->handlers.opensocket)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.opensocket); + } + if (ZEND_FCC_INITIALIZED(curl->handlers.closesocket)) { + zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.closesocket); + } +#endif + zend_get_gc_buffer_add_zval(gc_buffer, &curl->handlers.std_err); zend_get_gc_buffer_add_zval(gc_buffer, &curl->private_data); @@ -779,6 +824,191 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key, } #endif +#ifdef HAVE_SOCKETS +/* {{{ Wrap a libcurl socket descriptor in a fresh ext/sockets Socket object. + * When the descriptor's lifetime is owned by libcurl (CURLOPT_SOCKOPTFUNCTION), + * the caller must detach it again with php_curl_socket_object_detach() before + * releasing the object, otherwise the Socket destructor would close it. */ +static void php_curl_socket_object_init(zval *zsocket, curl_socket_t sockfd) +{ + object_init_ex(zsocket, socket_ce); + /* Only the descriptor needs to be set; the Socket constructor already + * initialized type to PF_UNSPEC and blocking to 1. We deliberately avoid + * socket_import_file_descriptor() here: it calls getsockname(), which fails + * with WSAEINVAL on Windows for a socket that libcurl has created but not yet + * connected (the CURLOPT_SOCKOPTFUNCTION phase), emitting a spurious warning. */ + Z_SOCKET_P(zsocket)->bsd_socket = sockfd; +} + +/* Detach the descriptor so releasing the Socket object does not close it. + * ext/sockets treats a negative bsd_socket as invalid on all platforms. */ +static zend_always_inline void php_curl_socket_object_detach(zval *zsocket) +{ + Z_SOCKET_P(zsocket)->bsd_socket = -1; +} + +/* {{{ curl_sockoptfunction */ +static int curl_sockoptfunction(void *clientp, curl_socket_t curlfd, curlsocktype purpose) +{ + php_curl *ch = (php_curl *)clientp; + int rval = CURL_SOCKOPT_OK; + + /* See the note in the reset-on-null setopt handling: a registered callback + * always has an initialized FCC, but guard defensively anyway. */ + if (!ZEND_FCC_INITIALIZED(ch->handlers.sockopt)) { + return rval; + } + + zval args[3]; + zval retval; + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + php_curl_socket_object_init(&args[1], curlfd); + ZVAL_LONG(&args[2], purpose); + + ch->in_callback = true; + zend_call_known_fcc(&ch->handlers.sockopt, &retval, /* param_count */ 3, args, /* named_params */ NULL); + ch->in_callback = false; + + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + if (Z_TYPE(retval) == IS_LONG) { + zend_long retval_long = Z_LVAL(retval); + if (retval_long == CURL_SOCKOPT_OK || retval_long == CURL_SOCKOPT_ERROR || retval_long == CURL_SOCKOPT_ALREADY_CONNECTED) { + rval = (int) retval_long; + } else { + zend_value_error("The CURLOPT_SOCKOPTFUNCTION callback must return one of CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR or CURL_SOCKOPT_ALREADY_CONNECTED"); + rval = CURL_SOCKOPT_ERROR; + } + } else { + zend_type_error("The CURLOPT_SOCKOPTFUNCTION callback must return one of CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR or CURL_SOCKOPT_ALREADY_CONNECTED"); + zval_ptr_dtor(&retval); + rval = CURL_SOCKOPT_ERROR; + } + } + + /* The descriptor is owned by libcurl; do not let the Socket object close it. */ + php_curl_socket_object_detach(&args[1]); + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + return rval; +} +/* }}} */ + +/* {{{ curl_opensocketfunction */ +static curl_socket_t curl_opensocketfunction(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) +{ + php_curl *ch = (php_curl *)clientp; + + if (!ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + /* Unreachable in practice (a null option restores libcurl's default), but + * fall back to the default socket creation just in case. */ + return socket(address->family, address->socktype, address->protocol); + } + + zval args[3]; + zval retval; + curl_socket_t sockfd = CURL_SOCKET_BAD; + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + ZVAL_LONG(&args[1], purpose); + + array_init(&args[2]); + add_assoc_long(&args[2], "family", address->family); + add_assoc_long(&args[2], "socktype", address->socktype); + add_assoc_long(&args[2], "protocol", address->protocol); + + char host[NI_MAXHOST]; + char serv[NI_MAXSERV]; + if (getnameinfo((struct sockaddr *) &address->addr, address->addrlen, + host, sizeof(host), serv, sizeof(serv), + NI_NUMERICHOST | NI_NUMERICSERV) == 0) { + add_assoc_string(&args[2], "ip", host); + add_assoc_long(&args[2], "port", strtol(serv, NULL, 10)); + } else { + add_assoc_string(&args[2], "ip", ""); + add_assoc_long(&args[2], "port", 0); + } + + ch->in_callback = true; + zend_call_known_fcc(&ch->handlers.opensocket, &retval, /* param_count */ 3, args, /* named_params */ NULL); + ch->in_callback = false; + + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + if (Z_TYPE(retval) == IS_FALSE) { + sockfd = CURL_SOCKET_BAD; + } else if (Z_TYPE(retval) == IS_OBJECT && instanceof_function(Z_OBJCE(retval), socket_ce)) { + php_socket *socket = Z_SOCKET_P(&retval); + sockfd = socket->bsd_socket; + /* libcurl owns the descriptor from now on; detach it from the Socket + * object so releasing it does not close the descriptor. */ + socket->bsd_socket = -1; + } else { + zend_type_error("The CURLOPT_OPENSOCKETFUNCTION callback must return a Socket or false"); + } + zval_ptr_dtor(&retval); + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[2]); + return sockfd; +} +/* }}} */ + +/* {{{ curl_closesocketfunction */ +static int curl_closesocketfunction(void *clientp, curl_socket_t item) +{ + php_curl *ch = (php_curl *)clientp; + + if (!ZEND_FCC_INITIALIZED(ch->handlers.closesocket)) { + /* Unreachable in practice; close the descriptor as libcurl would. */ +#ifdef PHP_WIN32 + closesocket(item); +#else + close(item); +#endif + return 0; + } + + zval args[2]; + zval retval; + + GC_ADDREF(&ch->std); + ZVAL_OBJ(&args[0], &ch->std); + php_curl_socket_object_init(&args[1], item); + + ch->in_callback = true; + zend_call_known_fcc(&ch->handlers.closesocket, &retval, /* param_count */ 2, args, /* named_params */ NULL); + ch->in_callback = false; + + if (!Z_ISUNDEF(retval)) { + _php_curl_verify_handlers(ch, /* reporterror */ true); + zval_ptr_dtor(&retval); + } + + /* libcurl delegates closing to this callback. If userland did not close the + * socket (e.g. via socket_close()), close it now so the descriptor cannot leak. */ + php_socket *socket = Z_SOCKET_P(&args[1]); + if (!IS_INVALID_SOCKET(socket)) { +#ifdef PHP_WIN32 + closesocket(socket->bsd_socket); +#else + close(socket->bsd_socket); +#endif + socket->bsd_socket = -1; + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + return 0; +} +/* }}} */ +#endif + /* {{{ curl_read */ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx) { @@ -1047,6 +1277,11 @@ void init_curl_handle(php_curl *ch) #endif #if LIBCURL_VERSION_NUM >= 0x075400 /* Available since 7.84.0 */ ch->handlers.sshhostkey = empty_fcall_info_cache; +#endif +#ifdef HAVE_SOCKETS + ch->handlers.sockopt = empty_fcall_info_cache; + ch->handlers.opensocket = empty_fcall_info_cache; + ch->handlers.closesocket = empty_fcall_info_cache; #endif ch->clone = emalloc(sizeof(uint32_t)); *ch->clone = 1; @@ -1218,6 +1453,11 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source) #if LIBCURL_VERSION_NUM >= 0x075400 /* Available since 7.84.0 */ php_curl_copy_fcc_with_option(ch, CURLOPT_SSH_HOSTKEYDATA, &ch->handlers.sshhostkey, &source->handlers.sshhostkey); #endif +#ifdef HAVE_SOCKETS + php_curl_copy_fcc_with_option(ch, CURLOPT_SOCKOPTDATA, &ch->handlers.sockopt, &source->handlers.sockopt); + php_curl_copy_fcc_with_option(ch, CURLOPT_OPENSOCKETDATA, &ch->handlers.opensocket, &source->handlers.opensocket); + php_curl_copy_fcc_with_option(ch, CURLOPT_CLOSESOCKETDATA, &ch->handlers.closesocket, &source->handlers.closesocket); +#endif ZVAL_COPY(&ch->private_data, &source->private_data); @@ -1566,6 +1806,27 @@ static bool php_curl_set_callable_handler(zend_fcall_info_cache *const handler_f break; \ } +/* Like HANDLE_CURL_OPTION_CALLABLE, but when the callback is set to null the + * curl option is reset to NULL so libcurl uses its native default instead of + * invoking our C callback with an uninitialized FCC. Used for the socket + * callbacks, whose default behaviour (creating/closing the descriptor) is best + * left to libcurl. */ +#define HANDLE_CURL_OPTION_CALLABLE_RESET_ON_NULL(curl_ptr, constant_no_function, handler_fcc, c_callback) \ + case constant_no_function##FUNCTION: { \ + bool result = php_curl_set_callable_handler(&curl_ptr->handler_fcc, zvalue, is_array_config, #constant_no_function "FUNCTION"); \ + if (!result) { \ + return FAILURE; \ + } \ + if (ZEND_FCC_INITIALIZED(curl_ptr->handler_fcc)) { \ + curl_easy_setopt(curl_ptr->cp, constant_no_function##FUNCTION, (c_callback)); \ + curl_easy_setopt(curl_ptr->cp, constant_no_function##DATA, curl_ptr); \ + } else { \ + curl_easy_setopt(curl_ptr->cp, constant_no_function##FUNCTION, NULL); \ + curl_easy_setopt(curl_ptr->cp, constant_no_function##DATA, NULL); \ + } \ + break; \ + } + static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue, bool is_array_config) /* {{{ */ { CURLcode error = CURLE_OK; @@ -1589,6 +1850,12 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_SSH_HOSTKEY, handlers.sshhostkey, curl_ssh_hostkeyfunction); #endif +#ifdef HAVE_SOCKETS + HANDLE_CURL_OPTION_CALLABLE_RESET_ON_NULL(ch, CURLOPT_SOCKOPT, handlers.sockopt, curl_sockoptfunction); + HANDLE_CURL_OPTION_CALLABLE_RESET_ON_NULL(ch, CURLOPT_OPENSOCKET, handlers.opensocket, curl_opensocketfunction); + HANDLE_CURL_OPTION_CALLABLE_RESET_ON_NULL(ch, CURLOPT_CLOSESOCKET, handlers.closesocket, curl_closesocketfunction); +#endif + /* Long options */ case CURLOPT_SSL_VERIFYHOST: lval = zval_get_long(zvalue); @@ -2746,6 +3013,17 @@ static void curl_free_obj(zend_object *object) _php_curl_verify_handlers(ch, /* reporterror */ false); +#ifdef HAVE_SOCKETS + /* curl_easy_cleanup() closes any pooled sockets, which would invoke the + * userland CURLOPT_CLOSESOCKETFUNCTION callback while the handle is being + * destroyed (potentially during GC or shutdown). Detach it first so libcurl + * closes those sockets natively without calling into PHP. */ + if (ZEND_FCC_INITIALIZED(ch->handlers.closesocket)) { + curl_easy_setopt(ch->cp, CURLOPT_CLOSESOCKETFUNCTION, NULL); + curl_easy_setopt(ch->cp, CURLOPT_CLOSESOCKETDATA, NULL); + } +#endif + curl_easy_cleanup(ch->cp); /* cURL destructors should be invoked only by last curl handle */ @@ -2803,6 +3081,17 @@ static void curl_free_obj(zend_object *object) zend_fcc_dtor(&ch->handlers.sshhostkey); } #endif +#ifdef HAVE_SOCKETS + if (ZEND_FCC_INITIALIZED(ch->handlers.sockopt)) { + zend_fcc_dtor(&ch->handlers.sockopt); + } + if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + zend_fcc_dtor(&ch->handlers.opensocket); + } + if (ZEND_FCC_INITIALIZED(ch->handlers.closesocket)) { + zend_fcc_dtor(&ch->handlers.closesocket); + } +#endif zval_ptr_dtor(&ch->postfields); zval_ptr_dtor(&ch->private_data); @@ -2892,6 +3181,18 @@ static void _php_curl_reset_handlers(php_curl *ch) zend_fcc_dtor(&ch->handlers.sshhostkey); } #endif + +#ifdef HAVE_SOCKETS + if (ZEND_FCC_INITIALIZED(ch->handlers.sockopt)) { + zend_fcc_dtor(&ch->handlers.sockopt); + } + if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) { + zend_fcc_dtor(&ch->handlers.opensocket); + } + if (ZEND_FCC_INITIALIZED(ch->handlers.closesocket)) { + zend_fcc_dtor(&ch->handlers.closesocket); + } +#endif } /* }}} */ diff --git a/ext/curl/tests/curl_setopt_CURLOPT_CLOSESOCKETFUNCTION.phpt b/ext/curl/tests/curl_setopt_CURLOPT_CLOSESOCKETFUNCTION.phpt new file mode 100644 index 000000000000..68720a859dd4 --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_CLOSESOCKETFUNCTION.phpt @@ -0,0 +1,72 @@ +--TEST-- +Curl option CURLOPT_CLOSESOCKETFUNCTION +--EXTENSIONS-- +curl +sockets +--FILE-- + $ch instanceof CurlHandle, + 'socket' => $socket instanceof Socket, + ]; + socket_close($socket); +}); + +echo "Testing close-socket callback\n"; +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); +var_dump($seen); +var_dump($closeCount >= 1); + +echo "\nTesting with invalid option value\n"; +try { + curl_setopt($ch, CURLOPT_CLOSESOCKETFUNCTION, 42); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with null as the callback\n"; +var_dump(curl_setopt($ch, CURLOPT_CLOSESOCKETFUNCTION, null)); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); + +echo "\nDone"; +?> +--EXPECT-- +Testing close-socket callback +string(25) "Hello World! +Hello World!" +int(0) +array(2) { + ["handle"]=> + bool(true) + ["socket"]=> + bool(true) +} +bool(true) + +Testing with invalid option value +curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_CLOSESOCKETFUNCTION, no array or string given + +Testing with null as the callback +bool(true) +string(25) "Hello World! +Hello World!" +int(0) + +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..d8c803ca10a7 --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_OPENSOCKETFUNCTION.phpt @@ -0,0 +1,110 @@ +--TEST-- +Curl option CURLOPT_OPENSOCKETFUNCTION +--EXTENSIONS-- +curl +sockets +--FILE-- + $ch instanceof CurlHandle, + 'purpose' => $purpose === CURLSOCKTYPE_IPCXN, + 'family_is_int' => is_int($address['family']), + 'socktype_is_int' => is_int($address['socktype']), + 'protocol_is_int' => is_int($address['protocol']), + 'ip_is_valid' => inet_pton($address['ip']) !== false, + 'port' => $address['port'], + ]; + if (!$allow) { + return false; + } + // Userland creates the socket libcurl then connects through. + return socket_create($address['family'], $address['socktype'], $address['protocol']); +}); + +echo "Testing with an allowed connection\n"; +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); +var_dump($seen['handle'], $seen['purpose'], $seen['family_is_int'], + $seen['socktype_is_int'], $seen['protocol_is_int'], $seen['ip_is_valid']); +var_dump($seen['port'] === $port); + +echo "\nTesting with a blocked connection (return false)\n"; +$allow = false; +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT); + +echo "\nTesting with curl_copy_handle\n"; +$allow = true; +$ch2 = curl_copy_handle($ch); +var_dump(curl_exec($ch2)); + +echo "\nTesting with invalid return type\n"; +curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function ($ch, $purpose, $address) { + return 42; +}); +try { + curl_exec($ch); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with invalid option value\n"; +try { + curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, 42); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with null as the callback\n"; +var_dump(curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, null)); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); + +echo "\nDone"; +?> +--EXPECT-- +Testing with an allowed connection +string(25) "Hello World! +Hello World!" +int(0) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) + +Testing with a blocked connection (return false) +bool(false) +bool(true) + +Testing with curl_copy_handle +string(25) "Hello World! +Hello World!" + +Testing with invalid return type +The CURLOPT_OPENSOCKETFUNCTION callback must return a Socket or false + +Testing with invalid option value +curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_OPENSOCKETFUNCTION, no array or string given + +Testing with null as the callback +bool(true) +string(25) "Hello World! +Hello World!" +int(0) + +Done diff --git a/ext/curl/tests/curl_setopt_CURLOPT_SOCKOPTFUNCTION.phpt b/ext/curl/tests/curl_setopt_CURLOPT_SOCKOPTFUNCTION.phpt new file mode 100644 index 000000000000..d6bb4abac21e --- /dev/null +++ b/ext/curl/tests/curl_setopt_CURLOPT_SOCKOPTFUNCTION.phpt @@ -0,0 +1,126 @@ +--TEST-- +Curl option CURLOPT_SOCKOPTFUNCTION +--EXTENSIONS-- +curl +sockets +--FILE-- + $ch instanceof CurlHandle, + 'socket' => $socket instanceof Socket, + 'purpose' => $purpose === CURLSOCKTYPE_IPCXN, + // The Socket wraps the live descriptor: socket options can be set on it. + 'set_option' => socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1), + ]; + return $returnValue; +}); + +echo "\nTesting with CURL_SOCKOPT_OK\n"; +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); +var_dump($seen); + +echo "\nTesting with CURL_SOCKOPT_ERROR\n"; +$returnValue = CURL_SOCKOPT_ERROR; +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch) !== 0); + +echo "\nTesting with curl_copy_handle\n"; +$returnValue = CURL_SOCKOPT_OK; +$ch2 = curl_copy_handle($ch); +var_dump(curl_exec($ch2)); + +echo "\nTesting with invalid return type\n"; +curl_setopt($ch, CURLOPT_SOCKOPTFUNCTION, function ($ch, $socket, $purpose) { + return 'not an int'; +}); +try { + curl_exec($ch); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with invalid return value\n"; +curl_setopt($ch, CURLOPT_SOCKOPTFUNCTION, function ($ch, $socket, $purpose) { + return 42; +}); +try { + curl_exec($ch); +} catch (\ValueError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with invalid option value\n"; +try { + curl_setopt($ch, CURLOPT_SOCKOPTFUNCTION, 42); +} catch (\TypeError $e) { + echo $e->getMessage(), \PHP_EOL; +} + +echo "\nTesting with null as the callback\n"; +var_dump(curl_setopt($ch, CURLOPT_SOCKOPTFUNCTION, null)); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch)); + +echo "\nDone"; +?> +--EXPECT-- +int(0) +int(1) +int(2) + +Testing with CURL_SOCKOPT_OK +string(25) "Hello World! +Hello World!" +int(0) +array(4) { + ["handle"]=> + bool(true) + ["socket"]=> + bool(true) + ["purpose"]=> + bool(true) + ["set_option"]=> + bool(true) +} + +Testing with CURL_SOCKOPT_ERROR +bool(false) +bool(true) + +Testing with curl_copy_handle +string(25) "Hello World! +Hello World!" + +Testing with invalid return type +The CURLOPT_SOCKOPTFUNCTION callback must return one of CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR or CURL_SOCKOPT_ALREADY_CONNECTED + +Testing with invalid return value +The CURLOPT_SOCKOPTFUNCTION callback must return one of CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR or CURL_SOCKOPT_ALREADY_CONNECTED + +Testing with invalid option value +curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SOCKOPTFUNCTION, no array or string given + +Testing with null as the callback +bool(true) +string(25) "Hello World! +Hello World!" +int(0) + +Done diff --git a/ext/curl/tests/curl_sockopt_trampoline.phpt b/ext/curl/tests/curl_sockopt_trampoline.phpt new file mode 100644 index 000000000000..584af036aeda --- /dev/null +++ b/ext/curl/tests/curl_sockopt_trampoline.phpt @@ -0,0 +1,34 @@ +--TEST-- +Test trampoline for curl option CURLOPT_SOCKOPTFUNCTION +--EXTENSIONS-- +curl +sockets +--FILE-- + +--EXPECT-- +Trampoline for trampoline +Hello World! +Hello World!