Skip to content
Open
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 connection libcurl
is about to make. (Xavier Leune)

- FPM:
. Use recommended TYPE, HELP order for OpenMetrics metadata.
Expand Down
9 changes: 9 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ 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. Its callback has the signature
function(CurlHandle $handle, string $address, int $port): bool and is
invoked after DNS resolution and before connect(), for each socket libcurl
is about to create. $address is the IP address, or the path of a UNIX
socket, whose $port is 0. Returning false refuses the connection, and
libcurl tries the next address, if any. A connection reused from a pool is
not vetted again; CURLOPT_PREREQFUNCTION runs before every request.
RFC: https://wiki.php.net/rfc/curl_opensocketfunction

- Intl:
. Added IntlRelativeDateTimeFormatter to format relative date and time offsets
Expand Down Expand Up @@ -172,6 +180,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
171 changes: 171 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>
#include <arpa/inet.h>
# ifdef AF_UNIX
# include <sys/un.h>
# 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,133 @@ static int curl_ssh_hostkeyfunction(void *clientp, int keytype, const char *key,
}
#endif

/* {{{ curl_opensocket_unix_path
The path of a UNIX domain socket, as libcurl built it. A Linux abstract socket
name starts with a NUL byte, which is kept: it is part of the name. */
#if defined(AF_UNIX) && !defined(PHP_WIN32)
static zend_string *curl_opensocket_unix_path(const struct curl_sockaddr *address)
{
const struct sockaddr_un *sa_un = (const struct sockaddr_un *) &address->addr;
size_t offset = offsetof(struct sockaddr_un, sun_path);
size_t len = address->addrlen > offset ? address->addrlen - offset : 0;

if (len > sizeof(sa_un->sun_path)) {
len = sizeof(sa_un->sun_path);
}
if (len > 0 && sa_un->sun_path[0] != '\0') {
/* A pathname: libcurl may count the terminating NUL in addrlen. */
len = zend_strnlen(sa_un->sun_path, len);
}

return zend_string_init(sa_un->sun_path, len, 0);
}
#endif
/* }}} */

/* {{{ curl_opensocketfunction
The PHP callback only allows or refuses the connection: no descriptor is ever
exposed to userland. */
static curl_socket_t curl_opensocketfunction(void *clientp, curlsocktype purpose, struct curl_sockaddr *address)
{
php_curl *ch = (php_curl *) clientp;

/* Unreachable in normal operation: libcurl's own socket creation is restored
* whenever no callback is installed, so the trampoline is only registered
* while the FCC is initialized. Refuse rather than connect, so that an
* unexpected state can never silently bypass the filter. */
if (UNEXPECTED(!ZEND_FCC_INITIALIZED(ch->handlers.opensocket))) {
return CURL_SOCKET_BAD;
}

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

/* Whatever cannot be described to the callback is refused rather than
* connected: a policy must never be bypassed by an endpoint it was not
* shown. CURLSOCKTYPE_IPCXN is the only purpose libcurl currently uses; a
* future one may well not be a destination address at all. */
if (UNEXPECTED(purpose != CURLSOCKTYPE_IPCXN)) {
return CURL_SOCKET_BAD;
}

zval args[3];
zval retval;
curl_socket_t rval = CURL_SOCKET_BAD;

switch (address->family) {
case AF_INET:
ZVAL_LONG(&args[2], ntohs(((const struct sockaddr_in *) &address->addr)->sin_port));
break;
#ifdef AF_INET6
case AF_INET6:
ZVAL_LONG(&args[2], ntohs(((const struct sockaddr_in6 *) &address->addr)->sin6_port));
break;
#endif
#if defined(AF_UNIX) && !defined(PHP_WIN32)
case AF_UNIX:
/* CURLOPT_UNIX_SOCKET_PATH or CURLOPT_ABSTRACT_UNIX_SOCKET: a path
* has no port, reported as 0 like CURLINFO_PRIMARY_PORT does. */
ZVAL_LONG(&args[2], 0);
break;
#endif
default:
return CURL_SOCKET_BAD;
}

#if defined(AF_UNIX) && !defined(PHP_WIN32)
if (address->family == AF_UNIX) {
ZVAL_STR(&args[1], curl_opensocket_unix_path(address));
} else
#endif
{
zend_string *ip = php_inet_ntop(&address->addr);
if (UNEXPECTED(ip == NULL)) {
return CURL_SOCKET_BAD;
}
ZVAL_STR(&args[1], ip);
}

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

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

/* retval is undefined when the callback threw, and must not be trusted when
* an exception was thrown after it was set, e.g. by a destructor: either
* way the connection is refused and the exception propagates out of
* curl_exec(). */
if (!Z_ISUNDEF(retval)) {
_php_curl_verify_handlers(ch, /* reporterror */ true);
if (UNEXPECTED(EG(exception))) {
/* Refused. */
} else if (EXPECTED(Z_TYPE(retval) == IS_TRUE || Z_TYPE(retval) == IS_FALSE)) {
if (Z_TYPE(retval) == IS_TRUE) {
Comment thread
xavierleune marked this conversation as resolved.
Resolved
rval = socket(address->family, address->socktype, address->protocol);
/* Where socket() supports SOCK_NONBLOCK, libcurl only makes a
* socket non-blocking if it came from this callback, which it
* checks after the call: if the callback removed the option,
* the socket would stay blocking and ignore the connect
* timeout. Doing it here is harmless otherwise. */
if (rval != CURL_SOCKET_BAD && php_set_sock_blocking(rval, false) != SUCCESS) {
closesocket(rval);
rval = CURL_SOCKET_BAD;
}
}
} else {
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 rval;
}
/* }}} */

/* {{{ curl_read */
static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
{
Expand Down Expand Up @@ -1154,6 +1295,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.opensocket = 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;
Expand Down Expand Up @@ -1332,6 +1474,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 +1870,27 @@ 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 installed = php_curl_set_callable_handler(&ch->handlers.opensocket, zvalue, is_array_config, "CURLOPT_OPENSOCKETFUNCTION");
if (!installed || !ZEND_FCC_INITIALIZED(ch->handlers.opensocket)) {
/* Restore libcurl's own socket creation, so that the trampoline
* is registered if and only if the FCC is initialized. A
* rejected callable also releases the previous handler, so
* leaving the option armed would point CURLOPT_OPENSOCKETDATA
* at a handle whose FCC is gone, and curl_copy_handle() would
* then carry that pointer into the copy. */
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, NULL);
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, NULL);
if (!installed) {
return FAILURE;
}
break;
}
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETFUNCTION, curl_opensocketfunction);
curl_easy_setopt(ch->cp, CURLOPT_OPENSOCKETDATA, ch);
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 +3089,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 +3184,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 reports an abstract UNIX socket name with its leading NUL byte
--EXTENSIONS--
curl
--SKIPIF--
<?php
if (PHP_OS_FAMILY !== 'Linux') die('skip abstract UNIX sockets are Linux only');
if (!defined('CURLOPT_ABSTRACT_UNIX_SOCKET')) die('skip CURLOPT_ABSTRACT_UNIX_SOCKET not available');
?>
--FILE--
<?php
/* The leading NUL byte is part of an abstract name: keeping it tells a policy
* that the name is not a filesystem path. */
$name = 'php-curl-' . getmypid();

$ch = curl_init('http://localhost/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_ABSTRACT_UNIX_SOCKET, $name);
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle, string $address, int $port) use ($name): bool {
var_dump($address === "\0" . $name, $port);
return false;
});
var_dump(curl_exec($ch));
var_dump(curl_errno($ch) === CURLE_COULDNT_CONNECT);
?>
--EXPECT--
bool(true)
int(0)
bool(false)
bool(true)
62 changes: 62 additions & 0 deletions ext/curl/tests/curl_opensocketfunction_address_family.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
CURLOPT_OPENSOCKETFUNCTION reports the address and port of IPv4 and IPv6 endpoints
--EXTENSIONS--
curl
--SKIPIF--
<?php
if (!(curl_version()['features'] & CURL_VERSION_IPV6)) die('skip libcurl built without IPv6');
/* The IPv6 probes below rely on libcurl attempting a literal ::1 address, which
* it will not do on a host without a usable IPv6 loopback. */
$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(). Proxies from the
* environment are ignored, or the callback would see the proxy instead. */
function probe(string $url): void {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROXY, '');
curl_setopt($ch, CURLOPT_OPENSOCKETFUNCTION, function (CurlHandle $handle, string $address, int $port): bool {
var_dump($address, $port);
return false;
});
curl_exec($ch);
}

echo "IPv4 literal\n";
probe('http://127.0.0.1:1/');

echo "\nIPv6 literal\n";
probe('http://[::1]:1/');

/* An IPv4-mapped IPv6 address reaches IPv4 from an AF_INET6 socket. Both forms
* normalise to the same string. */
echo "\nIPv4-mapped IPv6 address\n";
probe('http://[::ffff:127.0.0.1]:1/');

echo "\nIPv4-mapped IPv6 address, hexadecimal form\n";
probe('http://[::ffff:7f00:1]:1/');

echo "Done";
?>
--EXPECT--
IPv4 literal
string(9) "127.0.0.1"
int(1)

IPv6 literal
string(3) "::1"
int(1)

IPv4-mapped IPv6 address
string(16) "::ffff:127.0.0.1"
int(1)

IPv4-mapped IPv6 address, hexadecimal form
string(16) "::ffff:127.0.0.1"
int(1)
Done
Loading
Loading