Skip to content

Commit 3777478

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 8e22140 commit 3777478

13 files changed

Lines changed: 985 additions & 1 deletion

‎NEWS‎

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

513513
- Curl:
514514
. Add support for CURLINFO_SIZE_DELIVERED (libcurl >= 8.20.0). (Ayesh)
515+
. Added CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
516+
CURLOPT_CLOSESOCKETFUNCTION options, bridging libcurl's socket callbacks to
517+
ext/sockets Socket objects (requires the sockets extension).
518+
(Xavier Leune)
515519

516520
- Date:
517521
. Update timelib to 2022.16. (Derick)

‎UPGRADING‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,16 @@ PHP 8.6 UPGRADE NOTES
331331
failing with CURLE_SEND_FAIL_REWIND. The callback receives the CurlHandle,
332332
offset and origin, and must return one of CURL_SEEKFUNC_OK,
333333
CURL_SEEKFUNC_FAIL or CURL_SEEKFUNC_CANTSEEK.
334+
. Added the CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
335+
CURLOPT_CLOSESOCKETFUNCTION options, which let userland hook into libcurl's
336+
socket creation, configuration and teardown. The callbacks exchange
337+
ext/sockets Socket objects, e.g. to validate the resolved address before
338+
connecting (SSRF protection) or to set low-level socket options. Setting
339+
one of these three options requires the sockets extension to be loaded
340+
(otherwise an Error is thrown); the rest of ext/curl keeps working without
341+
it. The new CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR,
342+
CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN and CURLSOCKTYPE_ACCEPT
343+
constants are defined alongside them.
334344

335345
- Date:
336346
. Added a new Time\Duration class.

‎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
@@ -2352,6 +2352,63 @@
23522352
*/
23532353
const CURL_FNMATCHFUNC_NOMATCH = UNKNOWN;
23542354

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