• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sirn-se / websocket-php / 8346487915

19 Mar 2024 04:15PM UTC coverage: 22.584% (-77.4%) from 100.0%
8346487915

push

github

sirn-se
Temp test verification

2 of 2 new or added lines in 1 file covered. (100.0%)

742 existing lines in 32 files now uncovered.

222 of 983 relevant lines covered (22.58%)

0.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Server.php
1
<?php
2

3
/**
4
 * Copyright (C) 2014-2024 Textalk and contributors.
5
 * This file is part of Websocket PHP and is free software under the ISC License.
6
 */
7

8
namespace WebSocket;
9

10
use InvalidArgumentException;
11
use Phrity\Net\{
12
    SocketServer,
13
    StreamFactory,
14
    Uri
15
};
16
use Psr\Log\{
17
    LoggerAwareInterface,
18
    LoggerInterface,
19
    NullLogger
20
};
21
use Stringable;
22
use Throwable;
23
use WebSocket\Exception\{
24
    CloseException,
25
    ConnectionLevelInterface,
26
    Exception,
27
    HandshakeException,
28
    MessageLevelInterface,
29
    ServerException
30
};
31
use WebSocket\Http\{
32
    Response,
33
    ServerRequest
34
};
35
use WebSocket\Message\Message;
36
use WebSocket\Middleware\MiddlewareInterface;
37
use WebSocket\Trait\{
38
    ListenerTrait,
39
    SendMethodsTrait,
40
    StringableTrait
41
};
42

43
/**
44
 * WebSocket\Server class.
45
 * Entry class for WebSocket server.
46
 */
47
class Server implements LoggerAwareInterface, Stringable
48
{
49
    use ListenerTrait;
50
    use SendMethodsTrait;
51
    use StringableTrait;
52

53
    private const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
54

55
    // Settings
56
    private $port;
57
    private $scheme;
58
    private $logger;
59
    private $timeout = 60;
60
    private $frameSize = 4096;
61

62
    // Internal resources
63
    private $streamFactory;
64
    private $server;
65
    private $streams;
66
    private $running = false;
67
    private $connections = [];
68
    private $middlewares = [];
69

70

71
    /* ---------- Magic methods ------------------------------------------------------------------------------------ */
72

73
    /**
74
     * @param int $port Socket port to listen to
75
     * @param string $scheme Scheme (tcp or ssl)
76
     * @throws InvalidArgumentException If invalid port provided
77
     */
78
    public function __construct(int $port = 80, bool $ssl = false)
79
    {
UNCOV
80
        if ($port < 0 || $port > 65535) {
×
UNCOV
81
            throw new InvalidArgumentException("Invalid port '{$port}' provided");
×
82
        }
UNCOV
83
        $this->port = $port;
×
UNCOV
84
        $this->scheme = $ssl ? 'ssl' : 'tcp';
×
UNCOV
85
        $this->logger = new NullLogger();
×
UNCOV
86
        $this->setStreamFactory(new StreamFactory());
×
87
    }
88

89
    /**
90
     * Get string representation of instance.
91
     * @return string String representation
92
     */
93
    public function __toString(): string
94
    {
UNCOV
95
        return $this->stringable('%s', $this->server ? "{$this->scheme}://0.0.0.0:{$this->port}" : 'closed');
×
96
    }
97

98

99
    /* ---------- Configuration ------------------------------------------------------------------------------------ */
100

101
    /**
102
     * Set stream factory to use.
103
     * @param Phrity\Net\StreamFactory $streamFactory
104
     * @return self
105
     */
106
    public function setStreamFactory(StreamFactory $streamFactory): self
107
    {
UNCOV
108
        $this->streamFactory = $streamFactory;
×
UNCOV
109
        return $this;
×
110
    }
111

112
    /**
113
     * Set logger.
114
     * @param Psr\Log\LoggerInterface $logger Logger implementation
115
     * @return self
116
     */
117
    public function setLogger(LoggerInterface $logger): self
118
    {
UNCOV
119
        $this->logger = $logger;
×
UNCOV
120
        foreach ($this->connections as $connection) {
×
UNCOV
121
            $connection->setLogger($this->logger);
×
122
        }
UNCOV
123
        return $this;
×
124
    }
125

126
    /**
127
     * Set timeout.
128
     * @param int $timeout Timeout in seconds
129
     * @return self
130
     * @throws InvalidArgumentException If invalid timeout provided
131
     */
132
    public function setTimeout(int $timeout): self
133
    {
UNCOV
134
        if ($timeout < 0) {
×
UNCOV
135
            throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided");
×
136
        }
UNCOV
137
        $this->timeout = $timeout;
×
UNCOV
138
        foreach ($this->connections as $connection) {
×
UNCOV
139
            $connection->setTimeout($timeout);
×
140
        }
UNCOV
141
        return $this;
×
142
    }
143

144
    /**
145
     * Get timeout.
146
     * @return int Timeout in seconds
147
     */
148
    public function getTimeout(): int
149
    {
UNCOV
150
        return $this->timeout;
×
151
    }
152

153
    /**
154
     * Set frame size.
155
     * @param int $frameSize Frame size in bytes
156
     * @return self
157
     * @throws InvalidArgumentException If invalid frameSize provided
158
     */
159
    public function setFrameSize(int $frameSize): self
160
    {
UNCOV
161
        if ($frameSize < 1) {
×
UNCOV
162
            throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided");
×
163
        }
UNCOV
164
        $this->frameSize = $frameSize;
×
UNCOV
165
        foreach ($this->connections as $connection) {
×
UNCOV
166
            $connection->setFrameSize($frameSize);
×
167
        }
UNCOV
168
        return $this;
×
169
    }
170

171
    /**
172
     * Get frame size.
173
     * @return int Frame size in bytes
174
     */
175
    public function getFrameSize(): int
176
    {
UNCOV
177
        return $this->frameSize;
×
178
    }
179

180
    /**
181
     * Get socket port number.
182
     * @return int port
183
     */
184
    public function getPort(): int
185
    {
UNCOV
186
        return $this->port;
×
187
    }
188

189
    /**
190
     * Get connection scheme.
191
     * @return string scheme
192
     */
193
    public function getScheme(): string
194
    {
UNCOV
195
        return $this->scheme;
×
196
    }
197

198
    /**
199
     * Get connection scheme.
200
     * @return string scheme
201
     */
202
    public function isSsl(): bool
203
    {
UNCOV
204
        return $this->scheme === 'ssl';
×
205
    }
206

207
    /**
208
     * Number of currently connected clients.
209
     * @return int Connection count
210
     */
211
    public function getConnectionCount(): int
212
    {
UNCOV
213
        return count($this->connections);
×
214
    }
215

216
    /**
217
     * Add a middleware.
218
     * @param WebSocket\Middleware\MiddlewareInterface $middleware
219
     * @return self
220
     */
221
    public function addMiddleware(MiddlewareInterface $middleware): self
222
    {
UNCOV
223
        $this->middlewares[] = $middleware;
×
UNCOV
224
        foreach ($this->connections as $connection) {
×
UNCOV
225
            $connection->addMiddleware($middleware);
×
226
        }
UNCOV
227
        return $this;
×
228
    }
229

230

231
    /* ---------- Messaging operations ----------------------------------------------------------------------------- */
232

233
    /**
234
     * Send message (broadcast to all connected clients).
235
     * @param WebSocket\Message\Message $message Message to send
236
     */
237
    public function send(Message $message): Message
238
    {
UNCOV
239
        foreach ($this->connections as $connection) {
×
UNCOV
240
            if ($connection->isWritable()) {
×
UNCOV
241
                $connection->send($message);
×
242
            }
243
        }
UNCOV
244
        return $message;
×
245
    }
246

247

248
    /* ---------- Listener operations ------------------------------------------------------------------------------ */
249

250
    /**
251
     * Start server listener.
252
     * @throws Throwable On low level error
253
     */
254
    public function start(): void
255
    {
256
        // Create socket server
UNCOV
257
        if (empty($this->server)) {
×
UNCOV
258
            $this->createSocketServer();
×
259
        }
260

261
        // Check if running
UNCOV
262
        if ($this->running) {
×
UNCOV
263
            $this->logger->warning("[server] Server is already running");
×
UNCOV
264
            return;
×
265
        }
UNCOV
266
        $this->running = true;
×
UNCOV
267
        $this->logger->info("[server] Server is running");
×
268

269
        // Run handler
UNCOV
270
        while ($this->running) {
×
271
            try {
272
                // Clear closed connections
UNCOV
273
                $this->detachUnconnected();
×
274

275
                // Get streams with readable content
UNCOV
276
                $readables = $this->streams->waitRead($this->timeout);
×
UNCOV
277
                foreach ($readables as $key => $readable) {
×
278
                    try {
UNCOV
279
                        $connection = null;
×
280
                        // Accept new client connection
UNCOV
281
                        if ($key == '@server') {
×
UNCOV
282
                            $this->acceptSocket($readable);
×
UNCOV
283
                            continue;
×
284
                        }
285
                        // Read from connection
UNCOV
286
                        $connection = $this->connections[$key];
×
UNCOV
287
                        if ($message = $connection->pullMessage()) {
×
UNCOV
288
                            $this->dispatch($message->getOpcode(), [$this, $connection, $message]);
×
289
                        }
UNCOV
290
                    } catch (MessageLevelInterface $e) {
×
291
                        // Error, but keep connection open
UNCOV
292
                        $this->logger->error("[server] {$e->getMessage()}");
×
UNCOV
293
                        $this->dispatch('error', [$this, $connection, $e]);
×
UNCOV
294
                    } catch (ConnectionLevelInterface $e) {
×
295
                        // Error, disconnect connection
UNCOV
296
                        if ($connection) {
×
UNCOV
297
                            $this->streams->detach($key);
×
UNCOV
298
                            unset($this->connections[$key]);
×
UNCOV
299
                            $connection->disconnect();
×
300
                        }
UNCOV
301
                        $this->logger->error("[server] {$e->getMessage()}");
×
UNCOV
302
                        $this->dispatch('error', [$this, $connection, $e]);
×
UNCOV
303
                    } catch (CloseException $e) {
×
304
                        // Should close
UNCOV
305
                        if ($connection) {
×
UNCOV
306
                            $connection->close($e->getCloseStatus(), $e->getMessage());
×
307
                        }
UNCOV
308
                        $this->logger->error("[server] sss {$e->getMessage()}");
×
UNCOV
309
                        $this->dispatch('error', [$this, $connection, $e]);
×
310
                    }
311
                }
UNCOV
312
                foreach ($this->connections as $connection) {
×
UNCOV
313
                    $connection->tick();
×
314
                }
UNCOV
315
                $this->dispatch('tick', [$this]);
×
UNCOV
316
            } catch (Exception $e) {
×
317
                // Low-level error
UNCOV
318
                $this->logger->error("[server] {$e->getMessage()}");
×
UNCOV
319
                $this->dispatch('error', [$this, null, $e]);
×
UNCOV
320
            } catch (Throwable $e) {
×
321
                // Crash it
UNCOV
322
                $this->logger->error("[server] {$e->getMessage()}");
×
UNCOV
323
                $this->dispatch('error', [$this, null, $e]);
×
UNCOV
324
                $this->disconnect();
×
UNCOV
325
                throw $e;
×
326
            }
UNCOV
327
            gc_collect_cycles(); // Collect garbage
×
328
        }
329
    }
330

331
    /**
332
     * Stop server listener (resumable).
333
     */
334
    public function stop(): void
335
    {
UNCOV
336
        $this->running = false;
×
UNCOV
337
        $this->logger->info("[server] Server is stopped");
×
338
    }
339

340
    /**
341
     * If server is running (accepting connections and messages).
342
     * @return bool
343
     */
344
    public function isRunning(): bool
345
    {
UNCOV
346
        return $this->running;
×
347
    }
348

349

350
    /* ---------- Connection management ---------------------------------------------------------------------------- */
351

352
    /**
353
     * Disconnect all connections and stop server.
354
     */
355
    public function disconnect(): void
356
    {
UNCOV
357
        $this->running = false;
×
UNCOV
358
        foreach ($this->connections as $connection) {
×
UNCOV
359
            $connection->disconnect();
×
UNCOV
360
            $this->dispatch('disconnect', [$this, $connection]);
×
361
        }
UNCOV
362
        $this->connections = [];
×
UNCOV
363
        $this->server->close();
×
UNCOV
364
        $this->server = $this->streams = null;
×
UNCOV
365
        $this->logger->info('[server] Server disconnected');
×
366
    }
367

368

369
    /* ---------- Internal helper methods -------------------------------------------------------------------------- */
370

371
    // Create socket server
372
    protected function createSocketServer(): void
373
    {
374
        try {
UNCOV
375
            $uri = new Uri("{$this->scheme}://0.0.0.0:{$this->port}");
×
UNCOV
376
            $this->server = $this->streamFactory->createSocketServer($uri);
×
UNCOV
377
            $this->streams = $this->streamFactory->createStreamCollection();
×
UNCOV
378
            $this->streams->attach($this->server, '@server');
×
UNCOV
379
            $this->logger->info("[server] Starting server on {$uri}.");
×
UNCOV
380
        } catch (Throwable $e) {
×
UNCOV
381
            $error = "Server failed to start: {$e->getMessage()}";
×
UNCOV
382
            throw new ServerException($error);
×
383
        }
384
    }
385

386
    // Accept connection on socket server
387
    protected function acceptSocket(SocketServer $socket): void
388
    {
389
        try {
UNCOV
390
            $stream = $socket->accept();
×
UNCOV
391
            $name = $stream->getRemoteName();
×
UNCOV
392
            $this->streams->attach($stream, $name);
×
UNCOV
393
            $connection = new Connection($stream, false, true, $this->isSsl());
×
UNCOV
394
            $connection
×
UNCOV
395
                ->setLogger($this->logger)
×
UNCOV
396
                ->setFrameSize($this->frameSize)
×
UNCOV
397
                ->setTimeout($this->timeout)
×
UNCOV
398
                ;
×
UNCOV
399
            foreach ($this->middlewares as $middleware) {
×
UNCOV
400
                $connection->addMiddleware($middleware);
×
401
            }
UNCOV
402
            $request = $this->performHandshake($connection);
×
UNCOV
403
            $this->connections[$name] = $connection;
×
UNCOV
404
            $this->logger->info("[server] Accepted connection from {$name}.");
×
UNCOV
405
            $this->dispatch('connect', [$this, $connection, $request]);
×
UNCOV
406
        } catch (Exception $e) {
×
UNCOV
407
            if ($connection) {
×
UNCOV
408
                $connection->disconnect();
×
409
            }
UNCOV
410
            $error = "Server failed to accept: {$e->getMessage()}";
×
UNCOV
411
            throw $e;
×
412
        }
413
    }
414

415
    // Detach connections no longer available
416
    protected function detachUnconnected(): void
417
    {
UNCOV
418
        foreach ($this->connections as $key => $connection) {
×
UNCOV
419
            if (!$connection->isConnected()) {
×
UNCOV
420
                $this->streams->detach($key);
×
UNCOV
421
                unset($this->connections[$key]);
×
UNCOV
422
                $this->logger->info("[server] Disconnected {$key}.");
×
UNCOV
423
                $this->dispatch('disconnect', [$this, $connection]);
×
424
            }
425
        }
426
    }
427

428
    // Perform upgrade handshake on new connections.
429
    protected function performHandshake(Connection $connection): ServerRequest
430
    {
UNCOV
431
        $response = new Response(101);
×
UNCOV
432
        $exception = null;
×
433

434
        // Read handshake request
UNCOV
435
        $request = $connection->pullHttp();
×
436

437
        // Verify handshake request
438
        try {
UNCOV
439
            if ($request->getMethod() != 'GET') {
×
UNCOV
440
                throw new HandshakeException(
×
UNCOV
441
                    "Handshake request with invalid method: '{$request->getMethod()}'",
×
UNCOV
442
                    $response->withStatus(405)
×
UNCOV
443
                );
×
444
            }
UNCOV
445
            $connectionHeader = trim($request->getHeaderLine('Connection'));
×
UNCOV
446
            if (strtolower($connectionHeader) != 'upgrade') {
×
UNCOV
447
                throw new HandshakeException(
×
UNCOV
448
                    "Handshake request with invalid Connection header: '{$connectionHeader}'",
×
UNCOV
449
                    $response->withStatus(426)
×
UNCOV
450
                );
×
451
            }
UNCOV
452
            $upgradeHeader = trim($request->getHeaderLine('Upgrade'));
×
UNCOV
453
            if (strtolower($upgradeHeader) != 'websocket') {
×
UNCOV
454
                throw new HandshakeException(
×
UNCOV
455
                    "Handshake request with invalid Upgrade header: '{$upgradeHeader}'",
×
UNCOV
456
                    $response->withStatus(426)
×
UNCOV
457
                );
×
458
            }
UNCOV
459
            $versionHeader = trim($request->getHeaderLine('Sec-WebSocket-Version'));
×
UNCOV
460
            if ($versionHeader != '13') {
×
UNCOV
461
                throw new HandshakeException(
×
UNCOV
462
                    "Handshake request with invalid Sec-WebSocket-Version header: '{$versionHeader}'",
×
UNCOV
463
                    $response->withStatus(426)->withHeader('Sec-WebSocket-Version', '13')
×
UNCOV
464
                );
×
465
            }
UNCOV
466
            $keyHeader = trim($request->getHeaderLine('Sec-WebSocket-Key'));
×
UNCOV
467
            if (empty($keyHeader)) {
×
UNCOV
468
                throw new HandshakeException(
×
UNCOV
469
                    "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'",
×
UNCOV
470
                    $response->withStatus(426)
×
UNCOV
471
                );
×
472
            }
UNCOV
473
            if (strlen(base64_decode($keyHeader)) != 16) {
×
UNCOV
474
                throw new HandshakeException(
×
UNCOV
475
                    "Handshake request with invalid Sec-WebSocket-Key header: '{$keyHeader}'",
×
UNCOV
476
                    $response->withStatus(426)
×
UNCOV
477
                );
×
478
            }
479

UNCOV
480
            $responseKey = base64_encode(pack('H*', sha1($keyHeader . self::GUID)));
×
UNCOV
481
            $response = $response
×
UNCOV
482
                ->withHeader('Upgrade', 'websocket')
×
UNCOV
483
                ->withHeader('Connection', 'Upgrade')
×
UNCOV
484
                ->withHeader('Sec-WebSocket-Accept', $responseKey);
×
UNCOV
485
        } catch (HandshakeException $e) {
×
UNCOV
486
            $this->logger->warning("[server] {$e->getMessage()}");
×
UNCOV
487
            $response = $e->getResponse();
×
UNCOV
488
            $exception = $e;
×
489
        }
490

491
        // Respond to handshake
UNCOV
492
        $response = $connection->pushHttp($response);
×
UNCOV
493
        if ($response->getStatusCode() != 101) {
×
UNCOV
494
            $exception = new HandshakeException("Invalid status code {$response->getStatusCode()}", $response);
×
495
        }
496

UNCOV
497
        if ($exception) {
×
UNCOV
498
            throw $exception;
×
499
        }
500

UNCOV
501
        $this->logger->debug("[server] Handshake on {$request->getUri()->getPath()}");
×
UNCOV
502
        $connection->setHandshakeRequest($request);
×
UNCOV
503
        $connection->setHandshakeResponse($response);
×
504

UNCOV
505
        return $request;
×
506
    }
507
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc