Extracted middleware and handlers
This commit is contained in:
		
							
								
								
									
										191
									
								
								src/Http/Middleware/MercureHandler.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/Http/Middleware/MercureHandler.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\Mercureact\Http\Middleware;
 | 
			
		||||
 | 
			
		||||
use NoccyLabs\Mercureact\Broker\Message;
 | 
			
		||||
use NoccyLabs\Mercureact\Broker\TopicManager;
 | 
			
		||||
use NoccyLabs\Mercureact\Configuration;
 | 
			
		||||
use NoccyLabs\Mercureact\Http\Exeption\RequestException;
 | 
			
		||||
use NoccyLabs\Mercureact\Http\Exeption\SecurityException;
 | 
			
		||||
use NoccyLabs\SimpleJWT\JWTToken;
 | 
			
		||||
use Psr\Http\Message\ResponseInterface;
 | 
			
		||||
use Psr\Http\Message\ServerRequestInterface;
 | 
			
		||||
use React\EventLoop\Loop;
 | 
			
		||||
use React\EventLoop\LoopInterface;
 | 
			
		||||
use React\Http\Message\Response;
 | 
			
		||||
use React\Promise\Promise;
 | 
			
		||||
use React\Promise\PromiseInterface;
 | 
			
		||||
use React\Stream\ThroughStream;
 | 
			
		||||
use SplObjectStorage;
 | 
			
		||||
use Symfony\Component\Uid\Uuid;
 | 
			
		||||
 | 
			
		||||
class MercureHandler
 | 
			
		||||
{
 | 
			
		||||
    private LoopInterface $loop;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Configuration $config,
 | 
			
		||||
        private SplObjectStorage $eventClients,
 | 
			
		||||
        private TopicManager $topicManager,
 | 
			
		||||
        ?LoopInterface $loop=null
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        $this->loop = $loop ?? Loop::get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Mecure handler middleware
 | 
			
		||||
     * 
 | 
			
		||||
     * @param ServerRequestInterface $request
 | 
			
		||||
     * @param callable $next
 | 
			
		||||
     * @return PromiseInterface
 | 
			
		||||
     */
 | 
			
		||||
    public function __invoke(ServerRequestInterface $request, callable $next): PromiseInterface
 | 
			
		||||
    {
 | 
			
		||||
        return new Promise(
 | 
			
		||||
            function (callable $resolve, callable $reject) use ($next, $request) {
 | 
			
		||||
                if ($request->getUri()->getPath() == "/.well-known/mercure") {
 | 
			
		||||
                    if ($request->getMethod() == 'POST') {
 | 
			
		||||
                        $resolve($this->handleMercurePublish($request));
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                    $resolve($this->handleMercureClient($request));
 | 
			
		||||
                } else {
 | 
			
		||||
                    $resolve($next($request));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * 
 | 
			
		||||
     * @param ServerRequestInterface $request
 | 
			
		||||
     * @return ResponseInterface
 | 
			
		||||
     */
 | 
			
		||||
    private function handleMercureClient(ServerRequestInterface $request): ResponseInterface
 | 
			
		||||
    {
 | 
			
		||||
        $tok = $request->getAttribute('authorization');
 | 
			
		||||
        if ($tok instanceof JWTToken) {
 | 
			
		||||
            $claims = $tok->claims->getAll();
 | 
			
		||||
            if (isset($claims['mercure']['subscribe'])) {
 | 
			
		||||
                $subscribeClaims = $claims['mercure']['subscribe'];
 | 
			
		||||
                // TODO check topic against subscribeClaims
 | 
			
		||||
            }            
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $responseStream = new ThroughStream();
 | 
			
		||||
 | 
			
		||||
        $response = new Response(
 | 
			
		||||
            body: $responseStream
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->eventClients->attach($responseStream, $request);
 | 
			
		||||
        $responseStream->on('close', function () use ($responseStream) {
 | 
			
		||||
            $this->eventClients->detach($responseStream);;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return $response
 | 
			
		||||
            ->withHeader("Cache-Control", "no-store")
 | 
			
		||||
            ->withHeader("Content-Type", "text/event-stream");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * 
 | 
			
		||||
     * @param ServerRequestInterface $request
 | 
			
		||||
     * @return ResponseInterface
 | 
			
		||||
     */
 | 
			
		||||
    private function handleMercurePublish(ServerRequestInterface $request): ResponseInterface
 | 
			
		||||
    {
 | 
			
		||||
        if ($request->getHeaderLine('content-type') !== 'application/x-www-form-urlencoded') {
 | 
			
		||||
            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) {
 | 
			
		||||
            $claims = $tok->claims->getAll();
 | 
			
		||||
            if (isset($claims['mercure']['publish'])) {
 | 
			
		||||
                $publishClaims = $claims['mercure']['publish'];
 | 
			
		||||
                // TODO check topic against publishClaims
 | 
			
		||||
                if (!$this->checkTopicClaims($data['topic']??[], $publishClaims)) {
 | 
			
		||||
                    throw new SecurityException("Insufficient permissions for publish", SecurityException::ERR_NO_PERMISSION);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // FIXME reject if access denied
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Put an id in there if none already
 | 
			
		||||
        // TODO add a configurable for this
 | 
			
		||||
        if (!isset($data['id'])) {
 | 
			
		||||
            $data['id'] = (string)Uuid::v7();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Attempt to create the message
 | 
			
		||||
        $message = Message::fromData($data);
 | 
			
		||||
 | 
			
		||||
        $this->loop->futureTick(function () use ($message) {
 | 
			
		||||
            $this->publishMercureMessage($message);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * 
 | 
			
		||||
     * @param Message $message
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    private function publishMercureMessage(Message $message): void
 | 
			
		||||
    {
 | 
			
		||||
        // foreach ($this->webSocketClients as $webSocket) {
 | 
			
		||||
        //     $webSocket->write(json_encode([
 | 
			
		||||
        //         'type' => $message->type,
 | 
			
		||||
        //         'topic' => $message->topic,
 | 
			
		||||
        //         'data' => (@json_decode($message->data))??$message->data
 | 
			
		||||
        //     ]));
 | 
			
		||||
        // }
 | 
			
		||||
 | 
			
		||||
        $sseMessage = "";
 | 
			
		||||
        if ($message->type) {
 | 
			
		||||
            $sseMessage .= "event: ".$message->type."\n";
 | 
			
		||||
        }
 | 
			
		||||
        $sseMessage .= "data: ".$message->data."\n\n";
 | 
			
		||||
 | 
			
		||||
        foreach ($this->eventClients as $client) {
 | 
			
		||||
            $client->write($sseMessage);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user