How to encrypt non-blocking PHP socket streams?

I am attempting to use PHP's stream_socket_client() function in a non-blocking (asynchronous) fashion. Documentation on PHP's web site indicates that the STREAM_CLIENT_ASYNC_CONNECT option flag should enable this. However, the following code...

$start_time = microtime(true);
$sockets[$i] = stream_socket_client('ssl://74.125.47.109:993', $errint, $errstr, 1, STREAM_CLIENT_ASYNC_CONNECT);
$end_time = microtime(true);
echo "Total time taken: " . ($end_time-$start_time) . " secs.";

Outputs the following:

Total time taken: 0.76204109191895 secs.

Evidently, the function is blocking (also supported by the fact that omission of the STREAM_CLIENT_ASYC_CONNECT flag does not meaningfully change the "total time taken" script output.

Any ideas on why this may be happening, and how to enforce a non-blocking connection attempt?

Answers


Why the ssl:// wrapper approach doesn't work ...

It's impossible to use the ssl:// family of stream wrappers to establish non-blocking connections in PHP at this time and the reason is simple:

To negotiate an SSL/TLS handshake you must both send and receive data.

You simply can't duplex information like this inside a single operation (e.g. what the stream wrappers do) without blocking script execution. And because PHP was originally designed to function in strictly synchronous environments (i.e. the blocking web SAPIs where each request has its own process) this blocking behavior is the natural thing to do.

As a result, the ssl:// stream wrapper won't operate how you want it to even if you set the STREAM_CLIENT_ASYNC_CONNECT flag. However, it is still possible to use PHP's stream encryption capabilities in your non-blocking socket operations.

How to enable encryption on your non-blocking socket streams ...

The SSL/TLS protocols are executed on top of the underlying data transport protocol. This means that we only enable the encryption protocols after the TCP/UDP/etc. connection is established. As a result we're able to first connect to the remote party using the STREAM_CLIENT_ASYC_CONNECT async flag and subsequently enable crypto on the (now connected) socket using stream_socket_enable_crypto().

Simple example sans error handling

This example assumes you understand how to use stream_select() (or equivalent descriptor notification lib to work with sockets in a non-blocking way). There is no handling for potential socket errors.

<?php // connect + encrypt a socket asynchronously

$uri = 'tcp://www.google.com:443';
$timeout = 42;
$flags = STREAM_CLIENT_ASYNC_CONNECT;
$socket = stream_socket_client($uri, $errno, $errstr, $timeout, $flags);
stream_set_blocking($socket, false);

// Once the async connection is actually established it will be "writable."
// Here we use stream_select to find out when the socket becomes writable.
while (1) {
    $w = [$socket];
    $r = $e = [];
    if (stream_select($r, $w, $e, 30, 0)) {
        break; // the async connect is finished
    }
}

// Now that our socket is connected lets enable crypto
$crypto = STREAM_CRYPTO_METHOD_TLS_CLIENT;
while (1) {
    $w = [$socket];
    $r = $e = [];
    if (stream_select($r, $w, $e, 30, 0)) {
        break; // the async connect is finished
        $result = stream_socket_enable_crypto($socket, $enable=true, $crypto);
        if ($result === true) {
            // Crypto enabled, we're finished!
            break;
        } elseif ($result === false) {
            die("crypto failed :(");
        } else {
            // handshake isn't finished yet. Do another trip around the loop.
        }
    }
}

// Send/receive encrypted data on $socket here
Note on return values

It's very important to use the === equality when checking the results from our crypto enabling calls. As mentioned in the relevant manual entry:

Returns TRUE on success, FALSE if negotiation has failed or 0 if there isn't enough data and you should try again (only for non-blocking sockets).

If we don't use === we can't differentiate between false and 0.


Need Your Help

Why is this HtmlAgilityPack operation invalid when there are, indeed, matching elements?

c# screen-scraping html-agility-pack

I get "InvalidOperationException > Message=Sequence contains no matching element" with the following code: