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