Issues (46)

src/Components/WSClientTrait.php (21 issues)

1
<?php
2
3
namespace WSSC\Components;
4
5
use WSSC\Contracts\CommonsContract;
6
use WSSC\Exceptions\BadOpcodeException;
7
use WSSC\Exceptions\BadUriException;
8
use WSSC\Exceptions\ConnectionException;
9
10
trait WSClientTrait
11
{
12
    /**
13
     * Validates whether server sent valid upgrade response
14
     *
15
     * @param ClientConfig $config
16
     * @param string $pathWithQuery
17
     * @param string $key
18
     * @throws ConnectionException
19
     */
20
    private function validateResponse(ClientConfig $config, string $pathWithQuery, string $key)
21
    {
22
        $response = stream_get_line($this->socket, self::DEFAULT_RESPONSE_HEADER, "\r\n\r\n");
0 ignored issues
show
The constant WSSC\Components\WSClient...DEFAULT_RESPONSE_HEADER was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
23
        if (!preg_match(self::SEC_WEBSOCKET_ACCEPT_PTTRN, $response, $matches)) {
0 ignored issues
show
The constant WSSC\Components\WSClient..._WEBSOCKET_ACCEPT_PTTRN was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
24
            $address = $config->getScheme() . '://' . $config->getHost() . ':' . $config->getPort() . $pathWithQuery;
25
            throw new ConnectionException(
26
                "Connection to '{$address}' failed: Server sent invalid upgrade response:\n"
27
                . $response, CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE
28
            );
29
        }
30
31
        $keyAccept = trim($matches[1]);
32
        $expectedResponse = base64_encode(pack('H*', sha1($key . self::SERVER_KEY_ACCEPT)));
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::SERVER_KEY_ACCEPT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
33
        if ($keyAccept !== $expectedResponse) {
34
            throw new ConnectionException('Server sent bad upgrade response.',
35
                CommonsContract::CLIENT_INVALID_UPGRADE_RESPONSE);
36
        }
37
    }
38
39
    /**
40
     *  Gets host uri based on protocol
41
     *
42
     * @param ClientConfig $config
43
     * @return string
44
     * @throws BadUriException
45
     */
46
    private function getHostUri(ClientConfig $config): string
47
    {
48
        if (in_array($config->getScheme(), ['ws', 'wss'], true) === false) {
49
            throw new BadUriException(
50
                "Url should have scheme ws or wss, not '{$config->getScheme()}' from URI '$this->socketUrl' .",
51
                CommonsContract::CLIENT_INCORRECT_SCHEME
52
            );
53
        }
54
55
        return ($config->getScheme() === 'wss' ? 'ssl' : 'tcp') . '://' . $config->getHost();
56
    }
57
58
    /**
59
     * @param string $data
60
     * @return float|int
61
     * @throws ConnectionException
62
     */
63
    private function getPayloadLength(string $data)
64
    {
65
        $payloadLength = (int)ord($data[1]) & self::MASK_127; // Bits 1-7 in byte 1
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_127 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
66
        if ($payloadLength > self::MASK_125) {
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_125 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
67
            if ($payloadLength === self::MASK_126) {
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_126 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
68
                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
0 ignored issues
show
It seems like read() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

68
                /** @scrutinizer ignore-call */ 
69
                $data = $this->read(2); // 126: Payload is a 16-bit unsigned int
Loading history...
69
            } else {
70
                $data = $this->read(8); // 127: Payload is a 64-bit unsigned int
71
            }
72
            $payloadLength = bindec(self::sprintB($data));
73
        }
74
75
        return $payloadLength;
76
    }
77
78
    /**
79
     * @param string $data
80
     * @param int $payloadLength
81
     * @return string
82
     * @throws ConnectionException
83
     */
84
    private function getPayloadData(string $data, int $payloadLength): string
85
    {
86
        // Masking?
87
        $mask = (bool)(ord($data[1]) >> 7);  // Bit 0 in byte 1
88
        $payload = '';
89
        $maskingKey = '';
90
91
        // Get masking key.
92
        if ($mask) {
93
            $maskingKey = $this->read(4);
94
        }
95
96
        // Get the actual payload, if any (might not be for e.g. close frames.
97
        if ($payloadLength > 0) {
98
            $data = $this->read($payloadLength);
99
100
            if ($mask) {
101
                // Unmask payload.
102
                for ($i = 0; $i < $payloadLength; $i++) {
103
                    $payload .= ($data[$i] ^ $maskingKey[$i % 4]);
104
                }
105
            } else {
106
                $payload = $data;
107
            }
108
        }
109
110
        return $payload;
111
    }
112
113
    /**
114
     * @return null|string
115
     * @throws \WSSC\Exceptions\BadOpcodeException
116
     * @throws \InvalidArgumentException
117
     * @throws BadOpcodeException
118
     * @throws BadUriException
119
     * @throws ConnectionException
120
     * @throws \Exception
121
     */
122
    protected function receiveFragment(): ?string
123
    {
124
        // Just read the main fragment information first.
125
        $data = $this->read(2);
126
127
        // Is this the final fragment?  // Bit 0 in byte 0
128
        /// @todo Handle huge payloads with multiple fragments.
129
        $final = (bool)(ord($data[0]) & 1 << 7);
130
131
        // Parse opcode
132
        $opcodeInt = ord($data[0]) & 31; // Bits 4-7
133
        $opcodeInts = array_flip(self::$opcodes);
134
        if (!array_key_exists($opcodeInt, $opcodeInts)) {
135
            throw new ConnectionException("Bad opcode in websocket frame: $opcodeInt",
136
                CommonsContract::CLIENT_BAD_OPCODE);
137
        }
138
139
        $opcode = $opcodeInts[$opcodeInt];
140
141
        // record the opcode if we are not receiving a continutation fragment
142
        if ($opcode !== 'continuation') {
143
            $this->lastOpcode = $opcode;
0 ignored issues
show
Bug Best Practice introduced by
The property lastOpcode does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
144
        }
145
146
        $payloadLength = $this->getPayloadLength($data);
147
        $payload = $this->getPayloadData($data, $payloadLength);
0 ignored issues
show
It seems like $payloadLength can also be of type double; however, parameter $payloadLength of WSSC\Components\WSClientTrait::getPayloadData() does only seem to accept integer, 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

147
        $payload = $this->getPayloadData($data, /** @scrutinizer ignore-type */ $payloadLength);
Loading history...
148
149
        if ($opcode === CommonsContract::EVENT_TYPE_CLOSE) {
150
            // Get the close status.
151
            if ($payloadLength >= 2) {
152
                $statusBin = $payload[0] . $payload[1];
153
                $status = bindec(sprintf('%08b%08b', ord($payload[0]), ord($payload[1])));
154
                $this->closeStatus = $status;
0 ignored issues
show
Bug Best Practice introduced by
The property closeStatus does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
155
                $payload = substr($payload, 2);
156
157
                if (!$this->isClosing) {
158
                    $this->send($statusBin . 'Close acknowledged: ' . $status,
0 ignored issues
show
It seems like send() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

158
                    $this->/** @scrutinizer ignore-call */ 
159
                           send($statusBin . 'Close acknowledged: ' . $status,
Loading history...
159
                        CommonsContract::EVENT_TYPE_CLOSE); // Respond.
160
                }
161
            }
162
163
            if ($this->isClosing) {
164
                $this->isClosing = false; // A close response, all done.
0 ignored issues
show
Bug Best Practice introduced by
The property isClosing does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
165
            }
166
167
            fclose($this->socket);
168
            $this->isConnected = false;
0 ignored issues
show
Bug Best Practice introduced by
The property isConnected does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
169
        }
170
171
        if (!$final) {
172
            $this->hugePayload .= $payload;
173
174
            return NULL;
175
        } // this is the last fragment, and we are processing a huge_payload
176
177
        if ($this->hugePayload) {
178
            $payload = $this->hugePayload .= $payload;
179
            $this->hugePayload = NULL;
0 ignored issues
show
Bug Best Practice introduced by
The property hugePayload does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
180
        }
181
182
        return $payload;
183
    }
184
185
    /**
186
     * @param $final
187
     * @param $payload
188
     * @param $opcode
189
     * @param $masked
190
     * @throws ConnectionException
191
     * @throws \Exception
192
     */
193
    protected function sendFragment($final, $payload, $opcode, $masked)
194
    {
195
        // Binary string for header.
196
        $frameHeadBin = '';
197
        // Write FIN, final fragment bit.
198
        $frameHeadBin .= (bool)$final ? '1' : '0';
199
        // RSV 1, 2, & 3 false and unused.
200
        $frameHeadBin .= '000';
201
        // Opcode rest of the byte.
202
        $frameHeadBin .= sprintf('%04b', self::$opcodes[$opcode]);
203
        // Use masking?
204
        $frameHeadBin .= $masked ? '1' : '0';
205
206
        // 7 bits of payload length...
207
        $payloadLen = strlen($payload);
208
        if ($payloadLen > self::MAX_BYTES_READ) {
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MAX_BYTES_READ was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
209
            $frameHeadBin .= decbin(self::MASK_127);
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_127 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
210
            $frameHeadBin .= sprintf('%064b', $payloadLen);
211
        } else if ($payloadLen > self::MASK_125) {
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_125 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
212
            $frameHeadBin .= decbin(self::MASK_126);
0 ignored issues
show
The constant WSSC\Components\WSClientTrait::MASK_126 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
213
            $frameHeadBin .= sprintf('%016b', $payloadLen);
214
        } else {
215
            $frameHeadBin .= sprintf('%07b', $payloadLen);
216
        }
217
218
        $frame = '';
219
220
        // Write frame head to frame.
221
        foreach (str_split($frameHeadBin, 8) as $binstr) {
222
            $frame .= chr(bindec($binstr));
0 ignored issues
show
It seems like bindec($binstr) can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, 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

222
            $frame .= chr(/** @scrutinizer ignore-type */ bindec($binstr));
Loading history...
223
        }
224
        // Handle masking
225
        if ($masked) {
226
            // generate a random mask:
227
            $mask = '';
228
            for ($i = 0; $i < 4; $i++) {
229
                $mask .= chr(random_int(0, 255));
230
            }
231
            $frame .= $mask;
232
        }
233
234
        // Append payload to frame:
235
        for ($i = 0; $i < $payloadLen; $i++) {
236
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mask does not seem to be defined for all execution paths leading up to this point.
Loading history...
237
        }
238
239
        $this->write($frame);
0 ignored issues
show
It seems like write() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

239
        $this->/** @scrutinizer ignore-call */ 
240
               write($frame);
Loading history...
240
    }
241
}