1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Docker\Stream; |
4
|
|
|
use Psr\Http\Message\StreamInterface; |
5
|
|
|
|
6
|
|
|
/** |
7
|
|
|
* An interactive stream is used when communicating with an attached docker container |
8
|
|
|
* |
9
|
|
|
* It helps dealing with encoding and decoding frame from websocket protocol (hybi 10) |
10
|
|
|
* |
11
|
|
|
* @see https://tools.ietf.org/html/rfc6455#section-5.2 |
12
|
|
|
*/ |
13
|
|
|
class AttachWebsocketStream |
14
|
|
|
{ |
15
|
|
|
/** @var resource The underlying socket */ |
16
|
|
|
private $socket; |
17
|
|
|
|
18
|
1 |
|
public function __construct(StreamInterface $stream) |
19
|
|
|
{ |
20
|
1 |
|
$this->socket = $stream->detach(); |
21
|
1 |
|
} |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Send input to the container |
25
|
|
|
* |
26
|
|
|
* @param string $data Data to send |
27
|
|
|
*/ |
28
|
1 |
|
public function write($data) |
29
|
|
|
{ |
30
|
1 |
|
$rand = rand(0, 28); |
31
|
|
|
$frame = [ |
32
|
1 |
|
'fin' => 1, |
33
|
1 |
|
'rsv1' => 0, |
34
|
1 |
|
'rsv2' => 0, |
35
|
1 |
|
'rsv3' => 0, |
36
|
1 |
|
'opcode' => 1, // We always send text |
37
|
1 |
|
'mask' => 1, |
38
|
1 |
|
'len' => strlen($data), |
39
|
1 |
|
'mask_key' => substr(md5(uniqid()), $rand, 4), |
40
|
1 |
|
'data' => $data, |
41
|
1 |
|
]; |
42
|
|
|
|
43
|
1 |
View Code Duplication |
if ($frame['mask'] == 1) { |
|
|
|
|
44
|
1 |
|
for ($i = 0; $i < $frame['len']; $i++) { |
45
|
1 |
|
$frame['data']{$i} |
46
|
1 |
|
= chr(ord($frame['data']{$i}) ^ ord($frame['mask_key']{$i % 4})); |
47
|
1 |
|
} |
48
|
1 |
|
} |
49
|
|
|
|
50
|
1 |
|
if ($frame['len'] > pow(2, 16)) { |
51
|
|
|
$len = 127; |
52
|
1 |
|
} elseif ($frame['len'] > 125) { |
53
|
|
|
$len = 126; |
54
|
|
|
} else { |
55
|
1 |
|
$len = $frame['len']; |
56
|
|
|
} |
57
|
|
|
|
58
|
1 |
|
$firstByte = ($frame['fin'] << 7) | (($frame['rsv1'] << 7) >> 1) | (($frame['rsv2'] << 7) >> 2) | (($frame['rsv3'] << 7) >> 3) | (($frame['opcode'] << 4) >> 4); |
59
|
1 |
|
$secondByte = ($frame['mask'] << 7) | (($len << 1) >> 1); |
60
|
|
|
|
61
|
1 |
|
$this->socketWrite(chr($firstByte)); |
62
|
1 |
|
$this->socketWrite(chr($secondByte)); |
63
|
|
|
|
64
|
1 |
|
if ($len == 126) { |
65
|
|
|
$this->socketWrite(pack('n', $frame['len'])); |
66
|
1 |
|
} elseif ($len == 127) { |
67
|
|
|
$higher = $frame['len'] >> 32; |
68
|
|
|
$lower = ($frame['len'] << 32) >> 32; |
69
|
|
|
$this->socketWrite(pack('N', $higher)); |
70
|
|
|
$this->socketWrite(pack('N', $lower)); |
71
|
|
|
} |
72
|
|
|
|
73
|
1 |
|
if ($frame['mask'] == 1) { |
74
|
1 |
|
$this->socketWrite($frame['mask_key']); |
75
|
1 |
|
} |
76
|
|
|
|
77
|
1 |
|
$this->socketWrite($frame['data']); |
78
|
1 |
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Block until it receive a frame from websocket or return null if no more connexion |
82
|
|
|
* |
83
|
|
|
* @param int $waitTime Time to wait in seconds before return false |
84
|
|
|
* @param int $waitMicroTime Time to wait in microseconds before return false |
85
|
|
|
* @param boolean $getFrame Whether to return the frame of websocket or only the data |
86
|
|
|
* |
87
|
|
|
* @return null|false|string|array Null for socket not available, false for no message, string for the last message and the frame array if $getFrame is set to true |
88
|
|
|
*/ |
89
|
1 |
|
public function read($waitTime = 0, $waitMicroTime = 200000, $getFrame = false) |
90
|
|
|
{ |
91
|
1 |
|
if (!is_resource($this->socket) || feof($this->socket)) { |
92
|
|
|
return null; |
93
|
|
|
} |
94
|
|
|
|
95
|
1 |
|
$read = [$this->socket]; |
96
|
1 |
|
$write = null; |
97
|
1 |
|
$expect = null; |
98
|
|
|
|
99
|
1 |
|
if (stream_select($read, $write, $expect, $waitTime, $waitMicroTime) == 0) { |
100
|
1 |
|
return false; |
101
|
|
|
} |
102
|
|
|
|
103
|
1 |
|
$firstByte = $this->socketRead(1); |
104
|
1 |
|
$frame = []; |
105
|
1 |
|
$firstByte = ord($firstByte); |
106
|
1 |
|
$secondByte = ord($this->socketRead(1)); |
107
|
|
|
|
108
|
|
|
// First byte decoding |
109
|
1 |
|
$frame['fin'] = ($firstByte & 128) >> 7; |
110
|
1 |
|
$frame['rsv1'] = ($firstByte & 64) >> 6; |
111
|
1 |
|
$frame['rsv2'] = ($firstByte & 32) >> 5; |
112
|
1 |
|
$frame['rsv3'] = ($firstByte & 16) >> 4; |
113
|
1 |
|
$frame['opcode'] = ($firstByte & 15); |
114
|
|
|
|
115
|
|
|
// Second byte decoding |
116
|
1 |
|
$frame['mask'] = ($secondByte & 128) >> 7; |
117
|
1 |
|
$frame['len'] = ($secondByte & 127); |
118
|
|
|
|
119
|
|
|
// Get length of the frame |
120
|
1 |
|
if ($frame['len'] == 126) { |
121
|
|
|
$frame['len'] = unpack('n', $this->socketRead(2))[1]; |
122
|
1 |
|
} elseif ($frame['len'] == 127) { |
123
|
|
|
list($higher, $lower) = array_values(unpack('N2', $this->socketRead(8))); |
124
|
|
|
$frame['len'] = ($higher << 32) | $lower; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
// Get the mask key if needed |
128
|
1 |
|
if ($frame['mask'] == 1) { |
129
|
|
|
$frame['mask_key'] = $this->socketRead(4); |
130
|
|
|
} |
131
|
|
|
|
132
|
1 |
|
$frame['data'] = $this->socketRead($frame['len']); |
133
|
|
|
|
134
|
|
|
// Decode data if needed |
135
|
1 |
View Code Duplication |
if ($frame['mask'] == 1) { |
|
|
|
|
136
|
|
|
for ($i = 0; $i < $frame['len']; $i++) { |
137
|
|
|
$frame['data']{$i} = chr(ord($frame['data']{$i}) ^ ord($frame['mask_key']{$i % 4})); |
138
|
|
|
} |
139
|
|
|
} |
140
|
|
|
|
141
|
1 |
|
if ($getFrame) { |
142
|
|
|
return $frame; |
143
|
|
|
} |
144
|
|
|
|
145
|
1 |
|
return (string)$frame['data']; |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
/** |
149
|
|
|
* Force to have something of the expected size (block) |
150
|
|
|
* |
151
|
|
|
* @param $length |
152
|
|
|
* |
153
|
|
|
* @return string |
154
|
|
|
*/ |
155
|
1 |
|
private function socketRead($length) |
156
|
|
|
{ |
157
|
1 |
|
$read = ""; |
158
|
|
|
|
159
|
|
|
do { |
160
|
1 |
|
$read .= fread($this->socket, $length - strlen($read)); |
161
|
1 |
|
} while (strlen($read) < $length && !feof($this->socket)); |
162
|
|
|
|
163
|
1 |
|
return $read; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Write to the socket |
168
|
|
|
* |
169
|
|
|
* @param $data |
170
|
|
|
* |
171
|
|
|
* @return int |
172
|
|
|
*/ |
173
|
1 |
|
private function socketWrite($data) |
174
|
|
|
{ |
175
|
1 |
|
return fwrite($this->socket, $data); |
176
|
|
|
} |
177
|
|
|
} |
178
|
|
|
|
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.