Skip to content

Commit 54ee20d

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 9898293 commit 54ee20d

13 files changed

Lines changed: 995 additions & 1 deletion

NEWS

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

28+
- Curl:
29+
. Added CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
30+
CURLOPT_CLOSESOCKETFUNCTION options, bridging libcurl's socket callbacks to
31+
ext/sockets Socket objects (requires the sockets extension).
32+
2833
- Date:
2934
. Update timelib to 2022.16. (Derick)
3035

UPGRADING

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

174+
- Curl:
175+
. Added the CURLOPT_SOCKOPTFUNCTION, CURLOPT_OPENSOCKETFUNCTION and
176+
CURLOPT_CLOSESOCKETFUNCTION options, which let userland hook into libcurl's
177+
socket creation, configuration and teardown. The callbacks exchange
178+
ext/sockets Socket objects, e.g. to validate the resolved address before
179+
connecting (SSRF protection) or to set low-level socket options. Setting
180+
one of these three options requires the sockets extension to be loaded
181+
(otherwise an Error is thrown); the rest of ext/curl keeps working without
182+
it. The new CURL_SOCKOPT_OK, CURL_SOCKOPT_ERROR,
183+
CURL_SOCKOPT_ALREADY_CONNECTED, CURLSOCKTYPE_IPCXN and CURLSOCKTYPE_ACCEPT
184+
constants are defined alongside them.
185+
174186
- Fileinfo:
175187
. finfo_file() now works with remote streams.
176188

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
@@ -2327,6 +2327,63 @@
23272327
*/
23282328
const CURL_FNMATCHFUNC_NOMATCH = UNKNOWN;
23292329

2330+
#ifdef HAVE_SOCKETS
2331+
/**
2332+
* Used with CURLOPT_SOCKOPTFUNCTION, which receives a Socket wrapping the
2333+
* descriptor libcurl just created and must return one of the CURL_SOCKOPT_*
2334+
* constants below.
2335+
* @var int
2336+
* @cvalue CURLOPT_SOCKOPTFUNCTION
2337+
*/
2338+
const CURLOPT_SOCKOPTFUNCTION = UNKNOWN;
2339+
/**
2340+
* Used with CURLOPT_OPENSOCKETFUNCTION, which receives the resolved address and
2341+
* must return a Socket to use for the connection, or false to abort it.
2342+
* @var int
2343+
* @cvalue CURLOPT_OPENSOCKETFUNCTION
2344+
*/
2345+
const CURLOPT_OPENSOCKETFUNCTION = UNKNOWN;
2346+
/**
2347+
* Used with CURLOPT_CLOSESOCKETFUNCTION, which is notified when libcurl is done
2348+
* with a socket created by the open-socket callback.
2349+
* @var int
2350+
* @cvalue CURLOPT_CLOSESOCKETFUNCTION
2351+
*/
2352+
const CURLOPT_CLOSESOCKETFUNCTION = UNKNOWN;
2353+
/**
2354+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: proceed normally.
2355+
* @var int
2356+
* @cvalue CURL_SOCKOPT_OK
2357+
*/
2358+
const CURL_SOCKOPT_OK = UNKNOWN;
2359+
/**
2360+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: abort the connection.
2361+
* @var int
2362+
* @cvalue CURL_SOCKOPT_ERROR
2363+
*/
2364+
const CURL_SOCKOPT_ERROR = UNKNOWN;
2365+
/**
2366+
* Return value for the CURLOPT_SOCKOPTFUNCTION callback: the socket is already
2367+
* connected, so libcurl should skip its own connect step.
2368+
* @var int
2369+
* @cvalue CURL_SOCKOPT_ALREADY_CONNECTED
2370+
*/
2371+
const CURL_SOCKOPT_ALREADY_CONNECTED = UNKNOWN;
2372+
/**
2373+
* Purpose passed to the socket callbacks: a socket for a regular IP connection.
2374+
* @var int
2375+
* @cvalue CURLSOCKTYPE_IPCXN
2376+
*/
2377+
const CURLSOCKTYPE_IPCXN = UNKNOWN;
2378+
/**
2379+
* Purpose passed to the socket callbacks: a socket created from accept() (e.g.
2380+
* active FTP).
2381+
* @var int
2382+
* @cvalue CURLSOCKTYPE_ACCEPT
2383+
*/
2384+
const CURLSOCKTYPE_ACCEPT = UNKNOWN;
2385+
#endif
2386+
23302387
/* Available since 7.21.2 */
23312388
/**
23322389
* @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
@@ -84,6 +84,11 @@ typedef struct {
8484
#if LIBCURL_VERSION_NUM >= 0x075400 /* Available since 7.84.0 */
8585
zend_fcall_info_cache sshhostkey;
8686
#endif
87+
#ifdef HAVE_SOCKETS
88+
zend_fcall_info_cache sockopt;
89+
zend_fcall_info_cache opensocket;
90+
zend_fcall_info_cache closesocket;
91+
#endif
8792
} php_curl_handlers;
8893

8994
struct _php_curl_error {

0 commit comments

Comments
 (0)