Misc fixes, readme, comments

This commit is contained in:
Chris 2024-03-10 16:23:46 +01:00
parent 813150c92b
commit 83c34f4a47
6 changed files with 124 additions and 51 deletions

View File

@ -2,21 +2,52 @@
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.
## 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
**Mercureact is under development, and not ready for use in anything important.**
- [ ] Security Security Security
- [ ] Check JWTs on connect
- [ ] Check claims on subscribe and publish
- [ ] WebSocket authentication
- [ ] Subscription manager
- [ ] Publish events
- [ ] Server-Side Events distributor
- [x] Distribute events
- [ ] WebSocket distributor
- [ ] Setup subscriptions
- [ ] Dynamic subscriptions
- [x] Distribute events
* [ ] Read config from file
* [ ] Security Security Security
* [x] Check JWTs on connect
* [ ] Check claims on subscribe and publish
* [ ] WebSocket authentication
* [ ] Subscription/Topic manager
* [ ] Unify distribution
* [ ] Publish events
* [ ] Server-Side Events distributor
* [x] Distribute events over SSE
* [ ] WebSocket distributor
* [ ] Setup subscriptions
* [ ] Dynamic subscriptions
* [x] Distribute events over WS
* [ ] HTTP
* [ ] Break out HTTP middleware into classes
* [ ] HTTP middleware unittests

36
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "67776ecc95ce924ca24488a6af3615eb",
"content-hash": "794f155397a10dd2b61efedc40a661e8",
"packages": [
{
"name": "evenement/evenement",
@ -1676,16 +1676,16 @@
},
{
"name": "phpstan/phpstan",
"version": "1.10.59",
"version": "1.10.60",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "e607609388d3a6d418a50a49f7940e8086798281"
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e607609388d3a6d418a50a49f7940e8086798281",
"reference": "e607609388d3a6d418a50a49f7940e8086798281",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
"reference": "95dcea7d6c628a3f2f56d091d8a0219485a86bbe",
"shasum": ""
},
"require": {
@ -1734,20 +1734,20 @@
"type": "tidelift"
}
],
"time": "2024-02-20T13:59:13+00:00"
"time": "2024-03-07T13:30:19+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "11.0.1",
"version": "11.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "89702be0ad026873ef3a1605fe8726254eef4e2c"
"reference": "9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/89702be0ad026873ef3a1605fe8726254eef4e2c",
"reference": "89702be0ad026873ef3a1605fe8726254eef4e2c",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b",
"reference": "9e0a298b4dc6438a1e70ac8e1b3ea4980ae5a09b",
"shasum": ""
},
"require": {
@ -1804,7 +1804,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"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": [
{
@ -1812,7 +1812,7 @@
"type": "github"
}
],
"time": "2024-03-02T07:34:25+00:00"
"time": "2024-03-09T16:56:49+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -2061,16 +2061,16 @@
},
{
"name": "phpunit/phpunit",
"version": "11.0.4",
"version": "11.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "3f4261269c91370e9b2b3f64cc76c617c442c35a"
"reference": "da2de3900beab025398ba37705b0f5ecafb3e1ab"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3f4261269c91370e9b2b3f64cc76c617c442c35a",
"reference": "3f4261269c91370e9b2b3f64cc76c617c442c35a",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/da2de3900beab025398ba37705b0f5ecafb3e1ab",
"reference": "da2de3900beab025398ba37705b0f5ecafb3e1ab",
"shasum": ""
},
"require": {
@ -2141,7 +2141,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"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": [
{
@ -2157,7 +2157,7 @@
"type": "tidelift"
}
],
"time": "2024-02-29T16:21:10+00:00"
"time": "2024-03-09T12:12:14+00:00"
},
{
"name": "sebastian/cli-parser",

12
make-phar.sh Executable file
View 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
View File

@ -0,0 +1,12 @@
parameters:
level: 5
excludePaths:
- doc
- vendor
- tests
# Paths to include in the analysis
paths:
- src

View File

@ -7,4 +7,5 @@ use Exception;
class SecurityException extends Exception
{
const ERR_ACCESS_DENIED = 50001;
const ERR_NO_PERMISSION = 50002;
}

View File

@ -80,6 +80,7 @@ class Server
*/
private function createHttpServer(array $options): HttpServer
{
// TODO break out the middleware to facilitate testing
return new HttpServer(
$this->rejectionWrappingMiddleware(...),
$this->checkRequestSecurityMiddleware(...),
@ -91,7 +92,7 @@ class Server
}
/**
*
* Resolves unhandled requests with a 404 error
*
* @param ServerRequestInterface $request
* @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 callable $next
@ -275,6 +277,22 @@ class Server
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
$tok = $request->getAttribute('authorization');
if ($tok instanceof JWTToken) {
@ -282,22 +300,9 @@ class Server
if (isset($claims['mercure']['publish'])) {
$publishClaims = $claims['mercure']['publish'];
// TODO check topic against publishClaims
}
}
// 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;
if (!$this->checkTopicClaims($data['topic']??[], $publishClaims)) {
throw new SecurityException("Insufficient permissions for publish", SecurityException::ERR_NO_PERMISSION);
}
}
}
@ -316,6 +321,18 @@ class Server
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) {
$webSocket->write(json_encode([
'type' => $message->type,
//'topic' => $data['topic'],
'topic' => $message->topic,
'data' => (@json_decode($message->data))??$message->data
]));
}