From cf5fad2dd3480ddf5a9f3302acf935331fed236d Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:05:36 +0200 Subject: [PATCH] [RFC?] Add curl websocket bindings This adds bindings to Curl's websocket support that was added in Curl 7.86.0. Instead of just pass-through C bindings, the API added here is a little more high-level from a type-safety perspective. I.e. this introduces an enum CurlWsMessageType and a DTO class CurlWsFrame for nicer and safer API handling. Still WIP. --- Zend/Optimizer/zend_func_infos.h | 4 + ext/curl/curl.stub.php | 45 +++++++ ext/curl/curl_arginfo.h | 94 +++++++++++++- ext/curl/interface.c | 203 +++++++++++++++++++++++++++++++ ext/curl/php_curl.h | 4 + 5 files changed, 349 insertions(+), 1 deletion(-) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 0fc33ae2f6e1c..52051196a361e 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -49,6 +49,10 @@ static const func_info_t func_infos[] = { 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), +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ + F1("curl_ws_meta", MAY_BE_OBJECT), + F1("curl_ws_recv", MAY_BE_STRING|MAY_BE_FALSE), +#endif F1("date", MAY_BE_STRING), F1("gmdate", MAY_BE_STRING), F1("strftime", MAY_BE_STRING|MAY_BE_FALSE), diff --git a/ext/curl/curl.stub.php b/ext/curl/curl.stub.php index c9abe237339b5..fbe623a604976 100644 --- a/ext/curl/curl.stub.php +++ b/ext/curl/curl.stub.php @@ -3634,6 +3634,16 @@ * @cvalue CURLWS_RAW_MODE */ const CURLWS_RAW_MODE = UNKNOWN; +/** + * @var int + * @cvalue CURLWS_CONT + */ +const CURLWS_CONT = UNKNOWN; +/** + * @var int + * @cvalue CURLWS_OFFSET + */ +const CURLWS_OFFSET = UNKNOWN; #endif #if LIBCURL_VERSION_NUM >= 0x075700 /* Available since 7.87.0 */ @@ -3696,6 +3706,28 @@ final class CurlSharePersistentHandle public readonly array $options; } +enum CurlWsMessageType +{ + case Binary; + case Text; + case Close; + case Ping; + case Pong; +} + +/** + * @strict-properties + * @not-serializable + */ +final readonly class CurlWsFrame +{ + public CurlWsMessageType $type; + public bool $continued; + public int $offset; + public int $bytesLeft; + public int $length; +} + function curl_close(CurlHandle $handle): void {} /** @refcount 1 */ @@ -3789,3 +3821,16 @@ function curl_strerror(int $error_code): ?string {} * @refcount 1 */ function curl_version(): array|false {} + +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +/** @refcount 1 */ +function curl_ws_meta(CurlHandle $handle): CurlWsFrame {} + +/** + * @param CurlWsFrame $meta + * @refcount 1 + */ +function curl_ws_recv(CurlHandle $handle, int $length, &$meta = null): string|false {} + +function curl_ws_send(CurlHandle $handle, string $buffer, CurlWsMessageType $type = CurlWsMessageType::Text, int $frag_size = 0, int $flags = 0): int {} +#endif diff --git a/ext/curl/curl_arginfo.h b/ext/curl/curl_arginfo.h index 6a81d1e92c88f..580ca1a7d3329 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: 48fc95503f4f8cc96575ff6f31fdd1e4cdc5969e */ + * Stub hash: 75138fceceb89c452d40d5d61f107f3e28aa0f35 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_close, 0, 1, IS_VOID, 0) ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) @@ -146,6 +146,26 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_curl_version, 0, 0, MAY_BE_ARRAY|MAY_BE_FALSE) ZEND_END_ARG_INFO() +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_curl_ws_meta, 0, 1, CurlWsFrame, 0) + ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_curl_ws_recv, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) + ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) + ZEND_ARG_TYPE_INFO(0, length, IS_LONG, 0) + ZEND_ARG_INFO_WITH_DEFAULT_VALUE(1, meta, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_curl_ws_send, 0, 2, IS_LONG, 0) + ZEND_ARG_OBJ_INFO(0, handle, CurlHandle, 0) + ZEND_ARG_TYPE_INFO(0, buffer, IS_STRING, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, type, CurlWsMessageType, 0, "CurlWsMessageType::Text") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, frag_size, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() +#endif + ZEND_FUNCTION(curl_close); ZEND_FUNCTION(curl_copy_handle); ZEND_FUNCTION(curl_errno); @@ -183,6 +203,11 @@ ZEND_FUNCTION(curl_share_strerror); ZEND_FUNCTION(curl_share_init_persistent); ZEND_FUNCTION(curl_strerror); ZEND_FUNCTION(curl_version); +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +ZEND_FUNCTION(curl_ws_meta); +ZEND_FUNCTION(curl_ws_recv); +ZEND_FUNCTION(curl_ws_send); +#endif static const zend_function_entry ext_functions[] = { ZEND_FE(curl_close, arginfo_curl_close) @@ -222,6 +247,11 @@ static const zend_function_entry ext_functions[] = { 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) +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ + ZEND_FE(curl_ws_meta, arginfo_curl_ws_meta) + ZEND_FE(curl_ws_recv, arginfo_curl_ws_recv) + ZEND_FE(curl_ws_send, arginfo_curl_ws_send) +#endif ZEND_FE_END }; @@ -1111,6 +1141,12 @@ static void register_curl_symbols(int module_number) #if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ REGISTER_LONG_CONSTANT("CURLWS_RAW_MODE", CURLWS_RAW_MODE, CONST_PERSISTENT); #endif +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ + REGISTER_LONG_CONSTANT("CURLWS_CONT", CURLWS_CONT, CONST_PERSISTENT); +#endif +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ + REGISTER_LONG_CONSTANT("CURLWS_OFFSET", CURLWS_OFFSET, CONST_PERSISTENT); +#endif #if LIBCURL_VERSION_NUM >= 0x075700 /* Available since 7.87.0 */ REGISTER_LONG_CONSTANT("CURLOPT_CA_CACHE_TIMEOUT", CURLOPT_CA_CACHE_TIMEOUT, CONST_PERSISTENT); #endif @@ -1168,3 +1204,59 @@ static zend_class_entry *register_class_CurlSharePersistentHandle(void) return class_entry; } + +static zend_class_entry *register_class_CurlWsMessageType(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("CurlWsMessageType", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Binary", NULL); + + zend_enum_add_case_cstr(class_entry, "Text", NULL); + + zend_enum_add_case_cstr(class_entry, "Close", NULL); + + zend_enum_add_case_cstr(class_entry, "Ping", NULL); + + zend_enum_add_case_cstr(class_entry, "Pong", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_CurlWsFrame(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "CurlWsFrame", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE|ZEND_ACC_READONLY_CLASS); + + zval property_type_default_value; + ZVAL_UNDEF(&property_type_default_value); + zend_string *property_type_class_CurlWsMessageType = zend_string_init("CurlWsMessageType", sizeof("CurlWsMessageType")-1, 1); + zend_declare_typed_property(class_entry, ZSTR_KNOWN(ZEND_STR_TYPE), &property_type_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_CLASS(property_type_class_CurlWsMessageType, 0, 0)); + + zval property_continued_default_value; + ZVAL_UNDEF(&property_continued_default_value); + zend_string *property_continued_name = zend_string_init("continued", sizeof("continued") - 1, 1); + zend_declare_typed_property(class_entry, property_continued_name, &property_continued_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_BOOL)); + zend_string_release(property_continued_name); + + zval property_offset_default_value; + ZVAL_UNDEF(&property_offset_default_value); + zend_string *property_offset_name = zend_string_init("offset", sizeof("offset") - 1, 1); + zend_declare_typed_property(class_entry, property_offset_name, &property_offset_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_offset_name); + + zval property_bytesLeft_default_value; + ZVAL_UNDEF(&property_bytesLeft_default_value); + zend_string *property_bytesLeft_name = zend_string_init("bytesLeft", sizeof("bytesLeft") - 1, 1); + zend_declare_typed_property(class_entry, property_bytesLeft_name, &property_bytesLeft_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_bytesLeft_name); + + zval property_length_default_value; + ZVAL_UNDEF(&property_length_default_value); + zend_string *property_length_name = zend_string_init("length", sizeof("length") - 1, 1); + zend_declare_typed_property(class_entry, property_length_name, &property_length_default_value, ZEND_ACC_PUBLIC|ZEND_ACC_READONLY, NULL, (zend_type) ZEND_TYPE_INIT_MASK(MAY_BE_LONG)); + zend_string_release(property_length_name); + + return class_entry; +} diff --git a/ext/curl/interface.c b/ext/curl/interface.c index aebb336378bc0..545cfacb62398 100644 --- a/ext/curl/interface.c +++ b/ext/curl/interface.c @@ -53,6 +53,7 @@ #endif /* ZTS && HAVE_CURL_OLD_OPENSSL */ /* }}} */ +#include "zend_enum.h" #include "zend_smart_str.h" #include "ext/standard/info.h" #include "ext/standard/file.h" @@ -245,6 +246,10 @@ PHP_GSHUTDOWN_FUNCTION(curl) zend_class_entry *curl_ce; zend_class_entry *curl_share_ce; zend_class_entry *curl_share_persistent_ce; +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +zend_class_entry *curl_ws_frame_ce; +zend_class_entry *curl_ws_message_type_ce; +#endif static zend_object_handlers curl_object_handlers; static zend_object *curl_create_object(zend_class_entry *class_type); @@ -432,6 +437,12 @@ PHP_MINIT_FUNCTION(curl) curl_share_persistent_ce = register_class_CurlSharePersistentHandle(); curl_share_persistent_register_handlers(); +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ + curl_ws_frame_ce = register_class_CurlWsFrame(); + + curl_ws_message_type_ce = register_class_CurlWsMessageType(); +#endif + curlfile_register_class(); return SUCCESS; @@ -3117,3 +3128,195 @@ PHP_FUNCTION(curl_upkeep) } /*}}} */ #endif + +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +static zend_result curl_create_ws_frame_obj(zval *return_value, const struct curl_ws_frame *frame) +{ + const char *case_name; + if (frame->flags & CURLWS_BINARY) { + case_name = "Binary"; + } else if (frame->flags & CURLWS_TEXT) { + case_name = "Text"; + } else if (frame->flags & CURLWS_CLOSE) { + case_name = "Close"; + } else if (frame->flags & CURLWS_PING) { + case_name = "Ping"; + } else if (frame->flags & CURLWS_PONG) { + case_name = "Pong"; + } else { + /* Should not happen, here for defensive programming. */ + zend_throw_error(NULL, "Unable to determine message type"); + return FAILURE; + } + + object_init_ex(return_value, curl_ws_frame_ce); + zend_object *obj = Z_OBJ_P(return_value); + /* Note: object does not yet contain "age" field because at the time of writing this is always zero. */ + ZVAL_OBJ_COPY(OBJ_PROP_NUM(obj, 0), zend_enum_get_case_cstr(curl_ws_message_type_ce, case_name)); + ZVAL_BOOL(OBJ_PROP_NUM(obj, 1), frame->flags & CURLWS_CONT); + ZVAL_LONG(OBJ_PROP_NUM(obj, 2), (zend_long) frame->offset); + ZVAL_LONG(OBJ_PROP_NUM(obj, 3), (zend_long) frame->bytesleft); + ZVAL_LONG(OBJ_PROP_NUM(obj, 4), (zend_long) frame->len); + + return SUCCESS; +} + +PHP_FUNCTION(curl_ws_meta) +{ + zval *zid; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zid, curl_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_curl *ch = Z_CURL_P(zid); + + const struct curl_ws_frame *frame = curl_ws_meta(ch->cp); + if (UNEXPECTED(!frame)) { + zend_throw_error(NULL, "%s(): Called outside a valid websocket context", get_active_function_name()); + RETURN_THROWS(); + } + + (void) curl_create_ws_frame_obj(return_value, frame); +} + +PHP_FUNCTION(curl_ws_recv) +{ + zval *zid, *z_meta = NULL; + zend_long length; + size_t recv; + const struct curl_ws_frame *meta; + + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(zid, curl_ce) + Z_PARAM_LONG(length) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL_OR_NULL(z_meta) + ZEND_PARSE_PARAMETERS_END(); + + if (UNEXPECTED(length <= 0)) { + zend_argument_value_error(2, "must be greater than 0"); + RETURN_THROWS(); + } + if (UNEXPECTED(length > ZSTR_MAX_LEN)) { + zend_argument_value_error(2, "must be less than the maximum string length"); + RETURN_THROWS(); + } + + php_curl *ch = Z_CURL_P(zid); + + zend_string *buffer = zend_string_alloc(length, false); + + CURLcode retcode = curl_ws_recv(ch->cp, ZSTR_VAL(buffer), (size_t) length, &recv, &meta); + SAVE_CURL_ERROR(ch, retcode); + if (UNEXPECTED(retcode != CURLE_OK)) { + zend_string_efree(buffer); + RETURN_FALSE; + } + + if (recv < length) { + ZEND_ASSERT(!ZSTR_IS_INTERNED(buffer) && GC_REFCOUNT(buffer) == 1); + buffer = zend_string_truncate(buffer, recv, false); + } + ZSTR_VAL(buffer)[ZSTR_LEN(buffer)] = '\0'; + + if (z_meta) { + zval obj; + if (curl_create_ws_frame_obj(&obj, meta) == SUCCESS) { + ZEND_TRY_ASSIGN_REF_VALUE(z_meta, &obj); + } + } + + RETURN_NEW_STR(buffer); +} + +PHP_FUNCTION(curl_ws_send) +{ + zval *zid; + zend_string *buffer; + zend_object *type_zv = NULL; + zend_long frag_size = 0, flags = 0; + size_t sent = 0; + zend_long type_nr = CURLWS_TEXT; + + ZEND_PARSE_PARAMETERS_START(2, 5) + Z_PARAM_OBJECT_OF_CLASS(zid, curl_ce) + Z_PARAM_STR(buffer) + Z_PARAM_OPTIONAL + Z_PARAM_OBJ_OF_CLASS(type_zv, curl_ws_message_type_ce) + Z_PARAM_LONG(frag_size) + Z_PARAM_LONG(flags) + ZEND_PARSE_PARAMETERS_END(); + + php_curl *ch = Z_CURL_P(zid); + + if (type_zv) { + const zend_string *type = Z_STR_P(zend_enum_fetch_case_name(type_zv)); + switch (ZSTR_VAL(type)[1] + ZSTR_LEN(type)) { + case 'i' + sizeof("Binary")-1: + type_nr = CURLWS_BINARY; + break; + case 'e' + sizeof("Text")-1: + type_nr = CURLWS_TEXT; + break; + case 'l' + sizeof("Close")-1: + type_nr = CURLWS_CLOSE; + break; + case 'i' + sizeof("Ping")-1: + type_nr = CURLWS_PING; + break; + case 'o' + sizeof("Pong")-1: + type_nr = CURLWS_PONG; + break; + EMPTY_SWITCH_DEFAULT_CASE(); + } + } + + if (UNEXPECTED(frag_size < 0)) { + zend_argument_value_error(4, "must be greater than 0"); + RETURN_THROWS(); + } + + const curl_off_t max_curl_off_t = sizeof(curl_off_t) == 8 ? INT64_MAX : INT32_MAX; + if (UNEXPECTED(frag_size > max_curl_off_t)) { + zend_argument_value_error(4, "must be less than %" PRId64, max_curl_off_t); + RETURN_THROWS(); + } + + if (frag_size == 0) { + if (UNEXPECTED(flags & CURLWS_OFFSET)) { + zend_argument_value_error(4, "must not be zero when argument #5 ($flags) contains CURLWS_OFFSET"); + RETURN_THROWS(); + } + } else { + if (UNEXPECTED(!(flags & CURLWS_OFFSET))) { + zend_argument_value_error(4, "must be zero when argument #5 ($flags) does not contain CURLWS_OFFSET"); + RETURN_THROWS(); + } + } + + if (UNEXPECTED(flags & ~(CURLWS_CONT|CURLWS_OFFSET))) { + if (UNEXPECTED(flags & (CURLWS_BINARY|CURLWS_TEXT|CURLWS_CLOSE|CURLWS_PING|CURLWS_PONG))) { + zend_argument_value_error(5, "contains a message type flag, but the message type must be passed to argument #3 ($type)"); + RETURN_THROWS(); + } + zend_argument_value_error(5, "contains invalid flags (allowed flags: CURLWS_CONT, CURLWS_OFFSET)"); + RETURN_THROWS(); + } + + if (UNEXPECTED((flags & CURLWS_CONT) && type_nr != CURLWS_TEXT && type_nr != CURLWS_BINARY)) { + zend_argument_value_error(5, "contains CURLWS_CONT, which is only compatible with text or binary messages"); + RETURN_THROWS(); + } + + flags |= type_nr; + CURLcode retcode = curl_ws_send(ch->cp, ZSTR_VAL(buffer), ZSTR_LEN(buffer), &sent, (curl_off_t) frag_size, (unsigned int) flags); + SAVE_CURL_ERROR(ch, retcode); + if (UNEXPECTED(retcode != CURLE_OK)) { + ZEND_ASSERT(sent == 0); + RETURN_LONG(0); + } + + RETURN_LONG((zend_long) sent); +} +#endif diff --git a/ext/curl/php_curl.h b/ext/curl/php_curl.h index 6084d5935c706..e78a8f76d4384 100644 --- a/ext/curl/php_curl.h +++ b/ext/curl/php_curl.h @@ -39,6 +39,10 @@ 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; +#if LIBCURL_VERSION_NUM >= 0x075600 /* Available since 7.86.0 */ +PHP_CURL_API extern zend_class_entry *curl_ws_frame_ce; +PHP_CURL_API extern zend_class_entry *curl_ws_message_type_ce; +#endif PHP_CURL_API extern zend_class_entry *curl_CURLFile_class; PHP_CURL_API extern zend_class_entry *curl_CURLStringFile_class;