Completed
Push — master ( 2f5fa1...1675cb )
by Arthur
01:25
created

Connection::encode()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.1475
c 0
b 0
f 0
cc 8
nc 8
nop 3
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
class Connection implements ConnectionContract, CommonsContract
10
{
11
12
    private $socketConnection;
13
    private $clients;
14
15
    /**
16
     * Connection constructor.
17
     *
18
     * @param $sockConn
19
     * @param array $clients
20
     */
21
    public function __construct($sockConn, array $clients = [])
22
    {
23
        $this->socketConnection = $sockConn;
24
        $this->clients = $clients;
25
    }
26
27
    /**
28
     * Closes clients socket stream
29
     *
30
     * @throws \Exception
31
     */
32
    public function close(): void
33
    {
34
        if (is_resource($this->socketConnection)) {
35
            fwrite($this->socketConnection, $this->encode('', self::EVENT_TYPE_CLOSE));
36
            fclose($this->socketConnection);
37
        }
38
    }
39
40
    /**
41
     * This method is invoked when user implementation call $conn->send($data)
42
     * writes data to the clients stream socket
43
     *
44
     * @param string $data pure decoded data from server
45
     * @throws \Exception
46
     */
47
    public function send(string $data): void
48
    {
49
        fwrite($this->socketConnection, $this->encode($data));
50
    }
51
52
    /**
53
     * @param string $data data to send to clients
54
     * @throws \Exception
55
     */
56
    public function broadCast(string $data): void
57
    {
58
        foreach ($this->clients as $client) {
59
            if (is_resource($client)) { // check if not yet closed/broken etc
60
                fwrite($client, $this->encode($data));
61
            }
62
        }
63
    }
64
65
    /**
66
     * Encodes data before writing to the client socket stream
67
     *
68
     * @param string $payload
69
     * @param string $type
70
     * @param boolean $masked
71
     * @return mixed
72
     * @throws \Exception
73
     */
74
    private function encode($payload, string $type = self::EVENT_TYPE_TEXT, bool $masked = false)
75
    {
76
        $frameHead = $this->getOpType($type);
77
        $payloadLength = strlen($payload);
78
79
        // set mask and payload length (using 1, 3 or 9 bytes)
80
        if ($payloadLength > self::PAYLOAD_MAX_BITS) {
81
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), self::PAYLOAD_CHUNK);
82
            $frameHead[1] = ($masked === true) ? self::MASK_255 : self::MASK_127;
83
84
            for ($i = 0; $i < 8; $i++) {
85
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
86
            }
87
88
            // most significant bit MUST be 0
89
            if ($frameHead[2] > self::MASK_127) {
90
                return [
91
                    'type'    => $type,
92
                    'payload' => $payload,
93
                    'error'   => WebSocketServerContract::ERR_FRAME_TOO_LARGE,
94
                ];
95
            }
96
        } elseif ($payloadLength > self::MASK_125) {
97
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), self::PAYLOAD_CHUNK);
98
            $frameHead[1] = ($masked === true) ? self::MASK_254 : self::MASK_126;
99
            $frameHead[2] = bindec($payloadLengthBin[0]);
100
            $frameHead[3] = bindec($payloadLengthBin[1]);
101
        } else {
102
            $frameHead[1] = ($masked === true) ? $payloadLength + self::MASK_128 : $payloadLength;
103
        }
104
105
        return $this->getComposedFrame($frameHead, $payload, $payloadLength, $masked);
106
    }
107
108
    /**
109
     * Gets frame-head based on type of operation
110
     *
111
     * @param string $type  Types of operation encode-frames
112
     * @return array
113
     */
114
    private function getOpType(string $type): array
115
    {
116
        $frameHead = [];
117
118
        switch ($type) {
119
            case self::EVENT_TYPE_TEXT:
120
                // first byte indicates FIN, Text-Frame (10000001):
121
                $frameHead[0] = self::ENCODE_TEXT;
122
                break;
123
124
            case self::EVENT_TYPE_CLOSE:
125
                // first byte indicates FIN, Close Frame(10001000):
126
                $frameHead[0] = self::ENCODE_CLOSE;
127
                break;
128
129
            case self::EVENT_TYPE_PING:
130
                // first byte indicates FIN, Ping frame (10001001):
131
                $frameHead[0] = self::ENCODE_PING;
132
                break;
133
134
            case self::EVENT_TYPE_PONG:
135
                // first byte indicates FIN, Pong frame (10001010):
136
                $frameHead[0] = self::ENCODE_PONG;
137
                break;
138
        }
139
140
        return $frameHead;
141
    }
142
143
    private function getComposedFrame(array $frameHead, string $payload, int $payloadLength, bool $masked)
144
    {
145
        // convert frame-head to string:
146
        foreach (array_keys($frameHead) as $i) {
147
            $frameHead[$i] = chr($frameHead[$i]);
148
        }
149
150
        // generate a random mask:
151
        $mask = [];
152
        if ($masked === true) {
153 View Code Duplication
            for ($i = 0; $i < 4; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
154
                $mask[$i] = chr(random_int(0, self::MASK_255));
155
            }
156
157
            $frameHead = array_merge($frameHead, $mask);
158
        }
159
        $frame = implode('', $frameHead);
160
161
        // append payload to frame:
162 View Code Duplication
        for ($i = 0; $i < $payloadLength; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
163
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
164
        }
165
166
        return $frame;
167
    }
168
169
    /**
170
     * Gets unique socket id from resource
171
     *
172
     * @return int
173
     */
174
    public function getUniqueSocketId(): int
175
    {
176
        return (int)$this->socketConnection;
177
    }
178
179
    /**
180
     *  Gets client socket address host/port or UNIX path
181
     *
182
     * @return string
183
     */
184
    public function getPeerName(): string
185
    {
186
        return stream_socket_get_name($this->socketConnection, true);
187
    }
188
}
189