Skip to content

Commit 6b1cff4

Browse files
ext/curl: add CURLOPT_SEEKFUNCTION
Expose libcurl's CURLOPT_SEEKFUNCTION as a userland callable so a body streamed via CURLOPT_READFUNCTION can be rewound and resent when libcurl needs to replay it: on a redirect, on multi-pass authentication, or when a reused connection dies. Without a seek callback these transfers fail with CURLE_SEND_FAIL_REWIND, the gap behind bug #47204 and bug #80518. The callback receives the CurlHandle, offset and origin and returns one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK. It follows the existing callback options: a seek fcc on php_curl_handlers, a curl_seek trampoline that validates the return value like curl_prereqfunction, registration via HANDLE_CURL_OPTION_CALLABLE, duplication in curl_copy_handle and release in curl_free_obj. The option and constants exist since libcurl 7.18.0 so no version guards are needed.
1 parent 95b5b48 commit 6b1cff4

9 files changed

Lines changed: 151 additions & 1 deletion

File tree

NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ PHP NEWS
2525
- BCMath:
2626
. Added NUL-byte validation to BCMath functions. (jorgsowa)
2727

28+
- Curl:
29+
. Added CURLOPT_SEEKFUNCTION and the CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL
30+
and CURL_SEEKFUNC_CANTSEEK constants, letting libcurl rewind a streamed
31+
request body to resend it on a redirect, multi-pass authentication or a
32+
retried reused connection. (GrahamCampbell)
33+
2834
- Date:
2935
. Update timelib to 2022.16. (Derick)
3036

UPGRADING

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ PHP 8.6 UPGRADE NOTES
179179
. It is now possible to define the `__debugInfo()` magic method on enums.
180180
RFC: https://wiki.php.net/rfc/debugable-enums
181181

182+
- Curl:
183+
. Added CURLOPT_SEEKFUNCTION to register a callback that repositions a
184+
streamed request body so libcurl can rewind and resend it on a redirect,
185+
multi-pass authentication, or a retried reused connection instead of
186+
failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
187+
offset and origin, and must return one of CURL_SEEKFUNC_OK,
188+
CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
189+
182190
- Fileinfo:
183191
. finfo_file() now works with remote streams.
184192

@@ -367,6 +375,12 @@ PHP 8.6 UPGRADE NOTES
367375
10. New Global Constants
368376
========================================
369377

378+
- Curl:
379+
. CURLOPT_SEEKFUNCTION.
380+
. CURL_SEEKFUNC_OK.
381+
. CURL_SEEKFUNC_FAIL.
382+
. CURL_SEEKFUNC_CANTSEEK.
383+
370384
- Sockets:
371385
. TCP_USER_TIMEOUT (Linux only).
372386
. AF_UNSPEC.

ext/curl/curl.stub.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,11 @@
343343
* @cvalue CURLOPT_RETURNTRANSFER
344344
*/
345345
const CURLOPT_RETURNTRANSFER = UNKNOWN;
346+
/**
347+
* @var int
348+
* @cvalue CURLOPT_SEEKFUNCTION
349+
*/
350+
const CURLOPT_SEEKFUNCTION = UNKNOWN;
346351
/**
347352
* @var int
348353
* @cvalue CURLOPT_SHARE
@@ -1788,6 +1793,21 @@
17881793
* @cvalue CURL_READFUNC_PAUSE
17891794
*/
17901795
const CURL_READFUNC_PAUSE = UNKNOWN;
1796+
/**
1797+
* @var int
1798+
* @cvalue CURL_SEEKFUNC_OK
1799+
*/
1800+
const CURL_SEEKFUNC_OK = UNKNOWN;
1801+
/**
1802+
* @var int
1803+
* @cvalue CURL_SEEKFUNC_FAIL
1804+
*/
1805+
const CURL_SEEKFUNC_FAIL = UNKNOWN;
1806+
/**
1807+
* @var int
1808+
* @cvalue CURL_SEEKFUNC_CANTSEEK
1809+
*/
1810+
const CURL_SEEKFUNC_CANTSEEK = UNKNOWN;
17911811
/**
17921812
* @var int
17931813
* @cvalue CURL_WRITEFUNC_PAUSE

ext/curl/curl_arginfo.h

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/curl/curl_private.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ typedef struct {
7474
php_curl_write *write_header;
7575
php_curl_read *read;
7676
zval std_err;
77+
zend_fcall_info_cache seek;
7778
zend_fcall_info_cache progress;
7879
zend_fcall_info_cache xferinfo;
7980
zend_fcall_info_cache fnmatch;

ext/curl/interface.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,55 @@ static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
831831
}
832832
/* }}} */
833833

834+
/* {{{ curl_seek */
835+
static int curl_seek(void *clientp, curl_off_t offset, int origin)
836+
{
837+
php_curl *ch = (php_curl *)clientp;
838+
/* Default to CANTSEEK so libcurl can fall back (or fail the rewind
839+
cleanly) when no callback is set or the callback misbehaves, rather
840+
than resending the body from the wrong offset. */
841+
int rval = CURL_SEEKFUNC_CANTSEEK;
842+
843+
#if PHP_CURL_DEBUG
844+
fprintf(stderr, "curl_seek() called\n");
845+
fprintf(stderr, "clientp = %x, offset = %ld, origin = %d\n", clientp, offset, origin);
846+
#endif
847+
if (!ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
848+
return rval;
849+
}
850+
851+
zval args[3];
852+
zval retval;
853+
854+
GC_ADDREF(&ch->std);
855+
ZVAL_OBJ(&args[0], &ch->std);
856+
ZVAL_LONG(&args[1], offset);
857+
ZVAL_LONG(&args[2], origin);
858+
859+
ch->in_callback = true;
860+
zend_call_known_fcc(&ch->handlers.seek, &retval, /* param_count */ 3, args, /* named_params */ NULL);
861+
ch->in_callback = false;
862+
863+
if (!Z_ISUNDEF(retval)) {
864+
_php_curl_verify_handlers(ch, /* reporterror */ true);
865+
if (Z_TYPE(retval) == IS_LONG) {
866+
zend_long retval_long = Z_LVAL(retval);
867+
if (retval_long == CURL_SEEKFUNC_OK || retval_long == CURL_SEEKFUNC_FAIL || retval_long == CURL_SEEKFUNC_CANTSEEK) {
868+
rval = retval_long;
869+
} else {
870+
zend_value_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
871+
}
872+
} else {
873+
zend_type_error("The CURLOPT_SEEKFUNCTION callback must return one of CURL_SEEKFUNC_OK, CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK");
874+
}
875+
zval_ptr_dtor(&retval);
876+
}
877+
878+
zval_ptr_dtor(&args[0]);
879+
return rval;
880+
}
881+
/* }}} */
882+
834883
/* {{{ curl_write_header */
835884
static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx)
836885
{
@@ -1038,6 +1087,7 @@ void init_curl_handle(php_curl *ch)
10381087
ch->handlers.write = ecalloc(1, sizeof(php_curl_write));
10391088
ch->handlers.write_header = ecalloc(1, sizeof(php_curl_write));
10401089
ch->handlers.read = ecalloc(1, sizeof(php_curl_read));
1090+
ch->handlers.seek = empty_fcall_info_cache;
10411091
ch->handlers.progress = empty_fcall_info_cache;
10421092
ch->handlers.xferinfo = empty_fcall_info_cache;
10431093
ch->handlers.fnmatch = empty_fcall_info_cache;
@@ -1208,6 +1258,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
12081258
curl_easy_setopt(ch->cp, CURLOPT_WRITEHEADER, (void *) ch);
12091259
curl_easy_setopt(ch->cp, CURLOPT_DEBUGDATA, (void *) ch);
12101260

1261+
php_curl_copy_fcc_with_option(ch, CURLOPT_SEEKDATA, &ch->handlers.seek, &source->handlers.seek);
12111262
php_curl_copy_fcc_with_option(ch, CURLOPT_PROGRESSDATA, &ch->handlers.progress, &source->handlers.progress);
12121263
php_curl_copy_fcc_with_option(ch, CURLOPT_XFERINFODATA, &ch->handlers.xferinfo, &source->handlers.xferinfo);
12131264
php_curl_copy_fcc_with_option(ch, CURLOPT_FNMATCH_DATA, &ch->handlers.fnmatch, &source->handlers.fnmatch);
@@ -1577,6 +1628,7 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
15771628
HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_HEADER, write_header, PHP_CURL_IGNORE);
15781629
HANDLE_CURL_OPTION_CALLABLE_PHP_CURL_USER(ch, CURLOPT_READ, read, PHP_CURL_DIRECT);
15791630

1631+
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_SEEK, handlers.seek, curl_seek);
15801632
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_PROGRESS, handlers.progress, curl_progress);
15811633
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_XFERINFO, handlers.xferinfo, curl_xferinfo);
15821634
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
@@ -2781,6 +2833,9 @@ static void curl_free_obj(zend_object *object)
27812833
efree(ch->handlers.write_header);
27822834
efree(ch->handlers.read);
27832835

2836+
if (ZEND_FCC_INITIALIZED(ch->handlers.seek)) {
2837+
zend_fcc_dtor(&ch->handlers.seek);
2838+
}
27842839
if (ZEND_FCC_INITIALIZED(ch->handlers.progress)) {
27852840
zend_fcc_dtor(&ch->handlers.progress);
27862841
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
CURLOPT_SEEKFUNCTION is called to rewind a streamed upload across a redirect
3+
--EXTENSIONS--
4+
curl
5+
--FILE--
6+
<?php
7+
include 'server.inc';
8+
$host = curl_cli_server_start();
9+
10+
$body = 'Hello cURL seek!';
11+
$offset = 0;
12+
$seekCalls = 0;
13+
14+
$ch = curl_init("{$host}/get.inc?test=redirect");
15+
curl_setopt($ch, CURLOPT_UPLOAD, true);
16+
curl_setopt($ch, CURLOPT_INFILESIZE, strlen($body));
17+
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
18+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
19+
curl_setopt($ch, CURLOPT_READFUNCTION, function ($ch, $fd, int $length) use ($body, &$offset) {
20+
$chunk = substr($body, $offset, $length);
21+
$offset += strlen($chunk);
22+
return $chunk;
23+
});
24+
curl_setopt($ch, CURLOPT_SEEKFUNCTION, function ($ch, int $offset_, int $origin) use (&$offset, &$seekCalls) {
25+
if ($origin !== SEEK_SET) {
26+
return CURL_SEEKFUNC_CANTSEEK;
27+
}
28+
$seekCalls++;
29+
$offset = $offset_;
30+
return CURL_SEEKFUNC_OK;
31+
});
32+
33+
$response = curl_exec($ch);
34+
// The seek callback must have been invoked to rewind the body for the resend,
35+
// and the resent body must have reached the redirect target intact.
36+
var_dump($seekCalls > 0);
37+
var_dump(str_contains($response, $body));
38+
curl_close($ch);
39+
?>
40+
--EXPECT--
41+
bool(true)
42+
bool(true)

ext/curl/tests/curl_setopt_callables.phpt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ testOption($ch, CURLOPT_FNMATCH_FUNCTION);
2727
testOption($ch, CURLOPT_WRITEFUNCTION);
2828
testOption($ch, CURLOPT_HEADERFUNCTION);
2929
testOption($ch, CURLOPT_READFUNCTION);
30+
testOption($ch, CURLOPT_SEEKFUNCTION);
3031

3132
?>
3233
--EXPECT--
@@ -42,3 +43,5 @@ TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for opti
4243
TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_HEADERFUNCTION, function "undefined" not found or invalid function name
4344
TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
4445
TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_READFUNCTION, function "undefined" not found or invalid function name
46+
TypeError: curl_setopt(): Argument #3 ($value) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name
47+
TypeError: curl_setopt_array(): Argument #2 ($options) must be a valid callback for option CURLOPT_SEEKFUNCTION, function "undefined" not found or invalid function name

ext/curl/tests/responder/get.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
case 'method':
4747
echo $_SERVER['REQUEST_METHOD'];
4848
break;
49+
case 'redirect':
50+
// A 307 preserves the method and body, so libcurl must rewind the upload
51+
// (via CURLOPT_SEEKFUNCTION) before resending it to the new location.
52+
header('Location: /get.inc?test=input', true, 307);
53+
break;
4954
default:
5055
echo "Hello World!\n";
5156
echo "Hello World!";

0 commit comments

Comments
 (0)