Completed
Pull Request — master (#145)
by
unknown
03:39 queued 19s
created

WebSocketServer::removeConnection()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.024

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 6
cts 10
cp 0.6
rs 9.7666
c 0
b 0
f 0
cc 4
nc 5
nop 2
crap 5.024
1
<?php
2
3
/**
4
 * This file is a part of Woketo package.
5
 *
6
 * (c) Nekland <[email protected]>
7
 *
8
 * For the full license, take a look to the LICENSE file
9
 * on the root directory of this project
10
 */
11
12
namespace Nekland\Woketo\Server;
13
14
use Nekland\Woketo\Exception\ConfigException;
15
use Nekland\Woketo\Exception\RuntimeException;
16
use Nekland\Woketo\Message\MessageHandlerInterface;
17
use Nekland\Woketo\Rfc6455\FrameFactory;
18
use Nekland\Woketo\Rfc6455\Handshake\ServerHandshake;
19
use Nekland\Woketo\Rfc6455\MessageFactory;
20
use Nekland\Woketo\Rfc6455\FrameHandler\CloseFrameHandler;
21
use Nekland\Woketo\Rfc6455\FrameHandler\RsvCheckFrameHandler;
22
use Nekland\Woketo\Rfc6455\FrameHandler\WrongOpcodeFrameHandler;
23
use Nekland\Woketo\Rfc6455\FrameHandler\PingFrameHandler;
24
use Nekland\Woketo\Rfc6455\MessageProcessor;
25
use Nekland\Woketo\Utils\SimpleLogger;
26
use Psr\Log\LoggerInterface;
27
use Psr\Log\LogLevel;
28
use React\EventLoop\LoopInterface;
29
use React\Socket\ConnectionInterface;
30
use React\Socket\ServerInterface;
31
32
class WebSocketServer
33
{
34
    /**
35
     * @var int
36
     */
37
    private $port;
38
39
    /**
40
     * @var string
41
     */
42
    private $host;
43
44
    /**
45
     * @var ServerHandshake
46
     */
47
    private $handshake;
48
49
    /**
50
     * @var MessageHandlerInterface[]
51
     */
52
    private $messageHandlers;
53
54
    /**
55
     * @var Connection[]
56
     */
57
    private $connections;
58
59
    /**
60
     * @var LoopInterface
61
     */
62
    private $loop;
63
64
    /**
65
     * @var ServerInterface
66
     */
67
    private $server;
68
69
    /**
70
     * @var MessageProcessor
71
     */
72
    private $messageProcessor;
73
74
    /**
75
     * @var array
76
     */
77
    private $config;
78
79
    /**
80
     * @var LoggerInterface
81
     */
82
    private $logger;
83
84
    /**
85
     * @param int    $port   The number of the port to bind
86
     * @param string $host   The host to listen on (by default 127.0.0.1)
87
     * @param array  $config
88
     */
89 9
    public function __construct($port, $host = '127.0.0.1', $config = [])
90
    {
91 9
        $this->setConfig($config);
92 8
        $this->host = $host;
93 8
        $this->port = $port;
94 8
        $this->handshake = new ServerHandshake();
95 8
        $this->connections = [];
96 8
        $this->buildMessageProcessor();
97
98
        // Some optimization
99 7
        \gc_enable();       // As the process never stops, the garbage collector will be usefull, you may need to call it manually sometimes for performance purpose
100 7
        \set_time_limit(0); // It's by default on most server for cli apps but better be sure of that fact
101 7
    }
102
103
    /**
104
     * @param MessageHandlerInterface|string $messageHandler An instance of a class as string
105
     * @param string                         $uri            The URI you want to bind on
106
     */
107 5
    public function setMessageHandler($messageHandler, $uri = '*')
108
    {
109 5
        if (!$messageHandler instanceof MessageHandlerInterface &&  !\is_string($messageHandler)) {
110
            throw new \InvalidArgumentException('The message handler must be an instance of MessageHandlerInterface or a string.');
111
        }
112 5
        if (\is_string($messageHandler)) {
113
            try {
114
                $reflection = new \ReflectionClass($messageHandler);
115
                if(!$reflection->implementsInterface('Nekland\Woketo\Message\MessageHandlerInterface')) {
116
                    throw new \InvalidArgumentException('The messageHandler must implement MessageHandlerInterface');
117
                }
118
            } catch (\ReflectionException $e) {
119
                throw new \InvalidArgumentException('The messageHandler must be a string representing a class.');
120
            }
121
        }
122 5
        $this->messageHandlers[$uri] = $messageHandler;
123 5
    }
124
125
    /**
126
     * Launch the WebSocket server and an infinite loop that act on event.
127
     *
128
     * @throws \Exception
129
     */
130 5
    public function start()
131
    {
132 5
        if ($this->config['prod'] && \extension_loaded('xdebug')) {
133
            throw new \Exception('xdebug is enabled, it\'s a performance issue. Disable that extension or specify "prod" option to false.');
134
        }
135
136 5
        $this->loop = $this->loop ?? \React\EventLoop\Factory::create();
137 5
        $this->server = $this->server ?? new \React\Socket\TcpServer($this->host . ':' . $this->port, $this->loop);
138
139 5
        if ($this->config['ssl']) {
140
            $this->server = new \React\Socket\SecureServer($this->server, $this->loop, array_merge([
141
                'local_cert' => $this->config['certFile'],
142
                'passphrase' => $this->config['passphrase'],
143
            ], $this->config['sslContextOptions']));
144
            $this->getLogger()->info('Enabled ssl');
145
        }
146
147 5
        $this->server->on('connection', function (ConnectionInterface $socketStream) {
148 5
            $this->onNewConnection($socketStream);
149 5
        });
150
151 5
        $this->getLogger()->info('Listening on ' . $this->host . ':' . $this->port);
152
153 5
        $this->loop->run();
154 5
    }
155
156
    /**
157
     * @param ConnectionInterface $socketStream
158
     */
159
    private function onNewConnection(ConnectionInterface $socketStream)
160
    {
161 5
        $connection = new Connection($socketStream, function ($uri, Connection $connection) {
162 5
            return $this->getMessageHandler($uri, $connection);
163 5
        }, $this->loop, $this->messageProcessor);
164
165 5
        $socketStream->on('end', function () use($connection) {
166 1
            $this->onDisconnect($connection);
167 5
        });
168
169 5
        $connection->setLogger($this->getLogger());
170 5
        $connection->getLogger()->info(sprintf('Ip "%s" establish connection', $connection->getIp()));
171 5
        $this->connections[] = $connection;
172 5
    }
173
174
    /**
175
     *
176
     * @param Connection $connection
177
     */
178 1
    private function onDisconnect(Connection $connection)
179
    {
180 1
        $this->removeConnection($connection);
181 1
        $connection->getLogger()->info(sprintf('Ip "%s" left connection', $connection->getIp()));
182 1
    }
183
184
    /**
185
     * Remove a Connection instance by his object id
186
     * @param Connection        $connection
187
     * @param bool              $strict Define if the process should stop if unfoundable
188
     * @throws RuntimeException This method throw an exception if the $connection instance object isn't findable in websocket server's connections
189
     */
190 1
    private function removeConnection(Connection $connection, bool $strict = true)
191
    {
192 1
        $connectionId = spl_object_hash($connection);
193 1
        foreach ($this->connections as $index => $connectionIt) {
194 1
            if ($connectionId === spl_object_hash($connectionIt)) {
195 1
                unset($this->connections[$index]);
196 1
                return;
197
            }
198
        }
199
200
        $this->logger->critical('No connection found in the server connection list, impossible to delete the given connection id. Something wrong happened');
201
        if ($strict) {
202
            throw new RuntimeException('No connection found in the server connection list, impossible to delete the given connection id. Something wrong happened');
203
        }
204
    }
205
206
    /**
207
     * @param string $uri
208
     * @param Connection $connection
209
     * @return MessageHandlerInterface|null
210
     */
211 5
    private function getMessageHandler(string $uri, Connection $connection)
0 ignored issues
show
Unused Code introduced by
The parameter $connection is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
212
    {
213 5
        $handler = null;
214
215 5
        if (!empty($this->messageHandlers[$uri])) {
216 2
            $handler = $this->messageHandlers[$uri];
217
        }
218
219 5
        if (null === $handler && !empty($this->messageHandlers['*'])) {
220 2
            $handler = $this->messageHandlers['*'];
221
        }
222
223 5
        if (null !== $handler) {
224 4
            if (\is_string($handler)) {
225
                $handler = new $handler;
226
            }
227
228 4
            return $handler;
229
        }
230
231 1
        $this->logger->warning('Connection on ' . $uri . ' but no handler found.');
232 1
        return null;
233
    }
234
235
    /**
236
     * Build the message processor with configuration
237
     */
238 8
    private function buildMessageProcessor()
239
    {
240 8
        $this->messageProcessor = new MessageProcessor(
241 8
            false,
242 8
            new FrameFactory($this->config['frame']),
243 8
            new MessageFactory($this->config['message'])
244
        );
245 8
        $this->messageProcessor->addHandler(new PingFrameHandler());
246 8
        $this->messageProcessor->addHandler(new CloseFrameHandler());
247 8
        $this->messageProcessor->addHandler(new WrongOpcodeFrameHandler());
248 8
        $this->messageProcessor->addHandler(new RsvCheckFrameHandler());
249
250 8
        foreach ($this->config['messageHandlers'] as $handler) {
251 1
            if (!$handler instanceof MessageHandlerInterface) {
252 1
                throw new RuntimeException(sprintf('%s is not an instance of MessageHandlerInterface but must be !', get_class($handler)));
253
            }
254
        }
255 7
    }
256
257
    /**
258
     * Sets the configuration
259
     *
260
     * @param array $config
261
     * @throws ConfigException
262
     */
263 9
    private function setConfig(array $config)
264
    {
265 9
        $this->config = \array_merge([
266 9
            'frame' => [],
267
            'message' => [],
268
            'messageHandlers' => [],
269
            'prod' => true,
270
            'ssl' => false,
271
            'certFile' => '',
272
            'passphrase' => '',
273
            'sslContextOptions' => [],
274 9
        ], $config);
275
276 9
        if ($this->config['ssl'] && !is_file($this->config['certFile'])) {
277 1
            throw new ConfigException('With ssl configuration, you need to specify a certificate file.');
278
        }
279 8
    }
280
281
    /**
282
     * @return SimpleLogger|LoggerInterface
283
     */
284 5
    public function getLogger()
285
    {
286 5
        if (null === $this->logger) {
287 3
            return $this->logger = new SimpleLogger(!$this->config['prod']);
288
        }
289
290 5
        return $this->logger;
291
    }
292
293
    /**
294
     * Allows you to set a custom logger
295
     *
296
     * @param LoggerInterface $logger
297
     * @return WebSocketServer
298
     */
299 2
    public function setLogger(LoggerInterface $logger)
300
    {
301 2
        $this->logger = $logger;
302
303 2
        return $this;
304
    }
305
306
    /**
307
     * Allows to specify a loop that will be used instead of the reactphp generated loop.
308
     *
309
     * @param LoopInterface $loop
310
     * @return WebSocketServer
311
     */
312 5
    public function setLoop(LoopInterface $loop)
313
    {
314 5
        $this->loop = $loop;
315
316 5
        return $this;
317
    }
318
319
    /**
320
     * @param ServerInterface $server
321
     * @return WebSocketServer
322
     */
323 5
    public function setSocketServer(ServerInterface $server)
324
    {
325 5
        $this->server = $server;
326
327 5
        return $this;
328
    }
329
}
330