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. 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
View File

@ -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
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 class SecurityException extends Exception
{ {
const ERR_ACCESS_DENIED = 50001; const ERR_ACCESS_DENIED = 50001;
const ERR_NO_PERMISSION = 50002;
} }

View File

@ -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
])); ]));
} }