Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC - Added the apache_connection_stream() function for CGI WebSockets #14047

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

RichardTMiles
Copy link

@RichardTMiles RichardTMiles commented Apr 25, 2024

The accepted practice for WebSockets (WS) in Apache PHP is to use the stream_context_create is a PHP CLI process that Apache’s mod_proxy and mod_proxy_wstunnel forward connections to. This requires that you bind to an open port and have access to install Apache modules. The IETF standardized the WebSocket protocol as RFC 6455 in 2011 to enable full duplex communication. The issue Apache2handler PHP currently has is its ability to capture persistent input connections in a php_stream to then be used with stream_select. PHP-defined IO streams are currently not able to handle the requests as php://stdio is the actual code buffer and php://input is tied to HTTPD's ap_get_brigade function which acts as a wrapper to handle io filtering. Not only does ap_get_brigade not capture WS input, but it also caches the data as Apache2handler expects it to be static. PHP’s stream_select function also fails to cast this non-sealable stream (as expected). HTTPD can handle long upgrade connections, and according to the spec, the WS protocol is just an HTTP GET request with specific headers and io filters. This proposed addition includes a visual sample and example repository.

    public static function handleSingleUserConnections(): void
    {

        if (!defined('STDOUT')) {

            define('STDOUT', fopen('php://stdout', 'wb'));

        }

        // get all headers has a polyfill in our function.php
        $headers = getallheaders();

        self::handshake(STDOUT, $headers);

        $flush = self::outputBufferWebSocketEncoder();

        print posix_getpid() . PHP_EOL;

        $flush();

        // Here you can handle the WebSocket upgrade logic
        $websocket = apache_connection_stream();

        if (!is_resource($websocket)) {

            throw new Error('INPUT is not a valid resource');

        }

        $loop = 0;

        while (true) {

            try {

                ++$loop;

                if (!is_resource($websocket)) {

                    throw new Error('STDIN is not a valid resource');

                }

                $flush();

                $read = [$websocket];

                $number = stream_select($read, $write, $error, 10);

                if ($number === 0) {

                    self::colorCode("No streams are requesting to be processed. (loop: $loop; )", 'cyan');

                    continue;

                }

                self::colorCode("$number, stream(s) are requesting to be processed.");

                foreach ($read as $connection) {

                    if ($connection === $websocket) {

                        $data = self::decode($connection);

                        print_r($data);

                        $flush();

                    }

                }

            } catch (Throwable $e) {

                print_r($e);

            }

        }

    }

Here is a link to the repository with the full code example for both PHP-CLI WebSockets and PHP-SAPI apache_websocket_stream. The boilerplate is consistent in all the examples on the internet except for "Content-Length: 0” and "Content-Type: application/octet-stream” headers, this causes apache to disable its output buffer and output filter (compression encoding) and allows our binary data to pass.

The alternative to WebSockets is Long Polling, aka request interval polling. This is popular with CMS like WordPress due to its reliability, but is considered a poor option that only emulates real-time io with significant roundtrip overhead.
I believe this trade-off is made all too often by developers who:

  1. Want to make sure it works everywhere
  2. Can’t afford a dedicated server with sudo access.
  3. Have difficulties with mod_proxy or other Apache configurations that have to happen for a proper CLI implementation

This method is designed to be a zero configuration open with no external Apache Modules or configurations. Using it over long term request reties will help save organizations electricity and allowing the features to be available out of the box should help shorten the learning curve for the community. If this proposal is well received then I believe the next logical step is to add the protocol framing layer in php-src too.

I’m having difficulty registering for a wiki account at https://wiki.php.net/start?do=register as I only receive
That wasn't the answer we were expecting, despite multiple browser and email options.

…cket

Apache CGI doesn't support WebSocket Upgrade requests out-of-the-box. php://input uses apache brigade/buckets but these are not fired with websocket Input; moreover, the php://input stream is cached and not seekable. php://stdio is the php code buffer so it is not useful here either. This allow a direct connection to the client.
@nielsdos
Copy link
Member

I’m having difficulty registering for a wiki account at https://wiki.php.net/start?do=register as I only receive
That wasn't the answer we were expecting, despite multiple browser and email options.

What email address did you fill in for the last input box? I'm guessing you perhaps filled in your own email address?

@RichardTMiles RichardTMiles marked this pull request as draft April 26, 2024 01:08
When a connection is received you must hit enter in your server terminal to complete the connection. CLI server is single threaded, so only basic debugging can be done regarding websockets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants