Connection::close()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace WSSC\Components;
4
5
use WSSC\Contracts\CommonsContract;
6
use WSSC\Contracts\ConnectionContract;
7
use WSSC\Contracts\WebSocketServerContract;
8
9
/**
10
 * Class Connection
11
 * @package WSSC\Components
12
 */
13
class Connection implements ConnectionContract, CommonsContract
14
{
15
    /**
16
     * @var false|resource
17
     */
18
    private $socketConnection;
19
20
    /**
21
     * @var array
22
     */
23
    private array $clients;
24
25
    /**
26
     * Connection constructor.
27
     *
28
     * @param $sockConn
29
     * @param array $clients
30
     */
31
    public function __construct($sockConn, array $clients = [])
32
    {
33
        $this->socketConnection = $sockConn;
34
        $this->clients = $clients;
35
    }
36
37
    /**
38
     * Closes clients socket stream
39
     *
40
     * @throws \Exception
41
     */
42
    public function close(): void
43
    {
44
        if (is_resource($this->socketConnection)) {
45
            fwrite($this->socketConnection, $this->encode('', self::EVENT_TYPE_CLOSE));
0 ignored issues
show
Bug introduced by
It seems like $this->encode('', self::EVENT_TYPE_CLOSE) can also be of type array<string,string>; however, parameter $data of fwrite() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

45
            fwrite($this->socketConnection, /** @scrutinizer ignore-type */ $this->encode('', self::EVENT_TYPE_CLOSE));
Loading history...
46
            fclose($this->socketConnection);
47
        }
48
    }
49
50
    /**
51
     * This method is invoked when user implementation call $conn->send($data)
52
     * writes data to the clients stream socket
53
     *
54
     * @param string $data pure decoded data from server
55
     * @throws \Exception
56
     */
57
    public function send(string $data): void
58
    {
59
        fwrite($this->socketConnection, $this->encode($data));
0 ignored issues
show
Bug introduced by
It seems like $this->encode($data) can also be of type array<string,string>; however, parameter $data of fwrite() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
        fwrite($this->socketConnection, /** @scrutinizer ignore-type */ $this->encode($data));
Loading history...
Bug introduced by
It seems like $this->socketConnection can also be of type boolean; however, parameter $stream of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

59
        fwrite(/** @scrutinizer ignore-type */ $this->socketConnection, $this->encode($data));
Loading history...
60
    }
61
62
    /**
63
     * @param string $data data to send to clients
64
     * @throws \Exception
65
     */
66
    public function broadCast(string $data): void
67
    {
68
        foreach ($this->clients as $client) {
69
            if (is_resource($client)) { // check if not yet closed/broken etc
70
                fwrite($client, $this->encode($data));
0 ignored issues
show
Bug introduced by
It seems like $this->encode($data) can also be of type array<string,string>; however, parameter $data of fwrite() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

70
                fwrite($client, /** @scrutinizer ignore-type */ $this->encode($data));
Loading history...
71
            }
72
        }
73
    }
74
75
    /**
76
     * Broadcasting many messages with delay
77
     *
78
     * @param array $data An array of messages (strings) sent to many clients
79
     * @param int $delay Time in seconds to delay between messages
80
     * @throws \Exception
81
     */
82
    public function broadCastMany(array $data, int $delay = 0): void
83
    {
84
        foreach ($data as $message) {
85
            foreach ($this->clients as $client) {
86
                if (is_resource($client)) { // check if not yet closed/broken etc
87
                    fwrite($client, $this->encode($message));
0 ignored issues
show
Bug introduced by
It seems like $this->encode($message) can also be of type array<string,mixed|string>; however, parameter $data of fwrite() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

87
                    fwrite($client, /** @scrutinizer ignore-type */ $this->encode($message));
Loading history...
88
                }
89
            }
90
91
            if ($delay > 0) {
92
                sleep($delay);
93
            }
94
        }
95
    }
96
97
    /**
98
     * Encodes data before writing to the client socket stream
99
     *
100
     * @param string $payload
101
     * @param string $type
102
     * @param boolean $masked
103
     * @return mixed
104
     * @throws \Exception
105
     */
106
    private function encode($payload, string $type = self::EVENT_TYPE_TEXT, bool $masked = false)
107
    {
108
        $frameHead = $this->getOpType($type);
109
        $payloadLength = strlen($payload);
110
111
        // set mask and payload length (using 1, 3 or 9 bytes)
112
        if ($payloadLength > self::PAYLOAD_MAX_BITS) {
113
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), self::PAYLOAD_CHUNK);
114
            $frameHead[1] = ($masked === true) ? self::MASK_255 : self::MASK_127;
115
116
            for ($i = 0; $i < 8; $i++) {
117
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
118
            }
119
120
            // most significant bit MUST be 0
121
            if ($frameHead[2] > self::MASK_127) {
122
                return [
123
                    'type' => $type,
124
                    'payload' => $payload,
125
                    'error' => WebSocketServerContract::ERR_FRAME_TOO_LARGE,
126
                ];
127
            }
128
        } elseif ($payloadLength > self::MASK_125) {
129
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), self::PAYLOAD_CHUNK);
130
            $frameHead[1] = ($masked === true) ? self::MASK_254 : self::MASK_126;
131
            $frameHead[2] = bindec($payloadLengthBin[0]);
132
            $frameHead[3] = bindec($payloadLengthBin[1]);
133
        } else {
134
            $frameHead[1] = ($masked === true) ? $payloadLength + self::MASK_128 : $payloadLength;
135
        }
136
137
        return $this->getComposedFrame($frameHead, $payload, $payloadLength, $masked);
138
    }
139
140
    /**
141
     * Gets frame-head based on type of operation
142
     *
143
     * @param string $type Types of operation encode-frames
144
     * @return array
145
     */
146
    private function getOpType(string $type): array
147
    {
148
        $frameHead = [];
149
150
        switch ($type) {
151
            case self::EVENT_TYPE_TEXT:
152
                // first byte indicates FIN, Text-Frame (10000001):
153
                $frameHead[0] = self::ENCODE_TEXT;
154
                break;
155
156
            case self::EVENT_TYPE_CLOSE:
157
                // first byte indicates FIN, Close Frame(10001000):
158
                $frameHead[0] = self::ENCODE_CLOSE;
159
                break;
160
161
            case self::EVENT_TYPE_PING:
162
                // first byte indicates FIN, Ping frame (10001001):
163
                $frameHead[0] = self::ENCODE_PING;
164
                break;
165
166
            case self::EVENT_TYPE_PONG:
167
                // first byte indicates FIN, Pong frame (10001010):
168
                $frameHead[0] = self::ENCODE_PONG;
169
                break;
170
        }
171
172
        return $frameHead;
173
    }
174
175
    /**
176
     * @param array $frameHead
177
     * @param string $payload
178
     * @param int $payloadLength
179
     * @param bool $masked
180
     * @return string
181
     * @throws \Exception
182
     */
183
    private function getComposedFrame(array $frameHead, string $payload, int $payloadLength, bool $masked): string
184
    {
185
        // convert frame-head to string:
186
        foreach (array_keys($frameHead) as $i) {
187
            $frameHead[$i] = chr($frameHead[$i]);
188
        }
189
190
        // generate a random mask:
191
        $mask = [];
192
        if ($masked === true) {
193
            for ($i = 0; $i < 4; $i++) {
194
                $mask[$i] = chr(random_int(0, self::MASK_255));
195
            }
196
197
            $frameHead = array_merge($frameHead, $mask);
198
        }
199
        $frame = implode('', $frameHead);
200
201
        // append payload to frame:
202
        for ($i = 0; $i < $payloadLength; $i++) {
203
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
204
        }
205
206
        return $frame;
207
    }
208
209
    /**
210
     * Gets unique socket id from resource
211
     *
212
     * @return int
213
     */
214
    public function getUniqueSocketId(): int
215
    {
216
        return (int)$this->socketConnection;
217
    }
218
219
    /**
220
     *  Gets client socket address host/port or UNIX path
221
     *
222
     * @return string
223
     */
224
    public function getPeerName(): string
225
    {
226
        return stream_socket_get_name($this->socketConnection, true);
0 ignored issues
show
Bug introduced by
It seems like $this->socketConnection can also be of type boolean; however, parameter $socket of stream_socket_get_name() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        return stream_socket_get_name(/** @scrutinizer ignore-type */ $this->socketConnection, true);
Loading history...
227
    }
228
}
229