1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Knik\Gameap; |
4
|
|
|
|
5
|
|
|
use Knik\Binn\BinnList; |
6
|
|
|
use RuntimeException; |
7
|
|
|
|
8
|
|
|
abstract class Gdaemon |
9
|
|
|
{ |
10
|
|
|
const DAEMON_SERVER_MODE_NOAUTH = 0; |
11
|
|
|
const DAEMON_SERVER_MODE_AUTH = 1; |
12
|
|
|
const DAEMON_SERVER_MODE_CMD = 2; |
13
|
|
|
const DAEMON_SERVER_MODE_FILES = 3; |
14
|
|
|
|
15
|
|
|
const DAEMON_SERVER_STATUS_OK = 100; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* @var string |
19
|
|
|
*/ |
20
|
|
|
const SOCKET_MSG_ENDL = "\xFF\xFF\xFF\xFF"; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @var resource |
24
|
|
|
*/ |
25
|
|
|
private $_connection; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var resource |
29
|
|
|
*/ |
30
|
|
|
protected $_socket; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* @var string |
34
|
|
|
*/ |
35
|
|
|
protected $host; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var int |
39
|
|
|
*/ |
40
|
|
|
protected $port = 31717; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var int |
44
|
|
|
*/ |
45
|
|
|
protected $timeout = 10; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $configurable = [ |
51
|
|
|
'host', |
52
|
|
|
'port', |
53
|
|
|
// 'username', |
54
|
|
|
// 'password', |
55
|
|
|
// 'privateKey', |
56
|
|
|
// 'privateKeyPass', |
57
|
|
|
'timeout', |
58
|
|
|
]; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* @var int |
62
|
|
|
*/ |
63
|
|
|
protected $maxBufsize = 10240; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* @var bool |
67
|
|
|
*/ |
68
|
|
|
private $_auth = false; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Constructor. |
72
|
|
|
* |
73
|
|
|
* @param array $config |
74
|
|
|
*/ |
75
|
|
|
public function __construct(array $config) |
76
|
|
|
{ |
77
|
|
|
$this->setConfig($config); |
78
|
|
|
|
79
|
|
|
$this->connect(); |
80
|
|
|
$this->login($config['username'], $config['password'], $config['privateKey'], $config['privateKeyPass']); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* Disconnect on destruction. |
85
|
|
|
*/ |
86
|
|
|
public function __destruct() |
87
|
|
|
{ |
88
|
|
|
$this->disconnect(); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Set the config. |
93
|
|
|
* |
94
|
|
|
* @param array $config |
95
|
|
|
* |
96
|
|
|
* @return $this |
97
|
|
|
*/ |
98
|
|
|
public function setConfig(array $config) |
99
|
|
|
{ |
100
|
|
|
foreach ($this->configurable as $setting) { |
101
|
|
|
if ( ! isset($config[$setting])) { |
102
|
|
|
continue; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
if (property_exists($this, $setting)) { |
106
|
|
|
$this->$setting = $config[$setting]; |
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
return $this; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Connect to the server. |
115
|
|
|
*/ |
116
|
|
|
public function connect() |
117
|
|
|
{ |
118
|
|
|
// $this->_connection = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout); |
119
|
|
|
$this->_connection = stream_socket_client("tcp://{$this->host}:{$this->port}", $errno, $errstr, 30); |
|
|
|
|
120
|
|
|
|
121
|
|
|
if ( ! $this->_connection) { |
122
|
|
|
throw new RuntimeException('Could not connect to host: ' |
123
|
|
|
. $this->host |
124
|
|
|
. ', port:' . $this->port |
125
|
|
|
. "(Error $errno: $errstr)"); |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
$this->getSocket(); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* @return mixed |
133
|
|
|
*/ |
134
|
|
|
protected function getConnection() |
135
|
|
|
{ |
136
|
|
|
if (! is_resource($this->_connection)) { |
137
|
|
|
$this->disconnect(); |
138
|
|
|
$this->connect(); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
return $this->_connection; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* @param $username |
146
|
|
|
* @param $password |
147
|
|
|
* @param $privateKey |
148
|
|
|
* @param $privateKeyPass |
149
|
|
|
*/ |
150
|
|
|
protected function login($username, $password, $privateKey, $privateKeyPass) |
151
|
|
|
{ |
152
|
|
|
if ($this->_auth) { |
153
|
|
|
return; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
$writeBinn= new BinnList; |
157
|
|
|
|
158
|
|
|
$writeBinn->addInt16(self::DAEMON_SERVER_MODE_AUTH); |
159
|
|
|
$writeBinn->addStr($username); |
160
|
|
|
$writeBinn->addStr($password); |
161
|
|
|
$writeBinn->addInt16(3); // Set mode DAEMON_SERVER_MODE_FILES |
162
|
|
|
|
163
|
|
|
$fp = fopen($privateKey, "r"); |
164
|
|
|
$privateKey = fread($fp, 8192); |
|
|
|
|
165
|
|
|
fclose($fp); |
|
|
|
|
166
|
|
|
|
167
|
|
|
$res = openssl_get_privatekey($privateKey, $privateKeyPass); |
168
|
|
|
openssl_private_encrypt($writeBinn->serialize() . "\00", $encoded, $res); |
169
|
|
|
|
170
|
|
|
$read = $this->writeAndReadSocket($encoded); |
171
|
|
|
|
172
|
|
|
$decrypted = ""; |
173
|
|
|
if (!openssl_private_decrypt($read, $decrypted, $res)) { |
174
|
|
|
throw new RuntimeException('OpenSSL private decrypt error'); |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
if ($decrypted == '') { |
178
|
|
|
throw new RuntimeException('Empty decrypted results'); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$readBinn = new BinnList; |
182
|
|
|
$readBinn->binnOpen($decrypted); |
183
|
|
|
$results = $readBinn->unserialize(); |
184
|
|
|
|
185
|
|
|
if ($results[0] == self::DAEMON_SERVER_STATUS_OK) { |
186
|
|
|
$this->_auth = true; |
187
|
|
|
} else { |
188
|
|
|
throw new RuntimeException('Could not login with connection: ' . $this->host . ':' . $this->port |
189
|
|
|
. ', username: ' . $username); |
190
|
|
|
} |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Disconnect |
195
|
|
|
*/ |
196
|
|
|
public function disconnect() |
197
|
|
|
{ |
198
|
|
|
if (is_resource($this->_socket)) { |
199
|
|
|
socket_close($this->_socket); |
200
|
|
|
$this->_socket = null; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
if (is_resource($this->_connection)) { |
204
|
|
|
fclose($this->_connection); |
205
|
|
|
$this->_connection = null; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$this->_auth = false; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @return bool|null|resource |
213
|
|
|
*/ |
214
|
|
|
protected function getSocket() |
215
|
|
|
{ |
216
|
|
|
if (is_resource($this->_socket)) { |
217
|
|
|
return $this->_socket; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
set_error_handler(function () {}); |
221
|
|
|
$this->_socket = socket_import_stream($this->getConnection()); |
222
|
|
|
restore_error_handler(); |
223
|
|
|
|
224
|
|
|
if (! $this->_socket) { |
225
|
|
|
$this->disconnect(); |
226
|
|
|
throw new RuntimeException('Could not import socket'); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
stream_set_timeout($this->getConnection(), $this->timeout); |
230
|
|
|
socket_set_option($this->_socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => $this->timeout, 'usec' => 0)); |
231
|
|
|
socket_set_option($this->_socket, SOL_SOCKET, SO_SNDTIMEO, array('sec'=> $this->timeout, 'usec' => 0)); |
232
|
|
|
|
233
|
|
|
return $this->_socket; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @param integer |
238
|
|
|
* @param bool |
239
|
|
|
* @return bool|string |
240
|
|
|
*/ |
241
|
|
|
protected function readSocket($len = 0, $notTrimEndSymbols = false) |
242
|
|
|
{ |
243
|
|
|
$read = ''; |
244
|
|
|
$readed = 0; |
245
|
|
|
$tries = 0; |
246
|
|
|
while($len == 0 || $readed < $len) { |
247
|
|
|
$readlen = socket_recv($this->getSocket(), $readBuf, $this->maxBufsize, MSG_DONTWAIT); |
|
|
|
|
248
|
|
|
|
249
|
|
|
if ($readlen === false) { |
250
|
|
|
$error = socket_last_error($this->getSocket()); |
|
|
|
|
251
|
|
|
|
252
|
|
|
if ($error == 11 && $tries < 10) { |
253
|
|
|
// EAGAIN |
254
|
|
|
usleep(50000); |
255
|
|
|
$tries++; |
256
|
|
|
continue; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
throw new RuntimeException('Socket read failed: ' . socket_strerror($error)); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$tries = 0; |
263
|
|
|
$read .= $readBuf; |
264
|
|
|
$readed += $readlen; |
265
|
|
|
|
266
|
|
|
if (!$notTrimEndSymbols && strpos($read, self::SOCKET_MSG_ENDL) !== false) { |
267
|
|
|
// Message complete |
268
|
|
|
break; |
269
|
|
|
} |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
if ($read === false) { |
|
|
|
|
273
|
|
|
throw new RuntimeException('Socket read failed: ' . socket_strerror(socket_last_error($this->getSocket()))); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
return $notTrimEndSymbols ? $read : substr($read, 0, -4); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
/** |
280
|
|
|
* @param $buffer |
281
|
|
|
* @return int |
282
|
|
|
*/ |
283
|
|
|
protected function writeSocket($buffer) |
284
|
|
|
{ |
285
|
|
|
$result = socket_write($this->getSocket(), $buffer); |
|
|
|
|
286
|
|
|
|
287
|
|
|
if ($result === false) { |
288
|
|
|
throw new RuntimeException('Socket read failed: ' . socket_strerror(socket_last_error($this->getSocket()))); |
|
|
|
|
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
return $result; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Write data to socket and read |
296
|
|
|
* |
297
|
|
|
* @param string $buffer |
298
|
|
|
* @return bool|string |
299
|
|
|
*/ |
300
|
|
|
protected function writeAndReadSocket($buffer) |
301
|
|
|
{ |
302
|
|
|
$this->writeSocket($buffer . self::SOCKET_MSG_ENDL); |
303
|
|
|
|
304
|
|
|
$read = $this->readSocket(); |
305
|
|
|
|
306
|
|
|
if (!$read) { |
307
|
|
|
throw new RuntimeException('Read socket error'); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
return $read; |
311
|
|
|
} |
312
|
|
|
} |
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.