Rev 846 | Rev 1042 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Pure-PHP implementation of SSHv2. |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
8 | * Here are some examples of how to use this library: |
||
9 | * <code> |
||
10 | * <?php |
||
11 | * include 'vendor/autoload.php'; |
||
12 | * |
||
13 | * $ssh = new \phpseclib3\Net\SSH2('www.domain.tld'); |
||
14 | * if (!$ssh->login('username', 'password')) { |
||
15 | * exit('Login Failed'); |
||
16 | * } |
||
17 | * |
||
18 | * echo $ssh->exec('pwd'); |
||
19 | * echo $ssh->exec('ls -la'); |
||
20 | * ?> |
||
21 | * </code> |
||
22 | * |
||
23 | * <code> |
||
24 | * <?php |
||
25 | * include 'vendor/autoload.php'; |
||
26 | * |
||
27 | * $key = \phpseclib3\Crypt\PublicKeyLoader::load('...', '(optional) password'); |
||
28 | * |
||
29 | * $ssh = new \phpseclib3\Net\SSH2('www.domain.tld'); |
||
30 | * if (!$ssh->login('username', $key)) { |
||
31 | * exit('Login Failed'); |
||
32 | * } |
||
33 | * |
||
34 | * echo $ssh->read('username@username:~$'); |
||
35 | * $ssh->write("ls -la\n"); |
||
36 | * echo $ssh->read('username@username:~$'); |
||
37 | * ?> |
||
38 | * </code> |
||
39 | * |
||
874 | daniel-mar | 40 | * @category Net |
41 | * @package SSH2 |
||
827 | daniel-mar | 42 | * @author Jim Wigginton <terrafrost@php.net> |
43 | * @copyright 2007 Jim Wigginton |
||
44 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
45 | * @link http://phpseclib.sourceforge.net |
||
46 | */ |
||
47 | |||
48 | namespace phpseclib3\Net; |
||
49 | |||
50 | use phpseclib3\Common\Functions\Strings; |
||
51 | use phpseclib3\Crypt\Blowfish; |
||
52 | use phpseclib3\Crypt\ChaCha20; |
||
53 | use phpseclib3\Crypt\Common\AsymmetricKey; |
||
54 | use phpseclib3\Crypt\Common\PrivateKey; |
||
55 | use phpseclib3\Crypt\Common\PublicKey; |
||
56 | use phpseclib3\Crypt\Common\SymmetricKey; |
||
57 | use phpseclib3\Crypt\DH; |
||
58 | use phpseclib3\Crypt\DSA; |
||
59 | use phpseclib3\Crypt\EC; |
||
60 | use phpseclib3\Crypt\Hash; |
||
61 | use phpseclib3\Crypt\Random; |
||
62 | use phpseclib3\Crypt\RC4; |
||
63 | use phpseclib3\Crypt\Rijndael; |
||
64 | use phpseclib3\Crypt\RSA; |
||
874 | daniel-mar | 65 | use phpseclib3\Crypt\TripleDES; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. |
827 | daniel-mar | 66 | use phpseclib3\Crypt\Twofish; |
67 | use phpseclib3\Exception\ConnectionClosedException; |
||
68 | use phpseclib3\Exception\InsufficientSetupException; |
||
69 | use phpseclib3\Exception\NoSupportedAlgorithmsException; |
||
70 | use phpseclib3\Exception\UnableToConnectException; |
||
71 | use phpseclib3\Exception\UnsupportedAlgorithmException; |
||
72 | use phpseclib3\Exception\UnsupportedCurveException; |
||
73 | use phpseclib3\Math\BigInteger; |
||
74 | use phpseclib3\System\SSH\Agent; |
||
75 | |||
76 | /** |
||
77 | * Pure-PHP implementation of SSHv2. |
||
78 | * |
||
874 | daniel-mar | 79 | * @package SSH2 |
827 | daniel-mar | 80 | * @author Jim Wigginton <terrafrost@php.net> |
874 | daniel-mar | 81 | * @access public |
827 | daniel-mar | 82 | */ |
83 | class SSH2 |
||
84 | { |
||
85 | /**#@+ |
||
86 | * Compression Types |
||
87 | * |
||
874 | daniel-mar | 88 | * @access private |
827 | daniel-mar | 89 | */ |
90 | /** |
||
91 | * No compression |
||
92 | */ |
||
93 | const NET_SSH2_COMPRESSION_NONE = 1; |
||
94 | /** |
||
95 | * zlib compression |
||
96 | */ |
||
97 | const NET_SSH2_COMPRESSION_ZLIB = 2; |
||
98 | /** |
||
99 | * zlib@openssh.com |
||
100 | */ |
||
101 | const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3; |
||
102 | /**#@-*/ |
||
103 | |||
104 | // Execution Bitmap Masks |
||
105 | const MASK_CONSTRUCTOR = 0x00000001; |
||
106 | const MASK_CONNECTED = 0x00000002; |
||
107 | const MASK_LOGIN_REQ = 0x00000004; |
||
108 | const MASK_LOGIN = 0x00000008; |
||
109 | const MASK_SHELL = 0x00000010; |
||
110 | const MASK_WINDOW_ADJUST = 0x00000020; |
||
111 | |||
112 | /* |
||
113 | * Channel constants |
||
114 | * |
||
115 | * RFC4254 refers not to client and server channels but rather to sender and recipient channels. we don't refer |
||
116 | * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with |
||
117 | * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a |
||
118 | * recipient channel. at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel |
||
119 | * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snippet: |
||
120 | * The 'recipient channel' is the channel number given in the original |
||
121 | * open request, and 'sender channel' is the channel number allocated by |
||
122 | * the other side. |
||
123 | * |
||
124 | * @see \phpseclib3\Net\SSH2::send_channel_packet() |
||
125 | * @see \phpseclib3\Net\SSH2::get_channel_packet() |
||
874 | daniel-mar | 126 | * @access private |
827 | daniel-mar | 127 | */ |
128 | const CHANNEL_EXEC = 1; // PuTTy uses 0x100 |
||
129 | const CHANNEL_SHELL = 2; |
||
130 | const CHANNEL_SUBSYSTEM = 3; |
||
131 | const CHANNEL_AGENT_FORWARD = 4; |
||
132 | const CHANNEL_KEEP_ALIVE = 5; |
||
133 | |||
134 | /** |
||
135 | * Returns the message numbers |
||
136 | * |
||
874 | daniel-mar | 137 | * @access public |
827 | daniel-mar | 138 | * @see \phpseclib3\Net\SSH2::getLog() |
139 | */ |
||
140 | const LOG_SIMPLE = 1; |
||
141 | /** |
||
142 | * Returns the message content |
||
143 | * |
||
874 | daniel-mar | 144 | * @access public |
827 | daniel-mar | 145 | * @see \phpseclib3\Net\SSH2::getLog() |
146 | */ |
||
147 | const LOG_COMPLEX = 2; |
||
148 | /** |
||
149 | * Outputs the content real-time |
||
150 | * |
||
874 | daniel-mar | 151 | * @access public |
827 | daniel-mar | 152 | * @see \phpseclib3\Net\SSH2::getLog() |
153 | */ |
||
154 | const LOG_REALTIME = 3; |
||
155 | /** |
||
156 | * Dumps the content real-time to a file |
||
157 | * |
||
874 | daniel-mar | 158 | * @access public |
827 | daniel-mar | 159 | * @see \phpseclib3\Net\SSH2::getLog() |
160 | */ |
||
161 | const LOG_REALTIME_FILE = 4; |
||
162 | /** |
||
163 | * Make sure that the log never gets larger than this |
||
164 | * |
||
874 | daniel-mar | 165 | * @access public |
827 | daniel-mar | 166 | * @see \phpseclib3\Net\SSH2::getLog() |
167 | */ |
||
168 | const LOG_MAX_SIZE = 1048576; // 1024 * 1024 |
||
169 | |||
170 | /** |
||
171 | * Returns when a string matching $expect exactly is found |
||
172 | * |
||
874 | daniel-mar | 173 | * @access public |
827 | daniel-mar | 174 | * @see \phpseclib3\Net\SSH2::read() |
175 | */ |
||
176 | const READ_SIMPLE = 1; |
||
177 | /** |
||
178 | * Returns when a string matching the regular expression $expect is found |
||
179 | * |
||
874 | daniel-mar | 180 | * @access public |
827 | daniel-mar | 181 | * @see \phpseclib3\Net\SSH2::read() |
182 | */ |
||
183 | const READ_REGEX = 2; |
||
184 | /** |
||
185 | * Returns whenever a data packet is received. |
||
186 | * |
||
187 | * Some data packets may only contain a single character so it may be necessary |
||
188 | * to call read() multiple times when using this option |
||
189 | * |
||
874 | daniel-mar | 190 | * @access public |
827 | daniel-mar | 191 | * @see \phpseclib3\Net\SSH2::read() |
192 | */ |
||
193 | const READ_NEXT = 3; |
||
194 | |||
195 | /** |
||
196 | * The SSH identifier |
||
197 | * |
||
198 | * @var string |
||
874 | daniel-mar | 199 | * @access private |
827 | daniel-mar | 200 | */ |
201 | private $identifier; |
||
202 | |||
203 | /** |
||
204 | * The Socket Object |
||
205 | * |
||
206 | * @var resource|closed-resource|null |
||
874 | daniel-mar | 207 | * @access private |
827 | daniel-mar | 208 | */ |
209 | public $fsock; |
||
210 | |||
211 | /** |
||
212 | * Execution Bitmap |
||
213 | * |
||
214 | * The bits that are set represent functions that have been called already. This is used to determine |
||
215 | * if a requisite function has been successfully executed. If not, an error should be thrown. |
||
216 | * |
||
217 | * @var int |
||
874 | daniel-mar | 218 | * @access private |
827 | daniel-mar | 219 | */ |
220 | protected $bitmap = 0; |
||
221 | |||
222 | /** |
||
223 | * Error information |
||
224 | * |
||
225 | * @see self::getErrors() |
||
226 | * @see self::getLastError() |
||
227 | * @var array |
||
874 | daniel-mar | 228 | * @access private |
827 | daniel-mar | 229 | */ |
230 | private $errors = []; |
||
231 | |||
232 | /** |
||
233 | * Server Identifier |
||
234 | * |
||
235 | * @see self::getServerIdentification() |
||
236 | * @var string|false |
||
874 | daniel-mar | 237 | * @access private |
827 | daniel-mar | 238 | */ |
239 | protected $server_identifier = false; |
||
240 | |||
241 | /** |
||
242 | * Key Exchange Algorithms |
||
243 | * |
||
244 | * @see self::getKexAlgorithims() |
||
245 | * @var array|false |
||
874 | daniel-mar | 246 | * @access private |
827 | daniel-mar | 247 | */ |
248 | private $kex_algorithms = false; |
||
249 | |||
250 | /** |
||
251 | * Key Exchange Algorithm |
||
252 | * |
||
253 | * @see self::getMethodsNegotiated() |
||
254 | * @var string|false |
||
874 | daniel-mar | 255 | * @access private |
827 | daniel-mar | 256 | */ |
257 | private $kex_algorithm = false; |
||
258 | |||
259 | /** |
||
260 | * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
261 | * |
||
262 | * @see self::_key_exchange() |
||
263 | * @var int |
||
874 | daniel-mar | 264 | * @access private |
827 | daniel-mar | 265 | */ |
266 | private $kex_dh_group_size_min = 1536; |
||
267 | |||
268 | /** |
||
269 | * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
270 | * |
||
271 | * @see self::_key_exchange() |
||
272 | * @var int |
||
874 | daniel-mar | 273 | * @access private |
827 | daniel-mar | 274 | */ |
275 | private $kex_dh_group_size_preferred = 2048; |
||
276 | |||
277 | /** |
||
278 | * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
279 | * |
||
280 | * @see self::_key_exchange() |
||
281 | * @var int |
||
874 | daniel-mar | 282 | * @access private |
827 | daniel-mar | 283 | */ |
284 | private $kex_dh_group_size_max = 4096; |
||
285 | |||
286 | /** |
||
287 | * Server Host Key Algorithms |
||
288 | * |
||
289 | * @see self::getServerHostKeyAlgorithms() |
||
290 | * @var array|false |
||
874 | daniel-mar | 291 | * @access private |
827 | daniel-mar | 292 | */ |
293 | private $server_host_key_algorithms = false; |
||
294 | |||
295 | /** |
||
296 | * Encryption Algorithms: Client to Server |
||
297 | * |
||
298 | * @see self::getEncryptionAlgorithmsClient2Server() |
||
299 | * @var array|false |
||
874 | daniel-mar | 300 | * @access private |
827 | daniel-mar | 301 | */ |
302 | private $encryption_algorithms_client_to_server = false; |
||
303 | |||
304 | /** |
||
305 | * Encryption Algorithms: Server to Client |
||
306 | * |
||
307 | * @see self::getEncryptionAlgorithmsServer2Client() |
||
308 | * @var array|false |
||
874 | daniel-mar | 309 | * @access private |
827 | daniel-mar | 310 | */ |
311 | private $encryption_algorithms_server_to_client = false; |
||
312 | |||
313 | /** |
||
314 | * MAC Algorithms: Client to Server |
||
315 | * |
||
316 | * @see self::getMACAlgorithmsClient2Server() |
||
317 | * @var array|false |
||
874 | daniel-mar | 318 | * @access private |
827 | daniel-mar | 319 | */ |
320 | private $mac_algorithms_client_to_server = false; |
||
321 | |||
322 | /** |
||
323 | * MAC Algorithms: Server to Client |
||
324 | * |
||
325 | * @see self::getMACAlgorithmsServer2Client() |
||
326 | * @var array|false |
||
874 | daniel-mar | 327 | * @access private |
827 | daniel-mar | 328 | */ |
329 | private $mac_algorithms_server_to_client = false; |
||
330 | |||
331 | /** |
||
332 | * Compression Algorithms: Client to Server |
||
333 | * |
||
334 | * @see self::getCompressionAlgorithmsClient2Server() |
||
335 | * @var array|false |
||
874 | daniel-mar | 336 | * @access private |
827 | daniel-mar | 337 | */ |
338 | private $compression_algorithms_client_to_server = false; |
||
339 | |||
340 | /** |
||
341 | * Compression Algorithms: Server to Client |
||
342 | * |
||
343 | * @see self::getCompressionAlgorithmsServer2Client() |
||
344 | * @var array|false |
||
874 | daniel-mar | 345 | * @access private |
827 | daniel-mar | 346 | */ |
347 | private $compression_algorithms_server_to_client = false; |
||
348 | |||
349 | /** |
||
350 | * Languages: Server to Client |
||
351 | * |
||
352 | * @see self::getLanguagesServer2Client() |
||
353 | * @var array|false |
||
874 | daniel-mar | 354 | * @access private |
827 | daniel-mar | 355 | */ |
356 | private $languages_server_to_client = false; |
||
357 | |||
358 | /** |
||
359 | * Languages: Client to Server |
||
360 | * |
||
361 | * @see self::getLanguagesClient2Server() |
||
362 | * @var array|false |
||
874 | daniel-mar | 363 | * @access private |
827 | daniel-mar | 364 | */ |
365 | private $languages_client_to_server = false; |
||
366 | |||
367 | /** |
||
368 | * Preferred Algorithms |
||
369 | * |
||
370 | * @see self::setPreferredAlgorithms() |
||
371 | * @var array |
||
874 | daniel-mar | 372 | * @access private |
827 | daniel-mar | 373 | */ |
374 | private $preferred = []; |
||
375 | |||
376 | /** |
||
377 | * Block Size for Server to Client Encryption |
||
378 | * |
||
379 | * "Note that the length of the concatenation of 'packet_length', |
||
380 | * 'padding_length', 'payload', and 'random padding' MUST be a multiple |
||
381 | * of the cipher block size or 8, whichever is larger. This constraint |
||
382 | * MUST be enforced, even when using stream ciphers." |
||
383 | * |
||
384 | * -- http://tools.ietf.org/html/rfc4253#section-6 |
||
385 | * |
||
386 | * @see self::__construct() |
||
387 | * @see self::_send_binary_packet() |
||
388 | * @var int |
||
874 | daniel-mar | 389 | * @access private |
827 | daniel-mar | 390 | */ |
391 | private $encrypt_block_size = 8; |
||
392 | |||
393 | /** |
||
394 | * Block Size for Client to Server Encryption |
||
395 | * |
||
396 | * @see self::__construct() |
||
397 | * @see self::_get_binary_packet() |
||
398 | * @var int |
||
874 | daniel-mar | 399 | * @access private |
827 | daniel-mar | 400 | */ |
401 | private $decrypt_block_size = 8; |
||
402 | |||
403 | /** |
||
404 | * Server to Client Encryption Object |
||
405 | * |
||
406 | * @see self::_get_binary_packet() |
||
407 | * @var SymmetricKey|false |
||
874 | daniel-mar | 408 | * @access private |
827 | daniel-mar | 409 | */ |
410 | private $decrypt = false; |
||
411 | |||
412 | /** |
||
413 | * Decryption Algorithm Name |
||
414 | * |
||
415 | * @var string|null |
||
874 | daniel-mar | 416 | * @access private |
827 | daniel-mar | 417 | */ |
418 | private $decryptName; |
||
419 | |||
420 | /** |
||
421 | * Decryption Invocation Counter |
||
422 | * |
||
423 | * Used by GCM |
||
424 | * |
||
425 | * @var string|null |
||
874 | daniel-mar | 426 | * @access private |
827 | daniel-mar | 427 | */ |
428 | private $decryptInvocationCounter; |
||
429 | |||
430 | /** |
||
431 | * Fixed Part of Nonce |
||
432 | * |
||
433 | * Used by GCM |
||
434 | * |
||
435 | * @var string|null |
||
874 | daniel-mar | 436 | * @access private |
827 | daniel-mar | 437 | */ |
438 | private $decryptFixedPart; |
||
439 | |||
440 | /** |
||
441 | * Server to Client Length Encryption Object |
||
442 | * |
||
443 | * @see self::_get_binary_packet() |
||
444 | * @var object |
||
874 | daniel-mar | 445 | * @access private |
827 | daniel-mar | 446 | */ |
447 | private $lengthDecrypt = false; |
||
448 | |||
449 | /** |
||
450 | * Client to Server Encryption Object |
||
451 | * |
||
452 | * @see self::_send_binary_packet() |
||
453 | * @var SymmetricKey|false |
||
874 | daniel-mar | 454 | * @access private |
827 | daniel-mar | 455 | */ |
456 | private $encrypt = false; |
||
457 | |||
458 | /** |
||
459 | * Encryption Algorithm Name |
||
460 | * |
||
461 | * @var string|null |
||
874 | daniel-mar | 462 | * @access private |
827 | daniel-mar | 463 | */ |
464 | private $encryptName; |
||
465 | |||
466 | /** |
||
467 | * Encryption Invocation Counter |
||
468 | * |
||
469 | * Used by GCM |
||
470 | * |
||
471 | * @var string|null |
||
874 | daniel-mar | 472 | * @access private |
827 | daniel-mar | 473 | */ |
474 | private $encryptInvocationCounter; |
||
475 | |||
476 | /** |
||
477 | * Fixed Part of Nonce |
||
478 | * |
||
479 | * Used by GCM |
||
480 | * |
||
481 | * @var string|null |
||
874 | daniel-mar | 482 | * @access private |
827 | daniel-mar | 483 | */ |
484 | private $encryptFixedPart; |
||
485 | |||
486 | /** |
||
487 | * Client to Server Length Encryption Object |
||
488 | * |
||
489 | * @see self::_send_binary_packet() |
||
490 | * @var object |
||
874 | daniel-mar | 491 | * @access private |
827 | daniel-mar | 492 | */ |
493 | private $lengthEncrypt = false; |
||
494 | |||
495 | /** |
||
496 | * Client to Server HMAC Object |
||
497 | * |
||
498 | * @see self::_send_binary_packet() |
||
499 | * @var object |
||
874 | daniel-mar | 500 | * @access private |
827 | daniel-mar | 501 | */ |
502 | private $hmac_create = false; |
||
503 | |||
504 | /** |
||
505 | * Client to Server HMAC Name |
||
506 | * |
||
507 | * @var string|false |
||
874 | daniel-mar | 508 | * @access private |
827 | daniel-mar | 509 | */ |
510 | private $hmac_create_name; |
||
511 | |||
512 | /** |
||
513 | * Client to Server ETM |
||
514 | * |
||
515 | * @var int|false |
||
874 | daniel-mar | 516 | * @access private |
827 | daniel-mar | 517 | */ |
518 | private $hmac_create_etm; |
||
519 | |||
520 | /** |
||
521 | * Server to Client HMAC Object |
||
522 | * |
||
523 | * @see self::_get_binary_packet() |
||
524 | * @var object |
||
874 | daniel-mar | 525 | * @access private |
827 | daniel-mar | 526 | */ |
527 | private $hmac_check = false; |
||
528 | |||
529 | /** |
||
530 | * Server to Client HMAC Name |
||
531 | * |
||
532 | * @var string|false |
||
874 | daniel-mar | 533 | * @access private |
827 | daniel-mar | 534 | */ |
535 | private $hmac_check_name; |
||
536 | |||
537 | /** |
||
538 | * Server to Client ETM |
||
539 | * |
||
540 | * @var int|false |
||
874 | daniel-mar | 541 | * @access private |
827 | daniel-mar | 542 | */ |
543 | private $hmac_check_etm; |
||
544 | |||
545 | /** |
||
546 | * Size of server to client HMAC |
||
547 | * |
||
548 | * We need to know how big the HMAC will be for the server to client direction so that we know how many bytes to read. |
||
549 | * For the client to server side, the HMAC object will make the HMAC as long as it needs to be. All we need to do is |
||
550 | * append it. |
||
551 | * |
||
552 | * @see self::_get_binary_packet() |
||
553 | * @var int |
||
874 | daniel-mar | 554 | * @access private |
827 | daniel-mar | 555 | */ |
556 | private $hmac_size = false; |
||
557 | |||
558 | /** |
||
559 | * Server Public Host Key |
||
560 | * |
||
561 | * @see self::getServerPublicHostKey() |
||
562 | * @var string |
||
874 | daniel-mar | 563 | * @access private |
827 | daniel-mar | 564 | */ |
565 | private $server_public_host_key; |
||
566 | |||
567 | /** |
||
568 | * Session identifier |
||
569 | * |
||
570 | * "The exchange hash H from the first key exchange is additionally |
||
571 | * used as the session identifier, which is a unique identifier for |
||
572 | * this connection." |
||
573 | * |
||
574 | * -- http://tools.ietf.org/html/rfc4253#section-7.2 |
||
575 | * |
||
576 | * @see self::_key_exchange() |
||
577 | * @var string |
||
874 | daniel-mar | 578 | * @access private |
827 | daniel-mar | 579 | */ |
580 | private $session_id = false; |
||
581 | |||
582 | /** |
||
583 | * Exchange hash |
||
584 | * |
||
585 | * The current exchange hash |
||
586 | * |
||
587 | * @see self::_key_exchange() |
||
588 | * @var string |
||
874 | daniel-mar | 589 | * @access private |
827 | daniel-mar | 590 | */ |
591 | private $exchange_hash = false; |
||
592 | |||
593 | /** |
||
874 | daniel-mar | 594 | * Message Numbers |
595 | * |
||
596 | * @see self::__construct() |
||
597 | * @var array |
||
598 | * @access private |
||
599 | */ |
||
600 | private $message_numbers = []; |
||
601 | |||
602 | /** |
||
603 | * Disconnection Message 'reason codes' defined in RFC4253 |
||
604 | * |
||
605 | * @see self::__construct() |
||
606 | * @var array |
||
607 | * @access private |
||
608 | */ |
||
609 | private $disconnect_reasons = []; |
||
610 | |||
611 | /** |
||
612 | * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 |
||
613 | * |
||
614 | * @see self::__construct() |
||
615 | * @var array |
||
616 | * @access private |
||
617 | */ |
||
618 | private $channel_open_failure_reasons = []; |
||
619 | |||
620 | /** |
||
621 | * Terminal Modes |
||
622 | * |
||
623 | * @link http://tools.ietf.org/html/rfc4254#section-8 |
||
624 | * @see self::__construct() |
||
625 | * @var array |
||
626 | * @access private |
||
627 | */ |
||
628 | private $terminal_modes = []; |
||
629 | |||
630 | /** |
||
631 | * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes |
||
632 | * |
||
633 | * @link http://tools.ietf.org/html/rfc4254#section-5.2 |
||
634 | * @see self::__construct() |
||
635 | * @var array |
||
636 | * @access private |
||
637 | */ |
||
638 | private $channel_extended_data_type_codes = []; |
||
639 | |||
640 | /** |
||
827 | daniel-mar | 641 | * Send Sequence Number |
642 | * |
||
643 | * See 'Section 6.4. Data Integrity' of rfc4253 for more info. |
||
644 | * |
||
645 | * @see self::_send_binary_packet() |
||
646 | * @var int |
||
874 | daniel-mar | 647 | * @access private |
827 | daniel-mar | 648 | */ |
649 | private $send_seq_no = 0; |
||
650 | |||
651 | /** |
||
652 | * Get Sequence Number |
||
653 | * |
||
654 | * See 'Section 6.4. Data Integrity' of rfc4253 for more info. |
||
655 | * |
||
656 | * @see self::_get_binary_packet() |
||
657 | * @var int |
||
874 | daniel-mar | 658 | * @access private |
827 | daniel-mar | 659 | */ |
660 | private $get_seq_no = 0; |
||
661 | |||
662 | /** |
||
663 | * Server Channels |
||
664 | * |
||
665 | * Maps client channels to server channels |
||
666 | * |
||
667 | * @see self::get_channel_packet() |
||
668 | * @see self::exec() |
||
669 | * @var array |
||
874 | daniel-mar | 670 | * @access private |
827 | daniel-mar | 671 | */ |
672 | protected $server_channels = []; |
||
673 | |||
674 | /** |
||
675 | * Channel Buffers |
||
676 | * |
||
677 | * If a client requests a packet from one channel but receives two packets from another those packets should |
||
678 | * be placed in a buffer |
||
679 | * |
||
680 | * @see self::get_channel_packet() |
||
681 | * @see self::exec() |
||
682 | * @var array |
||
874 | daniel-mar | 683 | * @access private |
827 | daniel-mar | 684 | */ |
685 | private $channel_buffers = []; |
||
686 | |||
687 | /** |
||
688 | * Channel Status |
||
689 | * |
||
690 | * Contains the type of the last sent message |
||
691 | * |
||
692 | * @see self::get_channel_packet() |
||
693 | * @var array |
||
874 | daniel-mar | 694 | * @access private |
827 | daniel-mar | 695 | */ |
696 | protected $channel_status = []; |
||
697 | |||
698 | /** |
||
699 | * Packet Size |
||
700 | * |
||
701 | * Maximum packet size indexed by channel |
||
702 | * |
||
703 | * @see self::send_channel_packet() |
||
704 | * @var array |
||
874 | daniel-mar | 705 | * @access private |
827 | daniel-mar | 706 | */ |
707 | private $packet_size_client_to_server = []; |
||
708 | |||
709 | /** |
||
710 | * Message Number Log |
||
711 | * |
||
712 | * @see self::getLog() |
||
713 | * @var array |
||
874 | daniel-mar | 714 | * @access private |
827 | daniel-mar | 715 | */ |
716 | private $message_number_log = []; |
||
717 | |||
718 | /** |
||
719 | * Message Log |
||
720 | * |
||
721 | * @see self::getLog() |
||
722 | * @var array |
||
874 | daniel-mar | 723 | * @access private |
827 | daniel-mar | 724 | */ |
725 | private $message_log = []; |
||
726 | |||
727 | /** |
||
728 | * The Window Size |
||
729 | * |
||
730 | * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB) |
||
731 | * |
||
732 | * @var int |
||
733 | * @see self::send_channel_packet() |
||
734 | * @see self::exec() |
||
874 | daniel-mar | 735 | * @access private |
827 | daniel-mar | 736 | */ |
737 | protected $window_size = 0x7FFFFFFF; |
||
738 | |||
739 | /** |
||
740 | * What we resize the window to |
||
741 | * |
||
742 | * When PuTTY resizes the window it doesn't add an additional 0x7FFFFFFF bytes - it adds 0x40000000 bytes. |
||
743 | * Some SFTP clients (GoAnywhere) don't support adding 0x7FFFFFFF to the window size after the fact so |
||
744 | * we'll just do what PuTTY does |
||
745 | * |
||
746 | * @var int |
||
747 | * @see self::_send_channel_packet() |
||
748 | * @see self::exec() |
||
874 | daniel-mar | 749 | * @access private |
827 | daniel-mar | 750 | */ |
751 | private $window_resize = 0x40000000; |
||
752 | |||
753 | /** |
||
754 | * Window size, server to client |
||
755 | * |
||
756 | * Window size indexed by channel |
||
757 | * |
||
758 | * @see self::send_channel_packet() |
||
759 | * @var array |
||
874 | daniel-mar | 760 | * @access private |
827 | daniel-mar | 761 | */ |
762 | protected $window_size_server_to_client = []; |
||
763 | |||
764 | /** |
||
765 | * Window size, client to server |
||
766 | * |
||
767 | * Window size indexed by channel |
||
768 | * |
||
769 | * @see self::get_channel_packet() |
||
770 | * @var array |
||
874 | daniel-mar | 771 | * @access private |
827 | daniel-mar | 772 | */ |
773 | private $window_size_client_to_server = []; |
||
774 | |||
775 | /** |
||
776 | * Server signature |
||
777 | * |
||
778 | * Verified against $this->session_id |
||
779 | * |
||
780 | * @see self::getServerPublicHostKey() |
||
781 | * @var string |
||
874 | daniel-mar | 782 | * @access private |
827 | daniel-mar | 783 | */ |
784 | private $signature = ''; |
||
785 | |||
786 | /** |
||
787 | * Server signature format |
||
788 | * |
||
789 | * ssh-rsa or ssh-dss. |
||
790 | * |
||
791 | * @see self::getServerPublicHostKey() |
||
792 | * @var string |
||
874 | daniel-mar | 793 | * @access private |
827 | daniel-mar | 794 | */ |
795 | private $signature_format = ''; |
||
796 | |||
797 | /** |
||
798 | * Interactive Buffer |
||
799 | * |
||
800 | * @see self::read() |
||
801 | * @var string |
||
874 | daniel-mar | 802 | * @access private |
827 | daniel-mar | 803 | */ |
804 | private $interactiveBuffer = ''; |
||
805 | |||
806 | /** |
||
807 | * Current log size |
||
808 | * |
||
809 | * Should never exceed self::LOG_MAX_SIZE |
||
810 | * |
||
811 | * @see self::_send_binary_packet() |
||
812 | * @see self::_get_binary_packet() |
||
813 | * @var int |
||
874 | daniel-mar | 814 | * @access private |
827 | daniel-mar | 815 | */ |
816 | private $log_size; |
||
817 | |||
818 | /** |
||
819 | * Timeout |
||
820 | * |
||
821 | * @see self::setTimeout() |
||
874 | daniel-mar | 822 | * @access private |
827 | daniel-mar | 823 | */ |
824 | protected $timeout; |
||
825 | |||
826 | /** |
||
827 | * Current Timeout |
||
828 | * |
||
829 | * @see self::get_channel_packet() |
||
874 | daniel-mar | 830 | * @access private |
827 | daniel-mar | 831 | */ |
832 | protected $curTimeout; |
||
833 | |||
834 | /** |
||
835 | * Keep Alive Interval |
||
836 | * |
||
837 | * @see self::setKeepAlive() |
||
874 | daniel-mar | 838 | * @access private |
827 | daniel-mar | 839 | */ |
840 | private $keepAlive; |
||
841 | |||
842 | /** |
||
843 | * Real-time log file pointer |
||
844 | * |
||
845 | * @see self::_append_log() |
||
846 | * @var resource|closed-resource |
||
874 | daniel-mar | 847 | * @access private |
827 | daniel-mar | 848 | */ |
849 | private $realtime_log_file; |
||
850 | |||
851 | /** |
||
852 | * Real-time log file size |
||
853 | * |
||
854 | * @see self::_append_log() |
||
855 | * @var int |
||
874 | daniel-mar | 856 | * @access private |
827 | daniel-mar | 857 | */ |
858 | private $realtime_log_size; |
||
859 | |||
860 | /** |
||
861 | * Has the signature been validated? |
||
862 | * |
||
863 | * @see self::getServerPublicHostKey() |
||
864 | * @var bool |
||
874 | daniel-mar | 865 | * @access private |
827 | daniel-mar | 866 | */ |
867 | private $signature_validated = false; |
||
868 | |||
869 | /** |
||
870 | * Real-time log file wrap boolean |
||
871 | * |
||
872 | * @see self::_append_log() |
||
874 | daniel-mar | 873 | * @access private |
827 | daniel-mar | 874 | */ |
875 | private $realtime_log_wrap; |
||
876 | |||
877 | /** |
||
878 | * Flag to suppress stderr from output |
||
879 | * |
||
880 | * @see self::enableQuietMode() |
||
874 | daniel-mar | 881 | * @access private |
827 | daniel-mar | 882 | */ |
883 | private $quiet_mode = false; |
||
884 | |||
885 | /** |
||
886 | * Time of first network activity |
||
887 | * |
||
888 | * @var float |
||
874 | daniel-mar | 889 | * @access private |
827 | daniel-mar | 890 | */ |
891 | private $last_packet; |
||
892 | |||
893 | /** |
||
894 | * Exit status returned from ssh if any |
||
895 | * |
||
896 | * @var int |
||
874 | daniel-mar | 897 | * @access private |
827 | daniel-mar | 898 | */ |
899 | private $exit_status; |
||
900 | |||
901 | /** |
||
902 | * Flag to request a PTY when using exec() |
||
903 | * |
||
904 | * @var bool |
||
905 | * @see self::enablePTY() |
||
874 | daniel-mar | 906 | * @access private |
827 | daniel-mar | 907 | */ |
908 | private $request_pty = false; |
||
909 | |||
910 | /** |
||
911 | * Flag set while exec() is running when using enablePTY() |
||
912 | * |
||
913 | * @var bool |
||
874 | daniel-mar | 914 | * @access private |
827 | daniel-mar | 915 | */ |
916 | private $in_request_pty_exec = false; |
||
917 | |||
918 | /** |
||
919 | * Flag set after startSubsystem() is called |
||
920 | * |
||
921 | * @var bool |
||
874 | daniel-mar | 922 | * @access private |
827 | daniel-mar | 923 | */ |
924 | private $in_subsystem; |
||
925 | |||
926 | /** |
||
927 | * Contents of stdError |
||
928 | * |
||
929 | * @var string |
||
874 | daniel-mar | 930 | * @access private |
827 | daniel-mar | 931 | */ |
932 | private $stdErrorLog; |
||
933 | |||
934 | /** |
||
935 | * The Last Interactive Response |
||
936 | * |
||
937 | * @see self::_keyboard_interactive_process() |
||
938 | * @var string |
||
874 | daniel-mar | 939 | * @access private |
827 | daniel-mar | 940 | */ |
941 | private $last_interactive_response = ''; |
||
942 | |||
943 | /** |
||
944 | * Keyboard Interactive Request / Responses |
||
945 | * |
||
946 | * @see self::_keyboard_interactive_process() |
||
947 | * @var array |
||
874 | daniel-mar | 948 | * @access private |
827 | daniel-mar | 949 | */ |
950 | private $keyboard_requests_responses = []; |
||
951 | |||
952 | /** |
||
953 | * Banner Message |
||
954 | * |
||
955 | * Quoting from the RFC, "in some jurisdictions, sending a warning message before |
||
956 | * authentication may be relevant for getting legal protection." |
||
957 | * |
||
958 | * @see self::_filter() |
||
959 | * @see self::getBannerMessage() |
||
960 | * @var string |
||
874 | daniel-mar | 961 | * @access private |
827 | daniel-mar | 962 | */ |
963 | private $banner_message = ''; |
||
964 | |||
965 | /** |
||
966 | * Did read() timeout or return normally? |
||
967 | * |
||
968 | * @see self::isTimeout() |
||
969 | * @var bool |
||
874 | daniel-mar | 970 | * @access private |
827 | daniel-mar | 971 | */ |
972 | private $is_timeout = false; |
||
973 | |||
974 | /** |
||
975 | * Log Boundary |
||
976 | * |
||
977 | * @see self::_format_log() |
||
978 | * @var string |
||
874 | daniel-mar | 979 | * @access private |
827 | daniel-mar | 980 | */ |
981 | private $log_boundary = ':'; |
||
982 | |||
983 | /** |
||
984 | * Log Long Width |
||
985 | * |
||
986 | * @see self::_format_log() |
||
987 | * @var int |
||
874 | daniel-mar | 988 | * @access private |
827 | daniel-mar | 989 | */ |
990 | private $log_long_width = 65; |
||
991 | |||
992 | /** |
||
993 | * Log Short Width |
||
994 | * |
||
995 | * @see self::_format_log() |
||
996 | * @var int |
||
874 | daniel-mar | 997 | * @access private |
827 | daniel-mar | 998 | */ |
999 | private $log_short_width = 16; |
||
1000 | |||
1001 | /** |
||
1002 | * Hostname |
||
1003 | * |
||
1004 | * @see self::__construct() |
||
1005 | * @see self::_connect() |
||
1006 | * @var string |
||
874 | daniel-mar | 1007 | * @access private |
827 | daniel-mar | 1008 | */ |
1009 | private $host; |
||
1010 | |||
1011 | /** |
||
1012 | * Port Number |
||
1013 | * |
||
1014 | * @see self::__construct() |
||
1015 | * @see self::_connect() |
||
1016 | * @var int |
||
874 | daniel-mar | 1017 | * @access private |
827 | daniel-mar | 1018 | */ |
1019 | private $port; |
||
1020 | |||
1021 | /** |
||
1022 | * Number of columns for terminal window size |
||
1023 | * |
||
1024 | * @see self::getWindowColumns() |
||
1025 | * @see self::setWindowColumns() |
||
1026 | * @see self::setWindowSize() |
||
1027 | * @var int |
||
874 | daniel-mar | 1028 | * @access private |
827 | daniel-mar | 1029 | */ |
1030 | private $windowColumns = 80; |
||
1031 | |||
1032 | /** |
||
1033 | * Number of columns for terminal window size |
||
1034 | * |
||
1035 | * @see self::getWindowRows() |
||
1036 | * @see self::setWindowRows() |
||
1037 | * @see self::setWindowSize() |
||
1038 | * @var int |
||
874 | daniel-mar | 1039 | * @access private |
827 | daniel-mar | 1040 | */ |
1041 | private $windowRows = 24; |
||
1042 | |||
1043 | /** |
||
1044 | * Crypto Engine |
||
1045 | * |
||
1046 | * @see self::setCryptoEngine() |
||
1047 | * @see self::_key_exchange() |
||
1048 | * @var int |
||
874 | daniel-mar | 1049 | * @access private |
827 | daniel-mar | 1050 | */ |
1051 | private static $crypto_engine = false; |
||
1052 | |||
1053 | /** |
||
1054 | * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario |
||
1055 | * |
||
1056 | * @var Agent |
||
874 | daniel-mar | 1057 | * @access private |
827 | daniel-mar | 1058 | */ |
1059 | private $agent; |
||
1060 | |||
1061 | /** |
||
1062 | * Connection storage to replicates ssh2 extension functionality: |
||
1063 | * {@link http://php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-examples} |
||
1064 | * |
||
1065 | * @var array<string, SSH2|\WeakReference<SSH2>> |
||
1066 | */ |
||
1067 | private static $connections; |
||
1068 | |||
1069 | /** |
||
1070 | * Send the identification string first? |
||
1071 | * |
||
1072 | * @var bool |
||
874 | daniel-mar | 1073 | * @access private |
827 | daniel-mar | 1074 | */ |
1075 | private $send_id_string_first = true; |
||
1076 | |||
1077 | /** |
||
1078 | * Send the key exchange initiation packet first? |
||
1079 | * |
||
1080 | * @var bool |
||
874 | daniel-mar | 1081 | * @access private |
827 | daniel-mar | 1082 | */ |
1083 | private $send_kex_first = true; |
||
1084 | |||
1085 | /** |
||
1086 | * Some versions of OpenSSH incorrectly calculate the key size |
||
1087 | * |
||
1088 | * @var bool |
||
874 | daniel-mar | 1089 | * @access private |
827 | daniel-mar | 1090 | */ |
1091 | private $bad_key_size_fix = false; |
||
1092 | |||
1093 | /** |
||
1094 | * Should we try to re-connect to re-establish keys? |
||
1095 | * |
||
1096 | * @var bool |
||
874 | daniel-mar | 1097 | * @access private |
827 | daniel-mar | 1098 | */ |
1099 | private $retry_connect = false; |
||
1100 | |||
1101 | /** |
||
1102 | * Binary Packet Buffer |
||
1103 | * |
||
1104 | * @var string|false |
||
874 | daniel-mar | 1105 | * @access private |
827 | daniel-mar | 1106 | */ |
1107 | private $binary_packet_buffer = false; |
||
1108 | |||
1109 | /** |
||
1110 | * Preferred Signature Format |
||
1111 | * |
||
1112 | * @var string|false |
||
874 | daniel-mar | 1113 | * @access private |
827 | daniel-mar | 1114 | */ |
1115 | protected $preferred_signature_format = false; |
||
1116 | |||
1117 | /** |
||
1118 | * Authentication Credentials |
||
1119 | * |
||
1120 | * @var array |
||
874 | daniel-mar | 1121 | * @access private |
827 | daniel-mar | 1122 | */ |
1123 | protected $auth = []; |
||
1124 | |||
1125 | /** |
||
1126 | * Terminal |
||
1127 | * |
||
1128 | * @var string |
||
874 | daniel-mar | 1129 | * @access private |
827 | daniel-mar | 1130 | */ |
1131 | private $term = 'vt100'; |
||
1132 | |||
1133 | /** |
||
1134 | * The authentication methods that may productively continue authentication. |
||
1135 | * |
||
1136 | * @see https://tools.ietf.org/html/rfc4252#section-5.1 |
||
1137 | * @var array|null |
||
874 | daniel-mar | 1138 | * @access private |
827 | daniel-mar | 1139 | */ |
1140 | private $auth_methods_to_continue = null; |
||
1141 | |||
1142 | /** |
||
1143 | * Compression method |
||
1144 | * |
||
1145 | * @var int |
||
874 | daniel-mar | 1146 | * @access private |
827 | daniel-mar | 1147 | */ |
1148 | private $compress = self::NET_SSH2_COMPRESSION_NONE; |
||
1149 | |||
1150 | /** |
||
1151 | * Decompression method |
||
1152 | * |
||
1153 | * @var int |
||
874 | daniel-mar | 1154 | * @access private |
827 | daniel-mar | 1155 | */ |
1156 | private $decompress = self::NET_SSH2_COMPRESSION_NONE; |
||
1157 | |||
1158 | /** |
||
1159 | * Compression context |
||
1160 | * |
||
1161 | * @var resource|false|null |
||
874 | daniel-mar | 1162 | * @access private |
827 | daniel-mar | 1163 | */ |
1164 | private $compress_context; |
||
1165 | |||
1166 | /** |
||
1167 | * Decompression context |
||
1168 | * |
||
1169 | * @var resource|object |
||
874 | daniel-mar | 1170 | * @access private |
827 | daniel-mar | 1171 | */ |
1172 | private $decompress_context; |
||
1173 | |||
1174 | /** |
||
1175 | * Regenerate Compression Context |
||
1176 | * |
||
1177 | * @var bool |
||
874 | daniel-mar | 1178 | * @access private |
827 | daniel-mar | 1179 | */ |
1180 | private $regenerate_compression_context = false; |
||
1181 | |||
1182 | /** |
||
1183 | * Regenerate Decompression Context |
||
1184 | * |
||
1185 | * @var bool |
||
874 | daniel-mar | 1186 | * @access private |
827 | daniel-mar | 1187 | */ |
1188 | private $regenerate_decompression_context = false; |
||
1189 | |||
1190 | /** |
||
1191 | * Smart multi-factor authentication flag |
||
1192 | * |
||
1193 | * @var bool |
||
874 | daniel-mar | 1194 | * @access private |
827 | daniel-mar | 1195 | */ |
1196 | private $smartMFA = true; |
||
1197 | |||
1198 | /** |
||
1199 | * Default Constructor. |
||
1200 | * |
||
1201 | * $host can either be a string, representing the host, or a stream resource. |
||
1202 | * |
||
1203 | * @param mixed $host |
||
1204 | * @param int $port |
||
1205 | * @param int $timeout |
||
1206 | * @see self::login() |
||
874 | daniel-mar | 1207 | * @access public |
827 | daniel-mar | 1208 | */ |
1209 | public function __construct($host, $port = 22, $timeout = 10) |
||
1210 | { |
||
874 | daniel-mar | 1211 | $this->message_numbers = [ |
1212 | 1 => 'NET_SSH2_MSG_DISCONNECT', |
||
1213 | 2 => 'NET_SSH2_MSG_IGNORE', |
||
1214 | 3 => 'NET_SSH2_MSG_UNIMPLEMENTED', |
||
1215 | 4 => 'NET_SSH2_MSG_DEBUG', |
||
1216 | 5 => 'NET_SSH2_MSG_SERVICE_REQUEST', |
||
1217 | 6 => 'NET_SSH2_MSG_SERVICE_ACCEPT', |
||
1218 | 20 => 'NET_SSH2_MSG_KEXINIT', |
||
1219 | 21 => 'NET_SSH2_MSG_NEWKEYS', |
||
1220 | 30 => 'NET_SSH2_MSG_KEXDH_INIT', |
||
1221 | 31 => 'NET_SSH2_MSG_KEXDH_REPLY', |
||
1222 | 50 => 'NET_SSH2_MSG_USERAUTH_REQUEST', |
||
1223 | 51 => 'NET_SSH2_MSG_USERAUTH_FAILURE', |
||
1224 | 52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS', |
||
1225 | 53 => 'NET_SSH2_MSG_USERAUTH_BANNER', |
||
1226 | |||
1227 | 80 => 'NET_SSH2_MSG_GLOBAL_REQUEST', |
||
1228 | 81 => 'NET_SSH2_MSG_REQUEST_SUCCESS', |
||
1229 | 82 => 'NET_SSH2_MSG_REQUEST_FAILURE', |
||
1230 | 90 => 'NET_SSH2_MSG_CHANNEL_OPEN', |
||
1231 | 91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION', |
||
1232 | 92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE', |
||
1233 | 93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST', |
||
1234 | 94 => 'NET_SSH2_MSG_CHANNEL_DATA', |
||
1235 | 95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA', |
||
1236 | 96 => 'NET_SSH2_MSG_CHANNEL_EOF', |
||
1237 | 97 => 'NET_SSH2_MSG_CHANNEL_CLOSE', |
||
1238 | 98 => 'NET_SSH2_MSG_CHANNEL_REQUEST', |
||
1239 | 99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', |
||
1240 | 100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' |
||
1241 | ]; |
||
1242 | $this->disconnect_reasons = [ |
||
1243 | 1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', |
||
1244 | 2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', |
||
1245 | 3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', |
||
1246 | 4 => 'NET_SSH2_DISCONNECT_RESERVED', |
||
1247 | 5 => 'NET_SSH2_DISCONNECT_MAC_ERROR', |
||
1248 | 6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR', |
||
1249 | 7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE', |
||
1250 | 8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED', |
||
1251 | 9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE', |
||
1252 | 10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST', |
||
1253 | 11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION', |
||
1254 | 12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS', |
||
1255 | 13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER', |
||
1256 | 14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', |
||
1257 | 15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' |
||
1258 | ]; |
||
1259 | $this->channel_open_failure_reasons = [ |
||
1260 | 1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' |
||
1261 | ]; |
||
1262 | $this->terminal_modes = [ |
||
1263 | |||
1264 | ]; |
||
1265 | $this->channel_extended_data_type_codes = [ |
||
1266 | 1 => 'NET_SSH2_EXTENDED_DATA_STDERR' |
||
1267 | ]; |
||
1268 | |||
1269 | $this->define_array( |
||
1270 | $this->message_numbers, |
||
1271 | $this->disconnect_reasons, |
||
1272 | $this->channel_open_failure_reasons, |
||
1273 | $this->terminal_modes, |
||
1274 | $this->channel_extended_data_type_codes, |
||
1275 | [60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'], |
||
1276 | [60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'], |
||
1277 | [60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', |
||
1278 | 61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'], |
||
1279 | // RFC 4419 - diffie-hellman-group-exchange-sha{1,256} |
||
1280 | [30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD', |
||
1281 | 31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP', |
||
1282 | 32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT', |
||
1283 | 33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY', |
||
1284 | 34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'], |
||
1285 | // RFC 5656 - Elliptic Curves (for curve25519-sha256@libssh.org) |
||
1286 | [30 => 'NET_SSH2_MSG_KEX_ECDH_INIT', |
||
1287 | 31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY'] |
||
1288 | ); |
||
1289 | |||
827 | daniel-mar | 1290 | /** |
1291 | * Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508 |
||
1292 | * @var \WeakReference<SSH2>|SSH2 |
||
1293 | */ |
||
1294 | self::$connections[$this->getResourceId()] = class_exists('WeakReference') |
||
1295 | ? \WeakReference::create($this) |
||
1296 | : $this; |
||
1297 | |||
1298 | if (is_resource($host)) { |
||
1299 | $this->fsock = $host; |
||
1300 | return; |
||
1301 | } |
||
1302 | |||
1303 | if (Strings::is_stringable($host)) { |
||
1304 | $this->host = $host; |
||
1305 | $this->port = $port; |
||
1306 | $this->timeout = $timeout; |
||
1307 | } |
||
1308 | } |
||
1309 | |||
1310 | /** |
||
1311 | * Set Crypto Engine Mode |
||
1312 | * |
||
1313 | * Possible $engine values: |
||
1314 | * OpenSSL, mcrypt, Eval, PHP |
||
1315 | * |
||
1316 | * @param int $engine |
||
874 | daniel-mar | 1317 | * @access public |
827 | daniel-mar | 1318 | */ |
1319 | public static function setCryptoEngine($engine) |
||
1320 | { |
||
1321 | self::$crypto_engine = $engine; |
||
1322 | } |
||
1323 | |||
1324 | /** |
||
1325 | * Send Identification String First |
||
1326 | * |
||
1327 | * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, |
||
1328 | * both sides MUST send an identification string". It does not say which side sends it first. In |
||
1329 | * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1330 | * |
||
874 | daniel-mar | 1331 | * @access public |
827 | daniel-mar | 1332 | */ |
1333 | public function sendIdentificationStringFirst() |
||
1334 | { |
||
1335 | $this->send_id_string_first = true; |
||
1336 | } |
||
1337 | |||
1338 | /** |
||
1339 | * Send Identification String Last |
||
1340 | * |
||
1341 | * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, |
||
1342 | * both sides MUST send an identification string". It does not say which side sends it first. In |
||
1343 | * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1344 | * |
||
874 | daniel-mar | 1345 | * @access public |
827 | daniel-mar | 1346 | */ |
1347 | public function sendIdentificationStringLast() |
||
1348 | { |
||
1349 | $this->send_id_string_first = false; |
||
1350 | } |
||
1351 | |||
1352 | /** |
||
1353 | * Send SSH_MSG_KEXINIT First |
||
1354 | * |
||
1355 | * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending |
||
1356 | * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory |
||
1357 | * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1358 | * |
||
874 | daniel-mar | 1359 | * @access public |
827 | daniel-mar | 1360 | */ |
1361 | public function sendKEXINITFirst() |
||
1362 | { |
||
1363 | $this->send_kex_first = true; |
||
1364 | } |
||
1365 | |||
1366 | /** |
||
1367 | * Send SSH_MSG_KEXINIT Last |
||
1368 | * |
||
1369 | * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending |
||
1370 | * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory |
||
1371 | * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1372 | * |
||
874 | daniel-mar | 1373 | * @access public |
827 | daniel-mar | 1374 | */ |
1375 | public function sendKEXINITLast() |
||
1376 | { |
||
1377 | $this->send_kex_first = false; |
||
1378 | } |
||
1379 | |||
1380 | /** |
||
1381 | * Connect to an SSHv2 server |
||
1382 | * |
||
1383 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
1384 | * @throws \RuntimeException on other errors |
||
874 | daniel-mar | 1385 | * @access private |
827 | daniel-mar | 1386 | */ |
1387 | private function connect() |
||
1388 | { |
||
1389 | if ($this->bitmap & self::MASK_CONSTRUCTOR) { |
||
1390 | return; |
||
1391 | } |
||
1392 | |||
1393 | $this->bitmap |= self::MASK_CONSTRUCTOR; |
||
1394 | |||
1395 | $this->curTimeout = $this->timeout; |
||
1396 | |||
1397 | $this->last_packet = microtime(true); |
||
1398 | |||
1399 | if (!is_resource($this->fsock)) { |
||
1400 | $start = microtime(true); |
||
1401 | // with stream_select a timeout of 0 means that no timeout takes place; |
||
1402 | // with fsockopen a timeout of 0 means that you instantly timeout |
||
1403 | // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0 |
||
1404 | $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout); |
||
1405 | if (!$this->fsock) { |
||
1406 | $host = $this->host . ':' . $this->port; |
||
1407 | throw new UnableToConnectException(rtrim("Cannot connect to $host. Error $errno. $errstr")); |
||
1408 | } |
||
1409 | $elapsed = microtime(true) - $start; |
||
1410 | |||
1411 | if ($this->curTimeout) { |
||
1412 | $this->curTimeout -= $elapsed; |
||
1413 | if ($this->curTimeout < 0) { |
||
1414 | throw new \RuntimeException('Connection timed out whilst attempting to open socket connection'); |
||
1415 | } |
||
1416 | } |
||
1417 | } |
||
1418 | |||
1419 | $this->identifier = $this->generate_identifier(); |
||
1420 | |||
1421 | if ($this->send_id_string_first) { |
||
1422 | fputs($this->fsock, $this->identifier . "\r\n"); |
||
1423 | } |
||
1424 | |||
1425 | /* According to the SSH2 specs, |
||
1426 | |||
1427 | "The server MAY send other lines of data before sending the version |
||
1428 | string. Each line SHOULD be terminated by a Carriage Return and Line |
||
1429 | Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded |
||
1430 | in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients |
||
1431 | MUST be able to process such lines." */ |
||
1432 | $data = ''; |
||
1433 | while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) { |
||
1434 | $line = ''; |
||
1435 | while (true) { |
||
1436 | if ($this->curTimeout) { |
||
1437 | if ($this->curTimeout < 0) { |
||
1438 | throw new \RuntimeException('Connection timed out whilst receiving server identification string'); |
||
1439 | } |
||
1440 | $read = [$this->fsock]; |
||
1441 | $write = $except = null; |
||
1442 | $start = microtime(true); |
||
1443 | $sec = (int) floor($this->curTimeout); |
||
1444 | $usec = (int) (1000000 * ($this->curTimeout - $sec)); |
||
1445 | if (@stream_select($read, $write, $except, $sec, $usec) === false) { |
||
1446 | throw new \RuntimeException('Connection timed out whilst receiving server identification string'); |
||
1447 | } |
||
1448 | $elapsed = microtime(true) - $start; |
||
1449 | $this->curTimeout -= $elapsed; |
||
1450 | } |
||
1451 | |||
1452 | $temp = stream_get_line($this->fsock, 255, "\n"); |
||
1453 | if ($temp === false) { |
||
1454 | throw new \RuntimeException('Error reading from socket'); |
||
1455 | } |
||
1456 | if (strlen($temp) == 255) { |
||
1457 | continue; |
||
1458 | } |
||
1459 | |||
1460 | $line .= "$temp\n"; |
||
1461 | |||
1462 | // quoting RFC4253, "Implementers who wish to maintain |
||
1463 | // compatibility with older, undocumented versions of this protocol may |
||
1464 | // want to process the identification string without expecting the |
||
1465 | // presence of the carriage return character for reasons described in |
||
1466 | // Section 5 of this document." |
||
1467 | |||
1468 | //if (substr($line, -2) == "\r\n") { |
||
1469 | // break; |
||
1470 | //} |
||
1471 | |||
1472 | break; |
||
1473 | } |
||
1474 | |||
1475 | $data .= $line; |
||
1476 | } |
||
1477 | |||
1478 | if (feof($this->fsock)) { |
||
1479 | $this->bitmap = 0; |
||
1480 | throw new ConnectionClosedException('Connection closed by server'); |
||
1481 | } |
||
1482 | |||
1483 | $extra = $matches[1]; |
||
1484 | |||
1485 | if (defined('NET_SSH2_LOGGING')) { |
||
1486 | $this->append_log('<-', $matches[0]); |
||
1487 | $this->append_log('->', $this->identifier . "\r\n"); |
||
1488 | } |
||
1489 | |||
1490 | $this->server_identifier = trim($temp, "\r\n"); |
||
1491 | if (strlen($extra)) { |
||
1492 | $this->errors[] = $data; |
||
1493 | } |
||
1494 | |||
1495 | if (version_compare($matches[3], '1.99', '<')) { |
||
1496 | $this->bitmap = 0; |
||
1497 | throw new UnableToConnectException("Cannot connect to SSH $matches[3] servers"); |
||
1498 | } |
||
1499 | |||
1500 | if (!$this->send_id_string_first) { |
||
1501 | fputs($this->fsock, $this->identifier . "\r\n"); |
||
1502 | } |
||
1503 | |||
1504 | if (!$this->send_kex_first) { |
||
1505 | $response = $this->get_binary_packet(); |
||
1506 | |||
874 | daniel-mar | 1507 | if (is_bool($response) || !strlen($response) || ord($response[0]) != NET_SSH2_MSG_KEXINIT) { |
827 | daniel-mar | 1508 | $this->bitmap = 0; |
1509 | throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT'); |
||
1510 | } |
||
1511 | |||
1512 | $this->key_exchange($response); |
||
1513 | } |
||
1514 | |||
1515 | if ($this->send_kex_first) { |
||
1516 | $this->key_exchange(); |
||
1517 | } |
||
1518 | |||
1519 | $this->bitmap |= self::MASK_CONNECTED; |
||
1520 | |||
1521 | return true; |
||
1522 | } |
||
1523 | |||
1524 | /** |
||
1525 | * Generates the SSH identifier |
||
1526 | * |
||
1527 | * You should overwrite this method in your own class if you want to use another identifier |
||
1528 | * |
||
874 | daniel-mar | 1529 | * @access protected |
827 | daniel-mar | 1530 | * @return string |
1531 | */ |
||
1532 | private function generate_identifier() |
||
1533 | { |
||
1534 | $identifier = 'SSH-2.0-phpseclib_3.0'; |
||
1535 | |||
1536 | $ext = []; |
||
1537 | if (extension_loaded('sodium')) { |
||
1538 | $ext[] = 'libsodium'; |
||
1539 | } |
||
1540 | |||
1541 | if (extension_loaded('openssl')) { |
||
1542 | $ext[] = 'openssl'; |
||
1543 | } elseif (extension_loaded('mcrypt')) { |
||
1544 | $ext[] = 'mcrypt'; |
||
1545 | } |
||
1546 | |||
1547 | if (extension_loaded('gmp')) { |
||
1548 | $ext[] = 'gmp'; |
||
1549 | } elseif (extension_loaded('bcmath')) { |
||
1550 | $ext[] = 'bcmath'; |
||
1551 | } |
||
1552 | |||
1553 | if (!empty($ext)) { |
||
1554 | $identifier .= ' (' . implode(', ', $ext) . ')'; |
||
1555 | } |
||
1556 | |||
1557 | return $identifier; |
||
1558 | } |
||
1559 | |||
1560 | /** |
||
1561 | * Key Exchange |
||
1562 | * |
||
1563 | * @return bool |
||
1564 | * @param string|bool $kexinit_payload_server optional |
||
1565 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
1566 | * @throws \RuntimeException on other errors |
||
1567 | * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when none of the algorithms phpseclib has loaded are compatible |
||
874 | daniel-mar | 1568 | * @access private |
827 | daniel-mar | 1569 | */ |
1570 | private function key_exchange($kexinit_payload_server = false) |
||
1571 | { |
||
1572 | $preferred = $this->preferred; |
||
1573 | $send_kex = true; |
||
1574 | |||
1575 | $kex_algorithms = isset($preferred['kex']) ? |
||
1576 | $preferred['kex'] : |
||
1577 | SSH2::getSupportedKEXAlgorithms(); |
||
1578 | $server_host_key_algorithms = isset($preferred['hostkey']) ? |
||
1579 | $preferred['hostkey'] : |
||
1580 | SSH2::getSupportedHostKeyAlgorithms(); |
||
1581 | $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ? |
||
1582 | $preferred['server_to_client']['crypt'] : |
||
1583 | SSH2::getSupportedEncryptionAlgorithms(); |
||
1584 | $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ? |
||
1585 | $preferred['client_to_server']['crypt'] : |
||
1586 | SSH2::getSupportedEncryptionAlgorithms(); |
||
1587 | $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ? |
||
1588 | $preferred['server_to_client']['mac'] : |
||
1589 | SSH2::getSupportedMACAlgorithms(); |
||
1590 | $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ? |
||
1591 | $preferred['client_to_server']['mac'] : |
||
1592 | SSH2::getSupportedMACAlgorithms(); |
||
1593 | $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ? |
||
1594 | $preferred['server_to_client']['comp'] : |
||
1595 | SSH2::getSupportedCompressionAlgorithms(); |
||
1596 | $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ? |
||
1597 | $preferred['client_to_server']['comp'] : |
||
1598 | SSH2::getSupportedCompressionAlgorithms(); |
||
1599 | |||
1600 | // some SSH servers have buggy implementations of some of the above algorithms |
||
1601 | switch (true) { |
||
1602 | case $this->server_identifier == 'SSH-2.0-SSHD': |
||
1603 | case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK': |
||
1604 | if (!isset($preferred['server_to_client']['mac'])) { |
||
1605 | $s2c_mac_algorithms = array_values(array_diff( |
||
1606 | $s2c_mac_algorithms, |
||
1607 | ['hmac-sha1-96', 'hmac-md5-96'] |
||
1608 | )); |
||
1609 | } |
||
1610 | if (!isset($preferred['client_to_server']['mac'])) { |
||
1611 | $c2s_mac_algorithms = array_values(array_diff( |
||
1612 | $c2s_mac_algorithms, |
||
1613 | ['hmac-sha1-96', 'hmac-md5-96'] |
||
1614 | )); |
||
1615 | } |
||
1616 | } |
||
1617 | |||
1618 | $client_cookie = Random::string(16); |
||
1619 | |||
874 | daniel-mar | 1620 | $kexinit_payload_client = pack('Ca*', NET_SSH2_MSG_KEXINIT, $client_cookie); |
827 | daniel-mar | 1621 | $kexinit_payload_client .= Strings::packSSH2( |
1622 | 'L10bN', |
||
1623 | $kex_algorithms, |
||
1624 | $server_host_key_algorithms, |
||
1625 | $c2s_encryption_algorithms, |
||
1626 | $s2c_encryption_algorithms, |
||
1627 | $c2s_mac_algorithms, |
||
1628 | $s2c_mac_algorithms, |
||
1629 | $c2s_compression_algorithms, |
||
1630 | $s2c_compression_algorithms, |
||
1631 | [], // language, client to server |
||
1632 | [], // language, server to client |
||
1633 | false, // first_kex_packet_follows |
||
1634 | |||
1635 | ); |
||
1636 | |||
1637 | if ($kexinit_payload_server === false) { |
||
1638 | $this->send_binary_packet($kexinit_payload_client); |
||
1639 | |||
1640 | $kexinit_payload_server = $this->get_binary_packet(); |
||
1641 | |||
1642 | if ( |
||
1643 | is_bool($kexinit_payload_server) |
||
1644 | || !strlen($kexinit_payload_server) |
||
874 | daniel-mar | 1645 | || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT |
827 | daniel-mar | 1646 | ) { |
874 | daniel-mar | 1647 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
827 | daniel-mar | 1648 | throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT'); |
1649 | } |
||
1650 | |||
1651 | $send_kex = false; |
||
1652 | } |
||
1653 | |||
1654 | $response = $kexinit_payload_server; |
||
1655 | Strings::shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) |
||
1656 | $server_cookie = Strings::shift($response, 16); |
||
1657 | |||
1658 | list( |
||
1659 | $this->kex_algorithms, |
||
1660 | $this->server_host_key_algorithms, |
||
1661 | $this->encryption_algorithms_client_to_server, |
||
1662 | $this->encryption_algorithms_server_to_client, |
||
1663 | $this->mac_algorithms_client_to_server, |
||
1664 | $this->mac_algorithms_server_to_client, |
||
1665 | $this->compression_algorithms_client_to_server, |
||
1666 | $this->compression_algorithms_server_to_client, |
||
1667 | $this->languages_client_to_server, |
||
1668 | $this->languages_server_to_client, |
||
1669 | $first_kex_packet_follows |
||
1670 | ) = Strings::unpackSSH2('L10C', $response); |
||
1671 | |||
1672 | if ($send_kex) { |
||
1673 | $this->send_binary_packet($kexinit_payload_client); |
||
1674 | } |
||
1675 | |||
1676 | // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange |
||
1677 | |||
1678 | // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the |
||
1679 | // diffie-hellman key exchange as fast as possible |
||
1680 | $decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client); |
||
1681 | $decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt); |
||
1682 | if ($decryptKeyLength === null) { |
||
874 | daniel-mar | 1683 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1684 | throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found'); |
1685 | } |
||
1686 | |||
1687 | $encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server); |
||
1688 | $encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt); |
||
1689 | if ($encryptKeyLength === null) { |
||
874 | daniel-mar | 1690 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1691 | throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found'); |
1692 | } |
||
1693 | |||
1694 | // through diffie-hellman key exchange a symmetric key is obtained |
||
1695 | $this->kex_algorithm = self::array_intersect_first($kex_algorithms, $this->kex_algorithms); |
||
1696 | if ($this->kex_algorithm === false) { |
||
874 | daniel-mar | 1697 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1698 | throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found'); |
1699 | } |
||
1700 | |||
1701 | $server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms); |
||
1702 | if ($server_host_key_algorithm === false) { |
||
874 | daniel-mar | 1703 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1704 | throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found'); |
1705 | } |
||
1706 | |||
1707 | $mac_algorithm_out = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server); |
||
1708 | if ($mac_algorithm_out === false) { |
||
874 | daniel-mar | 1709 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1710 | throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found'); |
1711 | } |
||
1712 | |||
1713 | $mac_algorithm_in = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client); |
||
1714 | if ($mac_algorithm_in === false) { |
||
874 | daniel-mar | 1715 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1716 | throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found'); |
1717 | } |
||
1718 | |||
1719 | $compression_map = [ |
||
1720 | 'none' => self::NET_SSH2_COMPRESSION_NONE, |
||
1721 | 'zlib' => self::NET_SSH2_COMPRESSION_ZLIB, |
||
1722 | 'zlib@openssh.com' => self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH |
||
1723 | ]; |
||
1724 | |||
1725 | $compression_algorithm_in = self::array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client); |
||
1726 | if ($compression_algorithm_in === false) { |
||
874 | daniel-mar | 1727 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1728 | throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found'); |
1729 | } |
||
1730 | $this->decompress = $compression_map[$compression_algorithm_in]; |
||
1731 | |||
1732 | $compression_algorithm_out = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); |
||
1733 | if ($compression_algorithm_out === false) { |
||
874 | daniel-mar | 1734 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1735 | throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found'); |
1736 | } |
||
1737 | $this->compress = $compression_map[$compression_algorithm_out]; |
||
1738 | |||
1739 | switch ($this->kex_algorithm) { |
||
1740 | case 'diffie-hellman-group15-sha512': |
||
1741 | case 'diffie-hellman-group16-sha512': |
||
1742 | case 'diffie-hellman-group17-sha512': |
||
1743 | case 'diffie-hellman-group18-sha512': |
||
1744 | case 'ecdh-sha2-nistp521': |
||
1745 | $kexHash = new Hash('sha512'); |
||
1746 | break; |
||
1747 | case 'ecdh-sha2-nistp384': |
||
1748 | $kexHash = new Hash('sha384'); |
||
1749 | break; |
||
1750 | case 'diffie-hellman-group-exchange-sha256': |
||
1751 | case 'diffie-hellman-group14-sha256': |
||
1752 | case 'ecdh-sha2-nistp256': |
||
1753 | case 'curve25519-sha256@libssh.org': |
||
1754 | case 'curve25519-sha256': |
||
1755 | $kexHash = new Hash('sha256'); |
||
1756 | break; |
||
1757 | default: |
||
1758 | $kexHash = new Hash('sha1'); |
||
1759 | } |
||
1760 | |||
1761 | // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty. |
||
1762 | |||
1763 | $exchange_hash_rfc4419 = ''; |
||
1764 | |||
1765 | if (strpos($this->kex_algorithm, 'curve25519-sha256') === 0 || strpos($this->kex_algorithm, 'ecdh-sha2-nistp') === 0) { |
||
1766 | $curve = strpos($this->kex_algorithm, 'curve25519-sha256') === 0 ? |
||
1767 | 'Curve25519' : |
||
1768 | substr($this->kex_algorithm, 10); |
||
1769 | $ourPrivate = EC::createKey($curve); |
||
1770 | $ourPublicBytes = $ourPrivate->getPublicKey()->getEncodedCoordinates(); |
||
874 | daniel-mar | 1771 | $clientKexInitMessage = 'NET_SSH2_MSG_KEX_ECDH_INIT'; |
1772 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEX_ECDH_REPLY'; |
||
827 | daniel-mar | 1773 | } else { |
1774 | if (strpos($this->kex_algorithm, 'diffie-hellman-group-exchange') === 0) { |
||
1775 | $dh_group_sizes_packed = pack( |
||
1776 | 'NNN', |
||
1777 | $this->kex_dh_group_size_min, |
||
1778 | $this->kex_dh_group_size_preferred, |
||
1779 | $this->kex_dh_group_size_max |
||
1780 | ); |
||
1781 | $packet = pack( |
||
1782 | 'Ca*', |
||
874 | daniel-mar | 1783 | NET_SSH2_MSG_KEXDH_GEX_REQUEST, |
827 | daniel-mar | 1784 | $dh_group_sizes_packed |
1785 | ); |
||
1786 | $this->send_binary_packet($packet); |
||
874 | daniel-mar | 1787 | $this->updateLogHistory('UNKNOWN (34)', 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'); |
827 | daniel-mar | 1788 | |
1789 | $response = $this->get_binary_packet(); |
||
1790 | |||
1791 | list($type, $primeBytes, $gBytes) = Strings::unpackSSH2('Css', $response); |
||
874 | daniel-mar | 1792 | if ($type != NET_SSH2_MSG_KEXDH_GEX_GROUP) { |
1793 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1794 | throw new \UnexpectedValueException('Expected SSH_MSG_KEX_DH_GEX_GROUP'); |
1795 | } |
||
874 | daniel-mar | 1796 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEXDH_GEX_GROUP'); |
827 | daniel-mar | 1797 | $prime = new BigInteger($primeBytes, -256); |
1798 | $g = new BigInteger($gBytes, -256); |
||
1799 | |||
1800 | $exchange_hash_rfc4419 = $dh_group_sizes_packed . Strings::packSSH2( |
||
1801 | 'ss', |
||
1802 | $primeBytes, |
||
1803 | $gBytes |
||
1804 | ); |
||
1805 | |||
1806 | $params = DH::createParameters($prime, $g); |
||
874 | daniel-mar | 1807 | $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_GEX_INIT'; |
1808 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_GEX_REPLY'; |
||
827 | daniel-mar | 1809 | } else { |
1810 | $params = DH::createParameters($this->kex_algorithm); |
||
874 | daniel-mar | 1811 | $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_INIT'; |
1812 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_REPLY'; |
||
827 | daniel-mar | 1813 | } |
1814 | |||
1815 | $keyLength = min($kexHash->getLengthInBytes(), max($encryptKeyLength, $decryptKeyLength)); |
||
1816 | |||
1817 | $ourPrivate = DH::createKey($params, 16 * $keyLength); // 2 * 8 * $keyLength |
||
1818 | $ourPublic = $ourPrivate->getPublicKey()->toBigInteger(); |
||
1819 | $ourPublicBytes = $ourPublic->toBytes(true); |
||
1820 | } |
||
1821 | |||
874 | daniel-mar | 1822 | $data = pack('CNa*', constant($clientKexInitMessage), strlen($ourPublicBytes), $ourPublicBytes); |
827 | daniel-mar | 1823 | |
1824 | $this->send_binary_packet($data); |
||
1825 | |||
1826 | switch ($clientKexInitMessage) { |
||
874 | daniel-mar | 1827 | case 'NET_SSH2_MSG_KEX_ECDH_INIT': |
1828 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_INIT', 'NET_SSH2_MSG_KEX_ECDH_INIT'); |
||
827 | daniel-mar | 1829 | break; |
874 | daniel-mar | 1830 | case 'NET_SSH2_MSG_KEXDH_GEX_INIT': |
1831 | $this->updateLogHistory('UNKNOWN (32)', 'NET_SSH2_MSG_KEXDH_GEX_INIT'); |
||
827 | daniel-mar | 1832 | } |
1833 | |||
1834 | $response = $this->get_binary_packet(); |
||
1835 | |||
1836 | list( |
||
1837 | $type, |
||
1838 | $server_public_host_key, |
||
1839 | $theirPublicBytes, |
||
1840 | $this->signature |
||
1841 | ) = Strings::unpackSSH2('Csss', $response); |
||
1842 | |||
874 | daniel-mar | 1843 | if ($type != constant($serverKexReplyMessage)) { |
1844 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1845 | throw new \UnexpectedValueException("Expected $serverKexReplyMessage"); |
1846 | } |
||
1847 | switch ($serverKexReplyMessage) { |
||
874 | daniel-mar | 1848 | case 'NET_SSH2_MSG_KEX_ECDH_REPLY': |
1849 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEX_ECDH_REPLY'); |
||
827 | daniel-mar | 1850 | break; |
874 | daniel-mar | 1851 | case 'NET_SSH2_MSG_KEXDH_GEX_REPLY': |
1852 | $this->updateLogHistory('UNKNOWN (33)', 'NET_SSH2_MSG_KEXDH_GEX_REPLY'); |
||
827 | daniel-mar | 1853 | } |
1854 | |||
1855 | $this->server_public_host_key = $server_public_host_key; |
||
1856 | list($public_key_format) = Strings::unpackSSH2('s', $server_public_host_key); |
||
1857 | if (strlen($this->signature) < 4) { |
||
1858 | throw new \LengthException('The signature needs at least four bytes'); |
||
1859 | } |
||
1860 | $temp = unpack('Nlength', substr($this->signature, 0, 4)); |
||
1861 | $this->signature_format = substr($this->signature, 4, $temp['length']); |
||
1862 | |||
1863 | $keyBytes = DH::computeSecret($ourPrivate, $theirPublicBytes); |
||
1864 | if (($keyBytes & "\xFF\x80") === "\x00\x00") { |
||
1865 | $keyBytes = substr($keyBytes, 1); |
||
1866 | } elseif (($keyBytes[0] & "\x80") === "\x80") { |
||
1867 | $keyBytes = "\0$keyBytes"; |
||
1868 | } |
||
1869 | |||
1870 | $this->exchange_hash = Strings::packSSH2( |
||
1871 | 's5', |
||
1872 | $this->identifier, |
||
1873 | $this->server_identifier, |
||
1874 | $kexinit_payload_client, |
||
1875 | $kexinit_payload_server, |
||
1876 | $this->server_public_host_key |
||
1877 | ); |
||
1878 | $this->exchange_hash .= $exchange_hash_rfc4419; |
||
1879 | $this->exchange_hash .= Strings::packSSH2( |
||
1880 | 's3', |
||
1881 | $ourPublicBytes, |
||
1882 | $theirPublicBytes, |
||
1883 | $keyBytes |
||
1884 | ); |
||
1885 | |||
1886 | $this->exchange_hash = $kexHash->hash($this->exchange_hash); |
||
1887 | |||
1888 | if ($this->session_id === false) { |
||
1889 | $this->session_id = $this->exchange_hash; |
||
1890 | } |
||
1891 | |||
1892 | switch ($server_host_key_algorithm) { |
||
1893 | case 'rsa-sha2-256': |
||
1894 | case 'rsa-sha2-512': |
||
1895 | //case 'ssh-rsa': |
||
1896 | $expected_key_format = 'ssh-rsa'; |
||
1897 | break; |
||
1898 | default: |
||
1899 | $expected_key_format = $server_host_key_algorithm; |
||
1900 | } |
||
1901 | if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) { |
||
1902 | switch (true) { |
||
1903 | case $this->signature_format == $server_host_key_algorithm: |
||
1904 | case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512': |
||
1905 | case $this->signature_format != 'ssh-rsa': |
||
874 | daniel-mar | 1906 | $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 1907 | throw new \RuntimeException('Server Host Key Algorithm Mismatch (' . $this->signature_format . ' vs ' . $server_host_key_algorithm . ')'); |
1908 | } |
||
1909 | } |
||
1910 | |||
874 | daniel-mar | 1911 | $packet = pack('C', NET_SSH2_MSG_NEWKEYS); |
827 | daniel-mar | 1912 | $this->send_binary_packet($packet); |
1913 | |||
1914 | $response = $this->get_binary_packet(); |
||
1915 | |||
1916 | if ($response === false) { |
||
874 | daniel-mar | 1917 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 1918 | throw new ConnectionClosedException('Connection closed by server'); |
1919 | } |
||
1920 | |||
1921 | list($type) = Strings::unpackSSH2('C', $response); |
||
874 | daniel-mar | 1922 | if ($type != NET_SSH2_MSG_NEWKEYS) { |
1923 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1924 | throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS'); |
1925 | } |
||
1926 | |||
1927 | $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); |
||
1928 | |||
1929 | $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt); |
||
1930 | if ($this->encrypt) { |
||
1931 | if (self::$crypto_engine) { |
||
1932 | $this->encrypt->setPreferredEngine(self::$crypto_engine); |
||
1933 | } |
||
1934 | if ($this->encrypt->getBlockLengthInBytes()) { |
||
1935 | $this->encrypt_block_size = $this->encrypt->getBlockLengthInBytes(); |
||
1936 | } |
||
1937 | $this->encrypt->disablePadding(); |
||
1938 | |||
1939 | if ($this->encrypt->usesIV()) { |
||
1940 | $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); |
||
1941 | while ($this->encrypt_block_size > strlen($iv)) { |
||
1942 | $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); |
||
1943 | } |
||
1944 | $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); |
||
1945 | } |
||
1946 | |||
1947 | switch ($encrypt) { |
||
1948 | case 'aes128-gcm@openssh.com': |
||
1949 | case 'aes256-gcm@openssh.com': |
||
1950 | $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); |
||
1951 | $this->encryptFixedPart = substr($nonce, 0, 4); |
||
1952 | $this->encryptInvocationCounter = substr($nonce, 4, 8); |
||
1953 | // fall-through |
||
1954 | case 'chacha20-poly1305@openssh.com': |
||
1955 | break; |
||
1956 | default: |
||
1957 | $this->encrypt->enableContinuousBuffer(); |
||
1958 | } |
||
1959 | |||
1960 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); |
||
1961 | while ($encryptKeyLength > strlen($key)) { |
||
1962 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
1963 | } |
||
1964 | switch ($encrypt) { |
||
1965 | case 'chacha20-poly1305@openssh.com': |
||
1966 | $encryptKeyLength = 32; |
||
1967 | $this->lengthEncrypt = self::encryption_algorithm_to_crypt_instance($encrypt); |
||
1968 | $this->lengthEncrypt->setKey(substr($key, 32, 32)); |
||
1969 | } |
||
1970 | $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); |
||
1971 | $this->encryptName = $encrypt; |
||
1972 | } |
||
1973 | |||
1974 | $this->decrypt = self::encryption_algorithm_to_crypt_instance($decrypt); |
||
1975 | if ($this->decrypt) { |
||
1976 | if (self::$crypto_engine) { |
||
1977 | $this->decrypt->setPreferredEngine(self::$crypto_engine); |
||
1978 | } |
||
1979 | if ($this->decrypt->getBlockLengthInBytes()) { |
||
1980 | $this->decrypt_block_size = $this->decrypt->getBlockLengthInBytes(); |
||
1981 | } |
||
1982 | $this->decrypt->disablePadding(); |
||
1983 | |||
1984 | if ($this->decrypt->usesIV()) { |
||
1985 | $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); |
||
1986 | while ($this->decrypt_block_size > strlen($iv)) { |
||
1987 | $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); |
||
1988 | } |
||
1989 | $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); |
||
1990 | } |
||
1991 | |||
1992 | switch ($decrypt) { |
||
1993 | case 'aes128-gcm@openssh.com': |
||
1994 | case 'aes256-gcm@openssh.com': |
||
1995 | // see https://tools.ietf.org/html/rfc5647#section-7.1 |
||
1996 | $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); |
||
1997 | $this->decryptFixedPart = substr($nonce, 0, 4); |
||
1998 | $this->decryptInvocationCounter = substr($nonce, 4, 8); |
||
1999 | // fall-through |
||
2000 | case 'chacha20-poly1305@openssh.com': |
||
2001 | break; |
||
2002 | default: |
||
2003 | $this->decrypt->enableContinuousBuffer(); |
||
2004 | } |
||
2005 | |||
2006 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); |
||
2007 | while ($decryptKeyLength > strlen($key)) { |
||
2008 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
2009 | } |
||
2010 | switch ($decrypt) { |
||
2011 | case 'chacha20-poly1305@openssh.com': |
||
2012 | $decryptKeyLength = 32; |
||
2013 | $this->lengthDecrypt = self::encryption_algorithm_to_crypt_instance($decrypt); |
||
2014 | $this->lengthDecrypt->setKey(substr($key, 32, 32)); |
||
2015 | } |
||
2016 | $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); |
||
2017 | $this->decryptName = $decrypt; |
||
2018 | } |
||
2019 | |||
2020 | /* The "arcfour128" algorithm is the RC4 cipher, as described in |
||
2021 | [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream |
||
2022 | generated by the cipher MUST be discarded, and the first byte of the |
||
2023 | first encrypted packet MUST be encrypted using the 1537th byte of |
||
2024 | keystream. |
||
2025 | |||
2026 | -- http://tools.ietf.org/html/rfc4345#section-4 */ |
||
2027 | if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') { |
||
2028 | $this->encrypt->encrypt(str_repeat("\0", 1536)); |
||
2029 | } |
||
2030 | if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') { |
||
2031 | $this->decrypt->decrypt(str_repeat("\0", 1536)); |
||
2032 | } |
||
2033 | |||
2034 | if (!$this->encrypt->usesNonce()) { |
||
2035 | list($this->hmac_create, $createKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_out); |
||
2036 | } else { |
||
2037 | $this->hmac_create = new \stdClass(); |
||
2038 | $this->hmac_create_name = $mac_algorithm_out; |
||
2039 | //$mac_algorithm_out = 'none'; |
||
2040 | $createKeyLength = 0; |
||
2041 | } |
||
2042 | |||
2043 | if ($this->hmac_create instanceof Hash) { |
||
2044 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id); |
||
2045 | while ($createKeyLength > strlen($key)) { |
||
2046 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
2047 | } |
||
2048 | $this->hmac_create->setKey(substr($key, 0, $createKeyLength)); |
||
2049 | $this->hmac_create_name = $mac_algorithm_out; |
||
2050 | $this->hmac_create_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_out); |
||
2051 | } |
||
2052 | |||
2053 | if (!$this->decrypt->usesNonce()) { |
||
2054 | list($this->hmac_check, $checkKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_in); |
||
2055 | $this->hmac_size = $this->hmac_check->getLengthInBytes(); |
||
2056 | } else { |
||
2057 | $this->hmac_check = new \stdClass(); |
||
2058 | $this->hmac_check_name = $mac_algorithm_in; |
||
2059 | //$mac_algorithm_in = 'none'; |
||
2060 | $checkKeyLength = 0; |
||
2061 | $this->hmac_size = 0; |
||
2062 | } |
||
2063 | |||
2064 | if ($this->hmac_check instanceof Hash) { |
||
2065 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id); |
||
2066 | while ($checkKeyLength > strlen($key)) { |
||
2067 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
2068 | } |
||
2069 | $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); |
||
2070 | $this->hmac_check_name = $mac_algorithm_in; |
||
2071 | $this->hmac_check_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_in); |
||
2072 | } |
||
2073 | |||
2074 | $this->regenerate_compression_context = $this->regenerate_decompression_context = true; |
||
2075 | |||
2076 | return true; |
||
2077 | } |
||
2078 | |||
2079 | /** |
||
2080 | * Maps an encryption algorithm name to the number of key bytes. |
||
2081 | * |
||
2082 | * @param string $algorithm Name of the encryption algorithm |
||
2083 | * @return int|null Number of bytes as an integer or null for unknown |
||
874 | daniel-mar | 2084 | * @access private |
827 | daniel-mar | 2085 | */ |
2086 | private function encryption_algorithm_to_key_size($algorithm) |
||
2087 | { |
||
2088 | if ($this->bad_key_size_fix && self::bad_algorithm_candidate($algorithm)) { |
||
2089 | return 16; |
||
2090 | } |
||
2091 | |||
2092 | switch ($algorithm) { |
||
2093 | case 'none': |
||
2094 | return 0; |
||
2095 | case 'aes128-gcm@openssh.com': |
||
2096 | case 'aes128-cbc': |
||
2097 | case 'aes128-ctr': |
||
2098 | case 'arcfour': |
||
2099 | case 'arcfour128': |
||
2100 | case 'blowfish-cbc': |
||
2101 | case 'blowfish-ctr': |
||
2102 | case 'twofish128-cbc': |
||
2103 | case 'twofish128-ctr': |
||
2104 | return 16; |
||
2105 | case '3des-cbc': |
||
2106 | case '3des-ctr': |
||
2107 | case 'aes192-cbc': |
||
2108 | case 'aes192-ctr': |
||
2109 | case 'twofish192-cbc': |
||
2110 | case 'twofish192-ctr': |
||
2111 | return 24; |
||
2112 | case 'aes256-gcm@openssh.com': |
||
2113 | case 'aes256-cbc': |
||
2114 | case 'aes256-ctr': |
||
2115 | case 'arcfour256': |
||
2116 | case 'twofish-cbc': |
||
2117 | case 'twofish256-cbc': |
||
2118 | case 'twofish256-ctr': |
||
2119 | return 32; |
||
2120 | case 'chacha20-poly1305@openssh.com': |
||
2121 | return 64; |
||
2122 | } |
||
2123 | return null; |
||
2124 | } |
||
2125 | |||
2126 | /** |
||
2127 | * Maps an encryption algorithm name to an instance of a subclass of |
||
2128 | * \phpseclib3\Crypt\Common\SymmetricKey. |
||
2129 | * |
||
2130 | * @param string $algorithm Name of the encryption algorithm |
||
2131 | * @return SymmetricKey|null |
||
874 | daniel-mar | 2132 | * @access private |
827 | daniel-mar | 2133 | */ |
2134 | private static function encryption_algorithm_to_crypt_instance($algorithm) |
||
2135 | { |
||
2136 | switch ($algorithm) { |
||
2137 | case '3des-cbc': |
||
2138 | return new TripleDES('cbc'); |
||
2139 | case '3des-ctr': |
||
2140 | return new TripleDES('ctr'); |
||
2141 | case 'aes256-cbc': |
||
2142 | case 'aes192-cbc': |
||
2143 | case 'aes128-cbc': |
||
2144 | return new Rijndael('cbc'); |
||
2145 | case 'aes256-ctr': |
||
2146 | case 'aes192-ctr': |
||
2147 | case 'aes128-ctr': |
||
2148 | return new Rijndael('ctr'); |
||
2149 | case 'blowfish-cbc': |
||
2150 | return new Blowfish('cbc'); |
||
2151 | case 'blowfish-ctr': |
||
2152 | return new Blowfish('ctr'); |
||
2153 | case 'twofish128-cbc': |
||
2154 | case 'twofish192-cbc': |
||
2155 | case 'twofish256-cbc': |
||
2156 | case 'twofish-cbc': |
||
2157 | return new Twofish('cbc'); |
||
2158 | case 'twofish128-ctr': |
||
2159 | case 'twofish192-ctr': |
||
2160 | case 'twofish256-ctr': |
||
2161 | return new Twofish('ctr'); |
||
2162 | case 'arcfour': |
||
2163 | case 'arcfour128': |
||
2164 | case 'arcfour256': |
||
2165 | return new RC4(); |
||
2166 | case 'aes128-gcm@openssh.com': |
||
2167 | case 'aes256-gcm@openssh.com': |
||
2168 | return new Rijndael('gcm'); |
||
2169 | case 'chacha20-poly1305@openssh.com': |
||
2170 | return new ChaCha20(); |
||
2171 | } |
||
2172 | return null; |
||
2173 | } |
||
2174 | |||
2175 | /** |
||
2176 | * Maps an encryption algorithm name to an instance of a subclass of |
||
2177 | * \phpseclib3\Crypt\Hash. |
||
2178 | * |
||
2179 | * @param string $algorithm Name of the encryption algorithm |
||
2180 | * @return array{Hash, int}|null |
||
874 | daniel-mar | 2181 | * @access private |
827 | daniel-mar | 2182 | */ |
2183 | private static function mac_algorithm_to_hash_instance($algorithm) |
||
2184 | { |
||
2185 | switch ($algorithm) { |
||
2186 | case 'umac-64@openssh.com': |
||
2187 | case 'umac-64-etm@openssh.com': |
||
2188 | return [new Hash('umac-64'), 16]; |
||
2189 | case 'umac-128@openssh.com': |
||
2190 | case 'umac-128-etm@openssh.com': |
||
2191 | return [new Hash('umac-128'), 16]; |
||
2192 | case 'hmac-sha2-512': |
||
2193 | case 'hmac-sha2-512-etm@openssh.com': |
||
2194 | return [new Hash('sha512'), 64]; |
||
2195 | case 'hmac-sha2-256': |
||
2196 | case 'hmac-sha2-256-etm@openssh.com': |
||
2197 | return [new Hash('sha256'), 32]; |
||
2198 | case 'hmac-sha1': |
||
2199 | case 'hmac-sha1-etm@openssh.com': |
||
2200 | return [new Hash('sha1'), 20]; |
||
2201 | case 'hmac-sha1-96': |
||
2202 | return [new Hash('sha1-96'), 20]; |
||
2203 | case 'hmac-md5': |
||
2204 | return [new Hash('md5'), 16]; |
||
2205 | case 'hmac-md5-96': |
||
2206 | return [new Hash('md5-96'), 16]; |
||
2207 | } |
||
2208 | } |
||
2209 | |||
2210 | /* |
||
2211 | * Tests whether or not proposed algorithm has a potential for issues |
||
2212 | * |
||
2213 | * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html |
||
2214 | * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 |
||
2215 | * @param string $algorithm Name of the encryption algorithm |
||
2216 | * @return bool |
||
874 | daniel-mar | 2217 | * @access private |
827 | daniel-mar | 2218 | */ |
2219 | private static function bad_algorithm_candidate($algorithm) |
||
2220 | { |
||
2221 | switch ($algorithm) { |
||
2222 | case 'arcfour256': |
||
2223 | case 'aes192-ctr': |
||
2224 | case 'aes256-ctr': |
||
2225 | return true; |
||
2226 | } |
||
2227 | |||
2228 | return false; |
||
2229 | } |
||
2230 | |||
2231 | /** |
||
2232 | * Login |
||
2233 | * |
||
2234 | * The $password parameter can be a plaintext password, a \phpseclib3\Crypt\RSA|EC|DSA object, a \phpseclib3\System\SSH\Agent object or an array |
||
2235 | * |
||
2236 | * @param string $username |
||
2237 | * @param string|AsymmetricKey|array[]|Agent|null ...$args |
||
2238 | * @return bool |
||
2239 | * @see self::_login() |
||
874 | daniel-mar | 2240 | * @access public |
827 | daniel-mar | 2241 | */ |
2242 | public function login($username, ...$args) |
||
2243 | { |
||
2244 | $this->auth[] = func_get_args(); |
||
2245 | |||
2246 | // try logging with 'none' as an authentication method first since that's what |
||
2247 | // PuTTY does |
||
2248 | if (substr($this->server_identifier, 0, 15) != 'SSH-2.0-CoreFTP' && $this->auth_methods_to_continue === null) { |
||
2249 | if ($this->sublogin($username)) { |
||
2250 | return true; |
||
2251 | } |
||
2252 | if (!count($args)) { |
||
2253 | return false; |
||
2254 | } |
||
2255 | } |
||
2256 | return $this->sublogin($username, ...$args); |
||
2257 | } |
||
2258 | |||
2259 | /** |
||
2260 | * Login Helper |
||
2261 | * |
||
2262 | * @param string $username |
||
2263 | * @param string ...$args |
||
2264 | * @return bool |
||
2265 | * @see self::_login_helper() |
||
874 | daniel-mar | 2266 | * @access private |
827 | daniel-mar | 2267 | */ |
2268 | protected function sublogin($username, ...$args) |
||
2269 | { |
||
2270 | if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { |
||
2271 | $this->connect(); |
||
2272 | } |
||
2273 | |||
2274 | if (empty($args)) { |
||
2275 | return $this->login_helper($username); |
||
2276 | } |
||
2277 | |||
2278 | foreach ($args as $arg) { |
||
2279 | switch (true) { |
||
2280 | case $arg instanceof PublicKey: |
||
2281 | throw new \UnexpectedValueException('A PublicKey object was passed to the login method instead of a PrivateKey object'); |
||
2282 | case $arg instanceof PrivateKey: |
||
2283 | case $arg instanceof Agent: |
||
2284 | case is_array($arg): |
||
2285 | case Strings::is_stringable($arg): |
||
2286 | break; |
||
2287 | default: |
||
2288 | throw new \UnexpectedValueException('$password needs to either be an instance of \phpseclib3\Crypt\Common\PrivateKey, \System\SSH\Agent, an array or a string'); |
||
2289 | } |
||
2290 | } |
||
2291 | |||
2292 | while (count($args)) { |
||
2293 | if (!$this->auth_methods_to_continue || !$this->smartMFA) { |
||
2294 | $newargs = $args; |
||
2295 | $args = []; |
||
2296 | } else { |
||
2297 | $newargs = []; |
||
2298 | foreach ($this->auth_methods_to_continue as $method) { |
||
2299 | switch ($method) { |
||
2300 | case 'publickey': |
||
2301 | foreach ($args as $key => $arg) { |
||
2302 | if ($arg instanceof PrivateKey || $arg instanceof Agent) { |
||
2303 | $newargs[] = $arg; |
||
2304 | unset($args[$key]); |
||
2305 | break; |
||
2306 | } |
||
2307 | } |
||
2308 | break; |
||
2309 | case 'keyboard-interactive': |
||
2310 | $hasArray = $hasString = false; |
||
2311 | foreach ($args as $arg) { |
||
2312 | if ($hasArray || is_array($arg)) { |
||
2313 | $hasArray = true; |
||
2314 | break; |
||
2315 | } |
||
2316 | if ($hasString || Strings::is_stringable($arg)) { |
||
2317 | $hasString = true; |
||
2318 | break; |
||
2319 | } |
||
2320 | } |
||
2321 | if ($hasArray && $hasString) { |
||
2322 | foreach ($args as $key => $arg) { |
||
2323 | if (is_array($arg)) { |
||
2324 | $newargs[] = $arg; |
||
2325 | break 2; |
||
2326 | } |
||
2327 | } |
||
2328 | } |
||
2329 | // fall-through |
||
2330 | case 'password': |
||
2331 | foreach ($args as $key => $arg) { |
||
2332 | $newargs[] = $arg; |
||
2333 | unset($args[$key]); |
||
2334 | break; |
||
2335 | } |
||
2336 | } |
||
2337 | } |
||
2338 | } |
||
2339 | |||
2340 | if (!count($newargs)) { |
||
2341 | return false; |
||
2342 | } |
||
2343 | |||
2344 | foreach ($newargs as $arg) { |
||
2345 | if ($this->login_helper($username, $arg)) { |
||
2346 | return true; |
||
2347 | } |
||
2348 | } |
||
2349 | } |
||
2350 | return false; |
||
2351 | } |
||
2352 | |||
2353 | /** |
||
2354 | * Login Helper |
||
2355 | * |
||
2356 | * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} |
||
2357 | * by sending dummy SSH_MSG_IGNORE messages.} |
||
2358 | * |
||
2359 | * @param string $username |
||
2360 | * @param string|AsymmetricKey|array[]|Agent|null ...$args |
||
2361 | * @return bool |
||
2362 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
2363 | * @throws \RuntimeException on other errors |
||
874 | daniel-mar | 2364 | * @access private |
827 | daniel-mar | 2365 | */ |
2366 | private function login_helper($username, $password = null) |
||
2367 | { |
||
2368 | if (!($this->bitmap & self::MASK_CONNECTED)) { |
||
2369 | return false; |
||
2370 | } |
||
2371 | |||
2372 | if (!($this->bitmap & self::MASK_LOGIN_REQ)) { |
||
874 | daniel-mar | 2373 | $packet = Strings::packSSH2('Cs', NET_SSH2_MSG_SERVICE_REQUEST, 'ssh-userauth'); |
827 | daniel-mar | 2374 | $this->send_binary_packet($packet); |
2375 | |||
2376 | try { |
||
2377 | $response = $this->get_binary_packet(); |
||
2378 | } catch (\Exception $e) { |
||
2379 | if ($this->retry_connect) { |
||
2380 | $this->retry_connect = false; |
||
2381 | $this->connect(); |
||
2382 | return $this->login_helper($username, $password); |
||
2383 | } |
||
874 | daniel-mar | 2384 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 2385 | throw new ConnectionClosedException('Connection closed by server'); |
2386 | } |
||
2387 | |||
2388 | list($type, $service) = Strings::unpackSSH2('Cs', $response); |
||
874 | daniel-mar | 2389 | if ($type != NET_SSH2_MSG_SERVICE_ACCEPT || $service != 'ssh-userauth') { |
2390 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 2391 | throw new \UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT'); |
2392 | } |
||
2393 | $this->bitmap |= self::MASK_LOGIN_REQ; |
||
2394 | } |
||
2395 | |||
2396 | if (strlen($this->last_interactive_response)) { |
||
2397 | return !Strings::is_stringable($password) && !is_array($password) ? false : $this->keyboard_interactive_process($password); |
||
2398 | } |
||
2399 | |||
2400 | if ($password instanceof PrivateKey) { |
||
2401 | return $this->privatekey_login($username, $password); |
||
2402 | } |
||
2403 | |||
2404 | if ($password instanceof Agent) { |
||
2405 | return $this->ssh_agent_login($username, $password); |
||
2406 | } |
||
2407 | |||
2408 | if (is_array($password)) { |
||
2409 | if ($this->keyboard_interactive_login($username, $password)) { |
||
2410 | $this->bitmap |= self::MASK_LOGIN; |
||
2411 | return true; |
||
2412 | } |
||
2413 | return false; |
||
2414 | } |
||
2415 | |||
2416 | if (!isset($password)) { |
||
2417 | $packet = Strings::packSSH2( |
||
2418 | 'Cs3', |
||
874 | daniel-mar | 2419 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2420 | $username, |
2421 | 'ssh-connection', |
||
2422 | 'none' |
||
2423 | ); |
||
2424 | |||
2425 | $this->send_binary_packet($packet); |
||
2426 | |||
2427 | $response = $this->get_binary_packet(); |
||
2428 | |||
2429 | list($type) = Strings::unpackSSH2('C', $response); |
||
2430 | switch ($type) { |
||
874 | daniel-mar | 2431 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2432 | $this->bitmap |= self::MASK_LOGIN; |
2433 | return true; |
||
874 | daniel-mar | 2434 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2435 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
2436 | $this->auth_methods_to_continue = $auth_methods; |
||
2437 | // fall-through |
||
2438 | default: |
||
2439 | return false; |
||
2440 | } |
||
2441 | } |
||
2442 | |||
2443 | $packet = Strings::packSSH2( |
||
2444 | 'Cs3bs', |
||
874 | daniel-mar | 2445 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2446 | $username, |
2447 | 'ssh-connection', |
||
2448 | 'password', |
||
2449 | false, |
||
2450 | $password |
||
2451 | ); |
||
2452 | |||
2453 | // remove the username and password from the logged packet |
||
2454 | if (!defined('NET_SSH2_LOGGING')) { |
||
2455 | $logged = null; |
||
2456 | } else { |
||
2457 | $logged = Strings::packSSH2( |
||
2458 | 'Cs3bs', |
||
874 | daniel-mar | 2459 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2460 | $username, |
2461 | 'ssh-connection', |
||
2462 | 'password', |
||
2463 | false, |
||
2464 | 'password' |
||
2465 | ); |
||
2466 | } |
||
2467 | |||
2468 | $this->send_binary_packet($packet, $logged); |
||
2469 | |||
2470 | $response = $this->get_binary_packet(); |
||
874 | daniel-mar | 2471 | |
827 | daniel-mar | 2472 | list($type) = Strings::unpackSSH2('C', $response); |
2473 | switch ($type) { |
||
874 | daniel-mar | 2474 | case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed |
2475 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'); |
||
827 | daniel-mar | 2476 | |
2477 | list($message) = Strings::unpackSSH2('s', $response); |
||
2478 | $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $message; |
||
2479 | |||
874 | daniel-mar | 2480 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER); |
2481 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
||
827 | daniel-mar | 2482 | // can we use keyboard-interactive authentication? if not then either the login is bad or the server employees |
2483 | // multi-factor authentication |
||
2484 | list($auth_methods, $partial_success) = Strings::unpackSSH2('Lb', $response); |
||
2485 | $this->auth_methods_to_continue = $auth_methods; |
||
2486 | if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) { |
||
2487 | if ($this->keyboard_interactive_login($username, $password)) { |
||
2488 | $this->bitmap |= self::MASK_LOGIN; |
||
2489 | return true; |
||
2490 | } |
||
2491 | return false; |
||
2492 | } |
||
2493 | return false; |
||
874 | daniel-mar | 2494 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2495 | $this->bitmap |= self::MASK_LOGIN; |
2496 | return true; |
||
2497 | } |
||
2498 | |||
2499 | return false; |
||
2500 | } |
||
2501 | |||
2502 | /** |
||
2503 | * Login via keyboard-interactive authentication |
||
2504 | * |
||
2505 | * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details. This is not a full-featured keyboard-interactive authenticator. |
||
2506 | * |
||
2507 | * @param string $username |
||
2508 | * @param string|array $password |
||
2509 | * @return bool |
||
874 | daniel-mar | 2510 | * @access private |
827 | daniel-mar | 2511 | */ |
2512 | private function keyboard_interactive_login($username, $password) |
||
2513 | { |
||
2514 | $packet = Strings::packSSH2( |
||
2515 | 'Cs5', |
||
874 | daniel-mar | 2516 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2517 | $username, |
2518 | 'ssh-connection', |
||
2519 | 'keyboard-interactive', |
||
2520 | '', // language tag |
||
2521 | '' // submethods |
||
2522 | ); |
||
2523 | $this->send_binary_packet($packet); |
||
2524 | |||
2525 | return $this->keyboard_interactive_process($password); |
||
2526 | } |
||
2527 | |||
2528 | /** |
||
2529 | * Handle the keyboard-interactive requests / responses. |
||
2530 | * |
||
2531 | * @param string|array ...$responses |
||
2532 | * @return bool |
||
2533 | * @throws \RuntimeException on connection error |
||
874 | daniel-mar | 2534 | * @access private |
827 | daniel-mar | 2535 | */ |
2536 | private function keyboard_interactive_process(...$responses) |
||
2537 | { |
||
2538 | if (strlen($this->last_interactive_response)) { |
||
2539 | $response = $this->last_interactive_response; |
||
2540 | } else { |
||
2541 | $orig = $response = $this->get_binary_packet(); |
||
2542 | } |
||
2543 | |||
2544 | list($type) = Strings::unpackSSH2('C', $response); |
||
2545 | switch ($type) { |
||
874 | daniel-mar | 2546 | case NET_SSH2_MSG_USERAUTH_INFO_REQUEST: |
827 | daniel-mar | 2547 | list( |
2548 | , // name; may be empty |
||
2549 | , // instruction; may be empty |
||
2550 | , // language tag; may be empty |
||
2551 | $num_prompts |
||
2552 | ) = Strings::unpackSSH2('s3N', $response); |
||
2553 | |||
2554 | for ($i = 0; $i < count($responses); $i++) { |
||
2555 | if (is_array($responses[$i])) { |
||
2556 | foreach ($responses[$i] as $key => $value) { |
||
2557 | $this->keyboard_requests_responses[$key] = $value; |
||
2558 | } |
||
2559 | unset($responses[$i]); |
||
2560 | } |
||
2561 | } |
||
2562 | $responses = array_values($responses); |
||
2563 | |||
2564 | if (isset($this->keyboard_requests_responses)) { |
||
2565 | for ($i = 0; $i < $num_prompts; $i++) { |
||
2566 | list( |
||
2567 | $prompt, // prompt - ie. "Password: "; must not be empty |
||
2568 | // echo |
||
2569 | ) = Strings::unpackSSH2('sC', $response); |
||
2570 | foreach ($this->keyboard_requests_responses as $key => $value) { |
||
2571 | if (substr($prompt, 0, strlen($key)) == $key) { |
||
2572 | $responses[] = $value; |
||
2573 | break; |
||
2574 | } |
||
2575 | } |
||
2576 | } |
||
2577 | } |
||
2578 | |||
2579 | // see http://tools.ietf.org/html/rfc4256#section-3.2 |
||
2580 | if (strlen($this->last_interactive_response)) { |
||
2581 | $this->last_interactive_response = ''; |
||
2582 | } else { |
||
874 | daniel-mar | 2583 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST'); |
827 | daniel-mar | 2584 | } |
2585 | |||
2586 | if (!count($responses) && $num_prompts) { |
||
2587 | $this->last_interactive_response = $orig; |
||
2588 | return false; |
||
2589 | } |
||
2590 | |||
2591 | /* |
||
2592 | After obtaining the requested information from the user, the client |
||
2593 | MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message. |
||
2594 | */ |
||
2595 | // see http://tools.ietf.org/html/rfc4256#section-3.4 |
||
874 | daniel-mar | 2596 | $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses)); |
827 | daniel-mar | 2597 | for ($i = 0; $i < count($responses); $i++) { |
2598 | $packet .= Strings::packSSH2('s', $responses[$i]); |
||
2599 | $logged .= Strings::packSSH2('s', 'dummy-answer'); |
||
2600 | } |
||
2601 | |||
2602 | $this->send_binary_packet($packet, $logged); |
||
2603 | |||
874 | daniel-mar | 2604 | $this->updateLogHistory('UNKNOWN (61)', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'); |
827 | daniel-mar | 2605 | |
2606 | /* |
||
2607 | After receiving the response, the server MUST send either an |
||
2608 | SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another |
||
2609 | SSH_MSG_USERAUTH_INFO_REQUEST message. |
||
2610 | */ |
||
2611 | // maybe phpseclib should force close the connection after x request / responses? unless something like that is done |
||
2612 | // there could be an infinite loop of request / responses. |
||
2613 | return $this->keyboard_interactive_process(); |
||
874 | daniel-mar | 2614 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2615 | return true; |
874 | daniel-mar | 2616 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2617 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
2618 | $this->auth_methods_to_continue = $auth_methods; |
||
2619 | return false; |
||
2620 | } |
||
2621 | |||
2622 | return false; |
||
2623 | } |
||
2624 | |||
2625 | /** |
||
2626 | * Login with an ssh-agent provided key |
||
2627 | * |
||
2628 | * @param string $username |
||
2629 | * @param \phpseclib3\System\SSH\Agent $agent |
||
2630 | * @return bool |
||
874 | daniel-mar | 2631 | * @access private |
827 | daniel-mar | 2632 | */ |
2633 | private function ssh_agent_login($username, Agent $agent) |
||
2634 | { |
||
2635 | $this->agent = $agent; |
||
2636 | $keys = $agent->requestIdentities(); |
||
2637 | foreach ($keys as $key) { |
||
2638 | if ($this->privatekey_login($username, $key)) { |
||
2639 | return true; |
||
2640 | } |
||
2641 | } |
||
2642 | |||
2643 | return false; |
||
2644 | } |
||
2645 | |||
2646 | /** |
||
2647 | * Login with an RSA private key |
||
2648 | * |
||
2649 | * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} |
||
2650 | * by sending dummy SSH_MSG_IGNORE messages.} |
||
2651 | * |
||
2652 | * @param string $username |
||
2653 | * @param \phpseclib3\Crypt\Common\PrivateKey $privatekey |
||
2654 | * @return bool |
||
2655 | * @throws \RuntimeException on connection error |
||
874 | daniel-mar | 2656 | * @access private |
827 | daniel-mar | 2657 | */ |
2658 | private function privatekey_login($username, PrivateKey $privatekey) |
||
2659 | { |
||
2660 | $publickey = $privatekey->getPublicKey(); |
||
2661 | |||
2662 | if ($publickey instanceof RSA) { |
||
2663 | $privatekey = $privatekey->withPadding(RSA::SIGNATURE_PKCS1); |
||
2664 | $algos = ['rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa']; |
||
2665 | if (isset($this->preferred['hostkey'])) { |
||
2666 | $algos = array_intersect($this->preferred['hostkey'], $algos); |
||
2667 | } |
||
2668 | $algo = self::array_intersect_first($algos, $this->server_host_key_algorithms); |
||
2669 | switch ($algo) { |
||
2670 | case 'rsa-sha2-512': |
||
2671 | $hash = 'sha512'; |
||
2672 | $signatureType = 'rsa-sha2-512'; |
||
2673 | break; |
||
2674 | case 'rsa-sha2-256': |
||
2675 | $hash = 'sha256'; |
||
2676 | $signatureType = 'rsa-sha2-256'; |
||
2677 | break; |
||
2678 | //case 'ssh-rsa': |
||
2679 | default: |
||
2680 | $hash = 'sha1'; |
||
2681 | $signatureType = 'ssh-rsa'; |
||
2682 | } |
||
2683 | } elseif ($publickey instanceof EC) { |
||
2684 | $privatekey = $privatekey->withSignatureFormat('SSH2'); |
||
2685 | $curveName = $privatekey->getCurve(); |
||
2686 | switch ($curveName) { |
||
2687 | case 'Ed25519': |
||
2688 | $hash = 'sha512'; |
||
2689 | $signatureType = 'ssh-ed25519'; |
||
2690 | break; |
||
2691 | case 'secp256r1': // nistp256 |
||
2692 | $hash = 'sha256'; |
||
2693 | $signatureType = 'ecdsa-sha2-nistp256'; |
||
2694 | break; |
||
2695 | case 'secp384r1': // nistp384 |
||
2696 | $hash = 'sha384'; |
||
2697 | $signatureType = 'ecdsa-sha2-nistp384'; |
||
2698 | break; |
||
2699 | case 'secp521r1': // nistp521 |
||
2700 | $hash = 'sha512'; |
||
2701 | $signatureType = 'ecdsa-sha2-nistp521'; |
||
2702 | break; |
||
2703 | default: |
||
2704 | if (is_array($curveName)) { |
||
2705 | throw new UnsupportedCurveException('Specified Curves are not supported by SSH2'); |
||
2706 | } |
||
2707 | throw new UnsupportedCurveException('Named Curve of ' . $curveName . ' is not supported by phpseclib3\'s SSH2 implementation'); |
||
2708 | } |
||
2709 | } elseif ($publickey instanceof DSA) { |
||
2710 | $privatekey = $privatekey->withSignatureFormat('SSH2'); |
||
2711 | $hash = 'sha1'; |
||
2712 | $signatureType = 'ssh-dss'; |
||
2713 | } else { |
||
2714 | throw new UnsupportedAlgorithmException('Please use either an RSA key, an EC one or a DSA key'); |
||
2715 | } |
||
2716 | |||
2717 | $publickeyStr = $publickey->toString('OpenSSH', ['binary' => true]); |
||
2718 | |||
2719 | $part1 = Strings::packSSH2( |
||
2720 | 'Csss', |
||
874 | daniel-mar | 2721 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2722 | $username, |
2723 | 'ssh-connection', |
||
2724 | 'publickey' |
||
2725 | ); |
||
2726 | $part2 = Strings::packSSH2('ss', $signatureType, $publickeyStr); |
||
2727 | |||
2728 | $packet = $part1 . chr(0) . $part2; |
||
2729 | $this->send_binary_packet($packet); |
||
2730 | |||
2731 | $response = $this->get_binary_packet(); |
||
2732 | |||
2733 | list($type) = Strings::unpackSSH2('C', $response); |
||
2734 | switch ($type) { |
||
874 | daniel-mar | 2735 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2736 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
2737 | $this->auth_methods_to_continue = $auth_methods; |
||
2738 | $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE'; |
||
2739 | return false; |
||
874 | daniel-mar | 2740 | case NET_SSH2_MSG_USERAUTH_PK_OK: |
827 | daniel-mar | 2741 | // we'll just take it on faith that the public key blob and the public key algorithm name are as |
2742 | // they should be |
||
874 | daniel-mar | 2743 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PK_OK'); |
827 | daniel-mar | 2744 | break; |
874 | daniel-mar | 2745 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2746 | $this->bitmap |= self::MASK_LOGIN; |
2747 | return true; |
||
2748 | default: |
||
874 | daniel-mar | 2749 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2750 | throw new ConnectionClosedException('Unexpected response to publickey authentication pt 1'); |
2751 | } |
||
2752 | |||
2753 | $packet = $part1 . chr(1) . $part2; |
||
2754 | $privatekey = $privatekey->withHash($hash); |
||
2755 | $signature = $privatekey->sign(Strings::packSSH2('s', $this->session_id) . $packet); |
||
2756 | if ($publickey instanceof RSA) { |
||
2757 | $signature = Strings::packSSH2('ss', $signatureType, $signature); |
||
2758 | } |
||
2759 | $packet .= Strings::packSSH2('s', $signature); |
||
2760 | |||
2761 | $this->send_binary_packet($packet); |
||
2762 | |||
2763 | $response = $this->get_binary_packet(); |
||
2764 | |||
2765 | list($type) = Strings::unpackSSH2('C', $response); |
||
2766 | switch ($type) { |
||
874 | daniel-mar | 2767 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2768 | // either the login is bad or the server employs multi-factor authentication |
2769 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
||
2770 | $this->auth_methods_to_continue = $auth_methods; |
||
2771 | return false; |
||
874 | daniel-mar | 2772 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2773 | $this->bitmap |= self::MASK_LOGIN; |
2774 | return true; |
||
2775 | } |
||
2776 | |||
874 | daniel-mar | 2777 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2778 | throw new ConnectionClosedException('Unexpected response to publickey authentication pt 2'); |
2779 | } |
||
2780 | |||
2781 | /** |
||
2782 | * Set Timeout |
||
2783 | * |
||
2784 | * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely. setTimeout() makes it so it'll timeout. |
||
2785 | * Setting $timeout to false or 0 will mean there is no timeout. |
||
2786 | * |
||
2787 | * @param mixed $timeout |
||
874 | daniel-mar | 2788 | * @access public |
827 | daniel-mar | 2789 | */ |
2790 | public function setTimeout($timeout) |
||
2791 | { |
||
2792 | $this->timeout = $this->curTimeout = $timeout; |
||
2793 | } |
||
2794 | |||
2795 | /** |
||
2796 | * Set Keep Alive |
||
2797 | * |
||
2798 | * Sends an SSH2_MSG_IGNORE message every x seconds, if x is a positive non-zero number. |
||
2799 | * |
||
2800 | * @param int $interval |
||
874 | daniel-mar | 2801 | * @access public |
827 | daniel-mar | 2802 | */ |
2803 | public function setKeepAlive($interval) |
||
2804 | { |
||
2805 | $this->keepAlive = $interval; |
||
2806 | } |
||
2807 | |||
2808 | /** |
||
2809 | * Get the output from stdError |
||
2810 | * |
||
874 | daniel-mar | 2811 | * @access public |
827 | daniel-mar | 2812 | */ |
2813 | public function getStdError() |
||
2814 | { |
||
2815 | return $this->stdErrorLog; |
||
2816 | } |
||
2817 | |||
2818 | /** |
||
2819 | * Execute Command |
||
2820 | * |
||
2821 | * If $callback is set to false then \phpseclib3\Net\SSH2::get_channel_packet(self::CHANNEL_EXEC) will need to be called manually. |
||
2822 | * In all likelihood, this is not a feature you want to be taking advantage of. |
||
2823 | * |
||
2824 | * @param string $command |
||
2825 | * @return string|bool |
||
2826 | * @psalm-return ($callback is callable ? bool : string|bool) |
||
2827 | * @throws \RuntimeException on connection error |
||
874 | daniel-mar | 2828 | * @access public |
827 | daniel-mar | 2829 | */ |
2830 | public function exec($command, callable $callback = null) |
||
2831 | { |
||
2832 | $this->curTimeout = $this->timeout; |
||
2833 | $this->is_timeout = false; |
||
2834 | $this->stdErrorLog = ''; |
||
2835 | |||
2836 | if (!$this->isAuthenticated()) { |
||
2837 | return false; |
||
2838 | } |
||
2839 | |||
2840 | if ($this->in_request_pty_exec) { |
||
2841 | throw new \RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); |
||
2842 | } |
||
2843 | |||
2844 | // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to |
||
2845 | // be adjusted". 0x7FFFFFFF is, at 2GB, the max size. technically, it should probably be decremented, but, |
||
2846 | // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway. |
||
2847 | // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info |
||
2848 | $this->window_size_server_to_client[self::CHANNEL_EXEC] = $this->window_size; |
||
2849 | // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy |
||
2850 | // uses 0x4000, that's what will be used here, as well. |
||
2851 | $packet_size = 0x4000; |
||
2852 | |||
2853 | $packet = Strings::packSSH2( |
||
2854 | 'CsN3', |
||
874 | daniel-mar | 2855 | NET_SSH2_MSG_CHANNEL_OPEN, |
827 | daniel-mar | 2856 | 'session', |
2857 | self::CHANNEL_EXEC, |
||
2858 | $this->window_size_server_to_client[self::CHANNEL_EXEC], |
||
2859 | $packet_size |
||
2860 | ); |
||
2861 | $this->send_binary_packet($packet); |
||
2862 | |||
874 | daniel-mar | 2863 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 2864 | |
2865 | $this->get_channel_packet(self::CHANNEL_EXEC); |
||
2866 | |||
2867 | if ($this->request_pty === true) { |
||
874 | daniel-mar | 2868 | $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); |
827 | daniel-mar | 2869 | $packet = Strings::packSSH2( |
2870 | 'CNsCsN4s', |
||
874 | daniel-mar | 2871 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 2872 | $this->server_channels[self::CHANNEL_EXEC], |
2873 | 'pty-req', |
||
2874 | 1, |
||
2875 | $this->term, |
||
2876 | $this->windowColumns, |
||
2877 | $this->windowRows, |
||
2878 | 0, |
||
2879 | 0, |
||
2880 | $terminal_modes |
||
2881 | ); |
||
2882 | |||
2883 | $this->send_binary_packet($packet); |
||
2884 | |||
874 | daniel-mar | 2885 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 2886 | if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { |
874 | daniel-mar | 2887 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2888 | throw new \RuntimeException('Unable to request pseudo-terminal'); |
2889 | } |
||
2890 | |||
2891 | $this->in_request_pty_exec = true; |
||
2892 | } |
||
2893 | |||
2894 | // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things |
||
2895 | // down. the one place where it might be desirable is if you're doing something like \phpseclib3\Net\SSH2::exec('ping localhost &'). |
||
2896 | // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then |
||
2897 | // then immediately terminate. without such a request exec() will loop indefinitely. the ping process won't end but |
||
2898 | // neither will your script. |
||
2899 | |||
2900 | // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by |
||
2901 | // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the |
||
2902 | // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. |
||
2903 | $packet = Strings::packSSH2( |
||
2904 | 'CNsCs', |
||
874 | daniel-mar | 2905 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 2906 | $this->server_channels[self::CHANNEL_EXEC], |
2907 | 'exec', |
||
2908 | 1, |
||
2909 | $command |
||
2910 | ); |
||
2911 | $this->send_binary_packet($packet); |
||
2912 | |||
874 | daniel-mar | 2913 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 2914 | |
2915 | if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { |
||
2916 | return false; |
||
2917 | } |
||
2918 | |||
874 | daniel-mar | 2919 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 2920 | |
2921 | if ($callback === false || $this->in_request_pty_exec) { |
||
2922 | return true; |
||
2923 | } |
||
2924 | |||
2925 | $output = ''; |
||
2926 | while (true) { |
||
2927 | $temp = $this->get_channel_packet(self::CHANNEL_EXEC); |
||
2928 | switch (true) { |
||
2929 | case $temp === true: |
||
2930 | return is_callable($callback) ? true : $output; |
||
2931 | case $temp === false: |
||
2932 | return false; |
||
2933 | default: |
||
2934 | if (is_callable($callback)) { |
||
2935 | if ($callback($temp) === true) { |
||
2936 | $this->close_channel(self::CHANNEL_EXEC); |
||
2937 | return true; |
||
2938 | } |
||
2939 | } else { |
||
2940 | $output .= $temp; |
||
2941 | } |
||
2942 | } |
||
2943 | } |
||
2944 | } |
||
2945 | |||
2946 | /** |
||
2947 | * Creates an interactive shell |
||
2948 | * |
||
2949 | * @see self::read() |
||
2950 | * @see self::write() |
||
2951 | * @return bool |
||
2952 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
2953 | * @throws \RuntimeException on other errors |
||
874 | daniel-mar | 2954 | * @access private |
827 | daniel-mar | 2955 | */ |
2956 | private function initShell() |
||
2957 | { |
||
2958 | if ($this->in_request_pty_exec === true) { |
||
2959 | return true; |
||
2960 | } |
||
2961 | |||
2962 | $this->window_size_server_to_client[self::CHANNEL_SHELL] = $this->window_size; |
||
2963 | $packet_size = 0x4000; |
||
2964 | |||
2965 | $packet = Strings::packSSH2( |
||
2966 | 'CsN3', |
||
874 | daniel-mar | 2967 | NET_SSH2_MSG_CHANNEL_OPEN, |
827 | daniel-mar | 2968 | 'session', |
2969 | self::CHANNEL_SHELL, |
||
2970 | $this->window_size_server_to_client[self::CHANNEL_SHELL], |
||
2971 | $packet_size |
||
2972 | ); |
||
2973 | |||
2974 | $this->send_binary_packet($packet); |
||
2975 | |||
874 | daniel-mar | 2976 | $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 2977 | |
2978 | $this->get_channel_packet(self::CHANNEL_SHELL); |
||
2979 | |||
874 | daniel-mar | 2980 | $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); |
827 | daniel-mar | 2981 | $packet = Strings::packSSH2( |
2982 | 'CNsbsN4s', |
||
874 | daniel-mar | 2983 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 2984 | $this->server_channels[self::CHANNEL_SHELL], |
2985 | 'pty-req', |
||
2986 | true, // want reply |
||
2987 | $this->term, |
||
2988 | $this->windowColumns, |
||
2989 | $this->windowRows, |
||
2990 | 0, |
||
2991 | 0, |
||
2992 | $terminal_modes |
||
2993 | ); |
||
2994 | |||
2995 | $this->send_binary_packet($packet); |
||
2996 | |||
874 | daniel-mar | 2997 | $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 2998 | |
2999 | if (!$this->get_channel_packet(self::CHANNEL_SHELL)) { |
||
3000 | throw new \RuntimeException('Unable to request pty'); |
||
3001 | } |
||
3002 | |||
3003 | $packet = Strings::packSSH2( |
||
3004 | 'CNsb', |
||
874 | daniel-mar | 3005 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3006 | $this->server_channels[self::CHANNEL_SHELL], |
3007 | 'shell', |
||
3008 | true // want reply |
||
3009 | ); |
||
3010 | $this->send_binary_packet($packet); |
||
3011 | |||
3012 | $response = $this->get_channel_packet(self::CHANNEL_SHELL); |
||
3013 | if ($response === false) { |
||
3014 | throw new \RuntimeException('Unable to request shell'); |
||
3015 | } |
||
3016 | |||
874 | daniel-mar | 3017 | $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 3018 | |
3019 | $this->bitmap |= self::MASK_SHELL; |
||
3020 | |||
3021 | return true; |
||
3022 | } |
||
3023 | |||
3024 | /** |
||
3025 | * Return the channel to be used with read() / write() |
||
3026 | * |
||
3027 | * @see self::read() |
||
3028 | * @see self::write() |
||
3029 | * @return int |
||
874 | daniel-mar | 3030 | * @access public |
827 | daniel-mar | 3031 | */ |
3032 | private function get_interactive_channel() |
||
3033 | { |
||
3034 | switch (true) { |
||
3035 | case $this->in_subsystem: |
||
3036 | return self::CHANNEL_SUBSYSTEM; |
||
3037 | case $this->in_request_pty_exec: |
||
3038 | return self::CHANNEL_EXEC; |
||
3039 | default: |
||
3040 | return self::CHANNEL_SHELL; |
||
3041 | } |
||
3042 | } |
||
3043 | |||
3044 | /** |
||
3045 | * Return an available open channel |
||
3046 | * |
||
3047 | * @return int |
||
874 | daniel-mar | 3048 | * @access public |
827 | daniel-mar | 3049 | */ |
3050 | private function get_open_channel() |
||
3051 | { |
||
3052 | $channel = self::CHANNEL_EXEC; |
||
3053 | do { |
||
874 | daniel-mar | 3054 | if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { |
827 | daniel-mar | 3055 | return $channel; |
3056 | } |
||
3057 | } while ($channel++ < self::CHANNEL_SUBSYSTEM); |
||
3058 | |||
3059 | return false; |
||
3060 | } |
||
3061 | |||
3062 | /** |
||
3063 | * Request agent forwarding of remote server |
||
3064 | * |
||
3065 | * @return bool |
||
874 | daniel-mar | 3066 | * @access public |
827 | daniel-mar | 3067 | */ |
3068 | public function requestAgentForwarding() |
||
3069 | { |
||
3070 | $request_channel = $this->get_open_channel(); |
||
3071 | if ($request_channel === false) { |
||
3072 | return false; |
||
3073 | } |
||
3074 | |||
3075 | $packet = Strings::packSSH2( |
||
3076 | 'CNsC', |
||
874 | daniel-mar | 3077 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3078 | $this->server_channels[$request_channel], |
3079 | 'auth-agent-req@openssh.com', |
||
3080 | 1 |
||
3081 | ); |
||
3082 | |||
874 | daniel-mar | 3083 | $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 3084 | |
3085 | $this->send_binary_packet($packet); |
||
3086 | |||
3087 | if (!$this->get_channel_packet($request_channel)) { |
||
3088 | return false; |
||
3089 | } |
||
3090 | |||
874 | daniel-mar | 3091 | $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 3092 | |
3093 | return true; |
||
3094 | } |
||
3095 | |||
3096 | /** |
||
3097 | * Returns the output of an interactive shell |
||
3098 | * |
||
3099 | * Returns when there's a match for $expect, which can take the form of a string literal or, |
||
3100 | * if $mode == self::READ_REGEX, a regular expression. |
||
3101 | * |
||
3102 | * @see self::write() |
||
3103 | * @param string $expect |
||
3104 | * @param int $mode |
||
3105 | * @return string|bool|null |
||
3106 | * @throws \RuntimeException on connection error |
||
874 | daniel-mar | 3107 | * @access public |
827 | daniel-mar | 3108 | */ |
3109 | public function read($expect = '', $mode = self::READ_SIMPLE) |
||
3110 | { |
||
3111 | $this->curTimeout = $this->timeout; |
||
3112 | $this->is_timeout = false; |
||
3113 | |||
3114 | if (!$this->isAuthenticated()) { |
||
3115 | throw new InsufficientSetupException('Operation disallowed prior to login()'); |
||
3116 | } |
||
3117 | |||
3118 | if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) { |
||
3119 | throw new \RuntimeException('Unable to initiate an interactive shell session'); |
||
3120 | } |
||
3121 | |||
3122 | $channel = $this->get_interactive_channel(); |
||
3123 | |||
3124 | if ($mode == self::READ_NEXT) { |
||
3125 | return $this->get_channel_packet($channel); |
||
3126 | } |
||
3127 | |||
3128 | $match = $expect; |
||
3129 | while (true) { |
||
3130 | if ($mode == self::READ_REGEX) { |
||
3131 | preg_match($expect, substr($this->interactiveBuffer, -1024), $matches); |
||
3132 | $match = isset($matches[0]) ? $matches[0] : ''; |
||
3133 | } |
||
3134 | $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; |
||
3135 | if ($pos !== false) { |
||
3136 | return Strings::shift($this->interactiveBuffer, $pos + strlen($match)); |
||
3137 | } |
||
3138 | $response = $this->get_channel_packet($channel); |
||
3139 | if ($response === true) { |
||
3140 | $this->in_request_pty_exec = false; |
||
3141 | return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); |
||
3142 | } |
||
3143 | |||
3144 | $this->interactiveBuffer .= $response; |
||
3145 | } |
||
3146 | } |
||
3147 | |||
3148 | /** |
||
3149 | * Inputs a command into an interactive shell. |
||
3150 | * |
||
3151 | * @see SSH2::read() |
||
3152 | * @param string $cmd |
||
3153 | * @return void |
||
3154 | * @throws \RuntimeException on connection error |
||
3155 | */ |
||
3156 | public function write($cmd) |
||
3157 | { |
||
3158 | if (!$this->isAuthenticated()) { |
||
3159 | throw new InsufficientSetupException('Operation disallowed prior to login()'); |
||
3160 | } |
||
3161 | |||
3162 | if (!($this->bitmap & self::MASK_SHELL) && !$this->initShell()) { |
||
3163 | throw new \RuntimeException('Unable to initiate an interactive shell session'); |
||
3164 | } |
||
3165 | |||
3166 | $this->send_channel_packet($this->get_interactive_channel(), $cmd); |
||
3167 | } |
||
3168 | |||
3169 | /** |
||
3170 | * Start a subsystem. |
||
3171 | * |
||
3172 | * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept |
||
3173 | * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened. |
||
3174 | * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and |
||
3175 | * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented |
||
3176 | * if there's sufficient demand for such a feature. |
||
3177 | * |
||
3178 | * @see self::stopSubsystem() |
||
3179 | * @param string $subsystem |
||
3180 | * @return bool |
||
874 | daniel-mar | 3181 | * @access public |
827 | daniel-mar | 3182 | */ |
3183 | public function startSubsystem($subsystem) |
||
3184 | { |
||
3185 | $this->window_size_server_to_client[self::CHANNEL_SUBSYSTEM] = $this->window_size; |
||
3186 | |||
3187 | $packet = Strings::packSSH2( |
||
3188 | 'CsN3', |
||
874 | daniel-mar | 3189 | NET_SSH2_MSG_CHANNEL_OPEN, |
827 | daniel-mar | 3190 | 'session', |
3191 | self::CHANNEL_SUBSYSTEM, |
||
3192 | $this->window_size, |
||
3193 | 0x4000 |
||
3194 | ); |
||
3195 | |||
3196 | $this->send_binary_packet($packet); |
||
3197 | |||
874 | daniel-mar | 3198 | $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 3199 | |
3200 | $this->get_channel_packet(self::CHANNEL_SUBSYSTEM); |
||
3201 | |||
3202 | $packet = Strings::packSSH2( |
||
3203 | 'CNsCs', |
||
874 | daniel-mar | 3204 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3205 | $this->server_channels[self::CHANNEL_SUBSYSTEM], |
3206 | 'subsystem', |
||
3207 | 1, |
||
3208 | $subsystem |
||
3209 | ); |
||
3210 | $this->send_binary_packet($packet); |
||
3211 | |||
874 | daniel-mar | 3212 | $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 3213 | |
3214 | if (!$this->get_channel_packet(self::CHANNEL_SUBSYSTEM)) { |
||
3215 | return false; |
||
3216 | } |
||
3217 | |||
874 | daniel-mar | 3218 | $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 3219 | |
3220 | $this->bitmap |= self::MASK_SHELL; |
||
3221 | $this->in_subsystem = true; |
||
3222 | |||
3223 | return true; |
||
3224 | } |
||
3225 | |||
3226 | /** |
||
3227 | * Stops a subsystem. |
||
3228 | * |
||
3229 | * @see self::startSubsystem() |
||
3230 | * @return bool |
||
874 | daniel-mar | 3231 | * @access public |
827 | daniel-mar | 3232 | */ |
3233 | public function stopSubsystem() |
||
3234 | { |
||
3235 | $this->in_subsystem = false; |
||
3236 | $this->close_channel(self::CHANNEL_SUBSYSTEM); |
||
3237 | return true; |
||
3238 | } |
||
3239 | |||
3240 | /** |
||
3241 | * Closes a channel |
||
3242 | * |
||
3243 | * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call |
||
3244 | * |
||
874 | daniel-mar | 3245 | * @access public |
827 | daniel-mar | 3246 | */ |
3247 | public function reset() |
||
3248 | { |
||
3249 | $this->close_channel($this->get_interactive_channel()); |
||
3250 | } |
||
3251 | |||
3252 | /** |
||
3253 | * Is timeout? |
||
3254 | * |
||
3255 | * Did exec() or read() return because they timed out or because they encountered the end? |
||
3256 | * |
||
874 | daniel-mar | 3257 | * @access public |
827 | daniel-mar | 3258 | */ |
3259 | public function isTimeout() |
||
3260 | { |
||
3261 | return $this->is_timeout; |
||
3262 | } |
||
3263 | |||
3264 | /** |
||
3265 | * Disconnect |
||
3266 | * |
||
874 | daniel-mar | 3267 | * @access public |
827 | daniel-mar | 3268 | */ |
3269 | public function disconnect() |
||
3270 | { |
||
874 | daniel-mar | 3271 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3272 | if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) { |
3273 | fclose($this->realtime_log_file); |
||
3274 | } |
||
3275 | unset(self::$connections[$this->getResourceId()]); |
||
3276 | } |
||
3277 | |||
3278 | /** |
||
3279 | * Destructor. |
||
3280 | * |
||
3281 | * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call |
||
3282 | * disconnect(). |
||
3283 | * |
||
874 | daniel-mar | 3284 | * @access public |
827 | daniel-mar | 3285 | */ |
3286 | public function __destruct() |
||
3287 | { |
||
3288 | $this->disconnect(); |
||
3289 | } |
||
3290 | |||
3291 | /** |
||
3292 | * Is the connection still active? |
||
3293 | * |
||
3294 | * @return bool |
||
874 | daniel-mar | 3295 | * @access public |
827 | daniel-mar | 3296 | */ |
3297 | public function isConnected() |
||
3298 | { |
||
3299 | return (bool) ($this->bitmap & self::MASK_CONNECTED); |
||
3300 | } |
||
3301 | |||
3302 | /** |
||
3303 | * Have you successfully been logged in? |
||
3304 | * |
||
3305 | * @return bool |
||
874 | daniel-mar | 3306 | * @access public |
827 | daniel-mar | 3307 | */ |
3308 | public function isAuthenticated() |
||
3309 | { |
||
3310 | return (bool) ($this->bitmap & self::MASK_LOGIN); |
||
3311 | } |
||
3312 | |||
3313 | /** |
||
3314 | * Pings a server connection, or tries to reconnect if the connection has gone down |
||
3315 | * |
||
3316 | * Inspired by http://php.net/manual/en/mysqli.ping.php |
||
3317 | * |
||
3318 | * @return bool |
||
3319 | */ |
||
3320 | public function ping() |
||
3321 | { |
||
3322 | if (!$this->isAuthenticated()) { |
||
3323 | if (!empty($this->auth)) { |
||
3324 | return $this->reconnect(); |
||
3325 | } |
||
3326 | return false; |
||
3327 | } |
||
3328 | |||
3329 | $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE] = $this->window_size; |
||
3330 | $packet_size = 0x4000; |
||
3331 | $packet = Strings::packSSH2( |
||
3332 | 'CsN3', |
||
874 | daniel-mar | 3333 | NET_SSH2_MSG_CHANNEL_OPEN, |
827 | daniel-mar | 3334 | 'session', |
3335 | self::CHANNEL_KEEP_ALIVE, |
||
3336 | $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE], |
||
3337 | $packet_size |
||
3338 | ); |
||
3339 | |||
3340 | try { |
||
3341 | $this->send_binary_packet($packet); |
||
3342 | |||
874 | daniel-mar | 3343 | $this->channel_status[self::CHANNEL_KEEP_ALIVE] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 3344 | |
3345 | $response = $this->get_channel_packet(self::CHANNEL_KEEP_ALIVE); |
||
3346 | } catch (\RuntimeException $e) { |
||
3347 | return $this->reconnect(); |
||
3348 | } |
||
3349 | |||
3350 | $this->close_channel(self::CHANNEL_KEEP_ALIVE); |
||
3351 | return true; |
||
3352 | } |
||
3353 | |||
3354 | /** |
||
3355 | * In situ reconnect method |
||
3356 | * |
||
3357 | * @return boolean |
||
3358 | */ |
||
3359 | private function reconnect() |
||
3360 | { |
||
874 | daniel-mar | 3361 | $this->reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 3362 | $this->retry_connect = true; |
3363 | $this->connect(); |
||
3364 | foreach ($this->auth as $auth) { |
||
3365 | $result = $this->login(...$auth); |
||
3366 | } |
||
3367 | return $result; |
||
3368 | } |
||
3369 | |||
3370 | /** |
||
3371 | * Resets a connection for re-use |
||
3372 | * |
||
3373 | * @param int $reason |
||
874 | daniel-mar | 3374 | * @access private |
827 | daniel-mar | 3375 | */ |
3376 | protected function reset_connection($reason) |
||
3377 | { |
||
3378 | $this->disconnect_helper($reason); |
||
3379 | $this->decrypt = $this->encrypt = false; |
||
3380 | $this->decrypt_block_size = $this->encrypt_block_size = 8; |
||
3381 | $this->hmac_check = $this->hmac_create = false; |
||
3382 | $this->hmac_size = false; |
||
3383 | $this->session_id = false; |
||
3384 | $this->retry_connect = true; |
||
3385 | $this->get_seq_no = $this->send_seq_no = 0; |
||
3386 | } |
||
3387 | |||
3388 | /** |
||
3389 | * Gets Binary Packets |
||
3390 | * |
||
3391 | * See '6. Binary Packet Protocol' of rfc4253 for more info. |
||
3392 | * |
||
3393 | * @see self::_send_binary_packet() |
||
3394 | * @param bool $skip_channel_filter |
||
3395 | * @return bool|string |
||
874 | daniel-mar | 3396 | * @access private |
827 | daniel-mar | 3397 | */ |
3398 | private function get_binary_packet($skip_channel_filter = false) |
||
3399 | { |
||
3400 | if ($skip_channel_filter) { |
||
3401 | if (!is_resource($this->fsock)) { |
||
3402 | throw new \InvalidArgumentException('fsock is not a resource.'); |
||
3403 | } |
||
3404 | $read = [$this->fsock]; |
||
3405 | $write = $except = null; |
||
3406 | |||
3407 | if (!$this->curTimeout) { |
||
3408 | if ($this->keepAlive <= 0) { |
||
3409 | @stream_select($read, $write, $except, null); |
||
3410 | } else { |
||
3411 | if (!@stream_select($read, $write, $except, $this->keepAlive)) { |
||
874 | daniel-mar | 3412 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); |
827 | daniel-mar | 3413 | return $this->get_binary_packet(true); |
3414 | } |
||
3415 | } |
||
3416 | } else { |
||
3417 | if ($this->curTimeout < 0) { |
||
3418 | $this->is_timeout = true; |
||
3419 | return true; |
||
3420 | } |
||
3421 | |||
3422 | $start = microtime(true); |
||
3423 | |||
3424 | if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) { |
||
3425 | if (!@stream_select($read, $write, $except, $this->keepAlive)) { |
||
874 | daniel-mar | 3426 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); |
827 | daniel-mar | 3427 | $elapsed = microtime(true) - $start; |
3428 | $this->curTimeout -= $elapsed; |
||
3429 | return $this->get_binary_packet(true); |
||
3430 | } |
||
3431 | $elapsed = microtime(true) - $start; |
||
3432 | $this->curTimeout -= $elapsed; |
||
3433 | } |
||
3434 | |||
3435 | $sec = (int) floor($this->curTimeout); |
||
3436 | $usec = (int) (1000000 * ($this->curTimeout - $sec)); |
||
3437 | |||
3438 | // this can return a "stream_select(): unable to select [4]: Interrupted system call" error |
||
3439 | if (!@stream_select($read, $write, $except, $sec, $usec)) { |
||
3440 | $this->is_timeout = true; |
||
3441 | return true; |
||
3442 | } |
||
3443 | $elapsed = microtime(true) - $start; |
||
3444 | $this->curTimeout -= $elapsed; |
||
3445 | } |
||
3446 | } |
||
3447 | |||
3448 | if (!is_resource($this->fsock) || feof($this->fsock)) { |
||
3449 | $this->bitmap = 0; |
||
3450 | throw new ConnectionClosedException('Connection closed (by server) prematurely ' . $elapsed . 's'); |
||
3451 | } |
||
3452 | |||
3453 | $start = microtime(true); |
||
3454 | $raw = stream_get_contents($this->fsock, $this->decrypt_block_size); |
||
3455 | |||
3456 | if (!strlen($raw)) { |
||
3457 | $this->bitmap = 0; |
||
3458 | throw new ConnectionClosedException('No data received from server'); |
||
3459 | } |
||
3460 | |||
3461 | if ($this->decrypt) { |
||
3462 | switch ($this->decryptName) { |
||
3463 | case 'aes128-gcm@openssh.com': |
||
3464 | case 'aes256-gcm@openssh.com': |
||
3465 | $this->decrypt->setNonce( |
||
3466 | $this->decryptFixedPart . |
||
3467 | $this->decryptInvocationCounter |
||
3468 | ); |
||
3469 | Strings::increment_str($this->decryptInvocationCounter); |
||
3470 | $this->decrypt->setAAD($temp = Strings::shift($raw, 4)); |
||
3471 | extract(unpack('Npacket_length', $temp)); |
||
3472 | /** |
||
3473 | * @var integer $packet_length |
||
3474 | */ |
||
3475 | |||
3476 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3477 | $stop = microtime(true); |
||
3478 | $tag = stream_get_contents($this->fsock, $this->decrypt_block_size); |
||
3479 | $this->decrypt->setTag($tag); |
||
3480 | $raw = $this->decrypt->decrypt($raw); |
||
3481 | $raw = $temp . $raw; |
||
3482 | $remaining_length = 0; |
||
3483 | break; |
||
3484 | case 'chacha20-poly1305@openssh.com': |
||
3485 | // This should be impossible, but we are checking anyway to narrow the type for Psalm. |
||
3486 | if (!($this->decrypt instanceof ChaCha20)) { |
||
3487 | throw new \LogicException('$this->decrypt is not a ' . ChaCha20::class); |
||
3488 | } |
||
3489 | |||
3490 | $nonce = pack('N2', 0, $this->get_seq_no); |
||
3491 | |||
3492 | $this->lengthDecrypt->setNonce($nonce); |
||
3493 | $temp = $this->lengthDecrypt->decrypt($aad = Strings::shift($raw, 4)); |
||
3494 | extract(unpack('Npacket_length', $temp)); |
||
3495 | /** |
||
3496 | * @var integer $packet_length |
||
3497 | */ |
||
3498 | |||
3499 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3500 | $stop = microtime(true); |
||
3501 | $tag = stream_get_contents($this->fsock, 16); |
||
3502 | |||
3503 | $this->decrypt->setNonce($nonce); |
||
3504 | $this->decrypt->setCounter(0); |
||
3505 | // this is the same approach that's implemented in Salsa20::createPoly1305Key() |
||
3506 | // but we don't want to use the same AEAD construction that RFC8439 describes |
||
3507 | // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) |
||
3508 | $this->decrypt->setPoly1305Key( |
||
3509 | $this->decrypt->encrypt(str_repeat("\0", 32)) |
||
3510 | ); |
||
3511 | $this->decrypt->setAAD($aad); |
||
3512 | $this->decrypt->setCounter(1); |
||
3513 | $this->decrypt->setTag($tag); |
||
3514 | $raw = $this->decrypt->decrypt($raw); |
||
3515 | $raw = $temp . $raw; |
||
3516 | $remaining_length = 0; |
||
3517 | break; |
||
3518 | default: |
||
3519 | if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) { |
||
3520 | $raw = $this->decrypt->decrypt($raw); |
||
3521 | break; |
||
3522 | } |
||
3523 | extract(unpack('Npacket_length', $temp = Strings::shift($raw, 4))); |
||
3524 | /** |
||
3525 | * @var integer $packet_length |
||
3526 | */ |
||
3527 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3528 | $stop = microtime(true); |
||
3529 | $encrypted = $temp . $raw; |
||
3530 | $raw = $temp . $this->decrypt->decrypt($raw); |
||
3531 | $remaining_length = 0; |
||
3532 | } |
||
3533 | } |
||
3534 | |||
3535 | if (strlen($raw) < 5) { |
||
3536 | $this->bitmap = 0; |
||
3537 | throw new \RuntimeException('Plaintext is too short'); |
||
3538 | } |
||
3539 | extract(unpack('Npacket_length/Cpadding_length', Strings::shift($raw, 5))); |
||
3540 | /** |
||
3541 | * @var integer $packet_length |
||
3542 | * @var integer $padding_length |
||
3543 | */ |
||
3544 | |||
3545 | if (!isset($remaining_length)) { |
||
3546 | $remaining_length = $packet_length + 4 - $this->decrypt_block_size; |
||
3547 | } |
||
3548 | |||
3549 | $buffer = $this->read_remaining_bytes($remaining_length); |
||
3550 | |||
3551 | if (!isset($stop)) { |
||
3552 | $stop = microtime(true); |
||
3553 | } |
||
3554 | if (strlen($buffer)) { |
||
3555 | $raw .= $this->decrypt ? $this->decrypt->decrypt($buffer) : $buffer; |
||
3556 | } |
||
3557 | |||
3558 | $payload = Strings::shift($raw, $packet_length - $padding_length - 1); |
||
3559 | $padding = Strings::shift($raw, $padding_length); // should leave $raw empty |
||
3560 | |||
3561 | if ($this->hmac_check instanceof Hash) { |
||
3562 | $hmac = stream_get_contents($this->fsock, $this->hmac_size); |
||
3563 | if ($hmac === false || strlen($hmac) != $this->hmac_size) { |
||
874 | daniel-mar | 3564 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3565 | throw new \RuntimeException('Error reading socket'); |
3566 | } |
||
3567 | |||
3568 | $reconstructed = !$this->hmac_check_etm ? |
||
3569 | pack('NCa*', $packet_length, $padding_length, $payload . $padding) : |
||
3570 | $encrypted; |
||
3571 | if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
3572 | $this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no)); |
||
3573 | if ($hmac != $this->hmac_check->hash($reconstructed)) { |
||
874 | daniel-mar | 3574 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3575 | throw new \RuntimeException('Invalid UMAC'); |
3576 | } |
||
3577 | } else { |
||
3578 | if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) { |
||
874 | daniel-mar | 3579 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3580 | throw new \RuntimeException('Invalid HMAC'); |
3581 | } |
||
3582 | } |
||
3583 | } |
||
3584 | |||
3585 | switch ($this->decompress) { |
||
3586 | case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: |
||
3587 | if (!$this->isAuthenticated()) { |
||
3588 | break; |
||
3589 | } |
||
3590 | // fall-through |
||
3591 | case self::NET_SSH2_COMPRESSION_ZLIB: |
||
3592 | if ($this->regenerate_decompression_context) { |
||
3593 | $this->regenerate_decompression_context = false; |
||
3594 | |||
3595 | $cmf = ord($payload[0]); |
||
3596 | $cm = $cmf & 0x0F; |
||
3597 | if ($cm != 8) { // deflate |
||
3598 | user_error("Only CM = 8 ('deflate') is supported ($cm)"); |
||
3599 | } |
||
3600 | $cinfo = ($cmf & 0xF0) >> 4; |
||
3601 | if ($cinfo > 7) { |
||
3602 | user_error("CINFO above 7 is not allowed ($cinfo)"); |
||
3603 | } |
||
3604 | $windowSize = 1 << ($cinfo + 8); |
||
3605 | |||
3606 | $flg = ord($payload[1]); |
||
3607 | //$fcheck = $flg && 0x0F; |
||
3608 | if ((($cmf << 8) | $flg) % 31) { |
||
3609 | user_error('fcheck failed'); |
||
3610 | } |
||
3611 | $fdict = boolval($flg & 0x20); |
||
3612 | $flevel = ($flg & 0xC0) >> 6; |
||
3613 | |||
3614 | $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, ['window' => $cinfo + 8]); |
||
3615 | $payload = substr($payload, 2); |
||
3616 | } |
||
3617 | if ($this->decompress_context) { |
||
3618 | $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH); |
||
3619 | } |
||
3620 | } |
||
3621 | |||
3622 | $this->get_seq_no++; |
||
3623 | |||
3624 | if (defined('NET_SSH2_LOGGING')) { |
||
3625 | $current = microtime(true); |
||
874 | daniel-mar | 3626 | $message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; |
3627 | $message_number = '<- ' . $message_number . |
||
3628 | ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
||
827 | daniel-mar | 3629 | $this->append_log($message_number, $payload); |
3630 | $this->last_packet = $current; |
||
3631 | } |
||
3632 | |||
3633 | return $this->filter($payload, $skip_channel_filter); |
||
3634 | } |
||
3635 | |||
3636 | /** |
||
3637 | * Read Remaining Bytes |
||
3638 | * |
||
3639 | * @see self::get_binary_packet() |
||
3640 | * @param int $remaining_length |
||
3641 | * @return string |
||
874 | daniel-mar | 3642 | * @access private |
827 | daniel-mar | 3643 | */ |
3644 | private function read_remaining_bytes($remaining_length) |
||
3645 | { |
||
3646 | if (!$remaining_length) { |
||
3647 | return ''; |
||
3648 | } |
||
3649 | |||
3650 | $adjustLength = false; |
||
3651 | if ($this->decrypt) { |
||
3652 | switch (true) { |
||
3653 | case $this->decryptName == 'aes128-gcm@openssh.com': |
||
3654 | case $this->decryptName == 'aes256-gcm@openssh.com': |
||
3655 | case $this->decryptName == 'chacha20-poly1305@openssh.com': |
||
3656 | case $this->hmac_check instanceof Hash && $this->hmac_check_etm: |
||
3657 | $remaining_length += $this->decrypt_block_size - 4; |
||
3658 | $adjustLength = true; |
||
3659 | } |
||
3660 | } |
||
3661 | |||
3662 | // quoting <http://tools.ietf.org/html/rfc4253#section-6.1>, |
||
3663 | // "implementations SHOULD check that the packet length is reasonable" |
||
3664 | // PuTTY uses 0x9000 as the actual max packet size and so to shall we |
||
3665 | // don't do this when GCM mode is used since GCM mode doesn't encrypt the length |
||
3666 | if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { |
||
3667 | if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decryptName : '') && !($this->bitmap & SSH2::MASK_LOGIN)) { |
||
3668 | $this->bad_key_size_fix = true; |
||
874 | daniel-mar | 3669 | $this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 3670 | return false; |
3671 | } |
||
3672 | throw new \RuntimeException('Invalid size'); |
||
3673 | } |
||
3674 | |||
3675 | if ($adjustLength) { |
||
3676 | $remaining_length -= $this->decrypt_block_size - 4; |
||
3677 | } |
||
3678 | |||
3679 | $buffer = ''; |
||
3680 | while ($remaining_length > 0) { |
||
3681 | $temp = stream_get_contents($this->fsock, $remaining_length); |
||
3682 | if ($temp === false || feof($this->fsock)) { |
||
874 | daniel-mar | 3683 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 3684 | throw new \RuntimeException('Error reading from socket'); |
3685 | } |
||
3686 | $buffer .= $temp; |
||
3687 | $remaining_length -= strlen($temp); |
||
3688 | } |
||
3689 | |||
3690 | return $buffer; |
||
3691 | } |
||
3692 | |||
3693 | /** |
||
3694 | * Filter Binary Packets |
||
3695 | * |
||
3696 | * Because some binary packets need to be ignored... |
||
3697 | * |
||
3698 | * @see self::_get_binary_packet() |
||
3699 | * @param string $payload |
||
3700 | * @param bool $skip_channel_filter |
||
3701 | * @return string|bool |
||
874 | daniel-mar | 3702 | * @access private |
827 | daniel-mar | 3703 | */ |
3704 | private function filter($payload, $skip_channel_filter) |
||
3705 | { |
||
3706 | switch (ord($payload[0])) { |
||
874 | daniel-mar | 3707 | case NET_SSH2_MSG_DISCONNECT: |
827 | daniel-mar | 3708 | Strings::shift($payload, 1); |
3709 | list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); |
||
874 | daniel-mar | 3710 | $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n$message"; |
827 | daniel-mar | 3711 | $this->bitmap = 0; |
3712 | return false; |
||
874 | daniel-mar | 3713 | case NET_SSH2_MSG_IGNORE: |
827 | daniel-mar | 3714 | $payload = $this->get_binary_packet($skip_channel_filter); |
3715 | break; |
||
874 | daniel-mar | 3716 | case NET_SSH2_MSG_DEBUG: |
827 | daniel-mar | 3717 | Strings::shift($payload, 2); // second byte is "always_display" |
3718 | list($message) = Strings::unpackSSH2('s', $payload); |
||
3719 | $this->errors[] = "SSH_MSG_DEBUG: $message"; |
||
3720 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3721 | break; |
||
874 | daniel-mar | 3722 | case NET_SSH2_MSG_UNIMPLEMENTED: |
827 | daniel-mar | 3723 | return false; |
874 | daniel-mar | 3724 | case NET_SSH2_MSG_KEXINIT: |
827 | daniel-mar | 3725 | if ($this->session_id !== false) { |
3726 | if (!$this->key_exchange($payload)) { |
||
3727 | $this->bitmap = 0; |
||
3728 | return false; |
||
3729 | } |
||
3730 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3731 | } |
||
3732 | } |
||
3733 | |||
3734 | // see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in |
||
874 | daniel-mar | 3735 | if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && !is_bool($payload) && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) { |
827 | daniel-mar | 3736 | Strings::shift($payload, 1); |
3737 | list($this->banner_message) = Strings::unpackSSH2('s', $payload); |
||
3738 | $payload = $this->get_binary_packet(); |
||
3739 | } |
||
3740 | |||
3741 | // only called when we've already logged in |
||
3742 | if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { |
||
3743 | if (is_bool($payload)) { |
||
3744 | return $payload; |
||
3745 | } |
||
3746 | |||
3747 | switch (ord($payload[0])) { |
||
874 | daniel-mar | 3748 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 3749 | if (strlen($payload) == 31) { |
3750 | extract(unpack('cpacket_type/Nchannel/Nlength', $payload)); |
||
3751 | if (substr($payload, 9, $length) == 'keepalive@openssh.com' && isset($this->server_channels[$channel])) { |
||
3752 | if (ord(substr($payload, 9 + $length))) { // want reply |
||
874 | daniel-mar | 3753 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel])); |
827 | daniel-mar | 3754 | } |
3755 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3756 | } |
||
3757 | } |
||
3758 | break; |
||
874 | daniel-mar | 3759 | case NET_SSH2_MSG_CHANNEL_DATA: |
3760 | case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: |
||
3761 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
||
3762 | case NET_SSH2_MSG_CHANNEL_EOF: |
||
827 | daniel-mar | 3763 | if (!$skip_channel_filter && !empty($this->server_channels)) { |
3764 | $this->binary_packet_buffer = $payload; |
||
3765 | $this->get_channel_packet(true); |
||
3766 | $payload = $this->get_binary_packet(); |
||
3767 | } |
||
3768 | break; |
||
874 | daniel-mar | 3769 | case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 |
827 | daniel-mar | 3770 | Strings::shift($payload, 1); |
3771 | list($request_name) = Strings::unpackSSH2('s', $payload); |
||
3772 | $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name"; |
||
3773 | |||
3774 | try { |
||
874 | daniel-mar | 3775 | $this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE)); |
827 | daniel-mar | 3776 | } catch (\RuntimeException $e) { |
874 | daniel-mar | 3777 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3778 | } |
3779 | |||
3780 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3781 | break; |
||
874 | daniel-mar | 3782 | case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 |
827 | daniel-mar | 3783 | Strings::shift($payload, 1); |
3784 | list($data, $server_channel) = Strings::unpackSSH2('sN', $payload); |
||
3785 | switch ($data) { |
||
3786 | case 'auth-agent': |
||
3787 | case 'auth-agent@openssh.com': |
||
3788 | if (isset($this->agent)) { |
||
3789 | $new_channel = self::CHANNEL_AGENT_FORWARD; |
||
3790 | |||
3791 | list( |
||
3792 | $remote_window_size, |
||
3793 | $remote_maximum_packet_size |
||
3794 | ) = Strings::unpackSSH2('NN', $payload); |
||
3795 | |||
3796 | $this->packet_size_client_to_server[$new_channel] = $remote_window_size; |
||
3797 | $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; |
||
3798 | $this->window_size_client_to_server[$new_channel] = $this->window_size; |
||
3799 | |||
3800 | $packet_size = 0x4000; |
||
3801 | |||
3802 | $packet = pack( |
||
3803 | 'CN4', |
||
874 | daniel-mar | 3804 | NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, |
827 | daniel-mar | 3805 | $server_channel, |
3806 | $new_channel, |
||
3807 | $packet_size, |
||
3808 | $packet_size |
||
3809 | ); |
||
3810 | |||
3811 | $this->server_channels[$new_channel] = $server_channel; |
||
874 | daniel-mar | 3812 | $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; |
827 | daniel-mar | 3813 | $this->send_binary_packet($packet); |
3814 | } |
||
3815 | break; |
||
3816 | default: |
||
3817 | $packet = Strings::packSSH2( |
||
3818 | 'CN2ss', |
||
874 | daniel-mar | 3819 | NET_SSH2_MSG_CHANNEL_OPEN_FAILURE, |
827 | daniel-mar | 3820 | $server_channel, |
874 | daniel-mar | 3821 | NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, |
827 | daniel-mar | 3822 | '', // description |
3823 | '' // language tag |
||
3824 | ); |
||
3825 | |||
3826 | try { |
||
3827 | $this->send_binary_packet($packet); |
||
3828 | } catch (\RuntimeException $e) { |
||
874 | daniel-mar | 3829 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3830 | } |
3831 | } |
||
3832 | |||
3833 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3834 | break; |
||
874 | daniel-mar | 3835 | case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: |
827 | daniel-mar | 3836 | Strings::shift($payload, 1); |
3837 | list($channel, $window_size) = Strings::unpackSSH2('NN', $payload); |
||
3838 | |||
3839 | $this->window_size_client_to_server[$channel] += $window_size; |
||
3840 | |||
3841 | $payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet($skip_channel_filter); |
||
3842 | } |
||
3843 | } |
||
3844 | |||
3845 | return $payload; |
||
3846 | } |
||
3847 | |||
3848 | /** |
||
3849 | * Enable Quiet Mode |
||
3850 | * |
||
3851 | * Suppress stderr from output |
||
3852 | * |
||
874 | daniel-mar | 3853 | * @access public |
827 | daniel-mar | 3854 | */ |
3855 | public function enableQuietMode() |
||
3856 | { |
||
3857 | $this->quiet_mode = true; |
||
3858 | } |
||
3859 | |||
3860 | /** |
||
3861 | * Disable Quiet Mode |
||
3862 | * |
||
3863 | * Show stderr in output |
||
3864 | * |
||
874 | daniel-mar | 3865 | * @access public |
827 | daniel-mar | 3866 | */ |
3867 | public function disableQuietMode() |
||
3868 | { |
||
3869 | $this->quiet_mode = false; |
||
3870 | } |
||
3871 | |||
3872 | /** |
||
3873 | * Returns whether Quiet Mode is enabled or not |
||
3874 | * |
||
3875 | * @see self::enableQuietMode() |
||
3876 | * @see self::disableQuietMode() |
||
874 | daniel-mar | 3877 | * @access public |
827 | daniel-mar | 3878 | * @return bool |
3879 | */ |
||
3880 | public function isQuietModeEnabled() |
||
3881 | { |
||
3882 | return $this->quiet_mode; |
||
3883 | } |
||
3884 | |||
3885 | /** |
||
3886 | * Enable request-pty when using exec() |
||
3887 | * |
||
874 | daniel-mar | 3888 | * @access public |
827 | daniel-mar | 3889 | */ |
3890 | public function enablePTY() |
||
3891 | { |
||
3892 | $this->request_pty = true; |
||
3893 | } |
||
3894 | |||
3895 | /** |
||
3896 | * Disable request-pty when using exec() |
||
3897 | * |
||
874 | daniel-mar | 3898 | * @access public |
827 | daniel-mar | 3899 | */ |
3900 | public function disablePTY() |
||
3901 | { |
||
3902 | if ($this->in_request_pty_exec) { |
||
3903 | $this->close_channel(self::CHANNEL_EXEC); |
||
3904 | $this->in_request_pty_exec = false; |
||
3905 | } |
||
3906 | $this->request_pty = false; |
||
3907 | } |
||
3908 | |||
3909 | /** |
||
3910 | * Returns whether request-pty is enabled or not |
||
3911 | * |
||
3912 | * @see self::enablePTY() |
||
3913 | * @see self::disablePTY() |
||
874 | daniel-mar | 3914 | * @access public |
827 | daniel-mar | 3915 | * @return bool |
3916 | */ |
||
3917 | public function isPTYEnabled() |
||
3918 | { |
||
3919 | return $this->request_pty; |
||
3920 | } |
||
3921 | |||
3922 | /** |
||
3923 | * Gets channel data |
||
3924 | * |
||
3925 | * Returns the data as a string. bool(true) is returned if: |
||
3926 | * |
||
3927 | * - the server closes the channel |
||
3928 | * - if the connection times out |
||
3929 | * - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION |
||
3930 | * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS |
||
3931 | * |
||
3932 | * bool(false) is returned if: |
||
3933 | * |
||
3934 | * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE |
||
3935 | * |
||
3936 | * @param int $client_channel |
||
3937 | * @param bool $skip_extended |
||
3938 | * @return mixed |
||
3939 | * @throws \RuntimeException on connection error |
||
874 | daniel-mar | 3940 | * @access private |
827 | daniel-mar | 3941 | */ |
3942 | protected function get_channel_packet($client_channel, $skip_extended = false) |
||
3943 | { |
||
3944 | if (!empty($this->channel_buffers[$client_channel])) { |
||
3945 | switch ($this->channel_status[$client_channel]) { |
||
874 | daniel-mar | 3946 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 3947 | foreach ($this->channel_buffers[$client_channel] as $i => $packet) { |
3948 | switch (ord($packet[0])) { |
||
874 | daniel-mar | 3949 | case NET_SSH2_MSG_CHANNEL_SUCCESS: |
3950 | case NET_SSH2_MSG_CHANNEL_FAILURE: |
||
827 | daniel-mar | 3951 | unset($this->channel_buffers[$client_channel][$i]); |
3952 | return substr($packet, 1); |
||
3953 | } |
||
3954 | } |
||
3955 | break; |
||
3956 | default: |
||
3957 | return substr(array_shift($this->channel_buffers[$client_channel]), 1); |
||
3958 | } |
||
3959 | } |
||
3960 | |||
3961 | while (true) { |
||
3962 | if ($this->binary_packet_buffer !== false) { |
||
3963 | $response = $this->binary_packet_buffer; |
||
3964 | $this->binary_packet_buffer = false; |
||
3965 | } else { |
||
3966 | $response = $this->get_binary_packet(true); |
||
3967 | if ($response === true && $this->is_timeout) { |
||
3968 | if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) { |
||
3969 | $this->close_channel($client_channel); |
||
3970 | } |
||
3971 | return true; |
||
3972 | } |
||
3973 | if ($response === false) { |
||
874 | daniel-mar | 3974 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 3975 | throw new ConnectionClosedException('Connection closed by server'); |
3976 | } |
||
3977 | } |
||
3978 | |||
3979 | if ($client_channel == -1 && $response === true) { |
||
3980 | return true; |
||
3981 | } |
||
3982 | list($type, $channel) = Strings::unpackSSH2('CN', $response); |
||
3983 | |||
3984 | // will not be setup yet on incoming channel open request |
||
3985 | if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { |
||
3986 | $this->window_size_server_to_client[$channel] -= strlen($response); |
||
3987 | |||
3988 | // resize the window, if appropriate |
||
3989 | if ($this->window_size_server_to_client[$channel] < 0) { |
||
3990 | // PuTTY does something more analogous to the following: |
||
3991 | //if ($this->window_size_server_to_client[$channel] < 0x3FFFFFFF) { |
||
874 | daniel-mar | 3992 | $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_resize); |
827 | daniel-mar | 3993 | $this->send_binary_packet($packet); |
3994 | $this->window_size_server_to_client[$channel] += $this->window_resize; |
||
3995 | } |
||
3996 | |||
3997 | switch ($type) { |
||
874 | daniel-mar | 3998 | case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: |
827 | daniel-mar | 3999 | /* |
4000 | if ($client_channel == self::CHANNEL_EXEC) { |
||
4001 | $this->send_channel_packet($client_channel, chr(0)); |
||
4002 | } |
||
4003 | */ |
||
4004 | // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR |
||
4005 | list($data_type_code, $data) = Strings::unpackSSH2('Ns', $response); |
||
4006 | $this->stdErrorLog .= $data; |
||
4007 | if ($skip_extended || $this->quiet_mode) { |
||
4008 | continue 2; |
||
4009 | } |
||
874 | daniel-mar | 4010 | if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) { |
827 | daniel-mar | 4011 | return $data; |
4012 | } |
||
4013 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4014 | |||
4015 | continue 2; |
||
874 | daniel-mar | 4016 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
4017 | if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) { |
||
827 | daniel-mar | 4018 | continue 2; |
4019 | } |
||
4020 | list($value) = Strings::unpackSSH2('s', $response); |
||
4021 | switch ($value) { |
||
4022 | case 'exit-signal': |
||
4023 | list( |
||
4024 | , // FALSE |
||
4025 | $signal_name, |
||
4026 | , // core dumped |
||
4027 | $error_message |
||
4028 | ) = Strings::unpackSSH2('bsbs', $response); |
||
4029 | |||
4030 | $this->errors[] = "SSH_MSG_CHANNEL_REQUEST (exit-signal): $signal_name"; |
||
4031 | if (strlen($error_message)) { |
||
4032 | $this->errors[count($this->errors) - 1] .= "\r\n$error_message"; |
||
4033 | } |
||
4034 | |||
874 | daniel-mar | 4035 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); |
4036 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); |
||
827 | daniel-mar | 4037 | |
874 | daniel-mar | 4038 | $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF; |
827 | daniel-mar | 4039 | |
4040 | continue 3; |
||
4041 | case 'exit-status': |
||
4042 | list(, $this->exit_status) = Strings::unpackSSH2('CN', $response); |
||
4043 | |||
4044 | // "The client MAY ignore these messages." |
||
4045 | // -- http://tools.ietf.org/html/rfc4254#section-6.10 |
||
4046 | |||
4047 | continue 3; |
||
4048 | default: |
||
4049 | // "Some systems may not implement signals, in which case they SHOULD ignore this message." |
||
4050 | // -- http://tools.ietf.org/html/rfc4254#section-6.9 |
||
4051 | continue 3; |
||
4052 | } |
||
4053 | } |
||
4054 | |||
4055 | switch ($this->channel_status[$channel]) { |
||
874 | daniel-mar | 4056 | case NET_SSH2_MSG_CHANNEL_OPEN: |
827 | daniel-mar | 4057 | switch ($type) { |
874 | daniel-mar | 4058 | case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: |
827 | daniel-mar | 4059 | list( |
4060 | $this->server_channels[$channel], |
||
4061 | $window_size, |
||
4062 | $this->packet_size_client_to_server[$channel] |
||
4063 | ) = Strings::unpackSSH2('NNN', $response); |
||
4064 | |||
4065 | if ($window_size < 0) { |
||
4066 | $window_size &= 0x7FFFFFFF; |
||
4067 | $window_size += 0x80000000; |
||
4068 | } |
||
4069 | $this->window_size_client_to_server[$channel] = $window_size; |
||
4070 | $result = $client_channel == $channel ? true : $this->get_channel_packet($client_channel, $skip_extended); |
||
4071 | $this->on_channel_open(); |
||
4072 | return $result; |
||
874 | daniel-mar | 4073 | case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: |
4074 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
||
827 | daniel-mar | 4075 | throw new \RuntimeException('Unable to open channel'); |
4076 | default: |
||
4077 | if ($client_channel == $channel) { |
||
874 | daniel-mar | 4078 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4079 | throw new \RuntimeException('Unexpected response to open request'); |
4080 | } |
||
4081 | return $this->get_channel_packet($client_channel, $skip_extended); |
||
4082 | } |
||
4083 | break; |
||
874 | daniel-mar | 4084 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 4085 | switch ($type) { |
874 | daniel-mar | 4086 | case NET_SSH2_MSG_CHANNEL_SUCCESS: |
827 | daniel-mar | 4087 | return true; |
874 | daniel-mar | 4088 | case NET_SSH2_MSG_CHANNEL_FAILURE: |
827 | daniel-mar | 4089 | return false; |
874 | daniel-mar | 4090 | case NET_SSH2_MSG_CHANNEL_DATA: |
827 | daniel-mar | 4091 | list($data) = Strings::unpackSSH2('s', $response); |
4092 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4093 | return $this->get_channel_packet($client_channel, $skip_extended); |
||
4094 | default: |
||
874 | daniel-mar | 4095 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4096 | throw new \RuntimeException('Unable to fulfill channel request'); |
4097 | } |
||
874 | daniel-mar | 4098 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
4099 | return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->get_channel_packet($client_channel, $skip_extended); |
||
827 | daniel-mar | 4100 | } |
4101 | } |
||
4102 | |||
874 | daniel-mar | 4103 | // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA |
827 | daniel-mar | 4104 | |
4105 | switch ($type) { |
||
874 | daniel-mar | 4106 | case NET_SSH2_MSG_CHANNEL_DATA: |
827 | daniel-mar | 4107 | /* |
4108 | if ($channel == self::CHANNEL_EXEC) { |
||
4109 | // SCP requires null packets, such as this, be sent. further, in the case of the ssh.com SSH server |
||
4110 | // this actually seems to make things twice as fast. more to the point, the message right after |
||
4111 | // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise. |
||
4112 | // in OpenSSH it slows things down but only by a couple thousandths of a second. |
||
4113 | $this->send_channel_packet($channel, chr(0)); |
||
4114 | } |
||
4115 | */ |
||
4116 | list($data) = Strings::unpackSSH2('s', $response); |
||
4117 | |||
4118 | if ($channel == self::CHANNEL_AGENT_FORWARD) { |
||
4119 | $agent_response = $this->agent->forwardData($data); |
||
4120 | if (!is_bool($agent_response)) { |
||
4121 | $this->send_channel_packet($channel, $agent_response); |
||
4122 | } |
||
4123 | break; |
||
4124 | } |
||
4125 | |||
4126 | if ($client_channel == $channel) { |
||
4127 | return $data; |
||
4128 | } |
||
4129 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4130 | break; |
||
874 | daniel-mar | 4131 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
827 | daniel-mar | 4132 | $this->curTimeout = 5; |
4133 | |||
4134 | if ($this->bitmap & self::MASK_SHELL) { |
||
4135 | $this->bitmap &= ~self::MASK_SHELL; |
||
4136 | } |
||
874 | daniel-mar | 4137 | if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) { |
4138 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); |
||
827 | daniel-mar | 4139 | } |
4140 | |||
874 | daniel-mar | 4141 | $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE; |
827 | daniel-mar | 4142 | if ($client_channel == $channel) { |
4143 | return true; |
||
4144 | } |
||
4145 | // fall-through |
||
874 | daniel-mar | 4146 | case NET_SSH2_MSG_CHANNEL_EOF: |
827 | daniel-mar | 4147 | break; |
4148 | default: |
||
874 | daniel-mar | 4149 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4150 | throw new \RuntimeException("Error reading channel data ($type)"); |
4151 | } |
||
4152 | } |
||
4153 | } |
||
4154 | |||
4155 | /** |
||
4156 | * Sends Binary Packets |
||
4157 | * |
||
4158 | * See '6. Binary Packet Protocol' of rfc4253 for more info. |
||
4159 | * |
||
4160 | * @param string $data |
||
4161 | * @param string $logged |
||
4162 | * @see self::_get_binary_packet() |
||
4163 | * @return void |
||
874 | daniel-mar | 4164 | * @access private |
827 | daniel-mar | 4165 | */ |
4166 | protected function send_binary_packet($data, $logged = null) |
||
4167 | { |
||
4168 | if (!is_resource($this->fsock) || feof($this->fsock)) { |
||
4169 | $this->bitmap = 0; |
||
4170 | throw new ConnectionClosedException('Connection closed prematurely'); |
||
4171 | } |
||
4172 | |||
4173 | if (!isset($logged)) { |
||
4174 | $logged = $data; |
||
4175 | } |
||
4176 | |||
4177 | switch ($this->compress) { |
||
4178 | case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: |
||
4179 | if (!$this->isAuthenticated()) { |
||
4180 | break; |
||
4181 | } |
||
4182 | // fall-through |
||
4183 | case self::NET_SSH2_COMPRESSION_ZLIB: |
||
4184 | if (!$this->regenerate_compression_context) { |
||
4185 | $header = ''; |
||
4186 | } else { |
||
4187 | $this->regenerate_compression_context = false; |
||
4188 | $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, ['window' => 15]); |
||
4189 | $header = "\x78\x9C"; |
||
4190 | } |
||
4191 | if ($this->compress_context) { |
||
4192 | $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH); |
||
4193 | } |
||
4194 | } |
||
4195 | |||
4196 | // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 |
||
4197 | $packet_length = strlen($data) + 9; |
||
4198 | if ($this->encrypt && $this->encrypt->usesNonce()) { |
||
4199 | $packet_length -= 4; |
||
4200 | } |
||
4201 | // round up to the nearest $this->encrypt_block_size |
||
4202 | $packet_length += (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size; |
||
4203 | // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length |
||
4204 | $padding_length = $packet_length - strlen($data) - 5; |
||
4205 | switch (true) { |
||
4206 | case $this->encrypt && $this->encrypt->usesNonce(): |
||
4207 | case $this->hmac_create instanceof Hash && $this->hmac_create_etm: |
||
4208 | $padding_length += 4; |
||
4209 | $packet_length += 4; |
||
4210 | } |
||
4211 | |||
4212 | $padding = Random::string($padding_length); |
||
4213 | |||
4214 | // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself |
||
4215 | $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding); |
||
4216 | |||
4217 | $hmac = ''; |
||
4218 | if ($this->hmac_create instanceof Hash && !$this->hmac_create_etm) { |
||
4219 | if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
4220 | $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); |
||
4221 | $hmac = $this->hmac_create->hash($packet); |
||
4222 | } else { |
||
4223 | $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); |
||
4224 | } |
||
4225 | } |
||
4226 | |||
4227 | if ($this->encrypt) { |
||
4228 | switch ($this->encryptName) { |
||
4229 | case 'aes128-gcm@openssh.com': |
||
4230 | case 'aes256-gcm@openssh.com': |
||
4231 | $this->encrypt->setNonce( |
||
4232 | $this->encryptFixedPart . |
||
4233 | $this->encryptInvocationCounter |
||
4234 | ); |
||
4235 | Strings::increment_str($this->encryptInvocationCounter); |
||
4236 | $this->encrypt->setAAD($temp = ($packet & "\xFF\xFF\xFF\xFF")); |
||
4237 | $packet = $temp . $this->encrypt->encrypt(substr($packet, 4)); |
||
4238 | break; |
||
4239 | case 'chacha20-poly1305@openssh.com': |
||
4240 | // This should be impossible, but we are checking anyway to narrow the type for Psalm. |
||
4241 | if (!($this->encrypt instanceof ChaCha20)) { |
||
4242 | throw new \LogicException('$this->encrypt is not a ' . ChaCha20::class); |
||
4243 | } |
||
4244 | |||
4245 | $nonce = pack('N2', 0, $this->send_seq_no); |
||
4246 | |||
4247 | $this->encrypt->setNonce($nonce); |
||
4248 | $this->lengthEncrypt->setNonce($nonce); |
||
4249 | |||
4250 | $length = $this->lengthEncrypt->encrypt($packet & "\xFF\xFF\xFF\xFF"); |
||
4251 | |||
4252 | $this->encrypt->setCounter(0); |
||
4253 | // this is the same approach that's implemented in Salsa20::createPoly1305Key() |
||
4254 | // but we don't want to use the same AEAD construction that RFC8439 describes |
||
4255 | // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) |
||
4256 | $this->encrypt->setPoly1305Key( |
||
4257 | $this->encrypt->encrypt(str_repeat("\0", 32)) |
||
4258 | ); |
||
4259 | $this->encrypt->setAAD($length); |
||
4260 | $this->encrypt->setCounter(1); |
||
4261 | $packet = $length . $this->encrypt->encrypt(substr($packet, 4)); |
||
4262 | break; |
||
4263 | default: |
||
4264 | $packet = $this->hmac_create instanceof Hash && $this->hmac_create_etm ? |
||
4265 | ($packet & "\xFF\xFF\xFF\xFF") . $this->encrypt->encrypt(substr($packet, 4)) : |
||
4266 | $this->encrypt->encrypt($packet); |
||
4267 | } |
||
4268 | } |
||
4269 | |||
4270 | if ($this->hmac_create instanceof Hash && $this->hmac_create_etm) { |
||
4271 | if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
4272 | $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); |
||
4273 | $hmac = $this->hmac_create->hash($packet); |
||
4274 | } else { |
||
4275 | $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); |
||
4276 | } |
||
4277 | } |
||
4278 | |||
4279 | $this->send_seq_no++; |
||
4280 | |||
4281 | $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac; |
||
4282 | |||
4283 | $start = microtime(true); |
||
4284 | $sent = @fputs($this->fsock, $packet); |
||
4285 | $stop = microtime(true); |
||
4286 | |||
4287 | if (defined('NET_SSH2_LOGGING')) { |
||
4288 | $current = microtime(true); |
||
874 | daniel-mar | 4289 | $message_number = isset($this->message_numbers[ord($logged[0])]) ? $this->message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; |
4290 | $message_number = '-> ' . $message_number . |
||
4291 | ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
||
827 | daniel-mar | 4292 | $this->append_log($message_number, $logged); |
4293 | $this->last_packet = $current; |
||
4294 | } |
||
4295 | |||
4296 | if (strlen($packet) != $sent) { |
||
4297 | $this->bitmap = 0; |
||
4298 | throw new \RuntimeException("Only $sent of " . strlen($packet) . " bytes were sent"); |
||
4299 | } |
||
4300 | } |
||
4301 | |||
4302 | /** |
||
4303 | * Logs data packets |
||
4304 | * |
||
4305 | * Makes sure that only the last 1MB worth of packets will be logged |
||
4306 | * |
||
4307 | * @param string $message_number |
||
4308 | * @param string $message |
||
874 | daniel-mar | 4309 | * @access private |
827 | daniel-mar | 4310 | */ |
4311 | private function append_log($message_number, $message) |
||
4312 | { |
||
4313 | // remove the byte identifying the message type from all but the first two messages (ie. the identification strings) |
||
4314 | if (strlen($message_number) > 2) { |
||
4315 | Strings::shift($message); |
||
4316 | } |
||
4317 | |||
4318 | switch (NET_SSH2_LOGGING) { |
||
4319 | // useful for benchmarks |
||
4320 | case self::LOG_SIMPLE: |
||
4321 | $this->message_number_log[] = $message_number; |
||
4322 | break; |
||
4323 | // the most useful log for SSH2 |
||
4324 | case self::LOG_COMPLEX: |
||
4325 | $this->message_number_log[] = $message_number; |
||
4326 | $this->log_size += strlen($message); |
||
4327 | $this->message_log[] = $message; |
||
4328 | while ($this->log_size > self::LOG_MAX_SIZE) { |
||
4329 | $this->log_size -= strlen(array_shift($this->message_log)); |
||
4330 | array_shift($this->message_number_log); |
||
4331 | } |
||
4332 | break; |
||
4333 | // dump the output out realtime; packets may be interspersed with non packets, |
||
4334 | // passwords won't be filtered out and select other packets may not be correctly |
||
4335 | // identified |
||
4336 | case self::LOG_REALTIME: |
||
4337 | switch (PHP_SAPI) { |
||
4338 | case 'cli': |
||
4339 | $start = $stop = "\r\n"; |
||
4340 | break; |
||
4341 | default: |
||
4342 | $start = '<pre>'; |
||
4343 | $stop = '</pre>'; |
||
4344 | } |
||
4345 | echo $start . $this->format_log([$message], [$message_number]) . $stop; |
||
4346 | @flush(); |
||
4347 | @ob_flush(); |
||
4348 | break; |
||
4349 | // basically the same thing as self::LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILENAME |
||
4350 | // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. |
||
4351 | // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily |
||
4352 | // at the beginning of the file |
||
4353 | case self::LOG_REALTIME_FILE: |
||
4354 | if (!isset($this->realtime_log_file)) { |
||
4355 | // PHP doesn't seem to like using constants in fopen() |
||
4356 | $filename = NET_SSH2_LOG_REALTIME_FILENAME; |
||
4357 | $fp = fopen($filename, 'w'); |
||
4358 | $this->realtime_log_file = $fp; |
||
4359 | } |
||
4360 | if (!is_resource($this->realtime_log_file)) { |
||
4361 | break; |
||
4362 | } |
||
4363 | $entry = $this->format_log([$message], [$message_number]); |
||
4364 | if ($this->realtime_log_wrap) { |
||
4365 | $temp = "<<< START >>>\r\n"; |
||
4366 | $entry .= $temp; |
||
4367 | fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp)); |
||
4368 | } |
||
4369 | $this->realtime_log_size += strlen($entry); |
||
4370 | if ($this->realtime_log_size > self::LOG_MAX_SIZE) { |
||
4371 | fseek($this->realtime_log_file, 0); |
||
4372 | $this->realtime_log_size = strlen($entry); |
||
4373 | $this->realtime_log_wrap = true; |
||
4374 | } |
||
4375 | fputs($this->realtime_log_file, $entry); |
||
4376 | } |
||
4377 | } |
||
4378 | |||
4379 | /** |
||
4380 | * Sends channel data |
||
4381 | * |
||
4382 | * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate |
||
4383 | * |
||
4384 | * @param int $client_channel |
||
4385 | * @param string $data |
||
4386 | * @return void |
||
4387 | */ |
||
4388 | protected function send_channel_packet($client_channel, $data) |
||
4389 | { |
||
4390 | while (strlen($data)) { |
||
4391 | if (!$this->window_size_client_to_server[$client_channel]) { |
||
4392 | $this->bitmap ^= self::MASK_WINDOW_ADJUST; |
||
4393 | // using an invalid channel will let the buffers be built up for the valid channels |
||
4394 | $this->get_channel_packet(-1); |
||
4395 | $this->bitmap ^= self::MASK_WINDOW_ADJUST; |
||
4396 | } |
||
4397 | |||
4398 | /* The maximum amount of data allowed is determined by the maximum |
||
4399 | packet size for the channel, and the current window size, whichever |
||
4400 | is smaller. |
||
4401 | -- http://tools.ietf.org/html/rfc4254#section-5.2 */ |
||
4402 | $max_size = min( |
||
4403 | $this->packet_size_client_to_server[$client_channel], |
||
4404 | $this->window_size_client_to_server[$client_channel] |
||
4405 | ); |
||
4406 | |||
4407 | $temp = Strings::shift($data, $max_size); |
||
4408 | $packet = Strings::packSSH2( |
||
4409 | 'CNs', |
||
874 | daniel-mar | 4410 | NET_SSH2_MSG_CHANNEL_DATA, |
827 | daniel-mar | 4411 | $this->server_channels[$client_channel], |
4412 | $temp |
||
4413 | ); |
||
4414 | $this->window_size_client_to_server[$client_channel] -= strlen($temp); |
||
4415 | $this->send_binary_packet($packet); |
||
4416 | } |
||
4417 | } |
||
4418 | |||
4419 | /** |
||
4420 | * Closes and flushes a channel |
||
4421 | * |
||
4422 | * \phpseclib3\Net\SSH2 doesn't properly close most channels. For exec() channels are normally closed by the server |
||
4423 | * and for SFTP channels are presumably closed when the client disconnects. This functions is intended |
||
4424 | * for SCP more than anything. |
||
4425 | * |
||
4426 | * @param int $client_channel |
||
4427 | * @param bool $want_reply |
||
4428 | * @return void |
||
874 | daniel-mar | 4429 | * @access private |
827 | daniel-mar | 4430 | */ |
4431 | private function close_channel($client_channel, $want_reply = false) |
||
4432 | { |
||
4433 | // see http://tools.ietf.org/html/rfc4254#section-5.3 |
||
4434 | |||
874 | daniel-mar | 4435 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4436 | |
4437 | if (!$want_reply) { |
||
874 | daniel-mar | 4438 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4439 | } |
4440 | |||
874 | daniel-mar | 4441 | $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE; |
827 | daniel-mar | 4442 | |
4443 | $this->curTimeout = 5; |
||
4444 | |||
4445 | while (!is_bool($this->get_channel_packet($client_channel))) { |
||
4446 | } |
||
4447 | |||
4448 | if ($this->is_timeout) { |
||
4449 | $this->disconnect(); |
||
4450 | } |
||
4451 | |||
4452 | if ($want_reply) { |
||
874 | daniel-mar | 4453 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4454 | } |
4455 | |||
4456 | if ($this->bitmap & self::MASK_SHELL) { |
||
4457 | $this->bitmap &= ~self::MASK_SHELL; |
||
4458 | } |
||
4459 | } |
||
4460 | |||
4461 | /** |
||
4462 | * Disconnect |
||
4463 | * |
||
4464 | * @param int $reason |
||
4465 | * @return false |
||
874 | daniel-mar | 4466 | * @access protected |
827 | daniel-mar | 4467 | */ |
4468 | protected function disconnect_helper($reason) |
||
4469 | { |
||
4470 | if ($this->bitmap & self::MASK_CONNECTED) { |
||
874 | daniel-mar | 4471 | $data = Strings::packSSH2('CNss', NET_SSH2_MSG_DISCONNECT, $reason, '', ''); |
827 | daniel-mar | 4472 | try { |
4473 | $this->send_binary_packet($data); |
||
4474 | } catch (\Exception $e) { |
||
4475 | } |
||
4476 | } |
||
4477 | |||
4478 | $this->bitmap = 0; |
||
4479 | if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') { |
||
4480 | fclose($this->fsock); |
||
4481 | } |
||
4482 | |||
4483 | return false; |
||
4484 | } |
||
4485 | |||
4486 | /** |
||
874 | daniel-mar | 4487 | * Define Array |
4488 | * |
||
4489 | * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of |
||
4490 | * named constants from it, using the value as the name of the constant and the index as the value of the constant. |
||
4491 | * If any of the constants that would be defined already exists, none of the constants will be defined. |
||
4492 | * |
||
4493 | * @param mixed[] ...$args |
||
4494 | * @access protected |
||
4495 | */ |
||
4496 | protected function define_array(...$args) |
||
4497 | { |
||
4498 | foreach ($args as $arg) { |
||
4499 | foreach ($arg as $key => $value) { |
||
4500 | if (!defined($value)) { |
||
4501 | define($value, $key); |
||
4502 | } else { |
||
4503 | break 2; |
||
4504 | } |
||
4505 | } |
||
4506 | } |
||
4507 | } |
||
4508 | |||
4509 | /** |
||
827 | daniel-mar | 4510 | * Returns a log of the packets that have been sent and received. |
4511 | * |
||
4512 | * Returns a string if NET_SSH2_LOGGING == self::LOG_COMPLEX, an array if NET_SSH2_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING') |
||
4513 | * |
||
874 | daniel-mar | 4514 | * @access public |
827 | daniel-mar | 4515 | * @return array|false|string |
4516 | */ |
||
4517 | public function getLog() |
||
4518 | { |
||
4519 | if (!defined('NET_SSH2_LOGGING')) { |
||
4520 | return false; |
||
4521 | } |
||
4522 | |||
4523 | switch (NET_SSH2_LOGGING) { |
||
4524 | case self::LOG_SIMPLE: |
||
4525 | return $this->message_number_log; |
||
4526 | case self::LOG_COMPLEX: |
||
4527 | $log = $this->format_log($this->message_log, $this->message_number_log); |
||
4528 | return PHP_SAPI == 'cli' ? $log : '<pre>' . $log . '</pre>'; |
||
4529 | default: |
||
4530 | return false; |
||
4531 | } |
||
4532 | } |
||
4533 | |||
4534 | /** |
||
4535 | * Formats a log for printing |
||
4536 | * |
||
4537 | * @param array $message_log |
||
4538 | * @param array $message_number_log |
||
874 | daniel-mar | 4539 | * @access private |
827 | daniel-mar | 4540 | * @return string |
4541 | */ |
||
4542 | protected function format_log($message_log, $message_number_log) |
||
4543 | { |
||
4544 | $output = ''; |
||
4545 | for ($i = 0; $i < count($message_log); $i++) { |
||
4546 | $output .= $message_number_log[$i] . "\r\n"; |
||
4547 | $current_log = $message_log[$i]; |
||
4548 | $j = 0; |
||
4549 | do { |
||
4550 | if (strlen($current_log)) { |
||
4551 | $output .= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; |
||
4552 | } |
||
4553 | $fragment = Strings::shift($current_log, $this->log_short_width); |
||
4554 | $hex = substr(preg_replace_callback('#.#s', function ($matches) { |
||
4555 | return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); |
||
4556 | }, $fragment), strlen($this->log_boundary)); |
||
4557 | // replace non ASCII printable characters with dots |
||
4558 | // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters |
||
4559 | // also replace < with a . since < messes up the output on web browsers |
||
4560 | $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); |
||
4561 | $output .= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; |
||
4562 | $j++; |
||
4563 | } while (strlen($current_log)); |
||
4564 | $output .= "\r\n"; |
||
4565 | } |
||
4566 | |||
4567 | return $output; |
||
4568 | } |
||
4569 | |||
4570 | /** |
||
4571 | * Helper function for agent->on_channel_open() |
||
4572 | * |
||
4573 | * Used when channels are created to inform agent |
||
4574 | * of said channel opening. Must be called after |
||
4575 | * channel open confirmation received |
||
4576 | * |
||
874 | daniel-mar | 4577 | * @access private |
827 | daniel-mar | 4578 | */ |
4579 | private function on_channel_open() |
||
4580 | { |
||
4581 | if (isset($this->agent)) { |
||
4582 | $this->agent->registerChannelOpen($this); |
||
4583 | } |
||
4584 | } |
||
4585 | |||
4586 | /** |
||
4587 | * Returns the first value of the intersection of two arrays or false if |
||
4588 | * the intersection is empty. The order is defined by the first parameter. |
||
4589 | * |
||
4590 | * @param array $array1 |
||
4591 | * @param array $array2 |
||
4592 | * @return mixed False if intersection is empty, else intersected value. |
||
874 | daniel-mar | 4593 | * @access private |
827 | daniel-mar | 4594 | */ |
4595 | private static function array_intersect_first($array1, $array2) |
||
4596 | { |
||
4597 | foreach ($array1 as $value) { |
||
4598 | if (in_array($value, $array2)) { |
||
4599 | return $value; |
||
4600 | } |
||
4601 | } |
||
4602 | return false; |
||
4603 | } |
||
4604 | |||
4605 | /** |
||
4606 | * Returns all errors |
||
4607 | * |
||
4608 | * @return string[] |
||
874 | daniel-mar | 4609 | * @access public |
827 | daniel-mar | 4610 | */ |
4611 | public function getErrors() |
||
4612 | { |
||
4613 | return $this->errors; |
||
4614 | } |
||
4615 | |||
4616 | /** |
||
4617 | * Returns the last error |
||
4618 | * |
||
4619 | * @return string |
||
874 | daniel-mar | 4620 | * @access public |
827 | daniel-mar | 4621 | */ |
4622 | public function getLastError() |
||
4623 | { |
||
4624 | $count = count($this->errors); |
||
4625 | |||
4626 | if ($count > 0) { |
||
4627 | return $this->errors[$count - 1]; |
||
4628 | } |
||
4629 | } |
||
4630 | |||
4631 | /** |
||
4632 | * Return the server identification. |
||
4633 | * |
||
4634 | * @return string|false |
||
874 | daniel-mar | 4635 | * @access public |
827 | daniel-mar | 4636 | */ |
4637 | public function getServerIdentification() |
||
4638 | { |
||
4639 | $this->connect(); |
||
4640 | |||
4641 | return $this->server_identifier; |
||
4642 | } |
||
4643 | |||
4644 | /** |
||
4645 | * Returns a list of algorithms the server supports |
||
4646 | * |
||
4647 | * @return array |
||
874 | daniel-mar | 4648 | * @access public |
827 | daniel-mar | 4649 | */ |
4650 | public function getServerAlgorithms() |
||
4651 | { |
||
4652 | $this->connect(); |
||
4653 | |||
4654 | return [ |
||
4655 | 'kex' => $this->kex_algorithms, |
||
4656 | 'hostkey' => $this->server_host_key_algorithms, |
||
4657 | 'client_to_server' => [ |
||
4658 | 'crypt' => $this->encryption_algorithms_client_to_server, |
||
4659 | 'mac' => $this->mac_algorithms_client_to_server, |
||
4660 | 'comp' => $this->compression_algorithms_client_to_server, |
||
4661 | 'lang' => $this->languages_client_to_server |
||
4662 | ], |
||
4663 | 'server_to_client' => [ |
||
4664 | 'crypt' => $this->encryption_algorithms_server_to_client, |
||
4665 | 'mac' => $this->mac_algorithms_server_to_client, |
||
4666 | 'comp' => $this->compression_algorithms_server_to_client, |
||
4667 | 'lang' => $this->languages_server_to_client |
||
4668 | ] |
||
4669 | ]; |
||
4670 | } |
||
4671 | |||
4672 | /** |
||
4673 | * Returns a list of KEX algorithms that phpseclib supports |
||
4674 | * |
||
4675 | * @return array |
||
874 | daniel-mar | 4676 | * @access public |
827 | daniel-mar | 4677 | */ |
4678 | public static function getSupportedKEXAlgorithms() |
||
4679 | { |
||
4680 | $kex_algorithms = [ |
||
4681 | // Elliptic Curve Diffie-Hellman Key Agreement (ECDH) using |
||
4682 | // Curve25519. See doc/curve25519-sha256@libssh.org.txt in the |
||
4683 | // libssh repository for more information. |
||
4684 | 'curve25519-sha256', |
||
4685 | 'curve25519-sha256@libssh.org', |
||
4686 | |||
4687 | 'ecdh-sha2-nistp256', // RFC 5656 |
||
4688 | 'ecdh-sha2-nistp384', // RFC 5656 |
||
4689 | 'ecdh-sha2-nistp521', // RFC 5656 |
||
4690 | |||
4691 | 'diffie-hellman-group-exchange-sha256',// RFC 4419 |
||
4692 | 'diffie-hellman-group-exchange-sha1', // RFC 4419 |
||
4693 | |||
4694 | // Diffie-Hellman Key Agreement (DH) using integer modulo prime |
||
4695 | // groups. |
||
4696 | 'diffie-hellman-group14-sha256', |
||
4697 | 'diffie-hellman-group14-sha1', // REQUIRED |
||
4698 | 'diffie-hellman-group15-sha512', |
||
4699 | 'diffie-hellman-group16-sha512', |
||
4700 | 'diffie-hellman-group17-sha512', |
||
4701 | 'diffie-hellman-group18-sha512', |
||
4702 | |||
4703 | 'diffie-hellman-group1-sha1', // REQUIRED |
||
4704 | ]; |
||
4705 | |||
4706 | return $kex_algorithms; |
||
4707 | } |
||
4708 | |||
4709 | /** |
||
4710 | * Returns a list of host key algorithms that phpseclib supports |
||
4711 | * |
||
4712 | * @return array |
||
874 | daniel-mar | 4713 | * @access public |
827 | daniel-mar | 4714 | */ |
4715 | public static function getSupportedHostKeyAlgorithms() |
||
4716 | { |
||
4717 | return [ |
||
4718 | 'ssh-ed25519', // https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02 |
||
4719 | 'ecdsa-sha2-nistp256', // RFC 5656 |
||
4720 | 'ecdsa-sha2-nistp384', // RFC 5656 |
||
4721 | 'ecdsa-sha2-nistp521', // RFC 5656 |
||
4722 | 'rsa-sha2-256', // RFC 8332 |
||
4723 | 'rsa-sha2-512', // RFC 8332 |
||
4724 | 'ssh-rsa', // RECOMMENDED sign Raw RSA Key |
||
4725 | 'ssh-dss' // REQUIRED sign Raw DSS Key |
||
4726 | ]; |
||
4727 | } |
||
4728 | |||
4729 | /** |
||
4730 | * Returns a list of symmetric key algorithms that phpseclib supports |
||
4731 | * |
||
4732 | * @return array |
||
874 | daniel-mar | 4733 | * @access public |
827 | daniel-mar | 4734 | */ |
4735 | public static function getSupportedEncryptionAlgorithms() |
||
4736 | { |
||
4737 | $algos = [ |
||
4738 | // from <https://tools.ietf.org/html/rfc5647>: |
||
4739 | 'aes128-gcm@openssh.com', |
||
4740 | 'aes256-gcm@openssh.com', |
||
4741 | |||
4742 | // from <http://tools.ietf.org/html/rfc4345#section-4>: |
||
4743 | 'arcfour256', |
||
4744 | 'arcfour128', |
||
4745 | |||
4746 | //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key |
||
4747 | |||
4748 | // CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>: |
||
4749 | 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key |
||
4750 | 'aes192-ctr', // RECOMMENDED AES with 192-bit key |
||
4751 | 'aes256-ctr', // RECOMMENDED AES with 256-bit key |
||
4752 | |||
874 | daniel-mar | 4753 | // from <https://git.io/fhxOl>: |
827 | daniel-mar | 4754 | // one of the big benefits of chacha20-poly1305 is speed. the problem is... |
4755 | // libsodium doesn't generate the poly1305 keys in the way ssh does and openssl's PHP bindings don't even |
||
4756 | // seem to support poly1305 currently. so even if libsodium or openssl are being used for the chacha20 |
||
4757 | // part, pure-PHP has to be used for the poly1305 part and that's gonna cause a big slow down. |
||
4758 | // speed-wise it winds up being faster to use AES (when openssl or mcrypt are available) and some HMAC |
||
4759 | // (which is always gonna be super fast to compute thanks to the hash extension, which |
||
4760 | // "is bundled and compiled into PHP by default") |
||
4761 | 'chacha20-poly1305@openssh.com', |
||
4762 | |||
4763 | 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key |
||
4764 | 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key |
||
4765 | 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key |
||
4766 | |||
4767 | 'aes128-cbc', // RECOMMENDED AES with a 128-bit key |
||
4768 | 'aes192-cbc', // OPTIONAL AES with a 192-bit key |
||
4769 | 'aes256-cbc', // OPTIONAL AES in CBC mode, with a 256-bit key |
||
4770 | |||
4771 | 'twofish128-cbc', // OPTIONAL Twofish with a 128-bit key |
||
4772 | 'twofish192-cbc', // OPTIONAL Twofish with a 192-bit key |
||
4773 | 'twofish256-cbc', |
||
4774 | 'twofish-cbc', // OPTIONAL alias for "twofish256-cbc" |
||
4775 | // (this is being retained for historical reasons) |
||
4776 | |||
4777 | 'blowfish-ctr', // OPTIONAL Blowfish in SDCTR mode |
||
4778 | |||
4779 | 'blowfish-cbc', // OPTIONAL Blowfish in CBC mode |
||
4780 | |||
4781 | '3des-ctr', // RECOMMENDED Three-key 3DES in SDCTR mode |
||
4782 | |||
4783 | '3des-cbc', // REQUIRED three-key 3DES in CBC mode |
||
4784 | |||
4785 | //'none' // OPTIONAL no encryption; NOT RECOMMENDED |
||
4786 | ]; |
||
4787 | |||
4788 | if (self::$crypto_engine) { |
||
4789 | $engines = [self::$crypto_engine]; |
||
4790 | } else { |
||
4791 | $engines = [ |
||
4792 | 'libsodium', |
||
4793 | 'OpenSSL (GCM)', |
||
4794 | 'OpenSSL', |
||
4795 | 'mcrypt', |
||
4796 | 'Eval', |
||
4797 | 'PHP' |
||
4798 | ]; |
||
4799 | } |
||
4800 | |||
4801 | $ciphers = []; |
||
4802 | |||
4803 | foreach ($engines as $engine) { |
||
4804 | foreach ($algos as $algo) { |
||
4805 | $obj = self::encryption_algorithm_to_crypt_instance($algo); |
||
4806 | if ($obj instanceof Rijndael) { |
||
4807 | $obj->setKeyLength(preg_replace('#[^\d]#', '', $algo)); |
||
4808 | } |
||
4809 | switch ($algo) { |
||
4810 | case 'chacha20-poly1305@openssh.com': |
||
4811 | case 'arcfour128': |
||
4812 | case 'arcfour256': |
||
4813 | if ($engine != 'Eval') { |
||
4814 | continue 2; |
||
4815 | } |
||
4816 | break; |
||
4817 | case 'aes128-gcm@openssh.com': |
||
4818 | case 'aes256-gcm@openssh.com': |
||
4819 | if ($engine == 'OpenSSL') { |
||
4820 | continue 2; |
||
4821 | } |
||
4822 | $obj->setNonce('dummydummydu'); |
||
4823 | } |
||
4824 | if ($obj->isValidEngine($engine)) { |
||
4825 | $algos = array_diff($algos, [$algo]); |
||
4826 | $ciphers[] = $algo; |
||
4827 | } |
||
4828 | } |
||
4829 | } |
||
4830 | |||
4831 | return $ciphers; |
||
4832 | } |
||
4833 | |||
4834 | /** |
||
4835 | * Returns a list of MAC algorithms that phpseclib supports |
||
4836 | * |
||
4837 | * @return array |
||
874 | daniel-mar | 4838 | * @access public |
827 | daniel-mar | 4839 | */ |
4840 | public static function getSupportedMACAlgorithms() |
||
4841 | { |
||
4842 | return [ |
||
4843 | 'hmac-sha2-256-etm@openssh.com', |
||
4844 | 'hmac-sha2-512-etm@openssh.com', |
||
4845 | 'umac-64-etm@openssh.com', |
||
4846 | 'umac-128-etm@openssh.com', |
||
4847 | 'hmac-sha1-etm@openssh.com', |
||
4848 | |||
4849 | // from <http://www.ietf.org/rfc/rfc6668.txt>: |
||
4850 | 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) |
||
4851 | 'hmac-sha2-512',// OPTIONAL HMAC-SHA512 (digest length = key length = 64) |
||
4852 | |||
4853 | // from <https://tools.ietf.org/html/draft-miller-secsh-umac-01>: |
||
4854 | 'umac-64@openssh.com', |
||
4855 | 'umac-128@openssh.com', |
||
4856 | |||
4857 | 'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20) |
||
4858 | 'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20) |
||
4859 | 'hmac-md5-96', // OPTIONAL first 96 bits of HMAC-MD5 (digest length = 12, key length = 16) |
||
4860 | 'hmac-md5', // OPTIONAL HMAC-MD5 (digest length = key length = 16) |
||
4861 | //'none' // OPTIONAL no MAC; NOT RECOMMENDED |
||
4862 | ]; |
||
4863 | } |
||
4864 | |||
4865 | /** |
||
4866 | * Returns a list of compression algorithms that phpseclib supports |
||
4867 | * |
||
4868 | * @return array |
||
874 | daniel-mar | 4869 | * @access public |
827 | daniel-mar | 4870 | */ |
4871 | public static function getSupportedCompressionAlgorithms() |
||
4872 | { |
||
4873 | $algos = ['none']; // REQUIRED no compression |
||
4874 | if (function_exists('deflate_init')) { |
||
4875 | $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed |
||
4876 | $algos[] = 'zlib'; |
||
4877 | } |
||
4878 | return $algos; |
||
4879 | } |
||
4880 | |||
4881 | /** |
||
4882 | * Return list of negotiated algorithms |
||
4883 | * |
||
4884 | * Uses the same format as https://www.php.net/ssh2-methods-negotiated |
||
4885 | * |
||
4886 | * @return array |
||
874 | daniel-mar | 4887 | * @access public |
827 | daniel-mar | 4888 | */ |
4889 | public function getAlgorithmsNegotiated() |
||
4890 | { |
||
4891 | $this->connect(); |
||
4892 | |||
4893 | $compression_map = [ |
||
4894 | self::NET_SSH2_COMPRESSION_NONE => 'none', |
||
4895 | self::NET_SSH2_COMPRESSION_ZLIB => 'zlib', |
||
4896 | self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com' |
||
4897 | ]; |
||
4898 | |||
4899 | return [ |
||
4900 | 'kex' => $this->kex_algorithm, |
||
4901 | 'hostkey' => $this->signature_format, |
||
4902 | 'client_to_server' => [ |
||
4903 | 'crypt' => $this->encryptName, |
||
4904 | 'mac' => $this->hmac_create_name, |
||
4905 | 'comp' => $compression_map[$this->compress], |
||
4906 | ], |
||
4907 | 'server_to_client' => [ |
||
4908 | 'crypt' => $this->decryptName, |
||
4909 | 'mac' => $this->hmac_check_name, |
||
4910 | 'comp' => $compression_map[$this->decompress], |
||
4911 | ] |
||
4912 | ]; |
||
4913 | } |
||
4914 | |||
4915 | /** |
||
4916 | * Allows you to set the terminal |
||
4917 | * |
||
4918 | * @param string $term |
||
874 | daniel-mar | 4919 | * @access public |
827 | daniel-mar | 4920 | */ |
4921 | public function setTerminal($term) |
||
4922 | { |
||
4923 | $this->term = $term; |
||
4924 | } |
||
4925 | |||
4926 | /** |
||
4927 | * Accepts an associative array with up to four parameters as described at |
||
4928 | * <https://www.php.net/manual/en/function.ssh2-connect.php> |
||
4929 | * |
||
4930 | * @param array $methods |
||
874 | daniel-mar | 4931 | * @access public |
827 | daniel-mar | 4932 | */ |
4933 | public function setPreferredAlgorithms(array $methods) |
||
4934 | { |
||
4935 | $preferred = $methods; |
||
4936 | |||
4937 | if (isset($preferred['kex'])) { |
||
4938 | $preferred['kex'] = array_intersect( |
||
4939 | $preferred['kex'], |
||
4940 | static::getSupportedKEXAlgorithms() |
||
4941 | ); |
||
4942 | } |
||
4943 | |||
4944 | if (isset($preferred['hostkey'])) { |
||
4945 | $preferred['hostkey'] = array_intersect( |
||
4946 | $preferred['hostkey'], |
||
4947 | static::getSupportedHostKeyAlgorithms() |
||
4948 | ); |
||
4949 | } |
||
4950 | |||
4951 | $keys = ['client_to_server', 'server_to_client']; |
||
4952 | foreach ($keys as $key) { |
||
4953 | if (isset($preferred[$key])) { |
||
4954 | $a = &$preferred[$key]; |
||
4955 | if (isset($a['crypt'])) { |
||
4956 | $a['crypt'] = array_intersect( |
||
4957 | $a['crypt'], |
||
4958 | static::getSupportedEncryptionAlgorithms() |
||
4959 | ); |
||
4960 | } |
||
4961 | if (isset($a['comp'])) { |
||
4962 | $a['comp'] = array_intersect( |
||
4963 | $a['comp'], |
||
4964 | static::getSupportedCompressionAlgorithms() |
||
4965 | ); |
||
4966 | } |
||
4967 | if (isset($a['mac'])) { |
||
4968 | $a['mac'] = array_intersect( |
||
4969 | $a['mac'], |
||
4970 | static::getSupportedMACAlgorithms() |
||
4971 | ); |
||
4972 | } |
||
4973 | } |
||
4974 | } |
||
4975 | |||
4976 | $keys = [ |
||
4977 | 'kex', |
||
4978 | 'hostkey', |
||
4979 | 'client_to_server/crypt', |
||
4980 | 'client_to_server/comp', |
||
4981 | 'client_to_server/mac', |
||
4982 | 'server_to_client/crypt', |
||
4983 | 'server_to_client/comp', |
||
4984 | 'server_to_client/mac', |
||
4985 | ]; |
||
4986 | foreach ($keys as $key) { |
||
4987 | $p = $preferred; |
||
4988 | $m = $methods; |
||
4989 | |||
4990 | $subkeys = explode('/', $key); |
||
4991 | foreach ($subkeys as $subkey) { |
||
4992 | if (!isset($p[$subkey])) { |
||
4993 | continue 2; |
||
4994 | } |
||
4995 | $p = $p[$subkey]; |
||
4996 | $m = $m[$subkey]; |
||
4997 | } |
||
4998 | |||
4999 | if (count($p) != count($m)) { |
||
5000 | $diff = array_diff($m, $p); |
||
5001 | $msg = count($diff) == 1 ? |
||
5002 | ' is not a supported algorithm' : |
||
5003 | ' are not supported algorithms'; |
||
5004 | throw new UnsupportedAlgorithmException(implode(', ', $diff) . $msg); |
||
5005 | } |
||
5006 | } |
||
5007 | |||
5008 | $this->preferred = $preferred; |
||
5009 | } |
||
5010 | |||
5011 | /** |
||
5012 | * Returns the banner message. |
||
5013 | * |
||
5014 | * Quoting from the RFC, "in some jurisdictions, sending a warning message before |
||
5015 | * authentication may be relevant for getting legal protection." |
||
5016 | * |
||
5017 | * @return string |
||
874 | daniel-mar | 5018 | * @access public |
827 | daniel-mar | 5019 | */ |
5020 | public function getBannerMessage() |
||
5021 | { |
||
5022 | return $this->banner_message; |
||
5023 | } |
||
5024 | |||
5025 | /** |
||
5026 | * Returns the server public host key. |
||
5027 | * |
||
5028 | * Caching this the first time you connect to a server and checking the result on subsequent connections |
||
5029 | * is recommended. Returns false if the server signature is not signed correctly with the public host key. |
||
5030 | * |
||
5031 | * @return string|false |
||
5032 | * @throws \RuntimeException on badly formatted keys |
||
5033 | * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when the key isn't in a supported format |
||
874 | daniel-mar | 5034 | * @access public |
827 | daniel-mar | 5035 | */ |
5036 | public function getServerPublicHostKey() |
||
5037 | { |
||
5038 | if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { |
||
5039 | $this->connect(); |
||
5040 | } |
||
5041 | |||
5042 | $signature = $this->signature; |
||
5043 | $server_public_host_key = base64_encode($this->server_public_host_key); |
||
5044 | |||
5045 | if ($this->signature_validated) { |
||
5046 | return $this->bitmap ? |
||
5047 | $this->signature_format . ' ' . $server_public_host_key : |
||
5048 | false; |
||
5049 | } |
||
5050 | |||
5051 | $this->signature_validated = true; |
||
5052 | |||
5053 | switch ($this->signature_format) { |
||
5054 | case 'ssh-ed25519': |
||
5055 | case 'ecdsa-sha2-nistp256': |
||
5056 | case 'ecdsa-sha2-nistp384': |
||
5057 | case 'ecdsa-sha2-nistp521': |
||
5058 | $key = EC::loadFormat('OpenSSH', $server_public_host_key) |
||
5059 | ->withSignatureFormat('SSH2'); |
||
5060 | switch ($this->signature_format) { |
||
5061 | case 'ssh-ed25519': |
||
5062 | $hash = 'sha512'; |
||
5063 | break; |
||
5064 | case 'ecdsa-sha2-nistp256': |
||
5065 | $hash = 'sha256'; |
||
5066 | break; |
||
5067 | case 'ecdsa-sha2-nistp384': |
||
5068 | $hash = 'sha384'; |
||
5069 | break; |
||
5070 | case 'ecdsa-sha2-nistp521': |
||
5071 | $hash = 'sha512'; |
||
5072 | } |
||
5073 | $key = $key->withHash($hash); |
||
5074 | break; |
||
5075 | case 'ssh-dss': |
||
5076 | $key = DSA::loadFormat('OpenSSH', $server_public_host_key) |
||
5077 | ->withSignatureFormat('SSH2') |
||
5078 | ->withHash('sha1'); |
||
5079 | break; |
||
5080 | case 'ssh-rsa': |
||
5081 | case 'rsa-sha2-256': |
||
5082 | case 'rsa-sha2-512': |
||
5083 | // could be ssh-rsa, rsa-sha2-256, rsa-sha2-512 |
||
5084 | // we don't check here because we already checked in key_exchange |
||
5085 | // some signatures have the type embedded within the message and some don't |
||
5086 | list(, $signature) = Strings::unpackSSH2('ss', $signature); |
||
5087 | |||
5088 | $key = RSA::loadFormat('OpenSSH', $server_public_host_key) |
||
5089 | ->withPadding(RSA::SIGNATURE_PKCS1); |
||
5090 | switch ($this->signature_format) { |
||
5091 | case 'rsa-sha2-512': |
||
5092 | $hash = 'sha512'; |
||
5093 | break; |
||
5094 | case 'rsa-sha2-256': |
||
5095 | $hash = 'sha256'; |
||
5096 | break; |
||
5097 | //case 'ssh-rsa': |
||
5098 | default: |
||
5099 | $hash = 'sha1'; |
||
5100 | } |
||
5101 | $key = $key->withHash($hash); |
||
5102 | break; |
||
5103 | default: |
||
874 | daniel-mar | 5104 | $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 5105 | throw new NoSupportedAlgorithmsException('Unsupported signature format'); |
5106 | } |
||
5107 | |||
5108 | if (!$key->verify($this->exchange_hash, $signature)) { |
||
874 | daniel-mar | 5109 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 5110 | }; |
5111 | |||
5112 | return $this->signature_format . ' ' . $server_public_host_key; |
||
5113 | } |
||
5114 | |||
5115 | /** |
||
5116 | * Returns the exit status of an SSH command or false. |
||
5117 | * |
||
5118 | * @return false|int |
||
874 | daniel-mar | 5119 | * @access public |
827 | daniel-mar | 5120 | */ |
5121 | public function getExitStatus() |
||
5122 | { |
||
5123 | if (is_null($this->exit_status)) { |
||
5124 | return false; |
||
5125 | } |
||
5126 | return $this->exit_status; |
||
5127 | } |
||
5128 | |||
5129 | /** |
||
5130 | * Returns the number of columns for the terminal window size. |
||
5131 | * |
||
5132 | * @return int |
||
874 | daniel-mar | 5133 | * @access public |
827 | daniel-mar | 5134 | */ |
5135 | public function getWindowColumns() |
||
5136 | { |
||
5137 | return $this->windowColumns; |
||
5138 | } |
||
5139 | |||
5140 | /** |
||
5141 | * Returns the number of rows for the terminal window size. |
||
5142 | * |
||
5143 | * @return int |
||
874 | daniel-mar | 5144 | * @access public |
827 | daniel-mar | 5145 | */ |
5146 | public function getWindowRows() |
||
5147 | { |
||
5148 | return $this->windowRows; |
||
5149 | } |
||
5150 | |||
5151 | /** |
||
5152 | * Sets the number of columns for the terminal window size. |
||
5153 | * |
||
5154 | * @param int $value |
||
874 | daniel-mar | 5155 | * @access public |
827 | daniel-mar | 5156 | */ |
5157 | public function setWindowColumns($value) |
||
5158 | { |
||
5159 | $this->windowColumns = $value; |
||
5160 | } |
||
5161 | |||
5162 | /** |
||
5163 | * Sets the number of rows for the terminal window size. |
||
5164 | * |
||
5165 | * @param int $value |
||
874 | daniel-mar | 5166 | * @access public |
827 | daniel-mar | 5167 | */ |
5168 | public function setWindowRows($value) |
||
5169 | { |
||
5170 | $this->windowRows = $value; |
||
5171 | } |
||
5172 | |||
5173 | /** |
||
5174 | * Sets the number of columns and rows for the terminal window size. |
||
5175 | * |
||
5176 | * @param int $columns |
||
5177 | * @param int $rows |
||
874 | daniel-mar | 5178 | * @access public |
827 | daniel-mar | 5179 | */ |
5180 | public function setWindowSize($columns = 80, $rows = 24) |
||
5181 | { |
||
5182 | $this->windowColumns = $columns; |
||
5183 | $this->windowRows = $rows; |
||
5184 | } |
||
5185 | |||
5186 | /** |
||
5187 | * To String Magic Method |
||
5188 | * |
||
5189 | * @return string |
||
874 | daniel-mar | 5190 | * @access public |
827 | daniel-mar | 5191 | */ |
5192 | #[\ReturnTypeWillChange] |
||
5193 | public function __toString() |
||
5194 | { |
||
5195 | return $this->getResourceId(); |
||
5196 | } |
||
5197 | |||
5198 | /** |
||
5199 | * Get Resource ID |
||
5200 | * |
||
5201 | * We use {} because that symbols should not be in URL according to |
||
5202 | * {@link http://tools.ietf.org/html/rfc3986#section-2 RFC}. |
||
5203 | * It will safe us from any conflicts, because otherwise regexp will |
||
5204 | * match all alphanumeric domains. |
||
5205 | * |
||
5206 | * @return string |
||
5207 | */ |
||
5208 | public function getResourceId() |
||
5209 | { |
||
5210 | return '{' . spl_object_hash($this) . '}'; |
||
5211 | } |
||
5212 | |||
5213 | /** |
||
5214 | * Return existing connection |
||
5215 | * |
||
5216 | * @param string $id |
||
5217 | * |
||
5218 | * @return bool|SSH2 will return false if no such connection |
||
5219 | */ |
||
5220 | public static function getConnectionByResourceId($id) |
||
5221 | { |
||
5222 | if (isset(self::$connections[$id])) { |
||
5223 | return self::$connections[$id] instanceof \WeakReference ? self::$connections[$id]->get() : self::$connections[$id]; |
||
5224 | } |
||
5225 | return false; |
||
5226 | } |
||
5227 | |||
5228 | /** |
||
5229 | * Return all excising connections |
||
5230 | * |
||
5231 | * @return array<string, SSH2> |
||
5232 | */ |
||
5233 | public static function getConnections() |
||
5234 | { |
||
5235 | if (!class_exists('WeakReference')) { |
||
5236 | /** @var array<string, SSH2> */ |
||
5237 | return self::$connections; |
||
5238 | } |
||
5239 | $temp = []; |
||
5240 | foreach (self::$connections as $key => $ref) { |
||
5241 | $temp[$key] = $ref->get(); |
||
5242 | } |
||
5243 | return $temp; |
||
5244 | } |
||
5245 | |||
874 | daniel-mar | 5246 | /* |
827 | daniel-mar | 5247 | * Update packet types in log history |
5248 | * |
||
5249 | * @param string $old |
||
5250 | * @param string $new |
||
874 | daniel-mar | 5251 | * @access private |
827 | daniel-mar | 5252 | */ |
5253 | private function updateLogHistory($old, $new) |
||
5254 | { |
||
5255 | if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == self::LOG_COMPLEX) { |
||
5256 | $this->message_number_log[count($this->message_number_log) - 1] = str_replace( |
||
5257 | $old, |
||
5258 | $new, |
||
5259 | $this->message_number_log[count($this->message_number_log) - 1] |
||
5260 | ); |
||
5261 | } |
||
5262 | } |
||
5263 | |||
5264 | /** |
||
5265 | * Return the list of authentication methods that may productively continue authentication. |
||
5266 | * |
||
5267 | * @see https://tools.ietf.org/html/rfc4252#section-5.1 |
||
5268 | * @return array|null |
||
5269 | */ |
||
5270 | public function getAuthMethodsToContinue() |
||
5271 | { |
||
5272 | return $this->auth_methods_to_continue; |
||
5273 | } |
||
5274 | |||
5275 | /** |
||
5276 | * Enables "smart" multi-factor authentication (MFA) |
||
5277 | */ |
||
5278 | public function enableSmartMFA() |
||
5279 | { |
||
5280 | $this->smartMFA = true; |
||
5281 | } |
||
5282 | |||
5283 | /** |
||
5284 | * Disables "smart" multi-factor authentication (MFA) |
||
5285 | */ |
||
5286 | public function disableSmartMFA() |
||
5287 | { |
||
5288 | $this->smartMFA = false; |
||
5289 | } |
||
5290 | } |