Misc fixes, readme, comments
This commit is contained in:
parent
813150c92b
commit
83c34f4a47
59
README.md
59
README.md
@ -2,21 +2,52 @@
|
|||||||
|
|
||||||
This is an implementation of the Mercure realtime protocol on steroids, built using ReactPHP.
|
This is an implementation of the Mercure realtime protocol on steroids, built using ReactPHP.
|
||||||
|
|
||||||
|
**Mercureact is under development, and not ready for use in anything important.**
|
||||||
|
|
||||||
It is intended to be used standalone, but it may also be integrated into another PHP application.
|
It is intended to be used standalone, but it may also be integrated into another PHP application.
|
||||||
|
|
||||||
|
## Installing
|
||||||
|
|
||||||
|
As PHAR:
|
||||||
|
|
||||||
|
* Download the latest [release](/noccy/mercureact/releases) from the forge.
|
||||||
|
|
||||||
|
As Composer dependency:
|
||||||
|
|
||||||
|
* `composer require noccylabs/mercureact`
|
||||||
|
|
||||||
|
## Using as PHAR
|
||||||
|
|
||||||
|
*TODO.*
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# Make a copy of the dist config and edit it
|
||||||
|
$ cp mercureact.conf.dist mercurect.conf
|
||||||
|
$ editor mercureact.conf
|
||||||
|
# Use the config file when launching
|
||||||
|
$ ./mercureact.phar -c mercureact.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using as dependency
|
||||||
|
|
||||||
|
*TODO.*
|
||||||
|
|
||||||
## ToDos
|
## ToDos
|
||||||
|
|
||||||
**Mercureact is under development, and not ready for use in anything important.**
|
* [ ] Read config from file
|
||||||
|
* [ ] Security Security Security
|
||||||
- [ ] Security Security Security
|
* [x] Check JWTs on connect
|
||||||
- [ ] Check JWTs on connect
|
* [ ] Check claims on subscribe and publish
|
||||||
- [ ] Check claims on subscribe and publish
|
* [ ] WebSocket authentication
|
||||||
- [ ] WebSocket authentication
|
* [ ] Subscription/Topic manager
|
||||||
- [ ] Subscription manager
|
* [ ] Unify distribution
|
||||||
- [ ] Publish events
|
* [ ] Publish events
|
||||||
- [ ] Server-Side Events distributor
|
* [ ] Server-Side Events distributor
|
||||||
- [x] Distribute events
|
* [x] Distribute events over SSE
|
||||||
- [ ] WebSocket distributor
|
* [ ] WebSocket distributor
|
||||||
- [ ] Setup subscriptions
|
* [ ] Setup subscriptions
|
||||||
- [ ] Dynamic subscriptions
|
* [ ] Dynamic subscriptions
|
||||||
- [x] Distribute events
|
* [x] Distribute events over WS
|
||||||
|
* [ ] HTTP
|
||||||
|
* [ ] Break out HTTP middleware into classes
|
||||||
|
* [ ] HTTP middleware unittests
|
36
composer.lock
generated
36
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "67776ecc95ce924ca24488a6af3615eb",
|
"content-hash": "794f155397a10dd2b61efedc40a661e8",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "evenement/evenement",
|
"name": "evenement/evenement",
|
||||||
@ -1676,16 +1676,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpstan/phpstan",
|
"name": "phpstan/phpstan",
|
||||||
"version": "1.10.59",
|
"version": "1.10.60",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/phpstan/phpstan.git",
|
"url": "https://github.com/phpstan/phpstan.git",
|
||||||
"reference": "e607609388d3a6d418a50a49f7940e8086798281"
|
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e607609388d3a6d418a50a49f7940e8086798281",
|
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
|
||||||
"reference": "e607609388d3a6d418a50a49f7940e8086798281",
|
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1734,20 +1734,20 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-02-20T13:59:13+00:00"
|
"time": "2024-03-07T13:30:19+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-code-coverage",
|
"name": "phpunit/php-code-coverage",
|
||||||
"version": "11.0.1",
|
"version": "11.0.2",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||||
"reference": "89702be0ad026873ef3a1605fe8726254eef4e2c"
|
"reference": "9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/89702be0ad026873ef3a1605fe8726254eef4e2c",
|
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b",
|
||||||
"reference": "89702be0ad026873ef3a1605fe8726254eef4e2c",
|
"reference": "9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -1804,7 +1804,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.1"
|
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.2"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1812,7 +1812,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-03-02T07:34:25+00:00"
|
"time": "2024-03-09T16:56:49+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/php-file-iterator",
|
"name": "phpunit/php-file-iterator",
|
||||||
@ -2061,16 +2061,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpunit/phpunit",
|
"name": "phpunit/phpunit",
|
||||||
"version": "11.0.4",
|
"version": "11.0.5",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||||
"reference": "3f4261269c91370e9b2b3f64cc76c617c442c35a"
|
"reference": "da2de3900beab025398ba37705b0f5ecafb3e1ab"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f4261269c91370e9b2b3f64cc76c617c442c35a",
|
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/da2de3900beab025398ba37705b0f5ecafb3e1ab",
|
||||||
"reference": "3f4261269c91370e9b2b3f64cc76c617c442c35a",
|
"reference": "da2de3900beab025398ba37705b0f5ecafb3e1ab",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@ -2141,7 +2141,7 @@
|
|||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.4"
|
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.5"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -2157,7 +2157,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2024-02-29T16:21:10+00:00"
|
"time": "2024-03-09T12:12:14+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sebastian/cli-parser",
|
"name": "sebastian/cli-parser",
|
||||||
|
12
make-phar.sh
Executable file
12
make-phar.sh
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
REPO=$PWD
|
||||||
|
DIR=mercureact
|
||||||
|
pushd /tmp
|
||||||
|
rm -rf /tmp/$DIR
|
||||||
|
git clone "$REPO"
|
||||||
|
cd $DIR
|
||||||
|
composer install --no-dev
|
||||||
|
pharlite
|
||||||
|
popd
|
||||||
|
mv /tmp/$DIR/*.phar .
|
||||||
|
rm -rf /tmp/$DIR
|
12
phpstan.neon
Normal file
12
phpstan.neon
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
|
||||||
|
excludePaths:
|
||||||
|
- doc
|
||||||
|
- vendor
|
||||||
|
- tests
|
||||||
|
|
||||||
|
# Paths to include in the analysis
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
|
@ -7,4 +7,5 @@ use Exception;
|
|||||||
class SecurityException extends Exception
|
class SecurityException extends Exception
|
||||||
{
|
{
|
||||||
const ERR_ACCESS_DENIED = 50001;
|
const ERR_ACCESS_DENIED = 50001;
|
||||||
|
const ERR_NO_PERMISSION = 50002;
|
||||||
}
|
}
|
@ -80,6 +80,7 @@ class Server
|
|||||||
*/
|
*/
|
||||||
private function createHttpServer(array $options): HttpServer
|
private function createHttpServer(array $options): HttpServer
|
||||||
{
|
{
|
||||||
|
// TODO break out the middleware to facilitate testing
|
||||||
return new HttpServer(
|
return new HttpServer(
|
||||||
$this->rejectionWrappingMiddleware(...),
|
$this->rejectionWrappingMiddleware(...),
|
||||||
$this->checkRequestSecurityMiddleware(...),
|
$this->checkRequestSecurityMiddleware(...),
|
||||||
@ -91,7 +92,7 @@ class Server
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Resolves unhandled requests with a 404 error
|
||||||
*
|
*
|
||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
* @return PromiseInterface
|
* @return PromiseInterface
|
||||||
@ -106,7 +107,8 @@ class Server
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Wraps rejections into error messages, and also does some sanity checks on the returned
|
||||||
|
* data, making sure it is a response.
|
||||||
*
|
*
|
||||||
* @param ServerRequestInterface $request
|
* @param ServerRequestInterface $request
|
||||||
* @param callable $next
|
* @param callable $next
|
||||||
@ -275,6 +277,22 @@ class Server
|
|||||||
throw new \Exception("Invalid request");
|
throw new \Exception("Invalid request");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse out the urlencoded body. Pretty sure there is a better way to do this?
|
||||||
|
$body = explode("&", (string)$request->getBody());
|
||||||
|
$data = [];
|
||||||
|
foreach ($body as $param) {
|
||||||
|
if (!str_contains($param, "="))
|
||||||
|
throw new RequestException("Invalid request data", RequestException::ERR_INVALID_REQUEST_DATA);
|
||||||
|
[ $name, $value ] = array_map('urldecode', explode("=", $param, 2));
|
||||||
|
if (in_array($name, [ 'topic' ])) {
|
||||||
|
if (!isset($data[$name]))
|
||||||
|
$data[$name] = [];
|
||||||
|
$data[$name][] = $value;
|
||||||
|
} else {
|
||||||
|
$data[$name] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Grab the JWT token from the requests authorization attribute
|
// Grab the JWT token from the requests authorization attribute
|
||||||
$tok = $request->getAttribute('authorization');
|
$tok = $request->getAttribute('authorization');
|
||||||
if ($tok instanceof JWTToken) {
|
if ($tok instanceof JWTToken) {
|
||||||
@ -282,22 +300,9 @@ class Server
|
|||||||
if (isset($claims['mercure']['publish'])) {
|
if (isset($claims['mercure']['publish'])) {
|
||||||
$publishClaims = $claims['mercure']['publish'];
|
$publishClaims = $claims['mercure']['publish'];
|
||||||
// TODO check topic against publishClaims
|
// TODO check topic against publishClaims
|
||||||
}
|
if (!$this->checkTopicClaims($data['topic']??[], $publishClaims)) {
|
||||||
}
|
throw new SecurityException("Insufficient permissions for publish", SecurityException::ERR_NO_PERMISSION);
|
||||||
|
}
|
||||||
// Parse out the urlencoded body. Pretty sure there is a better way to do this?
|
|
||||||
$body = explode("&", (string)$request->getBody());
|
|
||||||
$data = [];
|
|
||||||
foreach ($body as $param) {
|
|
||||||
if (!str_contains($param, "=")) throw new RequestException("Invalid request data", RequestException::ERR_INVALID_REQUEST_DATA);
|
|
||||||
[ $name, $value ] = array_map('urldecode', explode("=", $param, 2));
|
|
||||||
// FIXME support multiple topics?
|
|
||||||
if (in_array($name, [ 'topic' ])) {
|
|
||||||
if (!isset($data[$name]))
|
|
||||||
$data[$name] = [];
|
|
||||||
$data[$name][] = $value;
|
|
||||||
} else {
|
|
||||||
$data[$name] = $value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,6 +321,18 @@ class Server
|
|||||||
return Response::plaintext("urn:uuid:".$message->id."\n");
|
return Response::plaintext("urn:uuid:".$message->id."\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function checkTopicClaims(string|array $topic, array $claims): bool
|
||||||
|
{
|
||||||
|
foreach ((array)$topic as $match) {
|
||||||
|
foreach ($claims as $claim) {
|
||||||
|
if ($claim === "*") return true;
|
||||||
|
if ($claim === $match) return true;
|
||||||
|
// TODO implement full matching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
@ -327,7 +344,7 @@ class Server
|
|||||||
foreach ($this->webSocketClients as $webSocket) {
|
foreach ($this->webSocketClients as $webSocket) {
|
||||||
$webSocket->write(json_encode([
|
$webSocket->write(json_encode([
|
||||||
'type' => $message->type,
|
'type' => $message->type,
|
||||||
//'topic' => $data['topic'],
|
'topic' => $message->topic,
|
||||||
'data' => (@json_decode($message->data))??$message->data
|
'data' => (@json_decode($message->data))??$message->data
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user