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