diff --git a/NEWS b/NEWS index 864a6b2e189d0..e0e0d1d98ae55 100644 --- a/NEWS +++ b/NEWS @@ -19,6 +19,7 @@ PHP NEWS - Curl: . Added curl_multi_get_handles(). (timwolla) + . Added curl_share_init_persistent(). (enorris) - Date: . Fix undefined behaviour problems regarding integer overflow in extreme edge diff --git a/UPGRADING b/UPGRADING index 63307556848d4..275c34a345c2c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -68,6 +68,11 @@ PHP 8.5 UPGRADE NOTES . Added support for Closures in constant expressions. RFC: https://wiki.php.net/rfc/closures_in_const_expr +- Curl: + . Added support for share handles that are persisted across multiple PHP + requests, safely allowing for more effective connection reuse. + RFC: https://wiki.php.net/rfc/curl_share_persistence_improvement + - DOM: . Added Dom\Element::$outerHTML. @@ -148,6 +153,9 @@ PHP 8.5 UPGRADE NOTES . curl_multi_get_handles() allows retrieving all CurlHandles current attached to a CurlMultiHandle. This includes both handles added using curl_multi_add_handle() and handles accepted by CURLMOPT_PUSHFUNCTION. + . curl_share_init_persistent() allows creating a share handle that is + persisted across multiple PHP requests. + RFC: https://wiki.php.net/rfc/curl_share_persistence_improvement - DOM: . Added Dom\Element::insertAdjacentHTML(). @@ -166,6 +174,11 @@ PHP 8.5 UPGRADE NOTES 7. New Classes and Interfaces ======================================== +- Curl: + . CurlSharePersistentHandle representing a share handle that is persisted + across multiple PHP requests. + RFC: https://wiki.php.net/rfc/curl_share_persistence_improvement + ======================================== 8. Removed Extensions and SAPIs ======================================== diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 2c3c4a4bdc4d1..42435e695a515 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -46,6 +46,7 @@ static const func_info_t func_infos[] = { F1("curl_multi_strerror", MAY_BE_STRING|MAY_BE_NULL), F1("curl_share_init", MAY_BE_OBJECT), F1("curl_share_strerror", MAY_BE_STRING|MAY_BE_NULL), + F1("curl_share_init_persistent", MAY_BE_OBJECT), F1("curl_strerror", MAY_BE_STRING|MAY_BE_NULL), F1("curl_version", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_ARRAY|MAY_BE_FALSE), F1("date", MAY_BE_STRING), diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index 8a20231da562b..9eb8bd5b49e87 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -3668,6 +3668,15 @@ final class CurlShareHandle { } +/** + * @strict-properties + * @not-serializable + */ +final class CurlSharePersistentHandle +{ + public readonly array $options; +} + function curl_close(CurlHandle $handle): void {} /** @refcount 1 */ @@ -3750,6 +3759,9 @@ function curl_share_setopt(CurlShareHandle $share_handle, int $option, mixed $va /** @refcount 1 */ function curl_share_strerror(int $error_code): ?string {} +/** @refcount 1 */ +function curl_share_init_persistent(array $share_options): CurlSharePersistentHandle {} + /** @refcount 1 */ function curl_strerror(int $error_code): ?string {} diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index 664cda7d32a97..e873e9e9f3277 100644 --- a/ext/curl/curl_arginfo.h +++ b/ext/curl/curl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: e2800e5ecc33f092576c7afcdb98d89825809669 */ + * Stub hash: 7d3cd96f8725c59be46817487bb8d06e04384269 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) @@ -137,6 +137,10 @@ ZEND_END_ARG_INFO() #define arginfo_curl_share_strerror arginfo_curl_multi_strerror +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_curl_share_init_persistent, 0, 1, CurlSharePersistentHandle, 0) + ZEND_ARG_TYPE_INFO(0, share_options, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + #define arginfo_curl_strerror arginfo_curl_multi_strerror ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_curl_version, 0, 0, MAY_BE_ARRAY|MAY_BE_FALSE) @@ -176,6 +180,7 @@ ZEND_FUNCTION(curl_share_errno); ZEND_FUNCTION(curl_share_init); ZEND_FUNCTION(curl_share_setopt); ZEND_FUNCTION(curl_share_strerror); +ZEND_FUNCTION(curl_share_init_persistent); ZEND_FUNCTION(curl_strerror); ZEND_FUNCTION(curl_version); @@ -214,6 +219,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(curl_share_init, arginfo_curl_share_init) ZEND_FE(curl_share_setopt, arginfo_curl_share_setopt) ZEND_FE(curl_share_strerror, arginfo_curl_share_strerror) + ZEND_FE(curl_share_init_persistent, arginfo_curl_share_init_persistent) ZEND_FE(curl_strerror, arginfo_curl_strerror) ZEND_FE(curl_version, arginfo_curl_version) ZEND_FE_END @@ -1137,3 +1143,19 @@ static zend_class_entry *register_class_CurlShareHandle(void) return class_entry; } + +static zend_class_entry *register_class_CurlSharePersistentHandle(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "CurlSharePersistentHandle", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + zval property_options_default_value; + ZVAL_UNDEF(&property_options_default_value); + zend_string *property_options_name = zend_string_init("options", sizeof("options") - 1, 1); + zend_declare_typed_property(class_entry, property_options_name, &property_options_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_ARRAY)); + zend_string_release(property_options_name); + + return class_entry; +} diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 8e8e9586db587..dc23b5910cfbc 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -40,9 +40,20 @@ #define SAVE_CURL_ERROR(__handle, __err) \ do { (__handle)->err.no = (int) __err; } while (0) + +ZEND_BEGIN_MODULE_GLOBALS(curl) + HashTable persistent_curlsh; +ZEND_END_MODULE_GLOBALS(curl) + +ZEND_EXTERN_MODULE_GLOBALS(curl) + +#define CURL_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(curl, v) + PHP_MINIT_FUNCTION(curl); PHP_MSHUTDOWN_FUNCTION(curl); PHP_MINFO_FUNCTION(curl); +PHP_GINIT_FUNCTION(curl); +PHP_GSHUTDOWN_FUNCTION(curl); typedef struct { zend_fcall_info_cache fcc; @@ -153,6 +164,8 @@ static inline php_curlsh *curl_share_from_obj(zend_object *obj) { void curl_multi_register_handlers(void); void curl_share_register_handlers(void); +void curl_share_persistent_register_handlers(void); +void curl_share_free_persistent_curlsh(zval *data); void curlfile_register_class(void); zend_result curl_cast_object(zend_object *obj, zval *result, int type); diff --git a/ext/curl/interface.c b/ext/curl/interface.c index aba5273d5496c..690f2ccc609c9 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -67,6 +67,8 @@ #include "curl_arginfo.h" +ZEND_DECLARE_MODULE_GLOBALS(curl) + #ifdef PHP_CURL_NEED_OPENSSL_TSL /* {{{ */ static MUTEX_T *php_curl_openssl_tsl = NULL; @@ -215,7 +217,11 @@ zend_module_entry curl_module_entry = { NULL, PHP_MINFO(curl), PHP_CURL_VERSION, - STANDARD_MODULE_PROPERTIES + PHP_MODULE_GLOBALS(curl), + PHP_GINIT(curl), + PHP_GSHUTDOWN(curl), + NULL, + STANDARD_MODULE_PROPERTIES_EX }; /* }}} */ @@ -223,10 +229,22 @@ zend_module_entry curl_module_entry = { ZEND_GET_MODULE (curl) #endif +PHP_GINIT_FUNCTION(curl) +{ + zend_hash_init(&curl_globals->persistent_curlsh, 0, NULL, curl_share_free_persistent_curlsh, true); + GC_MAKE_PERSISTENT_LOCAL(&curl_globals->persistent_curlsh); +} + +PHP_GSHUTDOWN_FUNCTION(curl) +{ + zend_hash_destroy(&curl_globals->persistent_curlsh); +} + /* CurlHandle class */ zend_class_entry *curl_ce; zend_class_entry *curl_share_ce; +zend_class_entry *curl_share_persistent_ce; static zend_object_handlers curl_object_handlers; static zend_object *curl_create_object(zend_class_entry *class_type); @@ -410,6 +428,10 @@ PHP_MINIT_FUNCTION(curl) curl_share_ce = register_class_CurlShareHandle(); curl_share_register_handlers(); + + curl_share_persistent_ce = register_class_CurlSharePersistentHandle(); + curl_share_persistent_register_handlers(); + curlfile_register_class(); return SUCCESS; @@ -2281,16 +2303,24 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue case CURLOPT_SHARE: { - if (Z_TYPE_P(zvalue) == IS_OBJECT && Z_OBJCE_P(zvalue) == curl_share_ce) { - php_curlsh *sh = Z_CURL_SHARE_P(zvalue); - curl_easy_setopt(ch->cp, CURLOPT_SHARE, sh->share); + if (Z_TYPE_P(zvalue) != IS_OBJECT) { + break; + } - if (ch->share) { - OBJ_RELEASE(&ch->share->std); - } - GC_ADDREF(&sh->std); - ch->share = sh; + if (Z_OBJCE_P(zvalue) != curl_share_ce && Z_OBJCE_P(zvalue) != curl_share_persistent_ce) { + break; } + + php_curlsh *sh = Z_CURL_SHARE_P(zvalue); + + curl_easy_setopt(ch->cp, CURLOPT_SHARE, sh->share); + + if (ch->share) { + OBJ_RELEASE(&ch->share->std); + } + + GC_ADDREF(&sh->std); + ch->share = sh; } break; diff --git a/ext/curl/php_curl.h b/ext/curl/php_curl.h index bc92c51121ec8..6084d5935c706 100644 --- a/ext/curl/php_curl.h +++ b/ext/curl/php_curl.h @@ -37,6 +37,7 @@ extern zend_module_entry curl_module_entry; PHP_CURL_API extern zend_class_entry *curl_ce; PHP_CURL_API extern zend_class_entry *curl_share_ce; +PHP_CURL_API extern zend_class_entry *curl_share_persistent_ce; PHP_CURL_API extern zend_class_entry *curl_multi_ce; PHP_CURL_API extern zend_class_entry *curl_CURLFile_class; PHP_CURL_API extern zend_class_entry *curl_CURLStringFile_class; diff --git a/ext/curl/share.c b/ext/curl/share.c index 406982d203275..8f3bb7543a20f 100644 --- a/ext/curl/share.c +++ b/ext/curl/share.c @@ -21,6 +21,7 @@ #endif #include "php.h" +#include "Zend/zend_exceptions.h" #include "curl_private.h" @@ -134,6 +135,151 @@ PHP_FUNCTION(curl_share_strerror) } /* }}} */ +/** + * Initialize a persistent curl share handle with the given share options, reusing an existing one if found. + * + * Throws an exception if the share options are invalid. + */ +PHP_FUNCTION(curl_share_init_persistent) +{ + // Options specified by the user. + HashTable *share_opts = NULL; + + // De-duplicated and sorted options. + zval share_opts_prop; + ZVAL_UNDEF(&share_opts_prop); + + // An ID representing the unique set of options specified by the user. + zend_ulong persistent_id = 0; + + php_curlsh *sh = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(share_opts) + ZEND_PARSE_PARAMETERS_END(); + + if (zend_hash_num_elements(share_opts) == 0) { + zend_argument_must_not_be_empty_error(1); + goto error; + } + + ZEND_HASH_FOREACH_VAL(share_opts, zval *entry) { + ZVAL_DEREF(entry); + + bool failed = false; + zend_ulong option = zval_try_get_long(entry, &failed); + + if (failed) { + zend_argument_type_error(1, "must contain only int values, %s given", zend_zval_value_name(entry)); + goto error; + } + + switch (option) { + // Disallowed options + case CURL_LOCK_DATA_COOKIE: + zend_argument_value_error(1, "must not contain CURL_LOCK_DATA_COOKIE because sharing cookies across PHP requests is unsafe"); + goto error; + + // Allowed options + case CURL_LOCK_DATA_DNS: + persistent_id |= 1 << 0; + break; + case CURL_LOCK_DATA_SSL_SESSION: + persistent_id |= 1 << 1; + break; + case CURL_LOCK_DATA_CONNECT: + persistent_id |= 1 << 2; + break; + case CURL_LOCK_DATA_PSL: + persistent_id |= 1 << 3; + break; + + // Unknown options + default: + zend_argument_value_error(1, "must contain only CURL_LOCK_DATA_* constants"); + goto error; + } + } ZEND_HASH_FOREACH_END(); + + // We're now decently confident that we'll be returning a CurlSharePersistentHandle object, so we construct it here. + object_init_ex(return_value, curl_share_persistent_ce); + + // Next we initialize a property field for the CurlSharePersistentHandle object with the enabled share options. + array_init(&share_opts_prop); + + if (persistent_id & (1 << 0)) { + add_next_index_long(&share_opts_prop, CURL_LOCK_DATA_DNS); + } + + if (persistent_id & (1 << 1)) { + add_next_index_long(&share_opts_prop, CURL_LOCK_DATA_SSL_SESSION); + } + + if (persistent_id & (1 << 2)) { + add_next_index_long(&share_opts_prop, CURL_LOCK_DATA_CONNECT); + } + + if (persistent_id & (1 << 3)) { + add_next_index_long(&share_opts_prop, CURL_LOCK_DATA_PSL); + } + + zend_update_property(curl_share_persistent_ce, Z_OBJ_P(return_value), ZEND_STRL("options"), &share_opts_prop); + + sh = Z_CURL_SHARE_P(return_value); + + // If we are able to find an existing persistent share handle, we can use it and return early. + zval *persisted = zend_hash_index_find(&CURL_G(persistent_curlsh), persistent_id); + + if (persisted) { + sh->share = Z_PTR_P(persisted); + + goto cleanup; + } + + // We could not find an existing share handle, so we'll have to create one. + sh->share = curl_share_init(); + + // Apply the options property to the handle. We avoid using $share_opts as zval_get_long may not necessarily return + // the same value as it did in the initial ZEND_HASH_FOREACH_VAL above. + ZEND_HASH_PACKED_FOREACH_VAL(Z_ARRVAL_P(&share_opts_prop), zval *entry) { + CURLSHcode curlsh_error = curl_share_setopt(sh->share, CURLSHOPT_SHARE, Z_LVAL_P(entry)); + + if (curlsh_error != CURLSHE_OK) { + zend_throw_exception_ex(NULL, 0, "Could not construct persistent cURL share handle: %s", curl_share_strerror(curlsh_error)); + + goto error; + } + } ZEND_HASH_FOREACH_END(); + + zend_hash_index_add_new_ptr(&CURL_G(persistent_curlsh), persistent_id, sh->share); + + cleanup: + zval_ptr_dtor(&share_opts_prop); + + return; + + error: + if (sh) { + curl_share_cleanup(sh->share); + } + + zval_ptr_dtor(&share_opts_prop); + + RETURN_THROWS(); +} + +/** + * Free a persistent curl share handle from the module global HashTable. + * + * See PHP_GINIT_FUNCTION in ext/curl/interface.c. + */ +void curl_share_free_persistent_curlsh(zval *data) +{ + CURLSH *handle = Z_PTR_P(data); + + curl_share_cleanup(handle); +} + /* CurlShareHandle class */ static zend_object *curl_share_create_object(zend_class_entry *class_type) { @@ -171,3 +317,23 @@ void curl_share_register_handlers(void) { curl_share_handlers.clone_obj = NULL; curl_share_handlers.compare = zend_objects_not_comparable; } + +/* CurlSharePersistentHandle class */ + +static zend_object_handlers curl_share_persistent_handlers; + +static zend_function *curl_share_persistent_get_constructor(zend_object *object) { + zend_throw_error(NULL, "Cannot directly construct CurlSharePersistentHandle, use curl_share_init_persistent() instead"); + return NULL; +} + +void curl_share_persistent_register_handlers(void) { + curl_share_persistent_ce->create_object = curl_share_create_object; + curl_share_persistent_ce->default_object_handlers = &curl_share_persistent_handlers; + + memcpy(&curl_share_persistent_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + curl_share_persistent_handlers.offset = XtOffsetOf(php_curlsh, std); + curl_share_persistent_handlers.get_constructor = curl_share_persistent_get_constructor; + curl_share_persistent_handlers.clone_obj = NULL; + curl_share_persistent_handlers.compare = zend_objects_not_comparable; +} diff --git a/ext/curl/tests/curl_persistent_share_001.phpt b/ext/curl/tests/curl_persistent_share_001.phpt new file mode 100644 index 0000000000000..70da0d5880f75 --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_001.phpt @@ -0,0 +1,46 @@ +--TEST-- +Basic curl persistent share handle test +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- +options); + +$sh2 = curl_share_init_persistent([CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT]); +$ch2 = get_localhost_curl_handle($sh2); + +// Expect the connect time on the subsequent request to be zero, since it's reusing the connection. +var_dump(curl_exec($ch1)); +var_dump(curl_exec($ch2)); +var_dump("second connect_time: " . (curl_getinfo($ch2)["connect_time"])); + +?> +--EXPECT-- +array(2) { + [0]=> + int(3) + [1]=> + int(5) +} +string(23) "Caddy is up and running" +string(23) "Caddy is up and running" +string(22) "second connect_time: 0" diff --git a/ext/curl/tests/curl_persistent_share_002.phpt b/ext/curl/tests/curl_persistent_share_002.phpt new file mode 100644 index 0000000000000..fbd6631b9ba13 --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_002.phpt @@ -0,0 +1,42 @@ +--TEST-- +Curl persistent share handle test with different options +--EXTENSIONS-- +curl +--SKIPIF-- + +--FILE-- +options != $sh2->options); + +// Expect the connect time on the subsequent request to be non-zero, since it's *not* reusing the connection. +var_dump(curl_exec($ch1)); +var_dump(curl_exec($ch2)); +var_dump("second connect_time: " . (curl_getinfo($ch2)["connect_time"])); + +?> +--EXPECTREGEX-- +bool\(true\) +string\(23\) "Caddy is up and running" +string\(23\) "Caddy is up and running" +string\(\d+\) "second connect_time: .*[1-9].*" diff --git a/ext/curl/tests/curl_persistent_share_003.phpt b/ext/curl/tests/curl_persistent_share_003.phpt new file mode 100644 index 0000000000000..60a30de4d4641 --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_003.phpt @@ -0,0 +1,16 @@ +--TEST-- +Curl persistent share handle test with invalid option +--EXTENSIONS-- +curl +--FILE-- +getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +curl_share_init_persistent(): Argument #1 ($share_options) must contain only CURL_LOCK_DATA_* constants diff --git a/ext/curl/tests/curl_persistent_share_004.phpt b/ext/curl/tests/curl_persistent_share_004.phpt new file mode 100644 index 0000000000000..fe0e7ec2cf0ed --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_004.phpt @@ -0,0 +1,16 @@ +--TEST-- +Curl persistent share handle test with disallowed option +--EXTENSIONS-- +curl +--FILE-- +getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +curl_share_init_persistent(): Argument #1 ($share_options) must not contain CURL_LOCK_DATA_COOKIE because sharing cookies across PHP requests is unsafe diff --git a/ext/curl/tests/curl_persistent_share_005.phpt b/ext/curl/tests/curl_persistent_share_005.phpt new file mode 100644 index 0000000000000..8737ba6c39a42 --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_005.phpt @@ -0,0 +1,18 @@ +--TEST-- +Curl persistent share handles cannot be used with curl_share_setopt +--EXTENSIONS-- +curl +--FILE-- +getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +curl_share_setopt(): Argument #1 ($share_handle) must be of type CurlShareHandle, CurlSharePersistentHandle given diff --git a/ext/curl/tests/curl_persistent_share_006.phpt b/ext/curl/tests/curl_persistent_share_006.phpt new file mode 100644 index 0000000000000..ffca764757fbb --- /dev/null +++ b/ext/curl/tests/curl_persistent_share_006.phpt @@ -0,0 +1,16 @@ +--TEST-- +Curl persistent share handles must be initialized with a non-empty $share_opts +--EXTENSIONS-- +curl +--FILE-- +getMessage() . PHP_EOL; +} + +?> +--EXPECT-- +curl_share_init_persistent(): Argument #1 ($share_options) must not be empty diff --git a/ext/curl/tests/skipif-nocaddy.inc b/ext/curl/tests/skipif-nocaddy.inc index 21a06c12af106..ae5442ff28ede 100644 --- a/ext/curl/tests/skipif-nocaddy.inc +++ b/ext/curl/tests/skipif-nocaddy.inc @@ -2,6 +2,7 @@ $ch = curl_init("https://localhost"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $body = curl_exec($ch);