Passed
Pull Request — master (#225)
by Anton
02:23
created

WebsocketsMiddleware::process()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 28
rs 8.8333
cc 7
nc 7
nop 2
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Broadcast\Middleware;
13
14
use Psr\Http\Message\ResponseFactoryInterface;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
use Psr\Http\Server\MiddlewareInterface;
18
use Psr\Http\Server\RequestHandlerInterface;
19
use Spiral\Broadcast\Config\WebsocketsConfig;
20
use Spiral\Core\Exception\LogicException;
21
use Spiral\Core\ResolverInterface;
22
use Spiral\Core\ScopeInterface;
23
use Spiral\Http\Exception\ClientException;
24
25
/**
26
 * Authorizes websocket connections to server and topics.
27
 */
28
final class WebsocketsMiddleware implements MiddlewareInterface
29
{
30
    /** @var WebsocketsConfig */
31
    private $config;
32
33
    /** @var ScopeInterface */
34
    private $scope;
35
36
    /** @var ResolverInterface */
37
    private $resolver;
38
39
    /** @var ResponseFactoryInterface */
40
    private $responseFactory;
41
42
    /**
43
     * @param WebsocketsConfig         $config
44
     * @param ScopeInterface           $scope
45
     * @param ResolverInterface        $resolver
46
     * @param ResponseFactoryInterface $responseFactory
47
     */
48
    public function __construct(
49
        WebsocketsConfig $config,
50
        ScopeInterface $scope,
51
        ResolverInterface $resolver,
52
        ResponseFactoryInterface $responseFactory
53
    ) {
54
        $this->config = $config;
55
        $this->scope = $scope;
56
        $this->resolver = $resolver;
57
        $this->responseFactory = $responseFactory;
58
    }
59
60
    /**
61
     * @param ServerRequestInterface  $request
62
     * @param RequestHandlerInterface $handler
63
     * @return ResponseInterface
64
     *
65
     * @throws ClientException
66
     * @throws \Throwable
67
     */
68
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
69
    {
70
        if ($request->getUri()->getPath() !== $this->config->getPath()) {
71
            return $handler->handle($request);
72
        }
73
74
        // server authorization
75
        if ($request->getAttribute('ws:joinServer', null) !== null) {
76
            if (!$this->authorizeServer($request)) {
77
                return $this->responseFactory->createResponse(403);
78
            }
79
80
            return $this->responseFactory->createResponse(200);
81
        }
82
83
        // topic authorization
84
        if (is_string($request->getAttribute('ws:joinTopics', null))) {
85
            $topics = explode(',', $request->getAttribute('ws:joinTopics'));
86
            foreach ($topics as $topic) {
87
                if (!$this->authorizeTopic($request, $topic)) {
88
                    return $this->responseFactory->createResponse(403);
89
                }
90
            }
91
92
            return $this->responseFactory->createResponse(200);
93
        }
94
95
        return $this->responseFactory->createResponse(403);
96
    }
97
98
    /**
99
     * @param ServerRequestInterface $request
100
     * @return bool
101
     *
102
     * @throws \Throwable
103
     */
104
    private function authorizeServer(ServerRequestInterface $request): bool
105
    {
106
        $callback = $this->config->getServerCallback();
107
        if ($callback === null) {
108
            return true;
109
        }
110
111
        return $this->invoke($request, $callback, []);
112
    }
113
114
    /**
115
     * @param ServerRequestInterface $request
116
     * @param string                 $topic
117
     * @return bool
118
     *
119
     * @throws \Throwable
120
     */
121
    private function authorizeTopic(ServerRequestInterface $request, string $topic): bool
122
    {
123
        $parameters = [];
124
        $callback = $this->config->findTopicCallback($topic, $parameters);
125
        if ($callback === null) {
126
            return false;
127
        }
128
129
        return $this->invoke($request, $callback, $parameters + ['topic' => $topic]);
130
    }
131
132
    /**
133
     * @param ServerRequestInterface $request
134
     * @param callable               $callback
135
     * @param array                  $parameters
136
     * @return bool
137
     *
138
     * @throws \Throwable
139
     */
140
    private function invoke(ServerRequestInterface $request, callable $callback, array $parameters = []): bool
141
    {
142
        /** @var \ReflectionFunctionAbstract $reflection */
143
        $reflection = null;
144
        switch (true) {
145
            case $callback instanceof \Closure || is_string($callback):
146
                $reflection = new \ReflectionFunction($callback);
147
                break;
148
            case is_array($callback) && is_object($callback[0]):
149
                $reflection = (new \ReflectionObject($callback[0]))->getMethod($callback[1]);
150
                break;
151
            case is_array($callback):
152
                $reflection = (new \ReflectionClass($callback[0]))->getMethod($callback[1]);
153
                break;
154
            default:
155
                throw new LogicException('Unable to invoke callable function');
156
        }
157
158
        return $this->scope->runScope(
159
            [
160
                ServerRequestInterface::class => $request
161
            ],
162
            function () use ($reflection, $parameters, $callback) {
163
                return call_user_func_array(
164
                    $callback,
165
                    $this->resolver->resolveArguments($reflection, $parameters)
166
                );
167
            }
168
        );
169
    }
170
}
171