Added anonymous/private logic
This commit is contained in:
		@@ -2,12 +2,15 @@
 | 
			
		||||
 | 
			
		||||
namespace NoccyLabs\Mercureact\Broker;
 | 
			
		||||
 | 
			
		||||
use NoccyLabs\SimpleJWT\JWTToken;
 | 
			
		||||
use Psr\Http\Message\ServerRequestInterface;
 | 
			
		||||
use React\Stream\WritableStreamInterface;
 | 
			
		||||
 | 
			
		||||
class SseSubscriber implements SubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private WritableStreamInterface $stream
 | 
			
		||||
        private WritableStreamInterface $stream,
 | 
			
		||||
        private ServerRequestInterface $request,
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
@@ -17,8 +20,8 @@ class SseSubscriber implements SubscriberInterface
 | 
			
		||||
        $this->stream->write($message->toString());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function isAuthorized(string $topics): bool
 | 
			
		||||
    public function isAuthorized(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return true;
 | 
			
		||||
        return $this->request->getAttribute('authorization') instanceof JWTToken;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,5 +6,5 @@ interface SubscriberInterface
 | 
			
		||||
{
 | 
			
		||||
    public function deliver(Message $message): void;
 | 
			
		||||
 | 
			
		||||
    public function isAuthorized(string $topics): bool;
 | 
			
		||||
    public function isAuthorized(): bool;
 | 
			
		||||
}
 | 
			
		||||
@@ -29,9 +29,17 @@ class Topic
 | 
			
		||||
    public function publish(Message $message)
 | 
			
		||||
    {
 | 
			
		||||
        // TODO check if message id has already been published
 | 
			
		||||
        if (isset($this->messages[$message->id])) return;
 | 
			
		||||
 | 
			
		||||
        $this->messages[$message->id] = $message;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        foreach ($this->subscribers as $subscriber) {
 | 
			
		||||
            // Deliver to all subscribers
 | 
			
		||||
            // Skip sending private messages to unauthorized subscribers
 | 
			
		||||
            if ($message->private && !$subscriber->isAuthorized()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
            // Deliver to the subscriber
 | 
			
		||||
            $subscriber->deliver($message);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,7 @@ class TopicManager
 | 
			
		||||
    public function subscribe(SubscriberInterface $subscriber, array $topics): void
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($topics as $topic) {
 | 
			
		||||
            if ($subscriber->isAuthorized($topic))
 | 
			
		||||
                $this->getTopic($topic)->addSubscriber($subscriber);
 | 
			
		||||
            $this->getTopic($topic)->addSubscriber($subscriber);
 | 
			
		||||
        }
 | 
			
		||||
        $this->subscribers->attach($subscriber);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -94,9 +94,23 @@ class MercureHandler
 | 
			
		||||
                );
 | 
			
		||||
            [ $name, $value ] = array_map('urldecode', explode("=", $param, 2));
 | 
			
		||||
            if ($name === 'topic') $topics[] = $value;
 | 
			
		||||
            // TODO check claims for access
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 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']['subscribe'])) {
 | 
			
		||||
                $subscribeClaims = $claims['mercure']['subscribe'];
 | 
			
		||||
                // TODO check topic against publishClaims
 | 
			
		||||
                if (!$this->checkTopicClaims($topics, $subscribeClaims)) {
 | 
			
		||||
                    throw new SecurityException("Insufficient permissions for subscribe", SecurityException::ERR_NO_PERMISSION);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // TODO add option to allow/disallow anonymous acess. should still respect 
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        $this->topicManager->subscribe($subscriber, $topics);
 | 
			
		||||
        //$this->eventClients->attach($responseStream, $request);
 | 
			
		||||
        $responseStream->on('close', function () use ($subscriber, $topics) {
 | 
			
		||||
@@ -142,13 +156,17 @@ class MercureHandler
 | 
			
		||||
            $claims = $tok->claims->getAll();
 | 
			
		||||
            if (isset($claims['mercure']['publish'])) {
 | 
			
		||||
                $publishClaims = $claims['mercure']['publish'];
 | 
			
		||||
                // TODO check topic against publishClaims
 | 
			
		||||
                // 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
 | 
			
		||||
            // reject if access denied
 | 
			
		||||
            throw new SecurityException(
 | 
			
		||||
                message: "Access denied",
 | 
			
		||||
                code: SecurityException::ERR_ACCESS_DENIED
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Put an id in there if none already
 | 
			
		||||
@@ -167,17 +185,19 @@ class MercureHandler
 | 
			
		||||
        return Response::plaintext("urn:uuid:".$message->id."\n");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function checkTopicClaims(string|array $topic, array $claims, bool $all=false): bool
 | 
			
		||||
    private function checkTopicClaims(string|array $topic, array $claims): bool
 | 
			
		||||
    {
 | 
			
		||||
        // TODO match all topics if $all, reject on mismatch
 | 
			
		||||
        $matched = 0;
 | 
			
		||||
        foreach ((array)$topic as $match) {
 | 
			
		||||
            foreach ($claims as $claim) {
 | 
			
		||||
                if ($claim === "*") return true;
 | 
			
		||||
                if ($claim === $match) return true;
 | 
			
		||||
                // TODO implement full matching
 | 
			
		||||
                // TODO implement matching of URI Templates
 | 
			
		||||
                if (($claim === "*") || ($claim === $match)) {
 | 
			
		||||
                    $matched++;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
        return ($matched == count($topic));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
namespace NoccyLabs\Mercureact\Http\Middleware;
 | 
			
		||||
 | 
			
		||||
use NoccyLabs\Mercureact\Configuration;
 | 
			
		||||
use NoccyLabs\Mercureact\Http\Exeption\SecurityException;
 | 
			
		||||
use NoccyLabs\Mercureact\Http\Exception\SecurityException;
 | 
			
		||||
use Psr\Http\Message\ResponseInterface;
 | 
			
		||||
use Psr\Http\Message\ServerRequestInterface;
 | 
			
		||||
use React\Http\Message\Response;
 | 
			
		||||
@@ -68,8 +68,8 @@ class ResponseMiddleware
 | 
			
		||||
                    strlen($response->getBody())
 | 
			
		||||
                );
 | 
			
		||||
                return $response
 | 
			
		||||
                    ->withAddedHeader('Link', '<https://'.$host.'/.well-known/mercure>; rel="mercure"')
 | 
			
		||||
                    ->withAddedHeader('Link', '<wss://'.$host.'/.well-known/mercure>; rel="mercure+ws"')
 | 
			
		||||
                    // ->withAddedHeader('Link', '<https://'.$host.'/.well-known/mercure>; rel="mercure"')
 | 
			
		||||
                    // ->withAddedHeader('Link', '<wss://'.$host.'/.well-known/mercure>; rel="mercure+ws"')
 | 
			
		||||
                    ->withHeader('Access-Control-Allow-Origin', '*')
 | 
			
		||||
                    ->withHeader('Content-Security-Policy', "default-src * 'self' http: 'unsafe-eval' 'unsafe-inline'; connect-src * 'self'")
 | 
			
		||||
                    ->withHeader('Cache-Control', 'must-revalidate')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user