Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ PHP NEWS
. Uploading multiple files with the same field name is now possible by
passing an array of CURLFile or CURLStringFile objects to
CURLOPT_POSTFIELDS. (Sjoerd Langkemper)
. Added CURLOPT_OPENSOCKETFUNCTION to allow or refuse each address libcurl
is about to connect to. (Xavier Leune, Nicolas Grekas)

- FPM:
. Use recommended TYPE, HELP order for OpenMetrics metadata.
Expand Down
8 changes: 8 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ PHP 8.7 UPGRADE NOTES
array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS.
. Support for upstream options CURLOPT_ECH, CURLOPT_HAPROXY_CLIENT_IP, and
CURLOPT_SERVER_RESPONSE_TIMEOUT_MS.
. Added CURLOPT_OPENSOCKETFUNCTION to register a callback that allows or
refuses each address libcurl is about to connect to, after DNS resolution
and before connect(). The callback receives the CurlHandle, the address
and the port. It must return true to let libcurl create the socket, or
false to make libcurl try the next address. The address is an IP address,
or the path of a UNIX domain socket with port 0. The callback isn't called
when a connection is reused, which CURLOPT_PREREQFUNCTION can check.

- Intl:
. Added IntlRelativeDateTimeFormatter to format relative date and time offsets
Expand Down Expand Up @@ -172,6 +179,7 @@ PHP 8.7 UPGRADE NOTES
CURLOPT_SERVER_RESPONSE_TIMEOUT_MS.
. CURLOPT_MAIL_RCPT_ALLOWFAILS is the correctly spelled alternative to
CURLOPT_MAIL_RCPT_ALLLOWFAILS.
. CURLOPT_OPENSOCKETFUNCTION.

========================================
11. Changes to INI File Handling
Expand Down
5 changes: 5 additions & 0 deletions ext/curl/curl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,11 @@
* @cvalue CURLOPT_NOSIGNAL
*/
const CURLOPT_NOSIGNAL = UNKNOWN;
/**
* @var int
* @cvalue CURLOPT_OPENSOCKETFUNCTION
*/
const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN;
/**
* @var int
* @cvalue CURLOPT_PORT
Expand Down
3 changes: 2 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 @@ -83,6 +83,7 @@ typedef struct {
zend_fcall_info_cache xferinfo;
zend_fcall_info_cache fnmatch;
zend_fcall_info_cache debug;
zend_fcall_info_cache opensocket;
#if LIBCURL_VERSION_NUM >= 0x075000 /* Available since 7.80.0 */
zend_fcall_info_cache prereq;
#endif
Expand Down
132 changes: 132 additions & 0 deletions ext/curl/interface.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@

#ifdef PHP_WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <sys/types.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
# ifdef AF_UNIX
# include <sys/un.h>
# define PHP_CURL_HAVE_AF_UNIX 1
# endif
#endif

#include <curl/curl.h>
Expand All @@ -36,6 +44,8 @@
#include "ext/standard/info.h"
#include "ext/standard/file.h"
#include "ext/standard/url.h"
#include "ext/standard/php_net.h"
#include "php_network.h"
#include "curl_private.h"

#ifdef __GNUC__
Expand Down Expand Up @@ -499,6 +509,10 @@ static HashTable *curl_get_gc(zend_object *object, zval **table, int *n)
zend_get_gc_buffer_add_fcc(gc_buffer, &curl->handlers.debug);
}

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

#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 @@ -837,6 +851,100 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key,
}
#endif

/* {{{ curl_opensocket */
static curl_socket_t curl_opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address)
{
php_curl *ch = (php_curl *) clientp;
curl_socket_t sock = CURL_SOCKET_BAD;

#if PHP_CURL_DEBUG
fprintf(stderr, "curl_opensocket() called\n");
fprintf(stderr, "clientp = %p, purpose = %d, family = %d, socktype = %d, protocol = %d\n", clientp, purpose, address->family, address->socktype, address->protocol);
#endif

/* Refuse what can't be passed to the callback. Once the callback threw, refuse
* the remaining addresses too. */
if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN || !ZEND_FCC_INITIALIZED(ch->handlers.opensocket) || EG(exception))) {
return sock;
}

zval args[3];
zend_long port = 0;
bool is_ip = true;

switch (address->family) {
case AF_INET:
port = ntohs(((const struct sockaddr_in *) &address->addr)->sin_port);
break;
#ifdef AF_INET6
case AF_INET6:
port = ntohs(((const struct sockaddr_in6 *) &address->addr)->sin6_port);
break;
#endif
#ifdef PHP_CURL_HAVE_AF_UNIX
case AF_UNIX: {
/* The path of the socket, with its leading NUL byte for an abstract one */
const struct sockaddr_un *sa_un = (const struct sockaddr_un *) &address->addr;
size_t len = 0;
if (address->addrlen > offsetof(struct sockaddr_un, sun_path)) {
len = MIN(address->addrlen - offsetof(struct sockaddr_un, sun_path), sizeof(sa_un->sun_path));
if (len && sa_un->sun_path[0] != '\0') {
const char *nul = memchr(sa_un->sun_path, '\0', len);
if (nul) {
len = nul - sa_un->sun_path;
}
}
}
ZVAL_STRINGL(&args[1], sa_un->sun_path, len);
is_ip = false;
break;
}
#endif
default:
return sock;
}

if (is_ip) {
zend_string *ip = php_inet_ntop(&address->addr);
if (UNEXPECTED(!ip)) {
return sock;
}
ZVAL_STR(&args[1], ip);
}

zval retval;

GC_ADDREF(&ch->std);
ZVAL_OBJ(&args[0], &ch->std);
ZVAL_LONG(&args[2], port);

php_curl_call_callback(ch, &ch->handlers.opensocket, &retval, /* argc */ 3, args);

if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
if (Z_TYPE(retval) == IS_TRUE) {
if (!EG(exception)) {
/* What libcurl does when no callback is set */
sock = socket(address->family, address->socktype, address->protocol);
/* libcurl doesn't make the socket non-blocking if the callback removed itself */
if (sock != CURL_SOCKET_BAD && php_set_sock_blocking(sock, false) != SUCCESS) {
closesocket(sock);
sock = CURL_SOCKET_BAD;
}
}
} else if (Z_TYPE(retval) != IS_FALSE) {
zend_type_error("The CURLOPT_OPENSOCKETFUNCTION callback must return a bool");
zval_ptr_dtor(&retval);
}
}

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

return sock;
}
/* }}} */

/* {{{ curl_read */
static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
{
Expand Down Expand Up @@ -1155,6 +1263,7 @@ void init_curl_handle(php_curl *ch)
ch->handlers.xferinfo = empty_fcall_info_cache;
ch->handlers.fnmatch = empty_fcall_info_cache;
ch->handlers.debug = empty_fcall_info_cache;
ch->handlers.opensocket = 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 @@ -1332,6 +1441,7 @@ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source)
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);
php_curl_copy_fcc_with_option(ch, CURLOPT_OPENSOCKETDATA, &ch->handlers.opensocket, &source->handlers.opensocket);
#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 @@ -1727,6 +1837,21 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_FNMATCH_, handlers.fnmatch, curl_fnmatch);
HANDLE_CURL_OPTION_CALLABLE(ch, CURLOPT_DEBUG, handlers.debug, curl_debug);

case CURLOPT_OPENSOCKETFUNCTION: {
bool result = php_curl_set_callable_handler(&ch->handlers.opensocket, zvalue, is_array_config, "CURLOPT_OPENSOCKETFUNCTION");
/* Without a callback, libcurl creates the sockets itself */
if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) {
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, curl_opensocket);
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, ch);
} else {
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, NULL);
}
if (!result) {
return FAILURE;
}
break;
}

#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 @@ -2925,6 +3050,9 @@ static void curl_free_obj(zend_object *object)
if (ZEND_FCC_INITIALIZED(ch->handlers.debug)) {
zend_fcc_dtor(&ch->handlers.debug);
}
if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) {
zend_fcc_dtor(&ch->handlers.opensocket);
}
#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 @@ -3017,6 +3145,10 @@ static void _php_curl_reset_handlers(php_curl *ch)
zend_fcc_dtor(&ch->handlers.debug);
}

if (ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) {
zend_fcc_dtor(&ch->handlers.opensocket);
}

#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
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
--TEST--
CURLOPT_OPENSOCKETFUNCTION: abstract UNIX domain socket
--EXTENSIONS--
curl
--SKIPIF--
<?php
if (PHP_OS_FAMILY !== 'Linux') die('skip Linux only');
if (!(curl_version()['features'] & CURL_VERSION_UNIX_SOCKETS)) die('skip libcurl without UNIX domain sockets');
?>
--FILE--
<?php
$ch = curl_init('http://localhost/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ABSTRACT_UNIX_SOCKET, 'php-curl-does-not-exist');
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle, string $address, int $port): bool {
/* The address keeps the leading NUL byte of abstract sockets */
var_dump($address === "\0php-curl-does-not-exist", $port);
return false;
});
var_dump(curl_exec($ch));
var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT);

echo "Done";
?>
--EXPECT--
bool(true)
int(0)
bool(false)
bool(true)
Done
37 changes: 37 additions & 0 deletions ext/curl/tests/curl_opensocketfunction_address.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
CURLOPT_OPENSOCKETFUNCTION: IPv6 addresses
--EXTENSIONS--
curl
--SKIPIF--
<?php
$s = @stream_socket_server('tcp://[::1]:0', $errno, $errstr);
if ($s === false) die('skip no IPv6 loopback available');
fclose($s);
?>
--FILE--
<?php
/* No listener is needed: the callback runs before connect() */
function probe(string $url): void {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle, string $address, int $port): bool {
var_dump($address, $port);
return false;
});
curl_exec($ch);
}

probe('http://[::1]:1/');
probe('http://[::ffff:127.0.0.1]:2/');
probe('http://[::ffff:7f00:1]:3/');

echo "Done";
?>
--EXPECT--
string(3) "::1"
int(1)
string(16) "::ffff:127.0.0.1"
int(2)
string(16) "::ffff:127.0.0.1"
int(3)
Done
45 changes: 45 additions & 0 deletions ext/curl/tests/curl_opensocketfunction_file_scheme.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
CURLOPT_OPENSOCKETFUNCTION is not called for file://, which creates no socket
--EXTENSIONS--
curl
--SKIPIF--
<?php
if (!in_array('file', curl_version()['protocols'], true)) die('skip file:// protocol not supported');
?>
--FILE--
<?php
$file = __DIR__ . '/curl_opensocketfunction_file_scheme.txt';
file_put_contents($file, "not a socket\n");

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'file://' . $file);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

$calls = 0;
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function () use (&$calls): bool {
$calls++;
return false;
});

/* The callback refuses everything, yet the file is read: CURLOPT_PROTOCOLS_STR
* should be used too. */
var_dump(curl_exec($ch));
var_dump($calls);

curl_setopt($ch, CURLOPT_PROTOCOLS_STR, 'http,https');
var_dump(curl_exec($ch));
var_dump(curl_errno($ch) === CURLE_UNSUPPORTED_PROTOCOL);

echo "Done";
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/curl_opensocketfunction_file_scheme.txt');
?>
--EXPECT--
string(13) "not a socket
"
int(0)
bool(false)
bool(true)
Done
Loading
Loading