1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00
Files
archived-php-src/ext/curl/tests/curl_setopt_CURLOPT_DEBUGFUNCTION.phpt
Ayesh Karunaratne ba748e7bb5 ext/curl: Add CURLOPT_DEBUGFUNCTION option (GH-15674)
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`](5f25d80d10)
[^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)
2024-09-24 10:56:56 +02:00

237 lines
6.8 KiB
PHP

--TEST--
Curl option CURLOPT_DEBUGFUNCTION
--EXTENSIONS--
curl
--FILE--
<?php
include 'server.inc';
$allowedTypes = [
CURLINFO_TEXT,
CURLINFO_HEADER_IN,
CURLINFO_HEADER_OUT,
CURLINFO_DATA_IN,
CURLINFO_DATA_OUT,
CURLINFO_SSL_DATA_OUT,
CURLINFO_SSL_DATA_IN,
];
var_dump(CURLOPT_DEBUGFUNCTION);
$host = curl_cli_server_start();
echo "\n===Testing with regular CURLOPT_VERBOSE with verbose=false===\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=file");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 0);
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, function() {
echo 'This should not be called';
}));
curl_exec($ch);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
echo "\n===Testing with regular CURLOPT_VERBOSE on stderr===\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "{$host}/get.inc?test=file");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
$stderr = fopen('php://temp', 'wb+');
curl_setopt($ch, CURLOPT_STDERR, $stderr);
$curlVerboseOutput = curl_exec($ch);
rewind($stderr);
$receivedOutput = stream_get_contents($stderr);
fclose($stderr);
var_dump(str_contains($receivedOutput, 'Host'));
echo "\n===Testing with CURLOPT_DEBUGFUNCTION happy path===\n";
$stderr = fopen('php://temp', 'wb+');
curl_setopt($ch, CURLOPT_STDERR, $stderr);
$debugOutput = [];
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, function(CurlHandle $curlHandle, int $type, string $data) use ($allowedTypes, &$debugOutput) {
if (!in_array($type, $allowedTypes)) {
throw new Exception('Unexpected type value: '. $type);
}
$debugOutput[$type][] = $data;
}));
$result = curl_exec($ch);
rewind($stderr);
$receivedOutputWithDebugFunction = stream_get_contents($stderr);
fclose($stderr);
echo "Received stderr empty:\n";
var_dump($result);
var_dump($receivedOutputWithDebugFunction);
// Header-out should be an exact match
var_dump(str_contains($receivedOutput, $debugOutput[CURLINFO_HEADER_OUT][0]));
// Header-in fields should match, except for the "Date" header that is dynamic.
foreach ($debugOutput[CURLINFO_HEADER_IN] as $headerReceived) {
if (str_starts_with($headerReceived, 'Date')) {
continue;
}
if (!str_contains($receivedOutput, $headerReceived)) {
throw new \Exception('DEBUGFUNCTION header field does not match the previous verbose debug message');
}
}
echo "\n===Test attempting to set CURLINFO_HEADER_OUT while CURLOPT_DEBUGFUNCTION in effect throws===\n";
$ch = curl_init();
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
try {
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
}
catch (\ValueError $e) {
var_dump($e->getMessage());
}
$chCopy = curl_copy_handle($ch);
try {
var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true));
}
catch (\ValueError $e) {
var_dump($e->getMessage());
}
var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null));
var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, true));
echo "\n===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw===\n";
$ch = curl_init();
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
$chCopy = curl_copy_handle($ch);
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, 'strlen'));
var_dump(curl_setopt($chCopy, CURLOPT_DEBUGFUNCTION, null));
var_dump(curl_setopt($chCopy, CURLINFO_HEADER_OUT, 1));
echo "\n===Test CURLOPT_DEBUGFUNCTION=null works===\n";
$ch = curl_init("{$host}/get.inc?test=file");
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, null));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
var_dump(curl_exec($ch));
echo "\n===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect===\n";
$ch = curl_init("{$host}/get.inc?test=file");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
var_dump(curl_setopt($ch, CURLINFO_HEADER_OUT, true));
var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
var_dump(curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() {}));
var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
var_dump($result = curl_exec($ch));
var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
echo "\n===Test CURLOPT_DEBUGFUNCTION can throw within callback===\n";
$ch = curl_init("{$host}/get.inc?test=file");
curl_setopt($ch, CURLOPT_DEBUGFUNCTION, static function() {
throw new \RuntimeException('This should get caught after verbose=true');
});
var_dump(curl_exec($ch));
curl_setopt($ch, CURLOPT_VERBOSE, true);
try {
var_dump($result = curl_exec($ch));
}
catch (\RuntimeException $e) {
var_dump($e->getMessage());
}
var_dump(curl_getinfo($ch, CURLINFO_HEADER_OUT));
echo "\n===Test CURLOPT_DEBUGFUNCTION on shared handles work===\n";
$ch = curl_init("{$host}/get.inc?test=file");
$called = false;
curl_setopt_array($ch, [
CURLOPT_VERBOSE => true,
CURLOPT_DEBUGFUNCTION => static function() use (&$called) {
$called = true;
},
]);
var_dump($called);
curl_exec($ch);
var_dump($called);
$called = false;
$ch2 = curl_copy_handle($ch);
curl_exec($ch2);
var_dump($called);
var_dump(curl_getinfo($ch2, CURLINFO_HEADER_OUT));
echo "\nDone";
?>
--EXPECTF--
int(20094)
===Testing with regular CURLOPT_VERBOSE with verbose=false===
bool(true)
===Testing with regular CURLOPT_VERBOSE on stderr===
bool(true)
===Testing with CURLOPT_DEBUGFUNCTION happy path===
bool(true)
Received stderr empty:
string(0) ""
string(0) ""
bool(true)
===Test attempting to set CURLINFO_HEADER_OUT while CURLOPT_DEBUGFUNCTION in effect throws===
bool(true)
bool(true)
bool(true)
bool(true)
string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set"
string(87) "CURLINFO_HEADER_OUT option must not be set when the CURLOPT_DEBUGFUNCTION option is set"
bool(true)
bool(true)
===Test attempting to set CURLOPT_DEBUGFUNCTION while CURLINFO_HEADER_OUT does not throw===
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
===Test CURLOPT_DEBUGFUNCTION=null works===
bool(true)
string(0) ""
===Test CURLINFO_HEADER_OUT works while CURLOPT_DEBUGFUNCTION in effect===
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)
string(0) ""
string(%d) "GET /get.inc?test=file HTTP/%s
Host: %s:%d
Accept: */*
"
===Test CURLOPT_DEBUGFUNCTION can throw within callback===
bool(true)
string(41) "This should get caught after verbose=true"
string(%d) "GET /get.inc?test=file HTTP/%s
Host: %s:%d
Accept: */*
"
===Test CURLOPT_DEBUGFUNCTION on shared handles work===
bool(false)
bool(true)
bool(true)
string(71) "GET /get.inc?test=file HTTP/%s
Host: %s:%d
Accept: */*
"
Done