From 8e66e5e2c1e02ab9781679959d8ac993e9a01754 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Tue, 10 Dec 2024 14:28:06 +0000 Subject: [PATCH] Web Sockets (#45) --- Dockerfile | 17 +- composer.json | 14 +- composer.lock | 1770 +++++++++++++++++++++++++++++++++++-- docker-compose.yml | 3 + entrypoint.sh | 8 +- index.php | 251 +++--- src/Controller/Api.php | 66 ++ src/Controller/Server.php | 180 ++++ src/Entity/Display.php | 35 + src/Entity/Message.php | 31 + src/Entity/Queue.php | 200 +++++ tests/Private API.http | 19 + tests/cmsGetStats.php | 85 -- tests/cmsSend.php | 53 +- tests/playerReq.php | 19 - tests/playerSub.php | 14 +- 16 files changed, 2389 insertions(+), 376 deletions(-) create mode 100644 src/Controller/Api.php create mode 100644 src/Controller/Server.php create mode 100644 src/Entity/Display.php create mode 100644 src/Entity/Message.php create mode 100644 src/Entity/Queue.php create mode 100644 tests/Private API.http delete mode 100644 tests/cmsGetStats.php delete mode 100644 tests/playerReq.php diff --git a/Dockerfile b/Dockerfile index 4b51c36..faff564 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,14 @@ -FROM composer:1.6 as composer +FROM composer AS composer COPY . /app RUN composer install --no-interaction --no-dev --ignore-platform-reqs --optimize-autoloader -FROM php:8.1-cli -MAINTAINER Xibo Signage Ltd +FROM php:8.2-cli +LABEL org.opencontainers.image.authors="Xibo Signage Ltd " -ENV XMR_DEBUG false -ENV XMR_QUEUE_POLL 5 -ENV XMR_QUEUE_SIZE 10 -ENV XMR_IPV6RESPSUPPORT false -ENV XMR_IPV6PUBSUPPORT false +ENV XMR_DEBUG=false +ENV XMR_QUEUE_POLL=5 +ENV XMR_QUEUE_SIZE=10 +ENV XMR_IPV6PUBSUPPORT=false RUN apt-get update && apt-get install -y libzmq3-dev git \ && rm -rf /var/lib/apt/lists/* @@ -24,7 +23,7 @@ RUN git clone https://github.com/zeromq/php-zmq.git \ RUN docker-php-ext-enable zmq -EXPOSE 9505 50001 +EXPOSE 8080 8081 9505 COPY ./entrypoint.sh /entrypoint.sh COPY . /opt/xmr diff --git a/composer.json b/composer.json index 347b9b9..739f8a4 100644 --- a/composer.json +++ b/composer.json @@ -21,13 +21,21 @@ "bin": ["bin/xmr.phar"], "config": { "platform": { - "php": "8.1", + "php": "8.2", "ext-zmq": "1" } }, "require": { - "php": ">=8.1", + "php": ">=8.2", "monolog/monolog": "^1.17", - "react/zmq": "^0.4.0" + "react/react": "^1.4", + "react/socket": "^1.16", + "react/zmq": "^0.4.0", + "cboden/ratchet": "^0.4.4" + }, + "autoload": { + "psr-4": { + "Xibo\\": "src/" + } } } diff --git a/composer.lock b/composer.lock index c3fe608..226ad28 100644 --- a/composer.lock +++ b/composer.lock @@ -4,32 +4,95 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "be10c4aceb6ae6e6de027b6f5dfc5028", + "content-hash": "2962bb0981206b9c30166891cdec6983", "packages": [ + { + "name": "cboden/ratchet", + "version": "v0.4.4", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Ratchet.git", + "reference": "5012dc954541b40c5599d286fd40653f5716a38f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Ratchet/zipball/5012dc954541b40c5599d286fd40653f5716a38f", + "reference": "5012dc954541b40c5599d286fd40653f5716a38f", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=5.4.2", + "ratchet/rfc6455": "^0.3.1", + "react/event-loop": ">=0.4", + "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5", + "symfony/http-foundation": "^2.6|^3.0|^4.0|^5.0|^6.0", + "symfony/routing": "^2.6|^3.0|^4.0|^5.0|^6.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\": "src/Ratchet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "PHP WebSocket library", + "homepage": "http://socketo.me", + "keywords": [ + "Ratchet", + "WebSockets", + "server", + "sockets", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/Ratchet/issues", + "source": "https://github.com/ratchetphp/Ratchet/tree/v0.4.4" + }, + "time": "2021-12-14T00:20:41+00:00" + }, { "name": "evenement/evenement", - "version": "v3.0.1", + "version": "v3.0.2", "source": { "type": "git", "url": "https://github.com/igorw/evenement.git", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7" + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/531bfb9d15f8aa57454f5f0285b18bec903b8fb7", - "reference": "531bfb9d15f8aa57454f5f0285b18bec903b8fb7", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", "shasum": "" }, "require": { "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "phpunit/phpunit": "^9 || ^6" }, "type": "library", "autoload": { - "psr-0": { - "Evenement": "src" + "psr-4": { + "Evenement\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -47,20 +110,196 @@ "event-dispatcher", "event-emitter" ], - "time": "2017-07-23T21:35:13+00:00" + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" }, { "name": "monolog/monolog", - "version": "1.27.0", + "version": "1.27.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "52ebd235c1f7e0d5e1b16464b695a28335f8e44a" + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/52ebd235c1f7e0d5e1b16464b695a28335f8e44a", - "reference": "52ebd235c1f7e0d5e1b16464b695a28335f8e44a", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", + "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", "shasum": "" }, "require": { @@ -119,6 +358,10 @@ "logging", "psr-3" ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/1.27.1" + }, "funding": [ { "url": "https://github.com/Seldaek", @@ -129,7 +372,115 @@ "type": "tidelift" } ], - "time": "2022-03-13T20:29:46+00:00" + "time": "2022-06-09T08:53:42+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" }, { "name": "psr/log", @@ -176,38 +527,37 @@ "psr", "psr-3" ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, "time": "2021-05-03T11:20:27+00:00" }, { - "name": "react/event-loop", - "version": "v1.2.0", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/be6dee480fc4692cec0504e65eb486e3be1aa6f2", - "reference": "be6dee480fc4692cec0504e65eb486e3be1aa6f2", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" - }, - "suggest": { - "ext-event": "~1.0 for ExtEventLoop", - "ext-pcntl": "For signal handling support when using the StreamSelectLoop", - "ext-uv": "* for ExtUvLoop" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", "autoload": { - "psr-4": { - "React\\EventLoop\\": "src" - } + "files": [ + "src/getallheaders.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -215,83 +565,1339 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "funding": [ - { - "url": "https://github.com/WyriHaximus", - "type": "github" - }, - { - "url": "https://github.com/clue", - "type": "github" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "time": "2021-07-11T12:31:24+00:00" + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "react/zmq", - "version": "v0.4.0", + "name": "ratchet/rfc6455", + "version": "v0.3.1", "source": { "type": "git", - "url": "https://github.com/friends-of-reactphp/zmq.git", - "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39" + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/friends-of-reactphp/zmq/zipball/13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", - "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0", - "ext-zmq": "*", - "php": ">=5.4.0", - "react/event-loop": "^1.0 || ^0.5 || ^0.4" + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" }, "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4" + "phpunit/phpunit": "^5.7", + "react/socket": "^1.3" }, "type": "library", "autoload": { "psr-4": { - "React\\ZMQ\\": "src" + "Ratchet\\RFC6455\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "ZeroMQ bindings for React.", - "keywords": [ + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" + }, + "time": "2021-12-09T23:20:49+00:00" + }, + { + "name": "react/async", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/async.git", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Async\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async utilities and fibers for ReactPHP", + "keywords": [ + "async", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/async/issues", + "source": "https://github.com/reactphp/async/tree/v4.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:40:02+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/http", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "php": ">=5.3.0", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" + }, + "require-dev": { + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": [ + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-11-20T15:24:08+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/promise-stream", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-stream.git", + "reference": "5c7ec3450f558deb779742e33967d837e2db7871" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-stream/zipball/5c7ec3450f558deb779742e33967d837e2db7871", + "reference": "5c7ec3450f558deb779742e33967d837e2db7871", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/promise": "^3 || ^2.1 || ^1.2", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.6" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "The missing link between Promise-land and Stream-land for ReactPHP", + "homepage": "https://github.com/reactphp/promise-stream", + "keywords": [ + "Buffer", + "async", + "promise", + "reactphp", + "stream", + "unwrap" + ], + "support": { + "issues": "https://github.com/reactphp/promise-stream/issues", + "source": "https://github.com/reactphp/promise-stream/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-12-13T11:32:02+00:00" + }, + { + "name": "react/promise-timer", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise-timer.git", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise-timer/zipball/4f70306ed66b8b44768941ca7f142092600fafc1", + "reference": "4f70306ed66b8b44768941ca7f142092600fafc1", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7.0 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\Timer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A trivial implementation of timeouts for Promises, built on top of ReactPHP.", + "homepage": "https://github.com/reactphp/promise-timer", + "keywords": [ + "async", + "event-loop", + "promise", + "reactphp", + "timeout", + "timer" + ], + "support": { + "issues": "https://github.com/reactphp/promise-timer/issues", + "source": "https://github.com/reactphp/promise-timer/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:27:45+00:00" + }, + { + "name": "react/react", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/reactphp.git", + "reference": "726e5de40567c9effaa8e5665b1a2621af8d7ee9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/reactphp/zipball/726e5de40567c9effaa8e5665b1a2621af8d7ee9", + "reference": "726e5de40567c9effaa8e5665b1a2621af8d7ee9", + "shasum": "" + }, + "require": { + "php": ">=5.3.8", + "react/async": "^4 || ^3 || ^2", + "react/cache": "^1.1", + "react/dns": "^1.11", + "react/event-loop": "^1.4", + "react/http": "^1.8", + "react/promise": "^3 || ^2.10 || ^1.2", + "react/promise-stream": "^1.6", + "react/promise-timer": "^1.9", + "react/socket": "^1.13", + "react/stream": "^1.3" + }, + "require-dev": { + "clue/stream-filter": "^1.3", + "phpunit/phpunit": "^9.6 || ^7.5 || ^5.7 || ^4.8.36", + "react/async": "^4.2@dev || ^3.2@dev || ^4 || ^3 || ^2", + "react/dns": "^1.12@dev", + "react/http": "^1.10@dev", + "react/promise": "^3@dev || ^2.10 || ^1.2", + "react/promise-stream": "^1.7@dev", + "react/promise-timer": "^1.10@dev", + "react/socket": "^1.14@dev" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ReactPHP: Event-driven, non-blocking I/O with PHP.", + "homepage": "https://reactphp.org/", + "keywords": [ + "asynchronous", + "reactor", + "reactphp" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/reactphp/reactphp/issues", + "source": "https://github.com/reactphp/reactphp/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-07-11T16:08:54+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "react/zmq", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/friends-of-reactphp/zmq.git", + "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/friends-of-reactphp/zmq/zipball/13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", + "reference": "13dec0bd2397adcc5d6aa54c8d7f0982fba66f39", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "ext-zmq": "*", + "php": ">=5.4.0", + "react/event-loop": "^1.0 || ^0.5 || ^0.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "~4.8.35 || ~5.7 || ~6.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ZMQ\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "ZeroMQ bindings for React.", + "keywords": [ "zeromq", "zmq" ], + "support": { + "issues": "https://github.com/friends-of-reactphp/zmq/issues", + "source": "https://github.com/friends-of-reactphp/zmq/tree/master" + }, "time": "2018-05-18T15:27:55+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/431771b7a6f662f1575b3cfc8fd7617aa9864d57", + "reference": "431771b7a6f662f1575b3cfc8fd7617aa9864d57", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T18:58:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.16", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/91e02e606b4b705c2f4fb42f7e7708b7923a3220", + "reference": "91e02e606b4b705c2f4fb42f7e7708b7923a3220", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.16" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T15:31:34+00:00" } ], "packages-dev": [], @@ -301,12 +1907,12 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=8.1" + "php": ">=8.2" }, "platform-dev": [], "platform-overrides": { - "php": "8.1", + "php": "8.2", "ext-zmq": "1" }, - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.3.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 29589cc..74ec577 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,6 +3,9 @@ version: "3" services: xmr: build: . + ports: + - "8080:8080" + - "8081:8081" environment: XMR_DEBUG: "true" volumes: diff --git a/entrypoint.sh b/entrypoint.sh index 039ab5a..adfeb5b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,12 +2,14 @@ # Write config.json echo '{' > /opt/xmr/config.json -echo ' "listenOn": "tcp://*:50001",' >> /opt/xmr/config.json -echo ' "pubOn": ["tcp://*:9505"],' >> /opt/xmr/config.json +echo ' "sockets": {' >> /opt/xmr/config.json +echo ' "ws": "0.0.0.0:8080",' >> /opt/xmr/config.json +echo ' "api": "0.0.0.0:8081",' >> /opt/xmr/config.json +echo ' "zmq": ["tcp://*:9505"]' >> /opt/xmr/config.json +echo ' },' >> /opt/xmr/config.json echo ' "queuePoll": '$XMR_QUEUE_POLL',' >> /opt/xmr/config.json echo ' "queueSize": '$XMR_QUEUE_SIZE',' >> /opt/xmr/config.json echo ' "debug": '$XMR_DEBUG',' >> /opt/xmr/config.json -echo ' "ipv6RespSupport": '$XMR_IPV6RESPSUPPORT',' >> /opt/xmr/config.json echo ' "ipv6PubSupport": '$XMR_IPV6PUBSUPPORT >> /opt/xmr/config.json echo '}' >> /opt/xmr/config.json diff --git a/index.php b/index.php index 3303339..0ae48fb 100644 --- a/index.php +++ b/index.php @@ -1,9 +1,9 @@ #!/usr/bin/env php . - * -sequenceDiagram -Player->> CMS: Register -Note right of Player: Register contains the XMR Channel -CMS->> XMR: PlayerAction -XMR->> CMS: ACK -XMR-->> Player: PlayerAction - * */ + +use Monolog\Handler\StreamHandler; +use Monolog\Logger; +use Ratchet\Http\HttpServer; +use Ratchet\Server\IoServer; +use Ratchet\WebSocket\WsServer; +use React\EventLoop\Loop; +use React\Http\Message\Response; +use Xibo\Controller\Api; +use Xibo\Controller\Server; +use Xibo\Entity\Queue; + require 'vendor/autoload.php'; -function exception_error_handler($severity, $message, $file, $line) { +// TODO: ratchet does not support PHP8 +error_reporting(E_ALL ^ E_DEPRECATED); + +set_error_handler(function($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // This error code is not included in error_reporting return; } throw new ErrorException($message, 0, $severity, $file, $line); -} -set_error_handler("exception_error_handler"); +}); // Decide where to look for the config file $dirname = (Phar::running(false) == '') ? __DIR__ : dirname(Phar::running(false)); $config = $dirname . '/config.json'; -if (!file_exists($config)) +if (!file_exists($config)) { throw new InvalidArgumentException('Missing ' . $config . ' file, please create one in ' . $dirname); +} $configString = file_get_contents($config); $config = json_decode($configString); -if ($config === null) +if ($config === null) { throw new InvalidArgumentException('Cannot decode config file ' . json_last_error_msg() . ' config string is [' . $configString . ']'); +} + +$logLevel = $config->debug ? Logger::DEBUG : Logger::WARNING; -if ($config->debug) - $logLevel = \Monolog\Logger::DEBUG; -else - $logLevel = \Monolog\Logger::WARNING; +// Set up logging to file +$log = new Logger('xmr'); +$log->pushHandler(new StreamHandler(STDOUT, $logLevel)); // Queue settings $queuePoll = (property_exists($config, 'queuePoll')) ? $config->queuePoll : 5; $queueSize = (property_exists($config, 'queueSize')) ? $config->queueSize : 10; -// Set up logging to file -$log = new \Monolog\Logger('xmr'); -$log->pushHandler(new \Monolog\Handler\StreamHandler(STDOUT, $logLevel)); -$log->info(sprintf('Starting up - listening for CMS on %s.', $config->listenOn)); +// Create an in memory message queue. +$messageQueue = new Queue(); try { - $loop = \React\EventLoop\Factory::create(); - - /** - * ZMQ context wraps the PHP implementation. - * @var \ZMQContext $context - */ - $context = new React\ZMQ\Context($loop); - - // Reply socket for requests from CMS - $responder = $context->getSocket(ZMQ::SOCKET_REP); - $responder->bind($config->listenOn); - - // Set RESP socket options - if (isset($config->ipv6RespSupport) && $config->ipv6RespSupport === true) { - $log->debug('RESP MQ Setting socket option for IPv6 to TRUE'); - $responder->setSockOpt(\ZMQ::SOCKOPT_IPV6, true); - } + $loop = Loop::get(); - // Pub socket for messages to Players (subs) - $publisher = $context->getSocket(ZMQ::SOCKET_PUB); + // Web Socket server + $messagingServer = new Server($messageQueue, $log); + $wsSocket = new React\Socket\SocketServer($config->sockets->ws); + $wsServer = new WsServer($messagingServer); + $ioServer = new IoServer( + new HttpServer($wsServer), + $wsSocket, + $loop + ); + + // Enable keep alive + $wsServer->enableKeepAlive($ioServer->loop); + + $log->info('WS listening on ' . $config->sockets->ws); + + // LEGACY: Pub socket for messages to Players (subs) + $publisher = (new React\ZMQ\Context($loop))->getSocket(ZMQ::SOCKET_PUB); // Set PUB socket options if (isset($config->ipv6PubSupport) && $config->ipv6PubSupport === true) { @@ -94,149 +100,104 @@ function exception_error_handler($severity, $message, $file, $line) { $publisher->setSockOpt(\ZMQ::SOCKOPT_IPV6, true); } - foreach ($config->pubOn as $pubOn) { + foreach ($config->sockets->zmq as $pubOn) { $log->info(sprintf('Bind to %s for Publish.', $pubOn)); $publisher->bind($pubOn); } - // Create an in memory message queue. - $messageStatsEmpty = [ - 'peakQueueSize' => 0, - 'messageCounters' => [ - 'total' => 0, - 'sent' => 0, - 'qos1' => 0, - 'qos2' => 0, - 'qos3' => 0, - 'qos4' => 0, - 'qos5' => 0, - 'qos6' => 0, - 'qos7' => 0, - 'qos8' => 0, - 'qos9' => 0, - 'qos10' => 0, - ] - ]; - $messageStats = $messageStatsEmpty; - $messageQueue = []; - - // REP - $responder->on('error', function ($e) use ($log) { - $log->error($e->getMessage()); - }); - - $responder->on('message', function ($msg) use ($log, $responder, $publisher, &$messageQueue, &$messageStats, $messageStatsEmpty) { + // Create a private API to receive messages from the CMS + $api = new Api($messageQueue, $log); + // Create a HTTP server to handle requests to the API + $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) use ($log, $api) { try { - // Log incoming message - $log->info($msg); - - if ($msg === 'stats') { - // Add the current queue size - $messageStats['currentQueueSize'] = count($messageQueue); - - // Send response - $responder->send(json_encode($messageStats), \ZMQ::MODE_DONTWAIT); - - // Reset the stats - $messageStats = $messageStatsEmpty; - } else { - // Parse the message and expect a "channel" element - $msg = json_decode($msg); - - if (!isset($msg->channel)) { - throw new InvalidArgumentException('Missing Channel'); - } - - if (!isset($msg->key)) { - throw new InvalidArgumentException('Missing Key'); - } - - if (!isset($msg->message)) { - throw new InvalidArgumentException('Missing Message'); - } - - // Respond to this message - $responder->send(true, \ZMQ::MODE_DONTWAIT); - - // Make sure QOS is set - if (!isset($msg->qos)) { - // Default to the highest priority for messages missing a QOS - $msg->qos = 10; - } - - // Add to stats - $messageStats['messageCounters']['total']++; - $messageStats['messageCounters']['qos' . $msg->qos]++; - - // Decide whether we should queue the message or send it immediately. - if ($msg->qos != 10) { - // Queue for the periodic poll to send - $log->debug('Queuing'); - $messageQueue[] = $msg; + if ($request->getMethod() !== 'POST') { + throw new Exception('Method not allowed'); + } - // Record peak queue - $currentQueueSize = count($messageQueue); - if ($currentQueueSize > $messageStats['peakQueueSize']) { - $messageStats['peakQueueSize'] = $currentQueueSize; - } - } else { - // Send Immediately - $log->debug('Sending Immediately'); - $messageStats['messageCounters']['sent']++; - $publisher->sendmulti([$msg->channel, $msg->key, $msg->message], \ZMQ::MODE_DONTWAIT); - } + $json = json_decode($request->getBody()->getContents(), true); + if ($json === false || !is_array($json)) { + throw new InvalidArgumentException('Not valid JSON'); } - } catch (InvalidArgumentException $e) { - // Return false - $responder->send(false, \ZMQ::MODE_DONTWAIT); - $log->error($e->getMessage()); + return $api->handleMessage($json); + } catch (Exception $e) { + $log->error('API: e = ' . $e->getMessage()); + return new Response( + 422, + ['Content-Type' => 'plain/text'], + $e->getMessage() + ); } }); + $socket = new React\Socket\SocketServer($config->sockets->api); + $http->listen($socket); + $http->on('error', function (Exception $exception) use ($log) { + $log->error('http: ' . $exception->getMessage()); + $log->debug('stack: ' . $exception->getTraceAsString()); + }); + + $log->info('HTTP listening'); // Queue Processor $log->debug('Adding a queue processor for every ' . $queuePoll . ' seconds'); - $loop->addPeriodicTimer($queuePoll, function() use ($log, $publisher, &$messageQueue, $queueSize, &$messageStats) { + $loop->addPeriodicTimer($queuePoll, function() use ($log, $messagingServer, $publisher, $messageQueue, $queueSize) { // Is there work to be done - if (count($messageQueue) > 0) { + if ($messageQueue->hasItems()) { $log->debug('Queue Poll - work to be done.'); - // Order the message queue according to QOS - usort($messageQueue, function($a, $b) { - return ($a->qos === $b->qos) ? 0 : (($a->qos < $b->qos) ? -1 : 1); - }); + $messageQueue->sortQueue(); $log->debug('Queue Poll - message queue sorted'); // Send up to X messages. for ($i = 0; $i < $queueSize; $i++) { - if ($i > count($messageQueue)) { + if ($i > $messageQueue->queueSize()) { $log->debug('Queue Poll - queue size reached'); break; } // Pop an element - $msg = array_pop($messageQueue); + $msg = $messageQueue->getItem(); // Send $log->debug('Sending ' . $i); - $messageStats['messageCounters']['sent']++; - $publisher->sendmulti([$msg->channel, $msg->key, $msg->message], \ZMQ::MODE_DONTWAIT); + // Where are we sending this item? + if ($msg->isWebSocket) { + $display = $messagingServer->getDisplayById($msg->channel); + if ($display === null) { + $log->info('Display ' . $msg->channel . ' not connected'); + continue; + } + $display->connection->send($msg->message); + } else { + $publisher->sendmulti([$msg->channel, $msg->key, $msg->message], \ZMQ::MODE_DONTWAIT); + } - $log->debug('Popped ' . $i . ' from the queue, new queue size ' . count($messageQueue)); + $log->debug('Popped ' . $i . ' from the queue, new queue size ' . $messageQueue->queueSize()); } } }); // Periodic updater - $loop->addPeriodicTimer(30, function() use ($log, $publisher) { + $loop->addPeriodicTimer(30, function() use ($log, $messagingServer, $publisher) { $log->debug('Heartbeat...'); + + // Send to all connected WS clients + $messagingServer->heartbeat(); + + // Send to PUB queue $publisher->sendmulti(["H", "", ""], \ZMQ::MODE_DONTWAIT); }); - // Run the react event loop + // Key management + $loop->addPeriodicTimer(3600, function() use ($log, $messageQueue) { + $log->debug('Key management...'); + $messageQueue->expireKeys(); + }); + + // Run the React event loop $loop->run(); } catch (Exception $e) { $log->error($e->getMessage()); diff --git a/src/Controller/Api.php b/src/Controller/Api.php new file mode 100644 index 0000000..655e955 --- /dev/null +++ b/src/Controller/Api.php @@ -0,0 +1,66 @@ +. + */ +namespace Xibo\Controller; + +use Psr\Log\LoggerInterface; +use React\Http\Message\Response; +use Xibo\Entity\Queue; + +class Api +{ + public function __construct( + private readonly Queue $queue, + private readonly LoggerInterface $logger + ) { + } + + /** + * Handle messages hitting the API + * @param array $message + * @return \React\Http\Message\Response + */ + public function handleMessage(array $message): Response + { + $type = $message['type'] ?? 'empty'; + + $this->logger->debug('handleMessage: type = ' . $type); + + if ($type === 'stats') { + // Success + return Response::json($this->queue->flushStats()); + } else if ($type === 'keys') { + // Register new keys for this CMS. + $this->queue->addKey($message['id'], $message['key']); + } else if ($type === 'multi') { + $this->logger->debug('Queuing multiple messages'); + foreach ($message['messages'] as $message) { + $this->queue->queueItem($message); + } + } else { + $this->logger->debug('Queuing'); + $this->queue->queueItem($message); + } + + // Success + return new Response(201); + } +} diff --git a/src/Controller/Server.php b/src/Controller/Server.php new file mode 100644 index 0000000..82df95f --- /dev/null +++ b/src/Controller/Server.php @@ -0,0 +1,180 @@ +. + */ +namespace Xibo\Controller; + +use Psr\Log\LoggerInterface; +use Ratchet\ConnectionInterface; +use Ratchet\MessageComponentInterface; +use Xibo\Entity\Display; +use Xibo\Entity\Queue; +use XiboSignage\Client\Client; + +class Server implements MessageComponentInterface +{ + /** @var Display[] */ + private array $displays = []; + private array $ids = []; + + public function __construct( + private readonly Queue $queue, + private readonly LoggerInterface $logger + ) { + } + + public function onOpen(ConnectionInterface $conn): void + { + $this->logger->debug('onOpen: ' . $conn->resourceId); + + $this->addDisplay( + $conn->resourceId, + $conn + ); + } + + public function onClose(ConnectionInterface $conn): void + { + $this->removeDisplay($conn->resourceId); + $this->logger->debug('onClose: ' . $conn->resourceId); + } + + public function onError(ConnectionInterface $conn, \Exception $e): void + { + $this->logger->debug('onError: ' . $conn->resourceId . ', e: ' . $e->getMessage()); + } + + public function onMessage(ConnectionInterface $from, $msg): void + { + $display = $this->getDisplayByResourceId($from->resourceId); + + $this->logger->debug('onMessage: ' . $display->resourceId); + + // Expect a JSON string + $json = json_decode($msg, true); + if ($json === null) { + $this->logger->error('onMessage: Invalid JSON'); + return; + } + + // We are only expecting one message, which initialises the connection. + try { + if (($json['type'] ?? 'empty') === 'init') { + // The display should pass us a key + $key = $json['key'] ?? null; + if (empty($key)) { + throw new \InvalidArgumentException('Missing key'); + } + + $channel = $json['channel'] ?? null; + if (empty($channel)) { + throw new \InvalidArgumentException('Missing channel'); + } + + // Validate the key provided + if (!$this->queue->authKey($key)) { + throw new \InvalidArgumentException('Invalid key'); + } + + // Valid key for the CMS + $this->linkDisplay($display, $channel); + } else { + throw new \Exception('Invalid message type'); + } + } catch (\Exception $e) { + $this->logger->error('onMessage: ' . $e->getMessage()); + + // Close the socket with an error (onClose gets called to remove the connection) + $display->connection->close(); + } + } + + public function heartbeat(): void + { + foreach ($this->displays as $display) { + if ($display->id !== null) { + $display->connection->send('H'); + } + } + } + + /** + * Add a display to the list of connections (unauthed at this point) + * @param string $resourceId + * @param \Ratchet\ConnectionInterface $connection + * @return \Xibo\Entity\Display + */ + private function addDisplay(string $resourceId, ConnectionInterface $connection): Display + { + $this->displays[$resourceId] = new Display($resourceId, $connection); + return $this->displays[$resourceId]; + } + + /** + * Link a display to an ID (which is the channel) + * @param \Xibo\Entity\Display $display + * @param string $id + * @return void + */ + private function linkDisplay(Display $display, string $id): void + { + // Make a pointer between this resource and the ID + $this->ids[$id] = $display->resourceId; + $display->id = $id; + } + + /** + * Remove a display + * @param string $resourceId + * @return void + */ + private function removeDisplay(string $resourceId): void + { + $display = $this->getDisplayByResourceId($resourceId); + if ($display !== null && $display->id !== null) { + unset($this->ids[$display->id]); + } + unset($this->displays[$resourceId]); + } + + /** + * Get a display by its ID (channel) + * @param string $id + * @return \Xibo\Entity\Display|null + */ + public function getDisplayById(string $id): ?Display + { + if (isset($this->ids[$id])) { + return $this->displays[$this->ids[$id]] ?? null; + } else { + return null; + } + } + + /** + * Get a display by its socket resource + * @param string $resourceId + * @return \Xibo\Entity\Display|null + */ + private function getDisplayByResourceId(string $resourceId): ?Display + { + return $this->displays[$resourceId] ?? null; + } +} diff --git a/src/Entity/Display.php b/src/Entity/Display.php new file mode 100644 index 0000000..2b7fe84 --- /dev/null +++ b/src/Entity/Display.php @@ -0,0 +1,35 @@ +. + */ +namespace Xibo\Entity; + +use Ratchet\ConnectionInterface; + +class Display +{ + public ?string $id = null; + + public function __construct( + public string $resourceId, + public ConnectionInterface $connection + ) { + } +} diff --git a/src/Entity/Message.php b/src/Entity/Message.php new file mode 100644 index 0000000..722e8bd --- /dev/null +++ b/src/Entity/Message.php @@ -0,0 +1,31 @@ +. + */ +namespace Xibo\Entity; + +class Message +{ + public string $channel; + public string $key; + public string $message; + public int $qos; + public bool $isWebSocket; +} diff --git a/src/Entity/Queue.php b/src/Entity/Queue.php new file mode 100644 index 0000000..5eba1d1 --- /dev/null +++ b/src/Entity/Queue.php @@ -0,0 +1,200 @@ +. + */ + +namespace Xibo\Entity; + +class Queue +{ + private array $instances = []; + + /** @var \Xibo\Entity\Message[] */ + private array $queue; + + private array $stats; + + public function __construct() + { + $this->queue = []; + $this->stats = [ + 'peakQueueSize' => 0, + 'messageCounters' => [ + 'total' => 0, + 'sent' => 0, + 'qos1' => 0, + 'qos2' => 0, + 'qos3' => 0, + 'qos4' => 0, + 'qos5' => 0, + 'qos6' => 0, + 'qos7' => 0, + 'qos8' => 0, + 'qos9' => 0, + 'qos10' => 0, + ] + ]; + + } + + public function hasItems(): bool + { + return count($this->queue); + } + + public function queueSize(): int + { + return count($this->queue); + } + + public function sortQueue(): void + { + // Order the message queue according to QOS + usort($this->queue, function($a, $b) { + return ($a->qos === $b->qos) ? 0 : (($a->qos < $b->qos) ? -1 : 1); + }); + } + + public function getItem(): Message + { + $this->stats['messageCounters']['sent']++; + + return array_pop($this->queue); + } + + /** + * @param array $message + * @return void + * @throws \InvalidArgumentException + */ + public function queueItem(array $message): void + { + $msg = new Message(); + + if (!isset($message['channel'])) { + throw new \InvalidArgumentException('Missing Channel'); + } + + if (!isset($message['key'])) { + throw new \InvalidArgumentException('Missing Key'); + } + + if (!isset($message['message'])) { + throw new \InvalidArgumentException('Missing Message'); + } + + // Make sure QOS is set + if (!isset($message['qos'])) { + // Default to the highest priority for messages missing a QOS + $message['qos'] = 10; + } + + $msg->channel = $message['channel']; + $msg->key = $message['key']; + $msg->message = $message['message']; + $msg->qos = $message['qos']; + $msg->isWebSocket = $message['isWebSocket'] ?? false; + + // Queue + $this->queue[] = $msg; + + // Update stats + $this->stats['messageCounters']['total']++; + $this->stats['messageCounters']['qos' . $msg->qos]++; + + $currentQueueSize = $this->queueSize(); + if ($currentQueueSize > $this->stats['peakQueueSize']) { + $this->stats['peakQueueSize'] = $currentQueueSize; + } + } + + public function flushStats(): array + { + $stats = $this->stats; + $stats['currentQueueSize'] = $this->queueSize(); + $this->clearStats(); + return $stats; + } + + private function clearStats(): void + { + $this->stats = [ + 'peakQueueSize' => 0, + 'messageCounters' => [ + 'total' => 0, + 'sent' => 0, + 'qos1' => 0, + 'qos2' => 0, + 'qos3' => 0, + 'qos4' => 0, + 'qos5' => 0, + 'qos6' => 0, + 'qos7' => 0, + 'qos8' => 0, + 'qos9' => 0, + 'qos10' => 0, + ] + ]; + } + + public function addKey(string $instance, string $key): void + { + if (!array_key_exists($instance, $this->instances)) { + $this->instances[$instance] = ['keys' => []]; + } + $this->instances[$instance]['keys'][] = [ + 'key' => $key, + 'expires' => time() + 86400, + ]; + } + + public function authKey(string $providedKey): bool + { + foreach ($this->instances as $instance) { + foreach ($instance['keys'] as $key) { + if ($key['key'] === $providedKey && time() < $key['expires']) { + return true; + } + } + } + + return false; + } + + public function expireKeys(): void + { + // Expire keys within each instance + foreach ($this->instances as $instance) { + for ($i = 0; $i < count($instance['keys']); $i++) { + // Expire any keys which are no longer in date. + if (time() >= $instance['keys'][$i]['expires']) { + unset($instance['keys'][$i]); + } + } + } + + // Remove instances with no keys + for ($j = 0; $j < count($this->instances); $j++) { + if (count($this->instances[$j]['keys']) <= 0) { + unset($this->instances[$j]); + } + } + } +} diff --git a/tests/Private API.http b/tests/Private API.http new file mode 100644 index 0000000..15a61ec --- /dev/null +++ b/tests/Private API.http @@ -0,0 +1,19 @@ +POST http://localhost:8081 +Content-Type: application/json + +{ + "type": "stats" +} + +### + +POST http://localhost:8081 +Content-Type: application/json + +{ + "type": "keys", + "id": "http://localhost", + "key": "123456" +} + +### diff --git a/tests/cmsGetStats.php b/tests/cmsGetStats.php deleted file mode 100644 index 2078530..0000000 --- a/tests/cmsGetStats.php +++ /dev/null @@ -1,85 +0,0 @@ -. - * - * This is a CMS send MOCK - * execute with: docker-compose exec xmr sh -c "cd /opt/xmr/tests; php cmsGetStats.php" - * - */ -require '../vendor/autoload.php'; - -try { - // Create a message and send. - send('tcp://localhost:50001', 'stats'); -} catch (Exception $e) { - echo $e->getMessage() . PHP_EOL; -} - -/** - * @param $connection - * @param $message - * @return bool|string - * @throws ZMQSocketException - */ -function send($connection, $message) -{ - echo 'Sending to ' . $connection . PHP_EOL; - - // Issue a message payload to XMR. - $context = new \ZMQContext(); - - // Connect to socket - $socket = new \ZMQSocket($context, \ZMQ::SOCKET_REQ); - $socket->connect($connection); - - // Send the message to the socket - $socket->send($message); - - // Need to replace this with a non-blocking recv() with a retry loop - $retries = 15; - $reply = false; - - do { - try { - // Try and receive - // if ZMQ::MODE_NOBLOCK/MODE_DONTWAIT is used and the operation would block boolean false - // shall be returned. - $reply = $socket->recv(\ZMQ::MODE_DONTWAIT); - - echo 'Received ' . var_export($reply, true) . PHP_EOL; - - if ($reply !== false) { - break; - } - } catch (\ZMQSocketException $sockEx) { - if ($sockEx->getCode() !== \ZMQ::ERR_EAGAIN) { - throw $sockEx; - } - } - - usleep(100000); - - } while (--$retries); - - // Disconnect socket - $socket->disconnect($connection); - - return $reply; -} \ No newline at end of file diff --git a/tests/cmsSend.php b/tests/cmsSend.php index 9100858..01cdf25 100644 --- a/tests/cmsSend.php +++ b/tests/cmsSend.php @@ -1,8 +1,8 @@ . - * - * This is a CMS send MOCK - * execute with: docker-compose exec xmr sh -c "cd /opt/xmr/tests; php cmsSend.php 1234" - * */ + +// execute with: docker-compose exec xmr sh -c "cd /opt/xmr/tests; php cmsSend.php 1234" require '../vendor/autoload.php'; $_MESSAGE_COUNT = 15; -$_ENCRYPT = true; +$_ENCRYPT = false; // Track $start = microtime(true); @@ -35,6 +33,7 @@ } $identity = $argv[1]; +$isWebSocket = ($argv[2] ?? false) === 'websocket'; // Get the Public Key $fp = fopen('key.pub', 'r'); @@ -42,12 +41,16 @@ fclose($fp); try { - // Issue a message payload to XMR. - $context = new \ZMQContext(); + //open connection + $ch = curl_init(); + + //set the url, number of POST vars, POST data + curl_setopt($ch,CURLOPT_URL, 'http://localhost:8081'); + curl_setopt($ch,CURLOPT_POST, true); + curl_setopt( $ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json')); - // Connect to socket - $socket = new \ZMQSocket($context, \ZMQ::SOCKET_REQ); - $socket->connect('tcp://localhost:50001'); + // So that curl_exec returns the contents of the cURL; rather than echoing it + curl_setopt($ch,CURLOPT_RETURNTRANSFER, true); // Queue up a bunch of messages to see what happens for ($i = 0; $i < $_MESSAGE_COUNT; $i++) { @@ -60,26 +63,32 @@ openssl_seal($i . ' - QOS1', $message, $eKeys, [$publicKey], 'RC4'); // Create a message and send. - send($socket, [ + $fields = [ 'channel' => $identity, 'key' => base64_encode($eKeys[0]), 'message' => base64_encode($message), - 'qos' => rand(1, 10) - ]); + 'qos' => rand(1, 10), + 'isWebSocket' => $isWebSocket, + ]; } else { - send($socket, [ + $fields = [ 'channel' => $identity, 'key' => 'key', 'message' => 'message ' . $i, - 'qos' => rand(1, 10) - ]); + 'qos' => rand(1, 10), + 'isWebSocket' => $isWebSocket, + ]; } + + curl_setopt($ch,CURLOPT_POSTFIELDS, json_encode($fields)); + + //execute post + $result = curl_exec($ch); + echo $result . PHP_EOL; + usleep(50); } - - // Disconnect socket - $socket->disconnect('tcp://localhost:50001'); } catch (Exception $e) { echo $e->getMessage() . PHP_EOL; } diff --git a/tests/playerReq.php b/tests/playerReq.php deleted file mode 100644 index 02e2644..0000000 --- a/tests/playerReq.php +++ /dev/null @@ -1,19 +0,0 @@ - -*/ - -$context = new ZMQContext(); - -// Socket to talk to server -echo "Connecting to hello world server…\n"; -$requester = new ZMQSocket($context, ZMQ::SOCKET_REQ); -$requester->connect("tcp://192.168.86.88:58587"); -echo "connected\n"; -$requester->send("Hello"); -echo "sent\n"; -$reply = $requester->recv(); -echo "Received reply " . $reply; \ No newline at end of file diff --git a/tests/playerSub.php b/tests/playerSub.php index 6b9aed1..3481e17 100644 --- a/tests/playerSub.php +++ b/tests/playerSub.php @@ -1,8 +1,8 @@ . - * - * This is a player subscription mock file. - * docker-compose exec xmr sh -c "cd /opt/xmr/tests; php playerSub.php 1234" - * */ +// docker-compose exec xmr sh -c "cd /opt/xmr/tests; php playerSub.php 1234" +// docker-compose exec xmr sh -c "cd /opt/xmr/tests; php playerSub.php 1234 websocket" require '../vendor/autoload.php'; if (!isset($argv[1])) { @@ -35,7 +33,7 @@ $privateKey = openssl_get_privatekey(fread($fp, 8192)); fclose($fp); -echo 'Sub to: ' . $identity; +echo 'Sub to: ' . $identity . PHP_EOL; // Sub $loop = React\EventLoop\Factory::create();