Skip to content

Commit

Permalink
ext/curl: Add CURLOPT_DEBUGFUNCTION option
Browse files Browse the repository at this point in the history
This adds support for `CURLOPT_DEBUGFUNCTION`[^1] Curl option to set a
custom callback that gets called with debug information during the
lifetime of a Curl request.

The callback gets called with the `CurlHandle` object, an integer
containing the type of the debug message, and a string containing the
debug message. The callback may get called multiple times with the
same message type during a request.

PHP already uses `CURLOPT_DEBUGFUNCTION` functionality to internally
to expose a Curl option named `CURLINFO_HEADER_OUT`.

However,`CURLINFO_HEADER_OUT` is not a "real" Curl option supported
by libcurl. Back in 2006, `CURLINFO_HEADER_OUT` was added[^2] as
a Curl option by using the debug-callback feature. Git history does
not run that back to show why `CURLINFO_HEADER_OUT` was added as a
Curl option, and why the other debug types (such as
`CURLINFO_HEADER_IN` were not added as Curl options, but this seems
to be a historical artifact when we added features without trying
to be close to libcurl options.

This approach has a few issues:

1. `CURLINFO_HEADER_OUT` is not an actual Curl option supported by
  upstream libcurl.

2. All of the Curl options have `CURLOPT_` prefix, and `CURLINFO_HEADER_OUT`
  is the only Curl "option" that uses the `CURLINFO` prefix. This exception
  is, however, noted[^3] in docs.

3. When `CURLINFO_HEADER_OUT` is set, the `CURLOPT_VERBOSE` is also implicitly
  set. This was reported[^4] to bugs.php.net, but the bug is marked as wontfix.

This commit adds support for `CURLOPT_DEBUGFUNCTION`. It extends the existing
`curl_debug` callback to store the header-in information if it encounters
a debug message with `CURLINFO_HEADER_OUT`. In all cases, if a callable
is set, it gets called.

`CURLOPT_DEBUGFUNCTION` intends to replace `CURLINFO_HEADER_OUT` Curl
option as a versatile alternative that can also be used to extract
other debug information such as SSL data, text information messages,
incoming headers, as well as headers sent out (which `CURLINFO_HEADER_OUT`
makes available).

The callables are allowed to throw exceptions, but the return values are
ignored.

`CURLOPT_DEBUGFUNCTION` requires `CURLOPT_VERBOSE` enabled, and setting
`CURLOPT_DEBUGFUNCTION` does _not_ implicitly enable `CURLOPT_VERBOSE`.

If the `CURLOPT_DEBUGFUNCTION` option is set, setting `CURLINFO_HEADER_OUT`
throws a `ValueError` exception. Setting `CURLOPT_DEBUGFUNCTION` _after_
enabling `CURLINFO_HEADER_OUT` is allowed. Technically, it is possible
for both functionality (calling user-provided callback _and_ storing
header-out data) is possible, setting `CURLINFO_HEADER_OUT` is not
allowed to encourage the use of `CURLOPT_DEBUGFUNCTION` function.

This commit also adds the rest of the `CURLINFO_` constants used as
the `type` integer value in `CURLOPT_DEBUGFUNCTION` callback.

---

[^1]: [cur.se - CURLOPT_DEBUGFUNCTION](https://curl.se/libcurl/c/CURLOPT_DEBUGFUNCTION.html)
[^2]: [`5f25d80`](5f25d80)
[^3]: [curl_setopt doc mentioning `CURLINFO_` prefix is intentional](https://www.php.net/manual/en/function.curl-setopt.php#:~:text=prefix%20is%20intentional)
[^4]: [bugs.php.net - `CURLOPT_VERBOSE` does not work with `CURLINFO_HEADER_OUT`](https://bugs.php.net/bug.php?id=65348)
  • Loading branch information
Ayesh committed Sep 4, 2024
1 parent fad899e commit 70bb7f5
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 10 deletions.
3 changes: 3 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ PHP NEWS
. Fixed bug GH-15693 (Unnecessary include in main.c bloats binary).
(nielsdos)

- Curl:
. Added CURLOPT_DEBUGFUNCTION as a Curl option. (Ayesh Karunaratne)

- DOM:
. Fixed bug GH-13988 (Storing DOMElement consume 4 times more memory in
PHP 8.1 than in PHP 8.0). (nielsdos)
Expand Down
14 changes: 14 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,13 @@ PHP 8.4 UPGRADE NOTES
to allow or abort the request.
. Added CURLOPT_SERVER_RESPONSE_TIMEOUT, which was formerly known as
CURLOPT_FTP_RESPONSE_TIMEOUT. Both constants hold the same value.
. Added CURLOPT_DEBUGFUNCTION support. This Curl option accepts a callable
that gets called during the request lifetime with the CurlHandle object,
an integer containing the debug message type, and a string containing the
debug message. The debug message type is one of CURLINFO_TEXT, CURLINFO_HEADER_IN,
CURLINFO_HEADER_OUT, CURLINFO_DATA_IN, CURLINFO_DATA_OUT, CURLINFO_SSL_DATA_OUT,
CURLINFO_SSL_DATA_IN constants. Once this option is set, CURLINFO_HEADER_OUT
must not be set because it uses the same libcurl functionality.

- Date:
. Added static methods
Expand Down Expand Up @@ -1017,6 +1024,13 @@ PHP 8.4 UPGRADE NOTES
. CURL_PREREQFUNC_OK.
. CURL_PREREQFUNC_ABORT.
. CURLOPT_SERVER_RESPONSE_TIMEOUT.
. CURLOPT_DEBUGFUNCTION.
. CURLINFO_TEXT.
. CURLINFO_HEADER_IN.
. CURLINFO_DATA_IN.
. CURLINFO_DATA_OUT.
. CURLINFO_SSL_DATA_OUT.
. CURLINFO_SSL_DATA_IN.

- Intl:
. The IntlDateFormatter class exposes now the new PATTERN constant
Expand Down
42 changes: 42 additions & 0 deletions ext/curl/curl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,48 @@
*/
const CURLOPT_XFERINFOFUNCTION = UNKNOWN;

/**
* @var int
* @cvalue CURLOPT_DEBUGFUNCTION
*/
const CURLOPT_DEBUGFUNCTION = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_TEXT
*/
const CURLINFO_TEXT = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_HEADER_IN
*/
const CURLINFO_HEADER_IN = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_DATA_IN
*/
const CURLINFO_DATA_IN = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_DATA_OUT
*/
const CURLINFO_DATA_OUT = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_SSL_DATA_OUT
*/
const CURLINFO_SSL_DATA_OUT = UNKNOWN;

/**
* @var int
* @cvalue CURLINFO_SSL_DATA_IN
*/
const CURLINFO_SSL_DATA_IN = UNKNOWN;

/* */
/**
* @var int
Expand Down
9 changes: 8 additions & 1 deletion ext/curl/curl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ext/curl/curl_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ typedef struct {
zend_fcall_info_cache progress;
zend_fcall_info_cache xferinfo;
zend_fcall_info_cache fnmatch;
zend_fcall_info_cache debug;
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
zend_fcall_info_cache prereq;
#endif
Expand Down
66 changes: 57 additions & 9 deletions ext/curl/interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n)
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.fnmatch);
}

if (ZEND_FCC_INITIALIZED(curl->handlers.debug)) {
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.debug);
}

#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);
Expand Down Expand Up @@ -915,18 +919,46 @@ static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx
}
/* }}} */

static int curl_debug(CURL *cp, curl_infotype type, char *buf, size_t buf_len, void *ctx) /* {{{ */
static int curl_debug(CURL *handle, curl_infotype type, char *data, size_t size, void *clientp) /* {{{ */
{
php_curl *ch = (php_curl *)ctx;
php_curl *ch = (php_curl *)clientp;

if (type == CURLINFO_HEADER_OUT) {
if (ch->header.str) {
zend_string_release_ex(ch->header.str, 0);
}
ch->header.str = zend_string_init(buf, buf_len, 0);
}
#if PHP_CURL_DEBUG
fprintf(stderr, "curl_debug() called\n");
fprintf(stderr, "type = %d, data = %s\n", type, data);
#endif

// Implicitly store the headers for compatibility with CURLINFO_HEADER_OUT
// used as a Curl option. Previously, setting CURLINFO_HEADER_OUT set curl_debug
// as the CURLOPT_DEBUGFUNCTION and stored the debug data when type is set to
// CURLINFO_HEADER_OUT. For backward compatibility, we now store the headers
// but also call the user-callback function if available.
if (type == CURLINFO_HEADER_OUT) {
if (ch->header.str) {
zend_string_release_ex(ch->header.str, 0);
}
ch->header.str = zend_string_init(data, size, 0);
}

if (!ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
return 0;
}

return 0;
zval args[3];

GC_ADDREF(&ch->std);
ZVAL_OBJ(&args[0], &ch->std);
ZVAL_LONG(&args[1], type);
ZVAL_STRINGL(&args[2], data, size);

ch->in_callback = true;
zend_call_known_fcc(&ch->handlers.debug, NULL, /* param_count */ 3, args, /* named_params */ NULL);
ch->in_callback = false;

zval_ptr_dtor(&args[0]);
zval_ptr_dtor(&args[2]);

return 0;
}
/* }}} */

Expand Down Expand Up @@ -1096,6 +1128,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.debug = empty_fcall_info_cache;
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
ch->handlers.prereq = empty_fcall_info_cache;
#endif
Expand Down Expand Up @@ -1272,6 +1305,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
php_curl_copy_fcc_with_option(ch, CURLOPT_PROGRESSDATA, &ch->handlers.progress, &source->handlers.progress);
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);
#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
Expand Down Expand Up @@ -1635,6 +1669,8 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PROGRESS, handlers.progress, curl_progress);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_XFERINFO, handlers.xferinfo, curl_xferinfo);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_DEBUG, handlers.debug, curl_debug);

#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PREREQ, handlers.prereq, curl_prereqfunction);
#endif
Expand Down Expand Up @@ -2225,6 +2261,11 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
}

case CURLINFO_HEADER_OUT:
if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
zend_value_error("CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set");
return FAILURE;
}

if (zend_is_true(zvalue)) {
curl_easy_setopt(ch->cp, CURLOPT_DEBUGFUNCTION, curl_debug);
curl_easy_setopt(ch->cp, CURLOPT_DEBUGDATA, (void *)ch);
Expand Down Expand Up @@ -2804,6 +2845,9 @@ static void curl_free_obj(zend_object *object)
if (ZEND_FCC_INITIALIZED(ch->handlers.fnmatch)) {
zend_fcc_dtor(&ch->handlers.fnmatch);
}
if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
zend_fcc_dtor(&ch->handlers.debug);
}
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) {
zend_fcc_dtor(&ch->handlers.prereq);
Expand Down Expand Up @@ -2887,6 +2931,10 @@ static void _php_curl_reset_handlers(php_curl *ch)
if (ZEND_FCC_INITIALIZED(ch->handlers.fnmatch)) {
zend_fcc_dtor(&ch->handlers.fnmatch);
}

if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
zend_fcc_dtor(&ch->handlers.debug);
}
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
if (ZEND_FCC_INITIALIZED(ch->handlers.prereq)) {
zend_fcc_dtor(&ch->handlers.prereq);
Expand Down
1 change: 1 addition & 0 deletions ext/curl/sync-constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
'CURLOPT_PROGRESSDATA',
'CURLOPT_XFERINFODATA',
'CURLOPT_PREREQDATA',
'CURLOPT_DEBUGDATA',
];

const IGNORED_PHP_CONSTANTS = [
Expand Down
Loading

0 comments on commit 70bb7f5

Please sign in to comment.