1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

RFC: Add CurlSharePersistentHandle objects (#16937)

see https://wiki.php.net/rfc/curl_share_persistence_improvement
This commit is contained in:
Eric Norris
2025-01-06 15:52:00 -05:00
committed by GitHub
parent 95d1476e27
commit d20880ce3b
16 changed files with 425 additions and 11 deletions

1
NEWS
View File

@@ -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

View File

@@ -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
========================================

View File

@@ -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),

View File

@@ -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 {}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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 (ch->share) {
OBJ_RELEASE(&ch->share->std);
}
GC_ADDREF(&sh->std);
ch->share = sh;
if (Z_TYPE_P(zvalue) != IS_OBJECT) {
break;
}
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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -0,0 +1,46 @@
--TEST--
Basic curl persistent share handle test
--EXTENSIONS--
curl
--SKIPIF--
<?php
include 'skipif-nocaddy.inc';
?>
--FILE--
<?php
function get_localhost_curl_handle(CurlSharePersistentHandle $sh): CurlHandle {
$ch = curl_init("https://localhost");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SHARE, $sh);
return $ch;
}
$sh1 = curl_share_init_persistent([CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_DNS]);
$ch1 = get_localhost_curl_handle($sh1);
// Expect the two options we set above, but sorted and de-duplicated.
var_dump($sh1->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"

View File

@@ -0,0 +1,42 @@
--TEST--
Curl persistent share handle test with different options
--EXTENSIONS--
curl
--SKIPIF--
<?php
include 'skipif-nocaddy.inc';
?>
--FILE--
<?php
function get_localhost_curl_handle(CurlSharePersistentHandle $sh): CurlHandle {
$ch = curl_init("https://localhost");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SHARE, $sh);
return $ch;
}
$sh1 = curl_share_init_persistent([CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT]);
$ch1 = get_localhost_curl_handle($sh1);
// Note that we're using different share options and thus should get a different persistent share
// handle.
$sh2 = curl_share_init_persistent([CURL_LOCK_DATA_CONNECT]);
$ch2 = get_localhost_curl_handle($sh2);
var_dump($sh1->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].*"

View File

@@ -0,0 +1,16 @@
--TEST--
Curl persistent share handle test with invalid option
--EXTENSIONS--
curl
--FILE--
<?php
try {
$sh = curl_share_init_persistent([CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_CONNECT, 30]);
} catch (\ValueError $e) {
echo $e->getMessage() . PHP_EOL;
}
?>
--EXPECT--
curl_share_init_persistent(): Argument #1 ($share_options) must contain only CURL_LOCK_DATA_* constants

View File

@@ -0,0 +1,16 @@
--TEST--
Curl persistent share handle test with disallowed option
--EXTENSIONS--
curl
--FILE--
<?php
try {
$sh = curl_share_init_persistent([CURL_LOCK_DATA_COOKIE]);
} catch (\ValueError $e) {
echo $e->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

View File

@@ -0,0 +1,18 @@
--TEST--
Curl persistent share handles cannot be used with curl_share_setopt
--EXTENSIONS--
curl
--FILE--
<?php
$sh = curl_share_init_persistent([CURL_LOCK_DATA_DNS]);
try {
curl_share_setopt($sh, CURLOPT_SHARE, CURL_LOCK_DATA_CONNECT);
} catch (\TypeError $e) {
echo $e->getMessage() . PHP_EOL;
}
?>
--EXPECT--
curl_share_setopt(): Argument #1 ($share_handle) must be of type CurlShareHandle, CurlSharePersistentHandle given

View File

@@ -0,0 +1,16 @@
--TEST--
Curl persistent share handles must be initialized with a non-empty $share_opts
--EXTENSIONS--
curl
--FILE--
<?php
try {
$sh = curl_share_init_persistent([]);
} catch (\ValueError $e) {
echo $e->getMessage() . PHP_EOL;
}
?>
--EXPECT--
curl_share_init_persistent(): Argument #1 ($share_options) must not be empty

View File

@@ -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);