Skip to content

Commit e7bcbf1

Browse files
committed
ext/curl: add socket callback options bridging to ext/sockets
Expose libcurl's CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and CURLOPT_CLOSESOCKETFUNCTION, letting userland hook into socket creation, configuration and teardown. These are useful for application security, in particular SSRF protection (validating the resolved address before connecting) and low-level socket hardening. Following the existing curl_write_header / curl_prereqfunction bridge model, the callbacks exchange ext/sockets Socket objects so they are fully usable in pure PHP (socket_create/socket_bind, socket_set_option, socket_close): - sockopt: fn(CurlHandle $ch, Socket $socket, int $purpose): int returns CURL_SOCKOPT_OK / _ERROR / _ALREADY_CONNECTED - opensocket: fn(CurlHandle $ch, int $purpose, array $address): Socket|false $address = [family, socktype, protocol, ip, port]; returning false aborts the connection (CURL_SOCKET_BAD) - closesocket: fn(CurlHandle $ch, Socket $socket): void The dependency on ext/sockets is optional both at build and at runtime (ZEND_MOD_OPTIONAL): the rest of ext/curl keeps working when sockets is not loaded, and the three socket-callback options simply throw a clear Error when invoked in that configuration. The Socket class entry is resolved lazily by name at MINIT rather than referenced as a link-time symbol, so curl never carries a hard symbol dependency on ext/sockets. Notable details: - Descriptors owned by libcurl are detached (bsd_socket = -1) before the temporary Socket object is released, to avoid a double close. Socket objects are created without calling socket_import_file_descriptor(), which on Windows emits a spurious WSAEINVAL warning on a not-yet-connected socket during the SOCKOPT phase. - Pooled connections still alive at curl_easy_cleanup() would otherwise invoke the userland CURLOPT_CLOSESOCKETFUNCTION callback during handle destruction. Calling into PHP from there is unsafe (an exception thrown from the callback would surface outside any try/catch). Setting the option back to NULL on the easy handle is not enough — libcurl caches the function pointer per connection. The close FCC is torn down before curl_easy_cleanup() so the trampoline falls through to its native-close fallback when libcurl invokes it. - The same teardown is applied to every easy handle attached to a CurlMultiHandle before curl_multi_cleanup() so the close callback never fires from within the multi's destructor either. - Stream-backed Sockets and already-closed Sockets returned from CURLOPT_OPENSOCKETFUNCTION are refused with a TypeError: a stream-backed Socket bypasses our bsd_socket detach (socket_free_obj() delegates close to the backing php_stream), and an already-closed Socket would bury the real cause under a generic CURLE_COULDNT_CONNECT. - When the sockopt callback throws, the abort path returns CURL_SOCKOPT_ERROR (matching opensocket / ssh_hostkey) so libcurl does not connect with a half-configured socket. - Setting an option to null restores libcurl's native default. New constants (only defined when ext/curl is built with sockets headers available, i.e. HAVE_SOCKETS): CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION, CURLOPT_CLOSESOCKETFUNCTION, CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR, CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN, CURLSOCKTYPE_ACCEPT.
1 parent dec19ed commit e7bcbf1

13 files changed

Lines changed: 988 additions & 1 deletion

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ PHP NEWS
218218

219219
- Curl:
220220
. Add support for CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). (Ayesh)
221+
. Added CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
222+
CURLOPT_CLOSESOCKETFUNCTION options, bridging libcurl's socket callbacks to
223+
ext/sockets Socket objects (requires the sockets extension).
224+
(Xavier Leune)
221225

222226
- Date:
223227
. Update timelib to 2022.16. (Derick)

UPGRADING

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ PHP 8.6 UPGRADE NOTES
250250
failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
251251
offset and origin, and must return one of CURL_SEEKFUNC_OK,
252252
CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
253+
. Added the CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
254+
CURLOPT_CLOSESOCKETFUNCTION options, which let userland hook into libcurl's
255+
socket creation, configuration and teardown. The callbacks exchange
256+
ext/sockets Socket objects, e.g. to validate the resolved address before
257+
connecting (SSRF protection) or to set low-level socket options. Setting
258+
one of these three options requires the sockets extension to be loaded
259+
(otherwise an Error is thrown); the rest of ext/curl keeps working without
260+
it. The new CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR,
261+
CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN and CURLSOCKTYPE_ACCEPT
262+
constants are defined alongside them.
253263

254264
- Fileinfo:
255265
. finfo_file() now works with remote streams.

ext/curl/config.m4

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ if test "$PHP_CURL" != "no"; then
7575
PHP_NEW_EXTENSION([curl],
7676
[interface.c multi.c share.c curl_file.c],
7777
[$ext_shared])
78+
dnl The CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKETFUNCTION callbacks bridge to
79+
dnl ext/sockets Socket objects (guarded by HAVE_SOCKETS). The dependency is
80+
dnl optional both at build and at runtime: without sockets the rest of curl
81+
dnl keeps working, and the three socket-callback options throw a clear error
82+
dnl when invoked. The hint below only enforces MINIT order when sockets is
83+
dnl built in alongside curl.
84+
PHP_ADD_EXTENSION_DEP(curl, sockets, true)
7885
PHP_INSTALL_HEADERS([ext/curl], [php_curl.h])
7986
PHP_SUBST([CURL_SHARED_LIBADD])
8087
fi

ext/curl/config.w32

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ if (PHP_CURL != "no") {
2525
WARNING("zstd in curl not enabled; library not found");
2626
}
2727
EXTENSION("curl", "interface.c multi.c share.c curl_file.c");
28+
// The socket callbacks (CURLOPT_SOCKOPT/OPENSOCKET/CLOSESOCKETFUNCTION)
29+
// bridge to ext/sockets Socket objects, guarded by HAVE_SOCKETS. Optional
30+
// at build and at runtime: without sockets the rest of curl keeps working
31+
// and only those three options throw a clear error when invoked. The hint
32+
// below ensures MINIT order when both extensions are built together.
33+
ADD_EXTENSION_DEP('curl', 'sockets', true);
2834
AC_DEFINE('HAVE_CURL', 1, "Define to 1 if the PHP extension 'curl' is available.");
2935
ADD_FLAG("CFLAGS_CURL", "/D PHP_CURL_EXPORTS=1");
3036
if (curl_location.match(/libcurl_a\.lib$/)) {

ext/curl/curl.stub.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2347,6 +2347,63 @@
23472347
*/
23482348
const CURL_FNMATCHFUNC_NOMATCH = UNKNOWN;
23492349

2350+
#ifdef HAVE_SOCKETS
2351+
/**
2352+
* Used with CURLOPT_SOCKOPTFUNCTION, which receives a Socket wrapping the
2353+
* descriptor libcurl just created and must return one of the CURL_SOCKOPT_*
2354+
* constants below.
2355+
* @var int
2356+
* @cvalue CURLOPT_SOCKOPTFUNCTION
2357+
*/
2358+
const CURLOPT_SOCKOPTFUNCTION = UNKNOWN;
2359+
/**
2360+
* Used with CURLOPT_OPENSOCKETFUNCTION, which receives the resolved address and
2361+
* must return a Socket to use for the connection, or false to abort it.
2362+
* @var int
2363+
* @cvalue CURLOPT_OPENSOCKETFUNCTION
2364+
*/
2365+
const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN;
2366+
/**
2367+
* Used with CURLOPT_CLOSESOCKETFUNCTION, which is notified when libcurl is done
2368+
* with a socket created by the open-socket callback.
2369+
* @var int
2370+
* @cvalue CURLOPT_CLOSESOCKETFUNCTION
2371+
*/
2372+
const CURLOPT_CLOSESOCKETFUNCTION = UNKNOWN;
2373+
/**
2374+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: proceed normally.
2375+
* @var int
2376+
* @cvalue CURL_SOCKOPT_OK
2377+
*/
2378+
const CURL_SOCKOPT_OK = UNKNOWN;
2379+
/**
2380+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: abort the connection.
2381+
* @var int
2382+
* @cvalue CURL_SOCKOPT_ERROR
2383+
*/
2384+
const CURL_SOCKOPT_ERROR = UNKNOWN;
2385+
/**
2386+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: the socket is already
2387+
* connected, so libcurl should skip its own connect step.
2388+
* @var int
2389+
* @cvalue CURL_SOCKOPT_ALREADY_CONNECTED
2390+
*/
2391+
const CURL_SOCKOPT_ALREADY_CONNECTED = UNKNOWN;
2392+
/**
2393+
* Purpose passed to the socket callbacks: a socket for a regular IP connection.
2394+
* @var int
2395+
* @cvalue CURLSOCKTYPE_IPCXN
2396+
*/
2397+
const CURLSOCKTYPE_IPCXN = UNKNOWN;
2398+
/**
2399+
* Purpose passed to the socket callbacks: a socket created from accept() (e.g.
2400+
* active FTP).
2401+
* @var int
2402+
* @cvalue CURLSOCKTYPE_ACCEPT
2403+
*/
2404+
const CURLSOCKTYPE_ACCEPT = UNKNOWN;
2405+
#endif
2406+
23502407
/* Available since 7.21.2 */
23512408
/**
23522409
* @var int

ext/curl/curl_arginfo.h

Lines changed: 11 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ typedef struct {
8585
#if LIBCURL_VERSION_NUM >= 0x075400 /* Available since 7.84.0 */
8686
zend_fcall_info_cache sshhostkey;
8787
#endif
88+
#ifdef HAVE_SOCKETS
89+
zend_fcall_info_cache sockopt;
90+
zend_fcall_info_cache opensocket;
91+
zend_fcall_info_cache closesocket;
92+
#endif
8893
} php_curl_handlers;
8994

9095
struct _php_curl_error {

0 commit comments

Comments
 (0)