File manager - Edit - /home/aresglob/public_html/wp/wp-includes/images/smilies/Net.zip
Back
PK G�l[�A�� � SSH1.phpnu �[��� <?php /** * Pure-PHP implementation of SSHv1. * * PHP versions 4 and 5 * * Here's a short example of how to use this library: * <code> * <?php * include 'Net/SSH1.php'; * * $ssh = new Net_SSH1('www.domain.tld'); * if (!$ssh->login('username', 'password')) { * exit('Login Failed'); * } * * echo $ssh->exec('ls -la'); * ?> * </code> * * Here's another short example: * <code> * <?php * include 'Net/SSH1.php'; * * $ssh = new Net_SSH1('www.domain.tld'); * if (!$ssh->login('username', 'password')) { * exit('Login Failed'); * } * * echo $ssh->read('username@username:~$'); * $ssh->write("ls -la\n"); * echo $ssh->read('username@username:~$'); * ?> * </code> * * More information on the SSHv1 specification can be found by reading * {@link http://www.snailbook.com/docs/protocol-1.5.txt protocol-1.5.txt}. * * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @category Net * @package Net_SSH1 * @author Jim Wigginton <terrafrost@php.net> * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ /**#@+ * Encryption Methods * * @see self::getSupportedCiphers() * @access public */ /** * No encryption * * Not supported. */ define('NET_SSH1_CIPHER_NONE', 0); /** * IDEA in CFB mode * * Not supported. */ define('NET_SSH1_CIPHER_IDEA', 1); /** * DES in CBC mode */ define('NET_SSH1_CIPHER_DES', 2); /** * Triple-DES in CBC mode * * All implementations are required to support this */ define('NET_SSH1_CIPHER_3DES', 3); /** * TRI's Simple Stream encryption CBC * * Not supported nor is it defined in the official SSH1 specs. OpenSSH, however, does define it (see cipher.h), * although it doesn't use it (see cipher.c) */ define('NET_SSH1_CIPHER_BROKEN_TSS', 4); /** * RC4 * * Not supported. * * @internal According to the SSH1 specs: * * "The first 16 bytes of the session key are used as the key for * the server to client direction. The remaining 16 bytes are used * as the key for the client to server direction. This gives * independent 128-bit keys for each direction." * * This library currently only supports encryption when the same key is being used for both directions. This is * because there's only one $crypto object. Two could be added ($encrypt and $decrypt, perhaps). */ define('NET_SSH1_CIPHER_RC4', 5); /** * Blowfish * * Not supported nor is it defined in the official SSH1 specs. OpenSSH, however, defines it (see cipher.h) and * uses it (see cipher.c) */ define('NET_SSH1_CIPHER_BLOWFISH', 6); /**#@-*/ /**#@+ * Authentication Methods * * @see self::getSupportedAuthentications() * @access public */ /** * .rhosts or /etc/hosts.equiv */ define('NET_SSH1_AUTH_RHOSTS', 1); /** * pure RSA authentication */ define('NET_SSH1_AUTH_RSA', 2); /** * password authentication * * This is the only method that is supported by this library. */ define('NET_SSH1_AUTH_PASSWORD', 3); /** * .rhosts with RSA host authentication */ define('NET_SSH1_AUTH_RHOSTS_RSA', 4); /**#@-*/ /**#@+ * Terminal Modes * * @link http://3sp.com/content/developer/maverick-net/docs/Maverick.SSH.PseudoTerminalModesMembers.html * @access private */ define('NET_SSH1_TTY_OP_END', 0); /**#@-*/ /** * The Response Type * * @see self::_get_binary_packet() * @access private */ define('NET_SSH1_RESPONSE_TYPE', 1); /** * The Response Data * * @see self::_get_binary_packet() * @access private */ define('NET_SSH1_RESPONSE_DATA', 2); /**#@+ * Execution Bitmap Masks * * @see self::bitmap * @access private */ define('NET_SSH1_MASK_CONSTRUCTOR', 0x00000001); define('NET_SSH1_MASK_CONNECTED', 0x00000002); define('NET_SSH1_MASK_LOGIN', 0x00000004); define('NET_SSH1_MASK_SHELL', 0x00000008); /**#@-*/ /**#@+ * @access public * @see self::getLog() */ /** * Returns the message numbers */ define('NET_SSH1_LOG_SIMPLE', 1); /** * Returns the message content */ define('NET_SSH1_LOG_COMPLEX', 2); /** * Outputs the content real-time */ define('NET_SSH1_LOG_REALTIME', 3); /** * Dumps the content real-time to a file */ define('NET_SSH1_LOG_REALTIME_FILE', 4); /**#@-*/ /**#@+ * @access public * @see self::read() */ /** * Returns when a string matching $expect exactly is found */ define('NET_SSH1_READ_SIMPLE', 1); /** * Returns when a string matching the regular expression $expect is found */ define('NET_SSH1_READ_REGEX', 2); /**#@-*/ /** * Pure-PHP implementation of SSHv1. * * @package Net_SSH1 * @author Jim Wigginton <terrafrost@php.net> * @access public */ class Net_SSH1 { /** * The SSH identifier * * @var string * @access private */ var $identifier = 'SSH-1.5-phpseclib'; /** * The Socket Object * * @var object * @access private */ var $fsock; /** * The cryptography object * * @var object * @access private */ var $crypto = false; /** * Execution Bitmap * * The bits that are set represent functions that have been called already. This is used to determine * if a requisite function has been successfully executed. If not, an error should be thrown. * * @var int * @access private */ var $bitmap = 0; /** * The Server Key Public Exponent * * Logged for debug purposes * * @see self::getServerKeyPublicExponent() * @var string * @access private */ var $server_key_public_exponent; /** * The Server Key Public Modulus * * Logged for debug purposes * * @see self::getServerKeyPublicModulus() * @var string * @access private */ var $server_key_public_modulus; /** * The Host Key Public Exponent * * Logged for debug purposes * * @see self::getHostKeyPublicExponent() * @var string * @access private */ var $host_key_public_exponent; /** * The Host Key Public Modulus * * Logged for debug purposes * * @see self::getHostKeyPublicModulus() * @var string * @access private */ var $host_key_public_modulus; /** * Supported Ciphers * * Logged for debug purposes * * @see self::getSupportedCiphers() * @var array * @access private */ var $supported_ciphers = array( NET_SSH1_CIPHER_NONE => 'No encryption', NET_SSH1_CIPHER_IDEA => 'IDEA in CFB mode', NET_SSH1_CIPHER_DES => 'DES in CBC mode', NET_SSH1_CIPHER_3DES => 'Triple-DES in CBC mode', NET_SSH1_CIPHER_BROKEN_TSS => 'TRI\'s Simple Stream encryption CBC', NET_SSH1_CIPHER_RC4 => 'RC4', NET_SSH1_CIPHER_BLOWFISH => 'Blowfish' ); /** * Supported Authentications * * Logged for debug purposes * * @see self::getSupportedAuthentications() * @var array * @access private */ var $supported_authentications = array( NET_SSH1_AUTH_RHOSTS => '.rhosts or /etc/hosts.equiv', NET_SSH1_AUTH_RSA => 'pure RSA authentication', NET_SSH1_AUTH_PASSWORD => 'password authentication', NET_SSH1_AUTH_RHOSTS_RSA => '.rhosts with RSA host authentication' ); /** * Server Identification * * @see self::getServerIdentification() * @var string * @access private */ var $server_identification = ''; /** * Protocol Flags * * @see self::Net_SSH1() * @var array * @access private */ var $protocol_flags = array(); /** * Protocol Flag Log * * @see self::getLog() * @var array * @access private */ var $protocol_flag_log = array(); /** * Message Log * * @see self::getLog() * @var array * @access private */ var $message_log = array(); /** * Real-time log file pointer * * @see self::_append_log() * @var resource * @access private */ var $realtime_log_file; /** * Real-time log file size * * @see self::_append_log() * @var int * @access private */ var $realtime_log_size; /** * Real-time log file wrap boolean * * @see self::_append_log() * @var bool * @access private */ var $realtime_log_wrap; /** * Interactive Buffer * * @see self::read() * @var array * @access private */ var $interactiveBuffer = ''; /** * Timeout * * @see self::setTimeout() * @access private */ var $timeout; /** * Current Timeout * * @see self::_get_channel_packet() * @access private */ var $curTimeout; /** * Log Boundary * * @see self::_format_log() * @access private */ var $log_boundary = ':'; /** * Log Long Width * * @see self::_format_log() * @access private */ var $log_long_width = 65; /** * Log Short Width * * @see self::_format_log() * @access private */ var $log_short_width = 16; /** * Hostname * * @see self::Net_SSH1() * @see self::_connect() * @var string * @access private */ var $host; /** * Port Number * * @see self::Net_SSH1() * @see self::_connect() * @var int * @access private */ var $port; /** * Timeout for initial connection * * Set by the constructor call. Calling setTimeout() is optional. If it's not called functions like * exec() won't timeout unless some PHP setting forces it too. The timeout specified in the constructor, * however, is non-optional. There will be a timeout, whether or not you set it. If you don't it'll be * 10 seconds. It is used by fsockopen() in that function. * * @see self::Net_SSH1() * @see self::_connect() * @var int * @access private */ var $connectionTimeout; /** * Default cipher * * @see self::Net_SSH1() * @see self::_connect() * @var int * @access private */ var $cipher; /** * Default Constructor. * * Connects to an SSHv1 server * * @param string $host * @param int $port * @param int $timeout * @param int $cipher * @return Net_SSH1 * @access public */ function __construct($host, $port = 22, $timeout = 10, $cipher = NET_SSH1_CIPHER_3DES) { if (!class_exists('Math_BigInteger')) { include_once 'Math/BigInteger.php'; } // Include Crypt_Random // the class_exists() will only be called if the crypt_random_string function hasn't been defined and // will trigger a call to __autoload() if you're wanting to auto-load classes // call function_exists() a second time to stop the include_once from being called outside // of the auto loader if (!function_exists('crypt_random_string') && !class_exists('Crypt_Random') && !function_exists('crypt_random_string')) { include_once 'Crypt/Random.php'; } $this->protocol_flags = array( 1 => 'NET_SSH1_MSG_DISCONNECT', 2 => 'NET_SSH1_SMSG_PUBLIC_KEY', 3 => 'NET_SSH1_CMSG_SESSION_KEY', 4 => 'NET_SSH1_CMSG_USER', 9 => 'NET_SSH1_CMSG_AUTH_PASSWORD', 10 => 'NET_SSH1_CMSG_REQUEST_PTY', 12 => 'NET_SSH1_CMSG_EXEC_SHELL', 13 => 'NET_SSH1_CMSG_EXEC_CMD', 14 => 'NET_SSH1_SMSG_SUCCESS', 15 => 'NET_SSH1_SMSG_FAILURE', 16 => 'NET_SSH1_CMSG_STDIN_DATA', 17 => 'NET_SSH1_SMSG_STDOUT_DATA', 18 => 'NET_SSH1_SMSG_STDERR_DATA', 19 => 'NET_SSH1_CMSG_EOF', 20 => 'NET_SSH1_SMSG_EXITSTATUS', 33 => 'NET_SSH1_CMSG_EXIT_CONFIRMATION' ); $this->_define_array($this->protocol_flags); $this->host = $host; $this->port = $port; $this->connectionTimeout = $timeout; $this->cipher = $cipher; } /** * PHP4 compatible Default Constructor. * * @see self::__construct() * @param string $host * @param int $port * @param int $timeout * @param int $cipher * @access public */ function Net_SSH1($host, $port = 22, $timeout = 10, $cipher = NET_SSH1_CIPHER_3DES) { $this->__construct($host, $port, $timeout, $cipher); } /** * Connect to an SSHv1 server * * @return bool * @access private */ function _connect() { $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->connectionTimeout); if (!$this->fsock) { user_error(rtrim("Cannot connect to {$this->host}:{$this->port}. Error $errno. $errstr")); return false; } $this->server_identification = $init_line = fgets($this->fsock, 255); if (defined('NET_SSH1_LOGGING')) { $this->_append_log('<-', $this->server_identification); $this->_append_log('->', $this->identifier . "\r\n"); } if (!preg_match('#SSH-([0-9\.]+)-(.+)#', $init_line, $parts)) { user_error('Can only connect to SSH servers'); return false; } if ($parts[1][0] != 1) { user_error("Cannot connect to SSH $parts[1] servers"); return false; } fputs($this->fsock, $this->identifier."\r\n"); $response = $this->_get_binary_packet(); if ($response[NET_SSH1_RESPONSE_TYPE] != NET_SSH1_SMSG_PUBLIC_KEY) { user_error('Expected SSH_SMSG_PUBLIC_KEY'); return false; } $anti_spoofing_cookie = $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 8); $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 4); if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 2) { return false; } $temp = unpack('nlen', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 2)); $server_key_public_exponent = new Math_BigInteger($this->_string_shift($response[NET_SSH1_RESPONSE_DATA], ceil($temp['len'] / 8)), 256); $this->server_key_public_exponent = $server_key_public_exponent; if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 2) { return false; } $temp = unpack('nlen', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 2)); $server_key_public_modulus = new Math_BigInteger($this->_string_shift($response[NET_SSH1_RESPONSE_DATA], ceil($temp['len'] / 8)), 256); $this->server_key_public_modulus = $server_key_public_modulus; $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 4); if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 2) { return false; } $temp = unpack('nlen', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 2)); $host_key_public_exponent = new Math_BigInteger($this->_string_shift($response[NET_SSH1_RESPONSE_DATA], ceil($temp['len'] / 8)), 256); $this->host_key_public_exponent = $host_key_public_exponent; if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 2) { return false; } $temp = unpack('nlen', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 2)); $host_key_public_modulus = new Math_BigInteger($this->_string_shift($response[NET_SSH1_RESPONSE_DATA], ceil($temp['len'] / 8)), 256); $this->host_key_public_modulus = $host_key_public_modulus; $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 4); // get a list of the supported ciphers if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 4) { return false; } extract(unpack('Nsupported_ciphers_mask', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 4))); foreach ($this->supported_ciphers as $mask => $name) { if (($supported_ciphers_mask & (1 << $mask)) == 0) { unset($this->supported_ciphers[$mask]); } } // get a list of the supported authentications if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 4) { return false; } extract(unpack('Nsupported_authentications_mask', $this->_string_shift($response[NET_SSH1_RESPONSE_DATA], 4))); foreach ($this->supported_authentications as $mask => $name) { if (($supported_authentications_mask & (1 << $mask)) == 0) { unset($this->supported_authentications[$mask]); } } $session_id = pack('H*', md5($host_key_public_modulus->toBytes() . $server_key_public_modulus->toBytes() . $anti_spoofing_cookie)); $session_key = crypt_random_string(32); $double_encrypted_session_key = $session_key ^ str_pad($session_id, 32, chr(0)); if ($server_key_public_modulus->compare($host_key_public_modulus) < 0) { $double_encrypted_session_key = $this->_rsa_crypt( $double_encrypted_session_key, array( $server_key_public_exponent, $server_key_public_modulus ) ); $double_encrypted_session_key = $this->_rsa_crypt( $double_encrypted_session_key, array( $host_key_public_exponent, $host_key_public_modulus ) ); } else { $double_encrypted_session_key = $this->_rsa_crypt( $double_encrypted_session_key, array( $host_key_public_exponent, $host_key_public_modulus ) ); $double_encrypted_session_key = $this->_rsa_crypt( $double_encrypted_session_key, array( $server_key_public_exponent, $server_key_public_modulus ) ); } $cipher = isset($this->supported_ciphers[$this->cipher]) ? $this->cipher : NET_SSH1_CIPHER_3DES; $data = pack('C2a*na*N', NET_SSH1_CMSG_SESSION_KEY, $cipher, $anti_spoofing_cookie, 8 * strlen($double_encrypted_session_key), $double_encrypted_session_key, 0); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_SESSION_KEY'); return false; } switch ($cipher) { //case NET_SSH1_CIPHER_NONE: // $this->crypto = new Crypt_Null(); // break; case NET_SSH1_CIPHER_DES: if (!class_exists('Crypt_DES')) { include_once 'Crypt/DES.php'; } $this->crypto = new Crypt_DES(); $this->crypto->disablePadding(); $this->crypto->enableContinuousBuffer(); $this->crypto->setKey(substr($session_key, 0, 8)); break; case NET_SSH1_CIPHER_3DES: if (!class_exists('Crypt_TripleDES')) { include_once 'Crypt/TripleDES.php'; } $this->crypto = new Crypt_TripleDES(CRYPT_DES_MODE_3CBC); $this->crypto->disablePadding(); $this->crypto->enableContinuousBuffer(); $this->crypto->setKey(substr($session_key, 0, 24)); break; //case NET_SSH1_CIPHER_RC4: // if (!class_exists('Crypt_RC4')) { // include_once 'Crypt/RC4.php'; // } // $this->crypto = new Crypt_RC4(); // $this->crypto->enableContinuousBuffer(); // $this->crypto->setKey(substr($session_key, 0, 16)); // break; } $response = $this->_get_binary_packet(); if ($response[NET_SSH1_RESPONSE_TYPE] != NET_SSH1_SMSG_SUCCESS) { user_error('Expected SSH_SMSG_SUCCESS'); return false; } $this->bitmap = NET_SSH1_MASK_CONNECTED; return true; } /** * Login * * @param string $username * @param string $password * @return bool * @access public */ function login($username, $password = '') { if (!($this->bitmap & NET_SSH1_MASK_CONSTRUCTOR)) { $this->bitmap |= NET_SSH1_MASK_CONSTRUCTOR; if (!$this->_connect()) { return false; } } if (!($this->bitmap & NET_SSH1_MASK_CONNECTED)) { return false; } $data = pack('CNa*', NET_SSH1_CMSG_USER, strlen($username), $username); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_USER'); return false; } $response = $this->_get_binary_packet(); if ($response === true) { return false; } if ($response[NET_SSH1_RESPONSE_TYPE] == NET_SSH1_SMSG_SUCCESS) { $this->bitmap |= NET_SSH1_MASK_LOGIN; return true; } elseif ($response[NET_SSH1_RESPONSE_TYPE] != NET_SSH1_SMSG_FAILURE) { user_error('Expected SSH_SMSG_SUCCESS or SSH_SMSG_FAILURE'); return false; } $data = pack('CNa*', NET_SSH1_CMSG_AUTH_PASSWORD, strlen($password), $password); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_AUTH_PASSWORD'); return false; } // remove the username and password from the last logged packet if (defined('NET_SSH1_LOGGING') && NET_SSH1_LOGGING == NET_SSH1_LOG_COMPLEX) { $data = pack('CNa*', NET_SSH1_CMSG_AUTH_PASSWORD, strlen('password'), 'password'); $this->message_log[count($this->message_log) - 1] = $data; } $response = $this->_get_binary_packet(); if ($response === true) { return false; } if ($response[NET_SSH1_RESPONSE_TYPE] == NET_SSH1_SMSG_SUCCESS) { $this->bitmap |= NET_SSH1_MASK_LOGIN; return true; } elseif ($response[NET_SSH1_RESPONSE_TYPE] == NET_SSH1_SMSG_FAILURE) { return false; } else { user_error('Expected SSH_SMSG_SUCCESS or SSH_SMSG_FAILURE'); return false; } } /** * Set Timeout * * $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. * Setting $timeout to false or 0 will mean there is no timeout. * * @param mixed $timeout */ function setTimeout($timeout) { $this->timeout = $this->curTimeout = $timeout; } /** * Executes a command on a non-interactive shell, returns the output, and quits. * * An SSH1 server will close the connection after a command has been executed on a non-interactive shell. SSH2 * servers don't, however, this isn't an SSH2 client. The way this works, on the server, is by initiating a * shell with the -s option, as discussed in the following links: * * {@link http://www.faqs.org/docs/bashman/bashref_65.html http://www.faqs.org/docs/bashman/bashref_65.html} * {@link http://www.faqs.org/docs/bashman/bashref_62.html http://www.faqs.org/docs/bashman/bashref_62.html} * * To execute further commands, a new Net_SSH1 object will need to be created. * * Returns false on failure and the output, otherwise. * * @see self::interactiveRead() * @see self::interactiveWrite() * @param string $cmd * @return mixed * @access public */ function exec($cmd, $block = true) { if (!($this->bitmap & NET_SSH1_MASK_LOGIN)) { user_error('Operation disallowed prior to login()'); return false; } $data = pack('CNa*', NET_SSH1_CMSG_EXEC_CMD, strlen($cmd), $cmd); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_EXEC_CMD'); return false; } if (!$block) { return true; } $output = ''; $response = $this->_get_binary_packet(); if ($response !== false) { do { $output.= substr($response[NET_SSH1_RESPONSE_DATA], 4); $response = $this->_get_binary_packet(); } while (is_array($response) && $response[NET_SSH1_RESPONSE_TYPE] != NET_SSH1_SMSG_EXITSTATUS); } $data = pack('C', NET_SSH1_CMSG_EXIT_CONFIRMATION); // i don't think it's really all that important if this packet gets sent or not. $this->_send_binary_packet($data); fclose($this->fsock); // reset the execution bitmap - a new Net_SSH1 object needs to be created. $this->bitmap = 0; return $output; } /** * Creates an interactive shell * * @see self::interactiveRead() * @see self::interactiveWrite() * @return bool * @access private */ function _initShell() { // connect using the sample parameters in protocol-1.5.txt. // according to wikipedia.org's entry on text terminals, "the fundamental type of application running on a text // terminal is a command line interpreter or shell". thus, opening a terminal session to run the shell. $data = pack('CNa*N4C', NET_SSH1_CMSG_REQUEST_PTY, strlen('vt100'), 'vt100', 24, 80, 0, 0, NET_SSH1_TTY_OP_END); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_REQUEST_PTY'); return false; } $response = $this->_get_binary_packet(); if ($response === true) { return false; } if ($response[NET_SSH1_RESPONSE_TYPE] != NET_SSH1_SMSG_SUCCESS) { user_error('Expected SSH_SMSG_SUCCESS'); return false; } $data = pack('C', NET_SSH1_CMSG_EXEC_SHELL); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_EXEC_SHELL'); return false; } $this->bitmap |= NET_SSH1_MASK_SHELL; //stream_set_blocking($this->fsock, 0); return true; } /** * Inputs a command into an interactive shell. * * @see self::interactiveWrite() * @param string $cmd * @return bool * @access public */ function write($cmd) { return $this->interactiveWrite($cmd); } /** * Returns the output of an interactive shell when there's a match for $expect * * $expect can take the form of a string literal or, if $mode == NET_SSH1_READ_REGEX, * a regular expression. * * @see self::write() * @param string $expect * @param int $mode * @return bool * @access public */ function read($expect, $mode = NET_SSH1_READ_SIMPLE) { if (!($this->bitmap & NET_SSH1_MASK_LOGIN)) { user_error('Operation disallowed prior to login()'); return false; } if (!($this->bitmap & NET_SSH1_MASK_SHELL) && !$this->_initShell()) { user_error('Unable to initiate an interactive shell session'); return false; } $match = $expect; while (true) { if ($mode == NET_SSH1_READ_REGEX) { preg_match($expect, $this->interactiveBuffer, $matches); $match = isset($matches[0]) ? $matches[0] : ''; } $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; if ($pos !== false) { return $this->_string_shift($this->interactiveBuffer, $pos + strlen($match)); } $response = $this->_get_binary_packet(); if ($response === true) { return $this->_string_shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); } $this->interactiveBuffer.= substr($response[NET_SSH1_RESPONSE_DATA], 4); } } /** * Inputs a command into an interactive shell. * * @see self::interactiveRead() * @param string $cmd * @return bool * @access public */ function interactiveWrite($cmd) { if (!($this->bitmap & NET_SSH1_MASK_LOGIN)) { user_error('Operation disallowed prior to login()'); return false; } if (!($this->bitmap & NET_SSH1_MASK_SHELL) && !$this->_initShell()) { user_error('Unable to initiate an interactive shell session'); return false; } $data = pack('CNa*', NET_SSH1_CMSG_STDIN_DATA, strlen($cmd), $cmd); if (!$this->_send_binary_packet($data)) { user_error('Error sending SSH_CMSG_STDIN'); return false; } return true; } /** * Returns the output of an interactive shell when no more output is available. * * Requires PHP 4.3.0 or later due to the use of the stream_select() function. If you see stuff like * "^[[00m", you're seeing ANSI escape codes. According to * {@link http://support.microsoft.com/kb/101875 How to Enable ANSI.SYS in a Command Window}, "Windows NT * does not support ANSI escape sequences in Win32 Console applications", so if you're a Windows user, * there's not going to be much recourse. * * @see self::interactiveRead() * @return string * @access public */ function interactiveRead() { if (!($this->bitmap & NET_SSH1_MASK_LOGIN)) { user_error('Operation disallowed prior to login()'); return false; } if (!($this->bitmap & NET_SSH1_MASK_SHELL) && !$this->_initShell()) { user_error('Unable to initiate an interactive shell session'); return false; } $read = array($this->fsock); $write = $except = null; if (stream_select($read, $write, $except, 0)) { $response = $this->_get_binary_packet(); return substr($response[NET_SSH1_RESPONSE_DATA], 4); } else { return ''; } } /** * Disconnect * * @access public */ function disconnect() { $this->_disconnect(); } /** * Destructor. * * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call * disconnect(). * * @access public */ function __destruct() { $this->_disconnect(); } /** * Disconnect * * @param string $msg * @access private */ function _disconnect($msg = 'Client Quit') { if ($this->bitmap) { $data = pack('C', NET_SSH1_CMSG_EOF); $this->_send_binary_packet($data); /* $response = $this->_get_binary_packet(); if ($response === true) { $response = array(NET_SSH1_RESPONSE_TYPE => -1); } switch ($response[NET_SSH1_RESPONSE_TYPE]) { case NET_SSH1_SMSG_EXITSTATUS: $data = pack('C', NET_SSH1_CMSG_EXIT_CONFIRMATION); break; default: $data = pack('CNa*', NET_SSH1_MSG_DISCONNECT, strlen($msg), $msg); } */ $data = pack('CNa*', NET_SSH1_MSG_DISCONNECT, strlen($msg), $msg); $this->_send_binary_packet($data); fclose($this->fsock); $this->bitmap = 0; } } /** * Gets Binary Packets * * See 'The Binary Packet Protocol' of protocol-1.5.txt for more info. * * Also, this function could be improved upon by adding detection for the following exploit: * http://www.securiteam.com/securitynews/5LP042K3FY.html * * @see self::_send_binary_packet() * @return array * @access private */ function _get_binary_packet() { if (feof($this->fsock)) { //user_error('connection closed prematurely'); return false; } if ($this->curTimeout) { $read = array($this->fsock); $write = $except = null; $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $sec = floor($this->curTimeout); $usec = 1000000 * ($this->curTimeout - $sec); // on windows this returns a "Warning: Invalid CRT parameters detected" error if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { //$this->_disconnect('Timeout'); return true; } $elapsed = strtok(microtime(), ' ') + strtok('') - $start; $this->curTimeout-= $elapsed; } $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $data = fread($this->fsock, 4); if (strlen($data) < 4) { return false; } $temp = unpack('Nlength', $data); $padding_length = 8 - ($temp['length'] & 7); $length = $temp['length'] + $padding_length; $raw = ''; while ($length > 0) { $temp = fread($this->fsock, $length); $raw.= $temp; $length-= strlen($temp); } $stop = strtok(microtime(), ' ') + strtok(''); if (strlen($raw) && $this->crypto !== false) { $raw = $this->crypto->decrypt($raw); } $padding = substr($raw, 0, $padding_length); $type = $raw[$padding_length]; $data = substr($raw, $padding_length + 1, -4); if (strlen($raw) < 4) { return false; } $temp = unpack('Ncrc', substr($raw, -4)); //if ( $temp['crc'] != $this->_crc($padding . $type . $data) ) { // user_error('Bad CRC in packet from server'); // return false; //} $type = ord($type); if (defined('NET_SSH1_LOGGING')) { $temp = isset($this->protocol_flags[$type]) ? $this->protocol_flags[$type] : 'UNKNOWN'; $temp = '<- ' . $temp . ' (' . round($stop - $start, 4) . 's)'; $this->_append_log($temp, $data); } return array( NET_SSH1_RESPONSE_TYPE => $type, NET_SSH1_RESPONSE_DATA => $data ); } /** * Sends Binary Packets * * Returns true on success, false on failure. * * @see self::_get_binary_packet() * @param string $data * @return bool * @access private */ function _send_binary_packet($data) { if (feof($this->fsock)) { //user_error('connection closed prematurely'); return false; } $length = strlen($data) + 4; $padding = crypt_random_string(8 - ($length & 7)); $orig = $data; $data = $padding . $data; $data.= pack('N', $this->_crc($data)); if ($this->crypto !== false) { $data = $this->crypto->encrypt($data); } $packet = pack('Na*', $length, $data); $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $result = strlen($packet) == fputs($this->fsock, $packet); $stop = strtok(microtime(), ' ') + strtok(''); if (defined('NET_SSH1_LOGGING')) { $temp = isset($this->protocol_flags[ord($orig[0])]) ? $this->protocol_flags[ord($orig[0])] : 'UNKNOWN'; $temp = '-> ' . $temp . ' (' . round($stop - $start, 4) . 's)'; $this->_append_log($temp, $orig); } return $result; } /** * Cyclic Redundancy Check (CRC) * * PHP's crc32 function is implemented slightly differently than the one that SSH v1 uses, so * we've reimplemented it. A more detailed discussion of the differences can be found after * $crc_lookup_table's initialization. * * @see self::_get_binary_packet() * @see self::_send_binary_packet() * @param string $data * @return int * @access private */ function _crc($data) { static $crc_lookup_table = array( 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D ); // For this function to yield the same output as PHP's crc32 function, $crc would have to be // set to 0xFFFFFFFF, initially - not 0x00000000 as it currently is. $crc = 0x00000000; $length = strlen($data); for ($i=0; $i<$length; $i++) { // We AND $crc >> 8 with 0x00FFFFFF because we want the eight newly added bits to all // be zero. PHP, unfortunately, doesn't always do this. 0x80000000 >> 8, as an example, // yields 0xFF800000 - not 0x00800000. The following link elaborates: // http://www.php.net/manual/en/language.operators.bitwise.php#57281 $crc = (($crc >> 8) & 0x00FFFFFF) ^ $crc_lookup_table[($crc & 0xFF) ^ ord($data[$i])]; } // In addition to having to set $crc to 0xFFFFFFFF, initially, the return value must be XOR'd with // 0xFFFFFFFF for this function to return the same thing that PHP's crc32 function would. return $crc; } /** * String Shift * * Inspired by array_shift * * @param string $string * @param int $index * @return string * @access private */ function _string_shift(&$string, $index = 1) { $substr = substr($string, 0, $index); $string = substr($string, $index); return $substr; } /** * RSA Encrypt * * Returns mod(pow($m, $e), $n), where $n should be the product of two (large) primes $p and $q and where $e * should be a number with the property that gcd($e, ($p - 1) * ($q - 1)) == 1. Could just make anything that * calls this call modexp, instead, but I think this makes things clearer, maybe... * * @see self::Net_SSH1() * @param Math_BigInteger $m * @param array $key * @return Math_BigInteger * @access private */ function _rsa_crypt($m, $key) { /* if (!class_exists('Crypt_RSA')) { include_once 'Crypt/RSA.php'; } $rsa = new Crypt_RSA(); $rsa->loadKey($key, CRYPT_RSA_PUBLIC_FORMAT_RAW); $rsa->setEncryptionMode(CRYPT_RSA_ENCRYPTION_PKCS1); return $rsa->encrypt($m); */ // To quote from protocol-1.5.txt: // The most significant byte (which is only partial as the value must be // less than the public modulus, which is never a power of two) is zero. // // The next byte contains the value 2 (which stands for public-key // encrypted data in the PKCS standard [PKCS#1]). Then, there are non- // zero random bytes to fill any unused space, a zero byte, and the data // to be encrypted in the least significant bytes, the last byte of the // data in the least significant byte. // Presumably the part of PKCS#1 they're refering to is "Section 7.2.1 Encryption Operation", // under "7.2 RSAES-PKCS1-v1.5" and "7 Encryption schemes" of the following URL: // ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf $modulus = $key[1]->toBytes(); $length = strlen($modulus) - strlen($m) - 3; $random = ''; while (strlen($random) != $length) { $block = crypt_random_string($length - strlen($random)); $block = str_replace("\x00", '', $block); $random.= $block; } $temp = chr(0) . chr(2) . $random . chr(0) . $m; $m = new Math_BigInteger($temp, 256); $m = $m->modPow($key[0], $key[1]); return $m->toBytes(); } /** * Define Array * * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of * named constants from it, using the value as the name of the constant and the index as the value of the constant. * If any of the constants that would be defined already exists, none of the constants will be defined. * * @param array $array * @access private */ function _define_array() { $args = func_get_args(); foreach ($args as $arg) { foreach ($arg as $key => $value) { if (!defined($value)) { define($value, $key); } else { break 2; } } } } /** * Returns a log of the packets that have been sent and received. * * Returns a string if NET_SSH1_LOGGING == NET_SSH1_LOG_COMPLEX, an array if NET_SSH1_LOGGING == NET_SSH1_LOG_SIMPLE and false if !defined('NET_SSH1_LOGGING') * * @access public * @return array|false|string */ function getLog() { if (!defined('NET_SSH1_LOGGING')) { return false; } switch (NET_SSH1_LOGGING) { case NET_SSH1_LOG_SIMPLE: return $this->message_number_log; break; case NET_SSH1_LOG_COMPLEX: return $this->_format_log($this->message_log, $this->protocol_flags_log); break; default: return false; } } /** * Formats a log for printing * * @param array $message_log * @param array $message_number_log * @access private * @return string */ function _format_log($message_log, $message_number_log) { $output = ''; for ($i = 0; $i < count($message_log); $i++) { $output.= $message_number_log[$i] . "\r\n"; $current_log = $message_log[$i]; $j = 0; do { if (strlen($current_log)) { $output.= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; } $fragment = $this->_string_shift($current_log, $this->log_short_width); $hex = substr(preg_replace_callback('#.#s', array($this, '_format_log_helper'), $fragment), strlen($this->log_boundary)); // replace non ASCII printable characters with dots // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters // also replace < with a . since < messes up the output on web browsers $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); $output.= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; $j++; } while (strlen($current_log)); $output.= "\r\n"; } return $output; } /** * Helper function for _format_log * * For use with preg_replace_callback() * * @param array $matches * @access private * @return string */ function _format_log_helper($matches) { return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); } /** * Return the server key public exponent * * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, * the raw bytes. This behavior is similar to PHP's md5() function. * * @param bool $raw_output * @return string * @access public */ function getServerKeyPublicExponent($raw_output = false) { return $raw_output ? $this->server_key_public_exponent->toBytes() : $this->server_key_public_exponent->toString(); } /** * Return the server key public modulus * * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, * the raw bytes. This behavior is similar to PHP's md5() function. * * @param bool $raw_output * @return string * @access public */ function getServerKeyPublicModulus($raw_output = false) { return $raw_output ? $this->server_key_public_modulus->toBytes() : $this->server_key_public_modulus->toString(); } /** * Return the host key public exponent * * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, * the raw bytes. This behavior is similar to PHP's md5() function. * * @param bool $raw_output * @return string * @access public */ function getHostKeyPublicExponent($raw_output = false) { return $raw_output ? $this->host_key_public_exponent->toBytes() : $this->host_key_public_exponent->toString(); } /** * Return the host key public modulus * * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, * the raw bytes. This behavior is similar to PHP's md5() function. * * @param bool $raw_output * @return string * @access public */ function getHostKeyPublicModulus($raw_output = false) { return $raw_output ? $this->host_key_public_modulus->toBytes() : $this->host_key_public_modulus->toString(); } /** * Return a list of ciphers supported by SSH1 server. * * Just because a cipher is supported by an SSH1 server doesn't mean it's supported by this library. If $raw_output * is set to true, returns, instead, an array of constants. ie. instead of array('Triple-DES in CBC mode'), you'll * get array(NET_SSH1_CIPHER_3DES). * * @param bool $raw_output * @return array * @access public */ function getSupportedCiphers($raw_output = false) { return $raw_output ? array_keys($this->supported_ciphers) : array_values($this->supported_ciphers); } /** * Return a list of authentications supported by SSH1 server. * * Just because a cipher is supported by an SSH1 server doesn't mean it's supported by this library. If $raw_output * is set to true, returns, instead, an array of constants. ie. instead of array('password authentication'), you'll * get array(NET_SSH1_AUTH_PASSWORD). * * @param bool $raw_output * @return array * @access public */ function getSupportedAuthentications($raw_output = false) { return $raw_output ? array_keys($this->supported_authentications) : array_values($this->supported_authentications); } /** * Return the server identification. * * @return string * @access public */ function getServerIdentification() { return rtrim($this->server_identification); } /** * Logs data packets * * Makes sure that only the last 1MB worth of packets will be logged * * @param string $data * @access private */ function _append_log($protocol_flags, $message) { switch (NET_SSH1_LOGGING) { // useful for benchmarks case NET_SSH1_LOG_SIMPLE: $this->protocol_flags_log[] = $protocol_flags; break; // the most useful log for SSH1 case NET_SSH1_LOG_COMPLEX: $this->protocol_flags_log[] = $protocol_flags; $this->_string_shift($message); $this->log_size+= strlen($message); $this->message_log[] = $message; while ($this->log_size > NET_SSH1_LOG_MAX_SIZE) { $this->log_size-= strlen(array_shift($this->message_log)); array_shift($this->protocol_flags_log); } break; // dump the output out realtime; packets may be interspersed with non packets, // passwords won't be filtered out and select other packets may not be correctly // identified case NET_SSH1_LOG_REALTIME: echo "<pre>\r\n" . $this->_format_log(array($message), array($protocol_flags)) . "\r\n</pre>\r\n"; @flush(); @ob_flush(); break; // basically the same thing as NET_SSH1_LOG_REALTIME with the caveat that NET_SSH1_LOG_REALTIME_FILE // needs to be defined and that the resultant log file will be capped out at NET_SSH1_LOG_MAX_SIZE. // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily // at the beginning of the file case NET_SSH1_LOG_REALTIME_FILE: if (!isset($this->realtime_log_file)) { // PHP doesn't seem to like using constants in fopen() $filename = NET_SSH1_LOG_REALTIME_FILE; $fp = fopen($filename, 'w'); $this->realtime_log_file = $fp; } if (!is_resource($this->realtime_log_file)) { break; } $entry = $this->_format_log(array($message), array($protocol_flags)); if ($this->realtime_log_wrap) { $temp = "<<< START >>>\r\n"; $entry.= $temp; fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp)); } $this->realtime_log_size+= strlen($entry); if ($this->realtime_log_size > NET_SSH1_LOG_MAX_SIZE) { fseek($this->realtime_log_file, 0); $this->realtime_log_size = strlen($entry); $this->realtime_log_wrap = true; } fputs($this->realtime_log_file, $entry); } } } PK G�l[A�zZ* Z* SCP.phpnu �[��� <?php /** * Pure-PHP implementation of SCP. * * PHP versions 4 and 5 * * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}. * * Here's a short example of how to use this library: * <code> * <?php * include 'Net/SCP.php'; * include 'Net/SSH2.php'; * * $ssh = new Net_SSH2('www.domain.tld'); * if (!$ssh->login('username', 'password')) { * exit('bad login'); * } * * $scp = new Net_SCP($ssh); * $scp->put('abcd', str_repeat('x', 1024*1024)); * ?> * </code> * * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @category Net * @package Net_SCP * @author Jim Wigginton <terrafrost@php.net> * @copyright 2010 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ /**#@+ * @access public * @see self::put() */ /** * Reads data from a local file. */ define('NET_SCP_LOCAL_FILE', 1); /** * Reads data from a string. */ define('NET_SCP_STRING', 2); /**#@-*/ /**#@+ * @access private * @see self::_send() * @see self::_receive() */ /** * SSH1 is being used. */ define('NET_SCP_SSH1', 1); /** * SSH2 is being used. */ define('NET_SCP_SSH2', 2); /**#@-*/ /** * Pure-PHP implementations of SCP. * * @package Net_SCP * @author Jim Wigginton <terrafrost@php.net> * @access public */ class Net_SCP { /** * SSH Object * * @var object * @access private */ var $ssh; /** * Packet Size * * @var int * @access private */ var $packet_size; /** * Mode * * @var int * @access private */ var $mode; /** * Default Constructor. * * Connects to an SSH server * * @param Net_SSH1|Net_SSH2 $ssh * @return Net_SCP * @access public */ function __construct($ssh) { if (!is_object($ssh)) { return; } switch (strtolower(get_class($ssh))) { case 'net_ssh2': $this->mode = NET_SCP_SSH2; break; case 'net_ssh1': $this->packet_size = 50000; $this->mode = NET_SCP_SSH1; break; default: return; } $this->ssh = $ssh; } /** * PHP4 compatible Default Constructor. * * @see self::__construct() * @param Net_SSH1|Net_SSH2 $ssh * @access public */ function Net_SCP($ssh) { $this->__construct($ssh); } /** * Uploads a file to the SCP server. * * By default, Net_SCP::put() does not read from the local filesystem. $data is dumped directly into $remote_file. * So, for example, if you set $data to 'filename.ext' and then do Net_SCP::get(), you will get a file, twelve bytes * long, containing 'filename.ext' as its contents. * * Setting $mode to NET_SCP_LOCAL_FILE will change the above behavior. With NET_SCP_LOCAL_FILE, $remote_file will * contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how * large $remote_file will be, as well. * * Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take * care of that, yourself. * * @param string $remote_file * @param string $data * @param int $mode * @param callable $callback * @return bool * @access public */ function put($remote_file, $data, $mode = NET_SCP_STRING, $callback = null) { if (!isset($this->ssh)) { return false; } if (empty($remote_file)) { user_error('remote_file cannot be blank', E_USER_NOTICE); return false; } if (!$this->ssh->exec('scp -t ' . escapeshellarg($remote_file), false)) { // -t = to return false; } $temp = $this->_receive(); if ($temp !== chr(0)) { return false; } if ($this->mode == NET_SCP_SSH2) { $this->packet_size = $this->ssh->packet_size_client_to_server[NET_SSH2_CHANNEL_EXEC] - 4; } $remote_file = basename($remote_file); if ($mode == NET_SCP_STRING) { $size = strlen($data); } else { if (!is_file($data)) { user_error("$data is not a valid file", E_USER_NOTICE); return false; } $fp = @fopen($data, 'rb'); if (!$fp) { return false; } $size = filesize($data); } $this->_send('C0644 ' . $size . ' ' . $remote_file . "\n"); $temp = $this->_receive(); if ($temp !== chr(0)) { return false; } $sent = 0; while ($sent < $size) { $temp = $mode & NET_SCP_STRING ? substr($data, $sent, $this->packet_size) : fread($fp, $this->packet_size); $this->_send($temp); $sent+= strlen($temp); if (is_callable($callback)) { call_user_func($callback, $sent); } } $this->_close(); if ($mode != NET_SCP_STRING) { fclose($fp); } return true; } /** * Downloads a file from the SCP server. * * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if * the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the * operation * * @param string $remote_file * @param string $local_file * @return mixed * @access public */ function get($remote_file, $local_file = false) { if (!isset($this->ssh)) { return false; } if (!$this->ssh->exec('scp -f ' . escapeshellarg($remote_file), false)) { // -f = from return false; } $this->_send("\0"); if (!preg_match('#(?<perms>[^ ]+) (?<size>\d+) (?<name>.+)#', rtrim($this->_receive()), $info)) { return false; } $this->_send("\0"); $size = 0; if ($local_file !== false) { $fp = @fopen($local_file, 'wb'); if (!$fp) { return false; } } $content = ''; while ($size < $info['size']) { $data = $this->_receive(); // SCP usually seems to split stuff out into 16k chunks $size+= strlen($data); if ($local_file === false) { $content.= $data; } else { fputs($fp, $data); } } $this->_close(); if ($local_file !== false) { fclose($fp); return true; } return $content; } /** * Sends a packet to an SSH server * * @param string $data * @access private */ function _send($data) { switch ($this->mode) { case NET_SCP_SSH2: $this->ssh->_send_channel_packet(NET_SSH2_CHANNEL_EXEC, $data); break; case NET_SCP_SSH1: $data = pack('CNa*', NET_SSH1_CMSG_STDIN_DATA, strlen($data), $data); $this->ssh->_send_binary_packet($data); } } /** * Receives a packet from an SSH server * * @return string * @access private */ function _receive() { switch ($this->mode) { case NET_SCP_SSH2: return $this->ssh->_get_channel_packet(NET_SSH2_CHANNEL_EXEC, true); case NET_SCP_SSH1: if (!$this->ssh->bitmap) { return false; } while (true) { $response = $this->ssh->_get_binary_packet(); switch ($response[NET_SSH1_RESPONSE_TYPE]) { case NET_SSH1_SMSG_STDOUT_DATA: if (strlen($response[NET_SSH1_RESPONSE_DATA]) < 4) { return false; } extract(unpack('Nlength', $response[NET_SSH1_RESPONSE_DATA])); return $this->ssh->_string_shift($response[NET_SSH1_RESPONSE_DATA], $length); case NET_SSH1_SMSG_STDERR_DATA: break; case NET_SSH1_SMSG_EXITSTATUS: $this->ssh->_send_binary_packet(chr(NET_SSH1_CMSG_EXIT_CONFIRMATION)); fclose($this->ssh->fsock); $this->ssh->bitmap = 0; return false; default: user_error('Unknown packet received', E_USER_NOTICE); return false; } } } } /** * Closes the connection to an SSH server * * @access private */ function _close() { switch ($this->mode) { case NET_SCP_SSH2: $this->ssh->_close_channel(NET_SSH2_CHANNEL_EXEC, true); break; case NET_SCP_SSH1: $this->ssh->disconnect(); } } } PK G�l[Z�9k� k� SSH2.phpnu �[��� <?php /** * Pure-PHP implementation of SSHv2. * * PHP versions 4 and 5 * * Here are some examples of how to use this library: * <code> * <?php * include 'Net/SSH2.php'; * * $ssh = new Net_SSH2('www.domain.tld'); * if (!$ssh->login('username', 'password')) { * exit('Login Failed'); * } * * echo $ssh->exec('pwd'); * echo $ssh->exec('ls -la'); * ?> * </code> * * <code> * <?php * include 'Crypt/RSA.php'; * include 'Net/SSH2.php'; * * $key = new Crypt_RSA(); * //$key->setPassword('whatever'); * $key->loadKey(file_get_contents('privatekey')); * * $ssh = new Net_SSH2('www.domain.tld'); * if (!$ssh->login('username', $key)) { * exit('Login Failed'); * } * * echo $ssh->read('username@username:~$'); * $ssh->write("ls -la\n"); * echo $ssh->read('username@username:~$'); * ?> * </code> * * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @category Net * @package Net_SSH2 * @author Jim Wigginton <terrafrost@php.net> * @copyright 2007 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ /**#@+ * Execution Bitmap Masks * * @see self::bitmap * @access private */ define('NET_SSH2_MASK_CONSTRUCTOR', 0x00000001); define('NET_SSH2_MASK_CONNECTED', 0x00000002); define('NET_SSH2_MASK_LOGIN_REQ', 0x00000004); define('NET_SSH2_MASK_LOGIN', 0x00000008); define('NET_SSH2_MASK_SHELL', 0x00000010); define('NET_SSH2_MASK_WINDOW_ADJUST', 0x00000020); /**#@-*/ /**#@+ * Channel constants * * RFC4254 refers not to client and server channels but rather to sender and recipient channels. we don't refer * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a * recepient channel. at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snipet: * The 'recipient channel' is the channel number given in the original * open request, and 'sender channel' is the channel number allocated by * the other side. * * @see self::_send_channel_packet() * @see self::_get_channel_packet() * @access private */ define('NET_SSH2_CHANNEL_EXEC', 1); // PuTTy uses 0x100 define('NET_SSH2_CHANNEL_SHELL', 2); define('NET_SSH2_CHANNEL_SUBSYSTEM', 3); define('NET_SSH2_CHANNEL_AGENT_FORWARD', 4); define('NET_SSH2_CHANNEL_KEEP_ALIVE', 5); /**#@-*/ /**#@+ * @access public * @see self::getLog() */ /** * Returns the message numbers */ define('NET_SSH2_LOG_SIMPLE', 1); /** * Returns the message content */ define('NET_SSH2_LOG_COMPLEX', 2); /** * Outputs the content real-time */ define('NET_SSH2_LOG_REALTIME', 3); /** * Dumps the content real-time to a file */ define('NET_SSH2_LOG_REALTIME_FILE', 4); /** * Make sure that the log never gets larger than this */ define('NET_SSH2_LOG_MAX_SIZE', 1024 * 1024); /**#@-*/ /**#@+ * @access public * @see self::read() */ /** * Returns when a string matching $expect exactly is found */ define('NET_SSH2_READ_SIMPLE', 1); /** * Returns when a string matching the regular expression $expect is found */ define('NET_SSH2_READ_REGEX', 2); /** * Returns whenever a data packet is received. * * Some data packets may only contain a single character so it may be necessary * to call read() multiple times when using this option */ define('NET_SSH2_READ_NEXT', 3); /**#@-*/ /** * Pure-PHP implementation of SSHv2. * * @package Net_SSH2 * @author Jim Wigginton <terrafrost@php.net> * @access public */ class Net_SSH2 { /** * The SSH identifier * * @var string * @access private */ var $identifier; /** * The Socket Object * * @var object * @access private */ var $fsock; /** * Execution Bitmap * * The bits that are set represent functions that have been called already. This is used to determine * if a requisite function has been successfully executed. If not, an error should be thrown. * * @var int * @access private */ var $bitmap = 0; /** * Error information * * @see self::getErrors() * @see self::getLastError() * @var string * @access private */ var $errors = array(); /** * Server Identifier * * @see self::getServerIdentification() * @var array|false * @access private */ var $server_identifier = false; /** * Key Exchange Algorithms * * @see self::getKexAlgorithims() * @var array|false * @access private */ var $kex_algorithms = false; /** * Key Exchange Algorithm * * @see self::getMethodsNegotiated() * @var string|false * @access private */ var $kex_algorithm = false; /** * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int * @access private */ var $kex_dh_group_size_min = 1536; /** * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int * @access private */ var $kex_dh_group_size_preferred = 2048; /** * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods * * @see self::_key_exchange() * @var int * @access private */ var $kex_dh_group_size_max = 4096; /** * Server Host Key Algorithms * * @see self::getServerHostKeyAlgorithms() * @var array|false * @access private */ var $server_host_key_algorithms = false; /** * Encryption Algorithms: Client to Server * * @see self::getEncryptionAlgorithmsClient2Server() * @var array|false * @access private */ var $encryption_algorithms_client_to_server = false; /** * Encryption Algorithms: Server to Client * * @see self::getEncryptionAlgorithmsServer2Client() * @var array|false * @access private */ var $encryption_algorithms_server_to_client = false; /** * MAC Algorithms: Client to Server * * @see self::getMACAlgorithmsClient2Server() * @var array|false * @access private */ var $mac_algorithms_client_to_server = false; /** * MAC Algorithms: Server to Client * * @see self::getMACAlgorithmsServer2Client() * @var array|false * @access private */ var $mac_algorithms_server_to_client = false; /** * Compression Algorithms: Client to Server * * @see self::getCompressionAlgorithmsClient2Server() * @var array|false * @access private */ var $compression_algorithms_client_to_server = false; /** * Compression Algorithms: Server to Client * * @see self::getCompressionAlgorithmsServer2Client() * @var array|false * @access private */ var $compression_algorithms_server_to_client = false; /** * Languages: Server to Client * * @see self::getLanguagesServer2Client() * @var array|false * @access private */ var $languages_server_to_client = false; /** * Languages: Client to Server * * @see self::getLanguagesClient2Server() * @var array|false * @access private */ var $languages_client_to_server = false; /** * Preferred Algorithms * * @see self::setPreferredAlgorithms() * @var array * @access private */ var $preferred = array(); /** * Block Size for Server to Client Encryption * * "Note that the length of the concatenation of 'packet_length', * 'padding_length', 'payload', and 'random padding' MUST be a multiple * of the cipher block size or 8, whichever is larger. This constraint * MUST be enforced, even when using stream ciphers." * * -- http://tools.ietf.org/html/rfc4253#section-6 * * @see self::Net_SSH2() * @see self::_send_binary_packet() * @var int * @access private */ var $encrypt_block_size = 8; /** * Block Size for Client to Server Encryption * * @see self::Net_SSH2() * @see self::_get_binary_packet() * @var int * @access private */ var $decrypt_block_size = 8; /** * Server to Client Encryption Object * * @see self::_get_binary_packet() * @var object * @access private */ var $decrypt = false; /** * Client to Server Encryption Object * * @see self::_send_binary_packet() * @var object * @access private */ var $encrypt = false; /** * Client to Server HMAC Object * * @see self::_send_binary_packet() * @var object * @access private */ var $hmac_create = false; /** * Server to Client HMAC Object * * @see self::_get_binary_packet() * @var object * @access private */ var $hmac_check = false; /** * Size of server to client HMAC * * 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. * 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 * append it. * * @see self::_get_binary_packet() * @var int * @access private */ var $hmac_size = false; /** * Server Public Host Key * * @see self::getServerPublicHostKey() * @var string * @access private */ var $server_public_host_key; /** * Session identifier * * "The exchange hash H from the first key exchange is additionally * used as the session identifier, which is a unique identifier for * this connection." * * -- http://tools.ietf.org/html/rfc4253#section-7.2 * * @see self::_key_exchange() * @var string * @access private */ var $session_id = false; /** * Exchange hash * * The current exchange hash * * @see self::_key_exchange() * @var string * @access private */ var $exchange_hash = false; /** * Message Numbers * * @see self::Net_SSH2() * @var array * @access private */ var $message_numbers = array(); /** * Disconnection Message 'reason codes' defined in RFC4253 * * @see self::Net_SSH2() * @var array * @access private */ var $disconnect_reasons = array(); /** * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 * * @see self::Net_SSH2() * @var array * @access private */ var $channel_open_failure_reasons = array(); /** * Terminal Modes * * @link http://tools.ietf.org/html/rfc4254#section-8 * @see self::Net_SSH2() * @var array * @access private */ var $terminal_modes = array(); /** * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes * * @link http://tools.ietf.org/html/rfc4254#section-5.2 * @see self::Net_SSH2() * @var array * @access private */ var $channel_extended_data_type_codes = array(); /** * Send Sequence Number * * See 'Section 6.4. Data Integrity' of rfc4253 for more info. * * @see self::_send_binary_packet() * @var int * @access private */ var $send_seq_no = 0; /** * Get Sequence Number * * See 'Section 6.4. Data Integrity' of rfc4253 for more info. * * @see self::_get_binary_packet() * @var int * @access private */ var $get_seq_no = 0; /** * Server Channels * * Maps client channels to server channels * * @see self::_get_channel_packet() * @see self::exec() * @var array * @access private */ var $server_channels = array(); /** * Channel Buffers * * If a client requests a packet from one channel but receives two packets from another those packets should * be placed in a buffer * * @see self::_get_channel_packet() * @see self::exec() * @var array * @access private */ var $channel_buffers = array(); /** * Channel Status * * Contains the type of the last sent message * * @see self::_get_channel_packet() * @var array * @access private */ var $channel_status = array(); /** * Packet Size * * Maximum packet size indexed by channel * * @see self::_send_channel_packet() * @var array * @access private */ var $packet_size_client_to_server = array(); /** * Message Number Log * * @see self::getLog() * @var array * @access private */ var $message_number_log = array(); /** * Message Log * * @see self::getLog() * @var array * @access private */ var $message_log = array(); /** * The Window Size * * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB) * * @var int * @see self::_send_channel_packet() * @see self::exec() * @access private */ var $window_size = 0x7FFFFFFF; /** * Window size, server to client * * Window size indexed by channel * * @see self::_send_channel_packet() * @var array * @access private */ var $window_size_server_to_client = array(); /** * Window size, client to server * * Window size indexed by channel * * @see self::_get_channel_packet() * @var array * @access private */ var $window_size_client_to_server = array(); /** * Server signature * * Verified against $this->session_id * * @see self::getServerPublicHostKey() * @var string * @access private */ var $signature = ''; /** * Server signature format * * ssh-rsa or ssh-dss. * * @see self::getServerPublicHostKey() * @var string * @access private */ var $signature_format = ''; /** * Interactive Buffer * * @see self::read() * @var array * @access private */ var $interactiveBuffer = ''; /** * Current log size * * Should never exceed NET_SSH2_LOG_MAX_SIZE * * @see self::_send_binary_packet() * @see self::_get_binary_packet() * @var int * @access private */ var $log_size; /** * Timeout * * @see self::setTimeout() * @access private */ var $timeout; /** * Current Timeout * * @see self::_get_channel_packet() * @access private */ var $curTimeout; /** * Real-time log file pointer * * @see self::_append_log() * @var resource * @access private */ var $realtime_log_file; /** * Real-time log file size * * @see self::_append_log() * @var int * @access private */ var $realtime_log_size; /** * Has the signature been validated? * * @see self::getServerPublicHostKey() * @var bool * @access private */ var $signature_validated = false; /** * Real-time log file wrap boolean * * @see self::_append_log() * @access private */ var $realtime_log_wrap; /** * Flag to suppress stderr from output * * @see self::enableQuietMode() * @access private */ var $quiet_mode = false; /** * Time of first network activity * * @var int * @access private */ var $last_packet; /** * Exit status returned from ssh if any * * @var int * @access private */ var $exit_status; /** * Flag to request a PTY when using exec() * * @var bool * @see self::enablePTY() * @access private */ var $request_pty = false; /** * Flag set while exec() is running when using enablePTY() * * @var bool * @access private */ var $in_request_pty_exec = false; /** * Flag set after startSubsystem() is called * * @var bool * @access private */ var $in_subsystem; /** * Contents of stdError * * @var string * @access private */ var $stdErrorLog; /** * The Last Interactive Response * * @see self::_keyboard_interactive_process() * @var string * @access private */ var $last_interactive_response = ''; /** * Keyboard Interactive Request / Responses * * @see self::_keyboard_interactive_process() * @var array * @access private */ var $keyboard_requests_responses = array(); /** * Banner Message * * Quoting from the RFC, "in some jurisdictions, sending a warning message before * authentication may be relevant for getting legal protection." * * @see self::_filter() * @see self::getBannerMessage() * @var string * @access private */ var $banner_message = ''; /** * Did read() timeout or return normally? * * @see self::isTimeout() * @var bool * @access private */ var $is_timeout = false; /** * Log Boundary * * @see self::_format_log() * @var string * @access private */ var $log_boundary = ':'; /** * Log Long Width * * @see self::_format_log() * @var int * @access private */ var $log_long_width = 65; /** * Log Short Width * * @see self::_format_log() * @var int * @access private */ var $log_short_width = 16; /** * Hostname * * @see self::Net_SSH2() * @see self::_connect() * @var string * @access private */ var $host; /** * Port Number * * @see self::Net_SSH2() * @see self::_connect() * @var int * @access private */ var $port; /** * Number of columns for terminal window size * * @see self::getWindowColumns() * @see self::setWindowColumns() * @see self::setWindowSize() * @var int * @access private */ var $windowColumns = 80; /** * Number of columns for terminal window size * * @see self::getWindowRows() * @see self::setWindowRows() * @see self::setWindowSize() * @var int * @access private */ var $windowRows = 24; /** * Crypto Engine * * @see self::setCryptoEngine() * @see self::_key_exchange() * @var int * @access private */ var $crypto_engine = false; /** * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario * * @var System_SSH_Agent * @access private */ var $agent; /** * Send the identification string first? * * @var bool * @access private */ var $send_id_string_first = true; /** * Send the key exchange initiation packet first? * * @var bool * @access private */ var $send_kex_first = true; /** * Some versions of OpenSSH incorrectly calculate the key size * * @var bool * @access private */ var $bad_key_size_fix = false; /** * Should we try to re-connect to re-establish keys? * * @var bool * @access private */ var $retry_connect = false; /** * Binary Packet Buffer * * @var string|false * @access private */ var $binary_packet_buffer = false; /** * Preferred Signature Format * * @var string|false * @access private */ var $preferred_signature_format = false; /** * Authentication Credentials * * @var array * @access private */ var $auth = array(); /** * Default Constructor. * * $host can either be a string, representing the host, or a stream resource. * * @param mixed $host * @param int $port * @param int $timeout * @see self::login() * @return Net_SSH2 * @access public */ function __construct($host, $port = 22, $timeout = 10) { // Include Math_BigInteger // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. if (!class_exists('Math_BigInteger')) { include_once 'Math/BigInteger.php'; } if (!function_exists('crypt_random_string')) { include_once 'Crypt/Random.php'; } if (!class_exists('Crypt_Hash')) { include_once 'Crypt/Hash.php'; } // include Crypt_Base so constants can be defined for setCryptoEngine() if (!class_exists('Crypt_Base')) { include_once 'Crypt/Base.php'; } $this->message_numbers = array( 1 => 'NET_SSH2_MSG_DISCONNECT', 2 => 'NET_SSH2_MSG_IGNORE', 3 => 'NET_SSH2_MSG_UNIMPLEMENTED', 4 => 'NET_SSH2_MSG_DEBUG', 5 => 'NET_SSH2_MSG_SERVICE_REQUEST', 6 => 'NET_SSH2_MSG_SERVICE_ACCEPT', 20 => 'NET_SSH2_MSG_KEXINIT', 21 => 'NET_SSH2_MSG_NEWKEYS', 30 => 'NET_SSH2_MSG_KEXDH_INIT', 31 => 'NET_SSH2_MSG_KEXDH_REPLY', 50 => 'NET_SSH2_MSG_USERAUTH_REQUEST', 51 => 'NET_SSH2_MSG_USERAUTH_FAILURE', 52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS', 53 => 'NET_SSH2_MSG_USERAUTH_BANNER', 80 => 'NET_SSH2_MSG_GLOBAL_REQUEST', 81 => 'NET_SSH2_MSG_REQUEST_SUCCESS', 82 => 'NET_SSH2_MSG_REQUEST_FAILURE', 90 => 'NET_SSH2_MSG_CHANNEL_OPEN', 91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION', 92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE', 93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST', 94 => 'NET_SSH2_MSG_CHANNEL_DATA', 95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA', 96 => 'NET_SSH2_MSG_CHANNEL_EOF', 97 => 'NET_SSH2_MSG_CHANNEL_CLOSE', 98 => 'NET_SSH2_MSG_CHANNEL_REQUEST', 99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', 100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' ); $this->disconnect_reasons = array( 1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', 2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', 3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', 4 => 'NET_SSH2_DISCONNECT_RESERVED', 5 => 'NET_SSH2_DISCONNECT_MAC_ERROR', 6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR', 7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE', 8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED', 9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE', 10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST', 11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION', 12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS', 13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER', 14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', 15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' ); $this->channel_open_failure_reasons = array( 1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' ); $this->terminal_modes = array( 0 => 'NET_SSH2_TTY_OP_END' ); $this->channel_extended_data_type_codes = array( 1 => 'NET_SSH2_EXTENDED_DATA_STDERR' ); $this->_define_array( $this->message_numbers, $this->disconnect_reasons, $this->channel_open_failure_reasons, $this->terminal_modes, $this->channel_extended_data_type_codes, array(60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'), array(60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'), array(60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', 61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'), // RFC 4419 - diffie-hellman-group-exchange-sha{1,256} array(30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD', 31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP', 32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT', 33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY', 34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST') ); if (is_resource($host)) { $this->fsock = $host; return; } if (is_string($host)) { $this->host = $host; $this->port = $port; $this->timeout = $timeout; } } /** * PHP4 compatible Default Constructor. * * @see self::__construct() * @param mixed $host * @param int $port * @param int $timeout * @access public */ function Net_SSH2($host, $port = 22, $timeout = 10) { $this->__construct($host, $port, $timeout); } /** * Set Crypto Engine Mode * * Possible $engine values: * CRYPT_MODE_INTERNAL, CRYPT_MODE_MCRYPT * * @param int $engine * @access public */ function setCryptoEngine($engine) { $this->crypto_engine = $engine; } /** * Send Identification String First * * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, * both sides MUST send an identification string". It does not say which side sends it first. In * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * * @access public */ function sendIdentificationStringFirst() { $this->send_id_string_first = true; } /** * Send Identification String Last * * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, * both sides MUST send an identification string". It does not say which side sends it first. In * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * * @access public */ function sendIdentificationStringLast() { $this->send_id_string_first = false; } /** * Send SSH_MSG_KEXINIT First * * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * * @access public */ function sendKEXINITFirst() { $this->send_kex_first = true; } /** * Send SSH_MSG_KEXINIT Last * * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy * * @access public */ function sendKEXINITLast() { $this->send_kex_first = false; } /** * Connect to an SSHv2 server * * @return bool * @access private */ function _connect() { if ($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR) { return false; } $this->bitmap |= NET_SSH2_MASK_CONSTRUCTOR; $this->curTimeout = $this->timeout; $this->last_packet = strtok(microtime(), ' ') + strtok(''); // == microtime(true) in PHP5 if (!is_resource($this->fsock)) { $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 // with stream_select a timeout of 0 means that no timeout takes place; // with fsockopen a timeout of 0 means that you instantly timeout // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0 $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout); if (!$this->fsock) { $host = $this->host . ':' . $this->port; user_error(rtrim("Cannot connect to $host. Error $errno. $errstr")); return false; } $elapsed = strtok(microtime(), ' ') + strtok('') - $start; if ($this->curTimeout) { $this->curTimeout-= $elapsed; if ($this->curTimeout < 0) { $this->is_timeout = true; return false; } } } $this->identifier = $this->_generate_identifier(); if ($this->send_id_string_first) { fputs($this->fsock, $this->identifier . "\r\n"); } /* According to the SSH2 specs, "The server MAY send other lines of data before sending the version string. Each line SHOULD be terminated by a Carriage Return and Line Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients MUST be able to process such lines." */ $temp = ''; $extra = ''; while (!feof($this->fsock) && !preg_match('#^SSH-(\d\.\d+)#', $temp, $matches)) { if (substr($temp, -2) == "\r\n") { $extra.= $temp; $temp = ''; } if ($this->curTimeout) { if ($this->curTimeout < 0) { $this->is_timeout = true; return false; } $read = array($this->fsock); $write = $except = null; $start = strtok(microtime(), ' ') + strtok(''); $sec = floor($this->curTimeout); $usec = 1000000 * ($this->curTimeout - $sec); // on windows this returns a "Warning: Invalid CRT parameters detected" error // the !count() is done as a workaround for <https://bugs.php.net/42682> if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { $this->is_timeout = true; return false; } $elapsed = strtok(microtime(), ' ') + strtok('') - $start; $this->curTimeout-= $elapsed; } $temp.= fgets($this->fsock, 255); } if (feof($this->fsock)) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (defined('NET_SSH2_LOGGING')) { $this->_append_log('<-', $extra . $temp); $this->_append_log('->', $this->identifier . "\r\n"); } $this->server_identifier = trim($temp, "\r\n"); if (strlen($extra)) { $this->errors[] = $extra; } if (version_compare($matches[1], '1.99', '<')) { user_error("Cannot connect to SSH $matches[1] servers"); return false; } if (!$this->send_id_string_first) { fputs($this->fsock, $this->identifier . "\r\n"); } if (!$this->send_kex_first) { $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response) || ord($response[0]) != NET_SSH2_MSG_KEXINIT) { user_error('Expected SSH_MSG_KEXINIT'); return false; } if (!$this->_key_exchange($response)) { return false; } } if ($this->send_kex_first && !$this->_key_exchange()) { return false; } $this->bitmap|= NET_SSH2_MASK_CONNECTED; return true; } /** * Generates the SSH identifier * * You should overwrite this method in your own class if you want to use another identifier * * @access protected * @return string */ function _generate_identifier() { $identifier = 'SSH-2.0-phpseclib_1.0'; $ext = array(); if (extension_loaded('openssl')) { $ext[] = 'openssl'; } elseif (extension_loaded('mcrypt')) { $ext[] = 'mcrypt'; } if (extension_loaded('gmp')) { $ext[] = 'gmp'; } elseif (extension_loaded('bcmath')) { $ext[] = 'bcmath'; } if (!empty($ext)) { $identifier .= ' (' . implode(', ', $ext) . ')'; } return $identifier; } /** * Key Exchange * * @param string $kexinit_payload_server optional * @access private */ function _key_exchange($kexinit_payload_server = false) { $preferred = $this->preferred; $kex_algorithms = isset($preferred['kex']) ? $preferred['kex'] : $this->getSupportedKEXAlgorithms(); $server_host_key_algorithms = isset($preferred['hostkey']) ? $preferred['hostkey'] : $this->getSupportedHostKeyAlgorithms(); $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ? $preferred['server_to_client']['crypt'] : $this->getSupportedEncryptionAlgorithms(); $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ? $preferred['client_to_server']['crypt'] : $this->getSupportedEncryptionAlgorithms(); $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ? $preferred['server_to_client']['mac'] : $this->getSupportedMACAlgorithms(); $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ? $preferred['client_to_server']['mac'] : $this->getSupportedMACAlgorithms(); $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ? $preferred['server_to_client']['comp'] : $this->getSupportedCompressionAlgorithms(); $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ? $preferred['client_to_server']['comp'] : $this->getSupportedCompressionAlgorithms(); // some SSH servers have buggy implementations of some of the above algorithms switch (true) { case $this->server_identifier == 'SSH-2.0-SSHD': case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK': if (!isset($preferred['server_to_client']['mac'])) { $s2c_mac_algorithms = array_values(array_diff( $s2c_mac_algorithms, array('hmac-sha1-96', 'hmac-md5-96') )); } if (!isset($preferred['client_to_server']['mac'])) { $c2s_mac_algorithms = array_values(array_diff( $c2s_mac_algorithms, array('hmac-sha1-96', 'hmac-md5-96') )); } } $str_kex_algorithms = implode(',', $kex_algorithms); $str_server_host_key_algorithms = implode(',', $server_host_key_algorithms); $encryption_algorithms_server_to_client = implode(',', $s2c_encryption_algorithms); $encryption_algorithms_client_to_server = implode(',', $c2s_encryption_algorithms); $mac_algorithms_server_to_client = implode(',', $s2c_mac_algorithms); $mac_algorithms_client_to_server = implode(',', $c2s_mac_algorithms); $compression_algorithms_server_to_client = implode(',', $s2c_compression_algorithms); $compression_algorithms_client_to_server = implode(',', $c2s_compression_algorithms); $client_cookie = crypt_random_string(16); $kexinit_payload_client = pack( 'Ca*Na*Na*Na*Na*Na*Na*Na*Na*Na*Na*CN', NET_SSH2_MSG_KEXINIT, $client_cookie, strlen($str_kex_algorithms), $str_kex_algorithms, strlen($str_server_host_key_algorithms), $str_server_host_key_algorithms, strlen($encryption_algorithms_client_to_server), $encryption_algorithms_client_to_server, strlen($encryption_algorithms_server_to_client), $encryption_algorithms_server_to_client, strlen($mac_algorithms_client_to_server), $mac_algorithms_client_to_server, strlen($mac_algorithms_server_to_client), $mac_algorithms_server_to_client, strlen($compression_algorithms_client_to_server), $compression_algorithms_client_to_server, strlen($compression_algorithms_server_to_client), $compression_algorithms_server_to_client, 0, '', 0, '', 0, 0 ); if ($this->send_kex_first) { if (!$this->_send_binary_packet($kexinit_payload_client)) { return false; } $kexinit_payload_server = $this->_get_binary_packet(); if ($kexinit_payload_server === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($kexinit_payload_server) || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT) { user_error('Expected SSH_MSG_KEXINIT'); return false; } } $response = $kexinit_payload_server; $this->_string_shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) $server_cookie = $this->_string_shift($response, 16); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->kex_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->server_host_key_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->encryption_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->encryption_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->mac_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->mac_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->compression_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->compression_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->languages_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->languages_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); if (!strlen($response)) { return false; } extract(unpack('Cfirst_kex_packet_follows', $this->_string_shift($response, 1))); $first_kex_packet_follows = $first_kex_packet_follows != 0; if (!$this->send_kex_first && !$this->_send_binary_packet($kexinit_payload_client)) { return false; } // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the // diffie-hellman key exchange as fast as possible $decrypt = $this->_array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client); $decryptKeyLength = $this->_encryption_algorithm_to_key_size($decrypt); if ($decryptKeyLength === null) { user_error('No compatible server to client encryption algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $encrypt = $this->_array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server); $encryptKeyLength = $this->_encryption_algorithm_to_key_size($encrypt); if ($encryptKeyLength === null) { user_error('No compatible client to server encryption algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $keyLength = $decryptKeyLength > $encryptKeyLength ? $decryptKeyLength : $encryptKeyLength; // through diffie-hellman key exchange a symmetric key is obtained $this->kex_algorithm = $kex_algorithm = $this->_array_intersect_first($kex_algorithms, $this->kex_algorithms); if ($kex_algorithm === false) { user_error('No compatible key exchange algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } if (strpos($kex_algorithm, 'diffie-hellman-group-exchange') === 0) { $dh_group_sizes_packed = pack( 'NNN', $this->kex_dh_group_size_min, $this->kex_dh_group_size_preferred, $this->kex_dh_group_size_max ); $packet = pack( 'Ca*', NET_SSH2_MSG_KEXDH_GEX_REQUEST, $dh_group_sizes_packed ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); if ($type != NET_SSH2_MSG_KEXDH_GEX_GROUP) { user_error('Expected SSH_MSG_KEX_DH_GEX_GROUP'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('NprimeLength', $this->_string_shift($response, 4))); $primeBytes = $this->_string_shift($response, $primeLength); $prime = new Math_BigInteger($primeBytes, -256); if (strlen($response) < 4) { return false; } extract(unpack('NgLength', $this->_string_shift($response, 4))); $gBytes = $this->_string_shift($response, $gLength); $g = new Math_BigInteger($gBytes, -256); $exchange_hash_rfc4419 = pack( 'a*Na*Na*', $dh_group_sizes_packed, $primeLength, $primeBytes, $gLength, $gBytes ); $clientKexInitMessage = NET_SSH2_MSG_KEXDH_GEX_INIT; $serverKexReplyMessage = NET_SSH2_MSG_KEXDH_GEX_REPLY; } else { switch ($kex_algorithm) { // see http://tools.ietf.org/html/rfc2409#section-6.2 and // http://tools.ietf.org/html/rfc2412, appendex E case 'diffie-hellman-group1-sha1': $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; break; // see http://tools.ietf.org/html/rfc3526#section-3 case 'diffie-hellman-group14-sha1': $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . '3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF'; break; } // For both diffie-hellman-group1-sha1 and diffie-hellman-group14-sha1 // the generator field element is 2 (decimal) and the hash function is sha1. $g = new Math_BigInteger(2); $prime = new Math_BigInteger($prime, 16); $exchange_hash_rfc4419 = ''; $clientKexInitMessage = NET_SSH2_MSG_KEXDH_INIT; $serverKexReplyMessage = NET_SSH2_MSG_KEXDH_REPLY; } switch ($kex_algorithm) { case 'diffie-hellman-group-exchange-sha256': $kexHash = new Crypt_Hash('sha256'); break; default: $kexHash = new Crypt_Hash('sha1'); } /* To increase the speed of the key exchange, both client and server may reduce the size of their private exponents. It should be at least twice as long as the key material that is generated from the shared secret. For more details, see the paper by van Oorschot and Wiener [VAN-OORSCHOT]. -- http://tools.ietf.org/html/rfc4419#section-6.2 */ $one = new Math_BigInteger(1); $keyLength = min($keyLength, $kexHash->getLength()); $max = $one->bitwise_leftShift(16 * $keyLength); // 2 * 8 * $keyLength $max = $max->subtract($one); $x = $one->random($one, $max); $e = $g->modPow($x, $prime); $eBytes = $e->toBytes(true); $data = pack('CNa*', $clientKexInitMessage, strlen($eBytes), $eBytes); if (!$this->_send_binary_packet($data)) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); if ($type != $serverKexReplyMessage) { user_error('Expected SSH_MSG_KEXDH_REPLY'); return false; } if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->server_public_host_key = $server_public_host_key = $this->_string_shift($response, $temp['length']); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $public_key_format = $this->_string_shift($server_public_host_key, $temp['length']); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $fBytes = $this->_string_shift($response, $temp['length']); $f = new Math_BigInteger($fBytes, -256); if (strlen($response) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->signature = $this->_string_shift($response, $temp['length']); if (strlen($this->signature) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($this->signature, 4)); $this->signature_format = $this->_string_shift($this->signature, $temp['length']); $key = $f->modPow($x, $prime); $keyBytes = $key->toBytes(true); $this->exchange_hash = pack( 'Na*Na*Na*Na*Na*a*Na*Na*Na*', strlen($this->identifier), $this->identifier, strlen($this->server_identifier), $this->server_identifier, strlen($kexinit_payload_client), $kexinit_payload_client, strlen($kexinit_payload_server), $kexinit_payload_server, strlen($this->server_public_host_key), $this->server_public_host_key, $exchange_hash_rfc4419, strlen($eBytes), $eBytes, strlen($fBytes), $fBytes, strlen($keyBytes), $keyBytes ); $this->exchange_hash = $kexHash->hash($this->exchange_hash); if ($this->session_id === false) { $this->session_id = $this->exchange_hash; } $server_host_key_algorithm = $this->_array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms); if ($server_host_key_algorithm === false) { user_error('No compatible server host key algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } switch ($server_host_key_algorithm) { case 'ssh-dss': $expected_key_format = 'ssh-dss'; break; //case 'rsa-sha2-256': //case 'rsa-sha2-512': //case 'ssh-rsa': default: $expected_key_format = 'ssh-rsa'; } if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) { switch (true) { case $this->signature_format == $server_host_key_algorithm: case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512': case $this->signature_format != 'ssh-rsa': user_error('Server Host Key Algorithm Mismatch'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } } $packet = pack( 'C', NET_SSH2_MSG_NEWKEYS ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); if ($type != NET_SSH2_MSG_NEWKEYS) { user_error('Expected SSH_MSG_NEWKEYS'); return false; } $this->encrypt = $this->_encryption_algorithm_to_crypt_instance($encrypt); $this->decrypt = $this->_encryption_algorithm_to_crypt_instance($decrypt); $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); if ($this->encrypt) { if ($this->crypto_engine) { $this->encrypt->setPreferredEngine($this->crypto_engine); } $this->encrypt->enableContinuousBuffer(); $this->encrypt->disablePadding(); if ($this->encrypt->getBlockLength()) { $this->encrypt_block_size = $this->encrypt->getBlockLength() >> 3; } $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); while ($this->encrypt_block_size > strlen($iv)) { $iv.= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); } $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); while ($encryptKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); $this->encrypt->name = $decrypt; } if ($this->decrypt) { if ($this->crypto_engine) { $this->decrypt->setPreferredEngine($this->crypto_engine); } $this->decrypt->enableContinuousBuffer(); $this->decrypt->disablePadding(); if ($this->decrypt->getBlockLength()) { $this->decrypt_block_size = $this->decrypt->getBlockLength() >> 3; } $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); while ($this->decrypt_block_size > strlen($iv)) { $iv.= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); } $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); while ($decryptKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); $this->decrypt->name = $decrypt; } /* The "arcfour128" algorithm is the RC4 cipher, as described in [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream generated by the cipher MUST be discarded, and the first byte of the first encrypted packet MUST be encrypted using the 1537th byte of keystream. -- http://tools.ietf.org/html/rfc4345#section-4 */ if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') { $this->encrypt->encrypt(str_repeat("\0", 1536)); } if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') { $this->decrypt->decrypt(str_repeat("\0", 1536)); } $mac_algorithm = $this->_array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server); if ($mac_algorithm === false) { user_error('No compatible client to server message authentication algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $createKeyLength = 0; // ie. $mac_algorithm == 'none' switch ($mac_algorithm) { case 'hmac-sha2-256': $this->hmac_create = new Crypt_Hash('sha256'); $createKeyLength = 32; break; case 'hmac-sha1': $this->hmac_create = new Crypt_Hash('sha1'); $createKeyLength = 20; break; case 'hmac-sha1-96': $this->hmac_create = new Crypt_Hash('sha1-96'); $createKeyLength = 20; break; case 'hmac-md5': $this->hmac_create = new Crypt_Hash('md5'); $createKeyLength = 16; break; case 'hmac-md5-96': $this->hmac_create = new Crypt_Hash('md5-96'); $createKeyLength = 16; } $this->hmac_create->name = $mac_algorithm; $mac_algorithm = $this->_array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client); if ($mac_algorithm === false) { user_error('No compatible server to client message authentication algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $checkKeyLength = 0; $this->hmac_size = 0; switch ($mac_algorithm) { case 'hmac-sha2-256': $this->hmac_check = new Crypt_Hash('sha256'); $checkKeyLength = 32; $this->hmac_size = 32; break; case 'hmac-sha1': $this->hmac_check = new Crypt_Hash('sha1'); $checkKeyLength = 20; $this->hmac_size = 20; break; case 'hmac-sha1-96': $this->hmac_check = new Crypt_Hash('sha1-96'); $checkKeyLength = 20; $this->hmac_size = 12; break; case 'hmac-md5': $this->hmac_check = new Crypt_Hash('md5'); $checkKeyLength = 16; $this->hmac_size = 16; break; case 'hmac-md5-96': $this->hmac_check = new Crypt_Hash('md5-96'); $checkKeyLength = 16; $this->hmac_size = 12; } $this->hmac_check->name = $mac_algorithm; $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id); while ($createKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->hmac_create->setKey(substr($key, 0, $createKeyLength)); $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id); while ($checkKeyLength > strlen($key)) { $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); } $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); $compression_algorithm = $this->_array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); if ($compression_algorithm === false) { user_error('No compatible client to server compression algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } //$this->decompress = $compression_algorithm == 'zlib'; $compression_algorithm = $this->_array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_client_to_server); if ($compression_algorithm === false) { user_error('No compatible server to client compression algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } //$this->compress = $compression_algorithm == 'zlib'; return true; } /** * Maps an encryption algorithm name to the number of key bytes. * * @param string $algorithm Name of the encryption algorithm * @return int|null Number of bytes as an integer or null for unknown * @access private */ function _encryption_algorithm_to_key_size($algorithm) { if ($this->bad_key_size_fix && $this->_bad_algorithm_candidate($algorithm)) { return 16; } switch ($algorithm) { case 'none': return 0; case 'aes128-cbc': case 'aes128-ctr': case 'arcfour': case 'arcfour128': case 'blowfish-cbc': case 'blowfish-ctr': case 'twofish128-cbc': case 'twofish128-ctr': return 16; case '3des-cbc': case '3des-ctr': case 'aes192-cbc': case 'aes192-ctr': case 'twofish192-cbc': case 'twofish192-ctr': return 24; case 'aes256-cbc': case 'aes256-ctr': case 'arcfour256': case 'twofish-cbc': case 'twofish256-cbc': case 'twofish256-ctr': return 32; } return null; } /** * Maps an encryption algorithm name to an instance of a subclass of * \phpseclib\Crypt\Base. * * @param string $algorithm Name of the encryption algorithm * @return mixed Instance of \phpseclib\Crypt\Base or null for unknown * @access private */ function _encryption_algorithm_to_crypt_instance($algorithm) { switch ($algorithm) { case '3des-cbc': if (!class_exists('Crypt_TripleDES')) { include_once 'Crypt/TripleDES.php'; } return new Crypt_TripleDES(); break; case '3des-ctr': if (!class_exists('Crypt_TripleDES')) { include_once 'Crypt/TripleDES.php'; } return new Crypt_TripleDES(CRYPT_DES_MODE_CTR); break; case 'aes256-cbc': case 'aes192-cbc': case 'aes128-cbc': if (!class_exists('Crypt_Rijndael')) { include_once 'Crypt/Rijndael.php'; } return new Crypt_Rijndael(); break; case 'aes256-ctr': case 'aes192-ctr': case 'aes128-ctr': if (!class_exists('Crypt_Rijndael')) { include_once 'Crypt/Rijndael.php'; } return new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CTR); $this->encrypt_block_size = 16; // eg. 128 / 8 break; case 'blowfish-cbc': if (!class_exists('Crypt_Blowfish')) { include_once 'Crypt/Blowfish.php'; } return new Crypt_Blowfish(); break; case 'blowfish-ctr': if (!class_exists('Crypt_Blowfish')) { include_once 'Crypt/Blowfish.php'; } return new Crypt_Blowfish(CRYPT_BLOWFISH_MODE_CTR); break; case 'twofish128-cbc': case 'twofish192-cbc': case 'twofish256-cbc': case 'twofish-cbc': if (!class_exists('Crypt_Twofish')) { include_once 'Crypt/Twofish.php'; } return new Crypt_Twofish(); break; case 'twofish128-ctr': case 'twofish192-ctr': case 'twofish256-ctr': if (!class_exists('Crypt_Twofish')) { include_once 'Crypt/Twofish.php'; } return new Crypt_Twofish(CRYPT_TWOFISH_MODE_CTR); break; case 'arcfour': case 'arcfour128': case 'arcfour256': if (!class_exists('Crypt_RC4')) { include_once 'Crypt/RC4.php'; } return new Crypt_RC4(); break; case 'none': //return new Crypt_Null(); } return null; } /** * Tests whether or not proposed algorithm has a potential for issues * * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 * @param string $algorithm Name of the encryption algorithm * @return bool * @access private */ function _bad_algorithm_candidate($algorithm) { switch ($algorithm) { case 'arcfour256': case 'aes192-ctr': case 'aes256-ctr': return true; } return false; } /** * Login * * The $password parameter can be a plaintext password, a Crypt_RSA object or an array * * @param string $username * @param mixed $password * @param mixed $... * @return bool * @see self::_login() * @access public */ function login($username) { $args = func_get_args(); $this->auth[] = $args; return call_user_func_array(array(&$this, '_login'), $args); } /** * Login Helper * * @param string $username * @param mixed $password * @param mixed $... * @return bool * @see self::_login_helper() * @access private */ function _login($username) { if (!($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR)) { if (!$this->_connect()) { return false; } } $args = array_slice(func_get_args(), 1); if (empty($args)) { return $this->_login_helper($username); } foreach ($args as $arg) { if ($this->_login_helper($username, $arg)) { return true; } } return false; } /** * Login Helper * * @param string $username * @param string $password * @return bool * @access private * @internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} * by sending dummy SSH_MSG_IGNORE messages. */ function _login_helper($username, $password = null) { if (!($this->bitmap & NET_SSH2_MASK_CONNECTED)) { return false; } if (!($this->bitmap & NET_SSH2_MASK_LOGIN_REQ)) { $packet = pack( 'CNa*', NET_SSH2_MSG_SERVICE_REQUEST, strlen('ssh-userauth'), 'ssh-userauth' ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { if ($this->retry_connect) { $this->retry_connect = false; if (!$this->_connect()) { return false; } return $this->_login_helper($username, $password); } $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); if ($type != NET_SSH2_MSG_SERVICE_ACCEPT) { user_error('Expected SSH_MSG_SERVICE_ACCEPT'); return false; } $this->bitmap |= NET_SSH2_MASK_LOGIN_REQ; } if (strlen($this->last_interactive_response)) { return !is_string($password) && !is_array($password) ? false : $this->_keyboard_interactive_process($password); } // although PHP5's get_class() preserves the case, PHP4's does not if (is_object($password)) { switch (strtolower(get_class($password))) { case 'crypt_rsa': return $this->_privatekey_login($username, $password); case 'system_ssh_agent': return $this->_ssh_agent_login($username, $password); } } if (is_array($password)) { if ($this->_keyboard_interactive_login($username, $password)) { $this->bitmap |= NET_SSH2_MASK_LOGIN; return true; } return false; } if (!isset($password)) { $packet = pack( 'CNa*Na*Na*', NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection', strlen('none'), 'none' ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); switch ($type) { case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= NET_SSH2_MASK_LOGIN; return true; //case NET_SSH2_MSG_USERAUTH_FAILURE: default: return false; } } $packet = pack( 'CNa*Na*Na*CNa*', NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection', strlen('password'), 'password', 0, strlen($password), $password ); // remove the username and password from the logged packet if (!defined('NET_SSH2_LOGGING')) { $logged = null; } else { $logged = pack( 'CNa*Na*Na*CNa*', NET_SSH2_MSG_USERAUTH_REQUEST, strlen('username'), 'username', strlen('ssh-connection'), 'ssh-connection', strlen('password'), 'password', 0, strlen('password'), 'password' ); } if (!$this->_send_binary_packet($packet, $logged)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); switch ($type) { case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed if (defined('NET_SSH2_LOGGING')) { $this->message_number_log[count($this->message_number_log) - 1] = 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'; } if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $this->_string_shift($response, $length); return $this->_disconnect(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER); case NET_SSH2_MSG_USERAUTH_FAILURE: // can we use keyboard-interactive authentication? if not then either the login is bad or the server employees // multi-factor authentication if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $auth_methods = explode(',', $this->_string_shift($response, $length)); if (!strlen($response)) { return false; } extract(unpack('Cpartial_success', $this->_string_shift($response, 1))); $partial_success = $partial_success != 0; if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) { if ($this->_keyboard_interactive_login($username, $password)) { $this->bitmap |= NET_SSH2_MASK_LOGIN; return true; } return false; } return false; case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= NET_SSH2_MASK_LOGIN; return true; } return false; } /** * Login via keyboard-interactive authentication * * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details. This is not a full-featured keyboard-interactive authenticator. * * @param string $username * @param string $password * @return bool * @access private */ function _keyboard_interactive_login($username, $password) { $packet = pack( 'CNa*Na*Na*Na*Na*', NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection', strlen('keyboard-interactive'), 'keyboard-interactive', 0, '', 0, '' ); if (!$this->_send_binary_packet($packet)) { return false; } return $this->_keyboard_interactive_process($password); } /** * Handle the keyboard-interactive requests / responses. * * @param string $responses... * @return bool * @access private */ function _keyboard_interactive_process() { $responses = func_get_args(); if (strlen($this->last_interactive_response)) { $response = $this->last_interactive_response; } else { $orig = $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); switch ($type) { case NET_SSH2_MSG_USERAUTH_INFO_REQUEST: if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->_string_shift($response, $length); // name; may be empty if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->_string_shift($response, $length); // instruction; may be empty if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->_string_shift($response, $length); // language tag; may be empty if (strlen($response) < 4) { return false; } extract(unpack('Nnum_prompts', $this->_string_shift($response, 4))); for ($i = 0; $i < count($responses); $i++) { if (is_array($responses[$i])) { foreach ($responses[$i] as $key => $value) { $this->keyboard_requests_responses[$key] = $value; } unset($responses[$i]); } } $responses = array_values($responses); if (isset($this->keyboard_requests_responses)) { for ($i = 0; $i < $num_prompts; $i++) { if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); // prompt - ie. "Password: "; must not be empty $prompt = $this->_string_shift($response, $length); //$echo = $this->_string_shift($response) != chr(0); foreach ($this->keyboard_requests_responses as $key => $value) { if (substr($prompt, 0, strlen($key)) == $key) { $responses[] = $value; break; } } } } // see http://tools.ietf.org/html/rfc4256#section-3.2 if (strlen($this->last_interactive_response)) { $this->last_interactive_response = ''; } elseif (defined('NET_SSH2_LOGGING')) { $this->message_number_log[count($this->message_number_log) - 1] = str_replace( 'UNKNOWN', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', $this->message_number_log[count($this->message_number_log) - 1] ); } if (!count($responses) && $num_prompts) { $this->last_interactive_response = $orig; return false; } /* After obtaining the requested information from the user, the client MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message. */ // see http://tools.ietf.org/html/rfc4256#section-3.4 $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses)); for ($i = 0; $i < count($responses); $i++) { $packet.= pack('Na*', strlen($responses[$i]), $responses[$i]); $logged.= pack('Na*', strlen('dummy-answer'), 'dummy-answer'); } if (!$this->_send_binary_packet($packet, $logged)) { return false; } if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX) { $this->message_number_log[count($this->message_number_log) - 1] = str_replace( 'UNKNOWN', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE', $this->message_number_log[count($this->message_number_log) - 1] ); } /* After receiving the response, the server MUST send either an SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another SSH_MSG_USERAUTH_INFO_REQUEST message. */ // maybe phpseclib should force close the connection after x request / responses? unless something like that is done // there could be an infinite loop of request / responses. return $this->_keyboard_interactive_process(); case NET_SSH2_MSG_USERAUTH_SUCCESS: return true; case NET_SSH2_MSG_USERAUTH_FAILURE: return false; } return false; } /** * Login with an ssh-agent provided key * * @param string $username * @param System_SSH_Agent $agent * @return bool * @access private */ function _ssh_agent_login($username, $agent) { $this->agent = $agent; $keys = $agent->requestIdentities(); foreach ($keys as $key) { if ($this->_privatekey_login($username, $key)) { return true; } } return false; } /** * Login with an RSA private key * * @param string $username * @param Crypt_RSA $password * @return bool * @access private * @internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} * by sending dummy SSH_MSG_IGNORE messages. */ function _privatekey_login($username, $privatekey) { // see http://tools.ietf.org/html/rfc4253#page-15 $publickey = $privatekey->getPublicKey(CRYPT_RSA_PUBLIC_FORMAT_RAW); if ($publickey === false) { return false; } $publickey = array( 'e' => $publickey['e']->toBytes(true), 'n' => $publickey['n']->toBytes(true) ); $publickey = pack( 'Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($publickey['e']), $publickey['e'], strlen($publickey['n']), $publickey['n'] ); switch ($this->signature_format) { case 'rsa-sha2-512': $hash = 'sha512'; $signatureType = 'rsa-sha2-512'; break; case 'rsa-sha2-256': $hash = 'sha256'; $signatureType = 'rsa-sha2-256'; break; //case 'ssh-rsa': default: $hash = 'sha1'; $signatureType = 'ssh-rsa'; } $part1 = pack( 'CNa*Na*Na*', NET_SSH2_MSG_USERAUTH_REQUEST, strlen($username), $username, strlen('ssh-connection'), 'ssh-connection', strlen('publickey'), 'publickey' ); $part2 = pack('Na*Na*', strlen($signatureType), $signatureType, strlen($publickey), $publickey); $packet = $part1 . chr(0) . $part2; if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); switch ($type) { case NET_SSH2_MSG_USERAUTH_FAILURE: if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE: ' . $this->_string_shift($response, $length); return false; case NET_SSH2_MSG_USERAUTH_PK_OK: // we'll just take it on faith that the public key blob and the public key algorithm name are as // they should be if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX) { $this->message_number_log[count($this->message_number_log) - 1] = str_replace( 'UNKNOWN', 'NET_SSH2_MSG_USERAUTH_PK_OK', $this->message_number_log[count($this->message_number_log) - 1] ); } } $packet = $part1 . chr(1) . $part2; $privatekey->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1); $privatekey->setHash($hash); $signature = $privatekey->sign(pack('Na*a*', strlen($this->session_id), $this->session_id, $packet)); $signature = pack('Na*Na*', strlen($signatureType), $signatureType, strlen($signature), $signature); $packet.= pack('Na*', strlen($signature), $signature); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); switch ($type) { case NET_SSH2_MSG_USERAUTH_FAILURE: // either the login is bad or the server employs multi-factor authentication return false; case NET_SSH2_MSG_USERAUTH_SUCCESS: $this->bitmap |= NET_SSH2_MASK_LOGIN; return true; } return false; } /** * Set Timeout * * $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. * Setting $timeout to false or 0 will mean there is no timeout. * * @param mixed $timeout * @access public */ function setTimeout($timeout) { $this->timeout = $this->curTimeout = $timeout; } /** * Get the output from stdError * * @access public */ function getStdError() { return $this->stdErrorLog; } /** * Execute Command * * If $callback is set to false then Net_SSH2::_get_channel_packet(NET_SSH2_CHANNEL_EXEC) will need to be called manually. * In all likelihood, this is not a feature you want to be taking advantage of. * * @param string $command * @param Callback $callback * @return string * @access public */ function exec($command, $callback = null) { $this->curTimeout = $this->timeout; $this->is_timeout = false; $this->stdErrorLog = ''; if (!$this->isAuthenticated()) { return false; } if ($this->in_request_pty_exec) { user_error('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); return false; } // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to // be adjusted". 0x7FFFFFFF is, at 2GB, the max size. technically, it should probably be decremented, but, // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway. // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info $this->window_size_server_to_client[NET_SSH2_CHANNEL_EXEC] = $this->window_size; // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy // uses 0x4000, that's what will be used here, as well. $packet_size = 0x4000; $packet = pack( 'CNa*N3', NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_EXEC, $this->window_size_server_to_client[NET_SSH2_CHANNEL_EXEC], $packet_size ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_OPEN; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC); if ($response === false) { return false; } if ($this->request_pty === true) { $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); $packet = pack( 'CNNa*CNa*N5a*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_EXEC], strlen('pty-req'), 'pty-req', 1, strlen('vt100'), 'vt100', $this->windowColumns, $this->windowRows, 0, 0, strlen($terminal_modes), $terminal_modes ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } list(, $type) = unpack('C', $this->_string_shift($response, 1)); switch ($type) { case NET_SSH2_MSG_CHANNEL_SUCCESS: break; case NET_SSH2_MSG_CHANNEL_FAILURE: default: user_error('Unable to request pseudo-terminal'); return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } $this->in_request_pty_exec = true; } // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things // down. the one place where it might be desirable is if you're doing something like Net_SSH2::exec('ping localhost &'). // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then // then immediately terminate. without such a request exec() will loop indefinitely. the ping process won't end but // neither will your script. // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. $packet = pack( 'CNNa*CNa*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_EXEC], strlen('exec'), 'exec', 1, strlen($command), $command ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC); if ($response === false) { return false; } $this->channel_status[NET_SSH2_CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA; if ($callback === false || $this->in_request_pty_exec) { return true; } $output = ''; while (true) { $temp = $this->_get_channel_packet(NET_SSH2_CHANNEL_EXEC); switch (true) { case $temp === true: return is_callable($callback) ? true : $output; case $temp === false: return false; default: if (is_callable($callback)) { if (call_user_func($callback, $temp) === true) { $this->_close_channel(NET_SSH2_CHANNEL_EXEC); return true; } } else { $output.= $temp; } } } } /** * Creates an interactive shell * * @see self::read() * @see self::write() * @return bool * @access private */ function _initShell() { if ($this->in_request_pty_exec === true) { return true; } $this->window_size_server_to_client[NET_SSH2_CHANNEL_SHELL] = $this->window_size; $packet_size = 0x4000; $packet = pack( 'CNa*N3', NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_SHELL, $this->window_size_server_to_client[NET_SSH2_CHANNEL_SHELL], $packet_size ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_OPEN; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SHELL); if ($response === false) { return false; } $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); $packet = pack( 'CNNa*CNa*N5a*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SHELL], strlen('pty-req'), 'pty-req', 1, strlen('vt100'), 'vt100', $this->windowColumns, $this->windowRows, 0, 0, strlen($terminal_modes), $terminal_modes ); if (!$this->_send_binary_packet($packet)) { return false; } $response = $this->_get_binary_packet(); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } if (!strlen($response)) { return false; } list(, $type) = unpack('C', $this->_string_shift($response, 1)); switch ($type) { case NET_SSH2_MSG_CHANNEL_SUCCESS: // if a pty can't be opened maybe commands can still be executed case NET_SSH2_MSG_CHANNEL_FAILURE: break; default: user_error('Unable to request pseudo-terminal'); return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } $packet = pack( 'CNNa*C', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SHELL], strlen('shell'), 'shell', 1 ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SHELL); if ($response === false) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; $this->bitmap |= NET_SSH2_MASK_SHELL; return true; } /** * Return the channel to be used with read() / write() * * @see self::read() * @see self::write() * @return int * @access public */ function _get_interactive_channel() { switch (true) { case $this->in_subsystem: return NET_SSH2_CHANNEL_SUBSYSTEM; case $this->in_request_pty_exec: return NET_SSH2_CHANNEL_EXEC; default: return NET_SSH2_CHANNEL_SHELL; } } /** * Return an available open channel * * @return int * @access public */ function _get_open_channel() { $channel = NET_SSH2_CHANNEL_EXEC; do { if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { return $channel; } } while ($channel++ < NET_SSH2_CHANNEL_SUBSYSTEM); return false; } /** * Returns the output of an interactive shell * * Returns when there's a match for $expect, which can take the form of a string literal or, * if $mode == NET_SSH2_READ_REGEX, a regular expression. * * @see self::write() * @param string $expect * @param int $mode * @return string * @access public */ function read($expect = '', $mode = NET_SSH2_READ_SIMPLE) { $this->curTimeout = $this->timeout; $this->is_timeout = false; if (!$this->isAuthenticated()) { user_error('Operation disallowed prior to login()'); return false; } if (!($this->bitmap & NET_SSH2_MASK_SHELL) && !$this->_initShell()) { user_error('Unable to initiate an interactive shell session'); return false; } $channel = $this->_get_interactive_channel(); if ($mode == NET_SSH2_READ_NEXT) { return $this->_get_channel_packet($channel); } $match = $expect; while (true) { if ($mode == NET_SSH2_READ_REGEX) { preg_match($expect, substr($this->interactiveBuffer, -1024), $matches); $match = isset($matches[0]) ? $matches[0] : ''; } $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; if ($pos !== false) { return $this->_string_shift($this->interactiveBuffer, $pos + strlen($match)); } $response = $this->_get_channel_packet($channel); if (is_bool($response)) { $this->in_request_pty_exec = false; return $response ? $this->_string_shift($this->interactiveBuffer, strlen($this->interactiveBuffer)) : false; } $this->interactiveBuffer.= $response; } } /** * Inputs a command into an interactive shell. * * @see self::read() * @param string $cmd * @return bool * @access public */ function write($cmd) { if (!$this->isAuthenticated()) { user_error('Operation disallowed prior to login()'); return false; } if (!($this->bitmap & NET_SSH2_MASK_SHELL) && !$this->_initShell()) { user_error('Unable to initiate an interactive shell session'); return false; } return $this->_send_channel_packet($this->_get_interactive_channel(), $cmd); } /** * Start a subsystem. * * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened. * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented * if there's sufficient demand for such a feature. * * @see self::stopSubsystem() * @param string $subsystem * @return bool * @access public */ function startSubsystem($subsystem) { $this->window_size_server_to_client[NET_SSH2_CHANNEL_SUBSYSTEM] = $this->window_size; $packet = pack( 'CNa*N3', NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_SUBSYSTEM, $this->window_size, 0x4000 ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_OPEN; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SUBSYSTEM); if ($response === false) { return false; } $packet = pack( 'CNNa*CNa*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_SUBSYSTEM], strlen('subsystem'), 'subsystem', 1, strlen($subsystem), $subsystem ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->_get_channel_packet(NET_SSH2_CHANNEL_SUBSYSTEM); if ($response === false) { return false; } $this->channel_status[NET_SSH2_CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; $this->bitmap |= NET_SSH2_MASK_SHELL; $this->in_subsystem = true; return true; } /** * Stops a subsystem. * * @see self::startSubsystem() * @return bool * @access public */ function stopSubsystem() { $this->in_subsystem = false; $this->_close_channel(NET_SSH2_CHANNEL_SUBSYSTEM); return true; } /** * Closes a channel * * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call * * @access public */ function reset() { $this->_close_channel($this->_get_interactive_channel()); } /** * Is timeout? * * Did exec() or read() return because they timed out or because they encountered the end? * * @access public */ function isTimeout() { return $this->is_timeout; } /** * Disconnect * * @access public */ function disconnect() { $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) { fclose($this->realtime_log_file); } } /** * Destructor. * * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call * disconnect(). * * @access public */ function __destruct() { $this->disconnect(); } /** * Is the connection still active? * * @return bool * @access public */ function isConnected() { return (bool) ($this->bitmap & NET_SSH2_MASK_CONNECTED); } /** * Have you successfully been logged in? * * @return bool * @access public */ function isAuthenticated() { return (bool) ($this->bitmap & NET_SSH2_MASK_LOGIN); } /** * Pings a server connection, or tries to reconnect if the connection has gone down * * Inspired by http://php.net/manual/en/mysqli.ping.php * * @return bool * @access public */ function ping() { if (!$this->isAuthenticated()) { if (!empty($this->auth)) { return $this->_reconnect(); } return false; } $this->window_size_server_to_client[NET_SSH2_CHANNEL_KEEP_ALIVE] = $this->window_size; $packet_size = 0x4000; $packet = pack( 'CNa*N3', NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_KEEP_ALIVE, $this->window_size_server_to_client[NET_SSH2_CHANNEL_KEEP_ALIVE], $packet_size ); if (!@$this->_send_binary_packet($packet)) { return $this->_reconnect(); } $this->channel_status[NET_SSH2_CHANNEL_KEEP_ALIVE] = NET_SSH2_MSG_CHANNEL_OPEN; $response = @$this->_get_channel_packet(NET_SSH2_CHANNEL_KEEP_ALIVE); if ($response !== false) { $this->_close_channel(NET_SSH2_CHANNEL_KEEP_ALIVE); return true; } return $this->_reconnect(); } /** * In situ reconnect method * * @return boolean * @access private */ function _reconnect() { $this->_reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST); $this->retry_connect = true; if (!$this->_connect()) { return false; } foreach ($this->auth as $auth) { $result = call_user_func_array(array(&$this, 'login'), $auth); } return $result; } /** * Resets a connection for re-use * * @param int $reason * @access private */ function _reset_connection($reason) { $this->_disconnect($reason); $this->decrypt = $this->encrypt = false; $this->decrypt_block_size = $this->encrypt_block_size = 8; $this->hmac_check = $this->hmac_create = false; $this->hmac_size = false; $this->session_id = false; $this->retry_connect = true; $this->get_seq_no = $this->send_seq_no = 0; } /** * Gets Binary Packets * * See '6. Binary Packet Protocol' of rfc4253 for more info. * * @see self::_send_binary_packet() * @return string * @access private */ function _get_binary_packet($skip_channel_filter = false) { if (!is_resource($this->fsock) || feof($this->fsock)) { $this->bitmap = 0; user_error('Connection closed prematurely'); return false; } $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $raw = fread($this->fsock, $this->decrypt_block_size); if (!strlen($raw)) { return ''; } if ($this->decrypt !== false) { $raw = $this->decrypt->decrypt($raw); } if ($raw === false) { user_error('Unable to decrypt content'); return false; } if (strlen($raw) < 5) { return false; } extract(unpack('Npacket_length/Cpadding_length', $this->_string_shift($raw, 5))); $remaining_length = $packet_length + 4 - $this->decrypt_block_size; // quoting <http://tools.ietf.org/html/rfc4253#section-6.1>, // "implementations SHOULD check that the packet length is reasonable" // PuTTY uses 0x9000 as the actual max packet size and so to shall we if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { if (!$this->bad_key_size_fix && $this->_bad_algorithm_candidate($this->decrypt->name) && !($this->bitmap & NET_SSH2_MASK_LOGIN)) { $this->bad_key_size_fix = true; $this->_reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); return false; } user_error('Invalid size'); return false; } $buffer = ''; while ($remaining_length > 0) { $temp = fread($this->fsock, $remaining_length); if ($temp === false || feof($this->fsock)) { $this->bitmap = 0; user_error('Error reading from socket'); return false; } $buffer.= $temp; $remaining_length-= strlen($temp); } $stop = strtok(microtime(), ' ') + strtok(''); if (strlen($buffer)) { $raw.= $this->decrypt !== false ? $this->decrypt->decrypt($buffer) : $buffer; } $payload = $this->_string_shift($raw, $packet_length - $padding_length - 1); $padding = $this->_string_shift($raw, $padding_length); // should leave $raw empty if ($this->hmac_check !== false) { $hmac = fread($this->fsock, $this->hmac_size); if ($hmac === false || strlen($hmac) != $this->hmac_size) { $this->bitmap = 0; user_error('Error reading socket'); return false; } elseif ($hmac != $this->hmac_check->hash(pack('NNCa*', $this->get_seq_no, $packet_length, $padding_length, $payload . $padding))) { user_error('Invalid HMAC'); return false; } } //if ($this->decompress) { // $payload = gzinflate(substr($payload, 2)); //} $this->get_seq_no++; if (defined('NET_SSH2_LOGGING')) { $current = strtok(microtime(), ' ') + strtok(''); $message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; $message_number = '<- ' . $message_number . ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; $this->_append_log($message_number, $payload); $this->last_packet = $current; } return $this->_filter($payload, $skip_channel_filter); } /** * Filter Binary Packets * * Because some binary packets need to be ignored... * * @see self::_get_binary_packet() * @return string * @access private */ function _filter($payload, $skip_channel_filter) { switch (ord($payload[0])) { case NET_SSH2_MSG_DISCONNECT: $this->_string_shift($payload, 1); if (strlen($payload) < 8) { return false; } extract(unpack('Nreason_code/Nlength', $this->_string_shift($payload, 8))); $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n" . $this->_string_shift($payload, $length); $this->bitmap = 0; return false; case NET_SSH2_MSG_IGNORE: $payload = $this->_get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_DEBUG: $this->_string_shift($payload, 2); if (strlen($payload) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($payload, 4))); $this->errors[] = 'SSH_MSG_DEBUG: ' . $this->_string_shift($payload, $length); $payload = $this->_get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_UNIMPLEMENTED: return false; case NET_SSH2_MSG_KEXINIT: if ($this->session_id !== false) { $this->send_kex_first = false; if (!$this->_key_exchange($payload)) { $this->bitmap = 0; return false; } $payload = $this->_get_binary_packet($skip_channel_filter); } } // 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 if (($this->bitmap & NET_SSH2_MASK_CONNECTED) && !$this->isAuthenticated() && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) { $this->_string_shift($payload, 1); if (strlen($payload) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($payload, 4))); $this->banner_message = $this->_string_shift($payload, $length); $payload = $this->_get_binary_packet(); } // only called when we've already logged in if (($this->bitmap & NET_SSH2_MASK_CONNECTED) && $this->isAuthenticated()) { switch (ord($payload[0])) { case NET_SSH2_MSG_CHANNEL_DATA: case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: case NET_SSH2_MSG_CHANNEL_REQUEST: case NET_SSH2_MSG_CHANNEL_CLOSE: case NET_SSH2_MSG_CHANNEL_EOF: if (!$skip_channel_filter && !empty($this->server_channels)) { $this->binary_packet_buffer = $payload; $this->_get_channel_packet(true); $payload = $this->_get_binary_packet(); } break; case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 if (strlen($payload) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($payload, 4))); $this->errors[] = 'SSH_MSG_GLOBAL_REQUEST: ' . $this->_string_shift($payload, $length); if (!$this->_send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE))) { return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } $payload = $this->_get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 $this->_string_shift($payload, 1); if (strlen($payload) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($payload, 4))); $data = $this->_string_shift($payload, $length); if (strlen($payload) < 4) { return false; } extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); switch ($data) { case 'auth-agent': case 'auth-agent@openssh.com': if (isset($this->agent)) { $new_channel = NET_SSH2_CHANNEL_AGENT_FORWARD; if (strlen($payload) < 8) { return false; } extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4))); extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4))); $this->packet_size_client_to_server[$new_channel] = $remote_window_size; $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; $this->window_size_client_to_server[$new_channel] = $this->window_size; $packet_size = 0x4000; $packet = pack( 'CN4', NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size ); $this->server_channels[$new_channel] = $server_channel; $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; if (!$this->_send_binary_packet($packet)) { return false; } } break; default: $packet = pack( 'CN3a*Na*', NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, '' ); if (!$this->_send_binary_packet($packet)) { return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } } $payload = $this->_get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: $this->_string_shift($payload, 1); if (strlen($payload) < 8) { return false; } extract(unpack('Nchannel', $this->_string_shift($payload, 4))); extract(unpack('Nwindow_size', $this->_string_shift($payload, 4))); $this->window_size_client_to_server[$channel]+= $window_size; $payload = ($this->bitmap & NET_SSH2_MASK_WINDOW_ADJUST) ? true : $this->_get_binary_packet($skip_channel_filter); } } return $payload; } /** * Enable Quiet Mode * * Suppress stderr from output * * @access public */ function enableQuietMode() { $this->quiet_mode = true; } /** * Disable Quiet Mode * * Show stderr in output * * @access public */ function disableQuietMode() { $this->quiet_mode = false; } /** * Returns whether Quiet Mode is enabled or not * * @see self::enableQuietMode() * @see self::disableQuietMode() * * @access public * @return bool */ function isQuietModeEnabled() { return $this->quiet_mode; } /** * Enable request-pty when using exec() * * @access public */ function enablePTY() { $this->request_pty = true; } /** * Disable request-pty when using exec() * * @access public */ function disablePTY() { if ($this->in_request_pty_exec) { $this->_close_channel(NET_SSH2_CHANNEL_EXEC); $this->in_request_pty_exec = false; } $this->request_pty = false; } /** * Returns whether request-pty is enabled or not * * @see self::enablePTY() * @see self::disablePTY() * * @access public * @return bool */ function isPTYEnabled() { return $this->request_pty; } /** * Gets channel data * * Returns the data as a string if it's available and false if not. * * @param $client_channel * @return mixed * @access private */ function _get_channel_packet($client_channel, $skip_extended = false) { if (!empty($this->channel_buffers[$client_channel])) { return array_shift($this->channel_buffers[$client_channel]); } while (true) { if ($this->binary_packet_buffer !== false) { $response = $this->binary_packet_buffer; $this->binary_packet_buffer = false; } else { $read = array($this->fsock); $write = $except = null; if (!$this->curTimeout) { @stream_select($read, $write, $except, null); } else { if ($this->curTimeout < 0) { $this->is_timeout = true; return true; } $read = array($this->fsock); $write = $except = null; $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $sec = floor($this->curTimeout); $usec = 1000000 * ($this->curTimeout - $sec); // on windows this returns a "Warning: Invalid CRT parameters detected" error if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { $this->is_timeout = true; if ($client_channel == NET_SSH2_CHANNEL_EXEC && !$this->request_pty) { $this->_close_channel($client_channel); } return true; } $elapsed = strtok(microtime(), ' ') + strtok('') - $start; $this->curTimeout-= $elapsed; } $response = $this->_get_binary_packet(true); if ($response === false) { $this->bitmap = 0; user_error('Connection closed by server'); return false; } } if ($client_channel == -1 && $response === true) { return true; } if (!strlen($response)) { return false; } extract(unpack('Ctype', $this->_string_shift($response, 1))); if (strlen($response) < 4) { return false; } if ($type == NET_SSH2_MSG_CHANNEL_OPEN) { extract(unpack('Nlength', $this->_string_shift($response, 4))); } else { extract(unpack('Nchannel', $this->_string_shift($response, 4))); } // will not be setup yet on incoming channel open request if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { $this->window_size_server_to_client[$channel]-= strlen($response); // resize the window, if appropriate if ($this->window_size_server_to_client[$channel] < 0) { $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size); if (!$this->_send_binary_packet($packet)) { return false; } $this->window_size_server_to_client[$channel]+= $this->window_size; } switch ($type) { case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: /* if ($client_channel == NET_SSH2_CHANNEL_EXEC) { $this->_send_channel_packet($client_channel, chr(0)); } */ // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR if (strlen($response) < 8) { return false; } extract(unpack('Ndata_type_code/Nlength', $this->_string_shift($response, 8))); $data = $this->_string_shift($response, $length); $this->stdErrorLog.= $data; if ($skip_extended || $this->quiet_mode) { continue 2; } if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) { return $data; } if (!isset($this->channel_buffers[$channel])) { $this->channel_buffers[$channel] = array(); } $this->channel_buffers[$channel][] = $data; continue 2; case NET_SSH2_MSG_CHANNEL_REQUEST: if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) { continue 2; } if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $value = $this->_string_shift($response, $length); switch ($value) { case 'exit-signal': $this->_string_shift($response, 1); if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->errors[] = 'SSH_MSG_CHANNEL_REQUEST (exit-signal): ' . $this->_string_shift($response, $length); $this->_string_shift($response, 1); if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); if ($length) { $this->errors[count($this->errors)].= "\r\n" . $this->_string_shift($response, $length); } $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF; continue 3; case 'exit-status': if (strlen($response) < 5) { return false; } extract(unpack('Cfalse/Nexit_status', $this->_string_shift($response, 5))); $this->exit_status = $exit_status; // "The client MAY ignore these messages." // -- http://tools.ietf.org/html/rfc4254#section-6.10 continue 3; default: // "Some systems may not implement signals, in which case they SHOULD ignore this message." // -- http://tools.ietf.org/html/rfc4254#section-6.9 continue 3; } } switch ($this->channel_status[$channel]) { case NET_SSH2_MSG_CHANNEL_OPEN: switch ($type) { case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: if (strlen($response) < 4) { return false; } extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); $this->server_channels[$channel] = $server_channel; if (strlen($response) < 4) { return false; } extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); if ($window_size < 0) { $window_size&= 0x7FFFFFFF; $window_size+= 0x80000000; } $this->window_size_client_to_server[$channel] = $window_size; if (strlen($response) < 4) { return false; } $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; $result = $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); $this->_on_channel_open(); return $result; //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: default: user_error('Unable to open channel'); return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } break; case NET_SSH2_MSG_CHANNEL_REQUEST: switch ($type) { case NET_SSH2_MSG_CHANNEL_SUCCESS: return true; case NET_SSH2_MSG_CHANNEL_FAILURE: return false; default: user_error('Unable to fulfill channel request'); return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } case NET_SSH2_MSG_CHANNEL_CLOSE: return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); } } // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA switch ($type) { case NET_SSH2_MSG_CHANNEL_DATA: /* if ($channel == NET_SSH2_CHANNEL_EXEC) { // SCP requires null packets, such as this, be sent. further, in the case of the ssh.com SSH server // this actually seems to make things twice as fast. more to the point, the message right after // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise. // in OpenSSH it slows things down but only by a couple thousandths of a second. $this->_send_channel_packet($channel, chr(0)); } */ if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $data = $this->_string_shift($response, $length); if ($channel == NET_SSH2_CHANNEL_AGENT_FORWARD) { $agent_response = $this->agent->_forward_data($data); if (!is_bool($agent_response)) { $this->_send_channel_packet($channel, $agent_response); } break; } if ($client_channel == $channel) { return $data; } if (!isset($this->channel_buffers[$channel])) { $this->channel_buffers[$channel] = array(); } $this->channel_buffers[$channel][] = $data; break; case NET_SSH2_MSG_CHANNEL_CLOSE: $this->curTimeout = 0; if ($this->bitmap & NET_SSH2_MASK_SHELL) { $this->bitmap&= ~NET_SSH2_MASK_SHELL; } if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) { $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); } $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE; if ($client_channel == $channel) { return true; } case NET_SSH2_MSG_CHANNEL_EOF: break; default: user_error('Error reading channel data'); return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } } } /** * Sends Binary Packets * * See '6. Binary Packet Protocol' of rfc4253 for more info. * * @param string $data * @param string $logged * @see self::_get_binary_packet() * @return bool * @access private */ function _send_binary_packet($data, $logged = null) { if (!is_resource($this->fsock) || feof($this->fsock)) { $this->bitmap = 0; user_error('Connection closed prematurely'); return false; } //if ($this->compress) { // // the -4 removes the checksum: // // http://php.net/function.gzcompress#57710 // $data = substr(gzcompress($data), 0, -4); //} // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 $packet_length = strlen($data) + 9; // round up to the nearest $this->encrypt_block_size $packet_length+= (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size; // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length $padding_length = $packet_length - strlen($data) - 5; $padding = crypt_random_string($padding_length); // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding); $hmac = $this->hmac_create !== false ? $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)) : ''; $this->send_seq_no++; if ($this->encrypt !== false) { $packet = $this->encrypt->encrypt($packet); } $packet.= $hmac; $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $result = strlen($packet) == fputs($this->fsock, $packet); $stop = strtok(microtime(), ' ') + strtok(''); if (defined('NET_SSH2_LOGGING')) { $current = strtok(microtime(), ' ') + strtok(''); $message_number = isset($this->message_numbers[ord($data[0])]) ? $this->message_numbers[ord($data[0])] : 'UNKNOWN (' . ord($data[0]) . ')'; $message_number = '-> ' . $message_number . ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; $this->_append_log($message_number, isset($logged) ? $logged : $data); $this->last_packet = $current; } return $result; } /** * Logs data packets * * Makes sure that only the last 1MB worth of packets will be logged * * @param string $data * @access private */ function _append_log($message_number, $message) { // remove the byte identifying the message type from all but the first two messages (ie. the identification strings) if (strlen($message_number) > 2) { $this->_string_shift($message); } switch (NET_SSH2_LOGGING) { // useful for benchmarks case NET_SSH2_LOG_SIMPLE: $this->message_number_log[] = $message_number; break; // the most useful log for SSH2 case NET_SSH2_LOG_COMPLEX: $this->message_number_log[] = $message_number; $this->log_size+= strlen($message); $this->message_log[] = $message; while ($this->log_size > NET_SSH2_LOG_MAX_SIZE) { $this->log_size-= strlen(array_shift($this->message_log)); array_shift($this->message_number_log); } break; // dump the output out realtime; packets may be interspersed with non packets, // passwords won't be filtered out and select other packets may not be correctly // identified case NET_SSH2_LOG_REALTIME: switch (PHP_SAPI) { case 'cli': $start = $stop = "\r\n"; break; default: $start = '<pre>'; $stop = '</pre>'; } echo $start . $this->_format_log(array($message), array($message_number)) . $stop; @flush(); @ob_flush(); break; // basically the same thing as NET_SSH2_LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILE // needs to be defined and that the resultant log file will be capped out at NET_SSH2_LOG_MAX_SIZE. // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily // at the beginning of the file case NET_SSH2_LOG_REALTIME_FILE: if (!isset($this->realtime_log_file)) { // PHP doesn't seem to like using constants in fopen() $filename = NET_SSH2_LOG_REALTIME_FILENAME; $fp = fopen($filename, 'w'); $this->realtime_log_file = $fp; } if (!is_resource($this->realtime_log_file)) { break; } $entry = $this->_format_log(array($message), array($message_number)); if ($this->realtime_log_wrap) { $temp = "<<< START >>>\r\n"; $entry.= $temp; fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp)); } $this->realtime_log_size+= strlen($entry); if ($this->realtime_log_size > NET_SSH2_LOG_MAX_SIZE) { fseek($this->realtime_log_file, 0); $this->realtime_log_size = strlen($entry); $this->realtime_log_wrap = true; } fputs($this->realtime_log_file, $entry); } } /** * Sends channel data * * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate * * @param int $client_channel * @param string $data * @return bool * @access private */ function _send_channel_packet($client_channel, $data) { while (strlen($data)) { if (!$this->window_size_client_to_server[$client_channel]) { $this->bitmap^= NET_SSH2_MASK_WINDOW_ADJUST; // using an invalid channel will let the buffers be built up for the valid channels $this->_get_channel_packet(-1); $this->bitmap^= NET_SSH2_MASK_WINDOW_ADJUST; } /* The maximum amount of data allowed is determined by the maximum packet size for the channel, and the current window size, whichever is smaller. -- http://tools.ietf.org/html/rfc4254#section-5.2 */ $max_size = min( $this->packet_size_client_to_server[$client_channel], $this->window_size_client_to_server[$client_channel] ); $temp = $this->_string_shift($data, $max_size); $packet = pack( 'CN2a*', NET_SSH2_MSG_CHANNEL_DATA, $this->server_channels[$client_channel], strlen($temp), $temp ); $this->window_size_client_to_server[$client_channel]-= strlen($temp); if (!$this->_send_binary_packet($packet)) { return false; } } return true; } /** * Closes and flushes a channel * * Net_SSH2 doesn't properly close most channels. For exec() channels are normally closed by the server * and for SFTP channels are presumably closed when the client disconnects. This functions is intended * for SCP more than anything. * * @param int $client_channel * @param bool $want_reply * @return bool * @access private */ function _close_channel($client_channel, $want_reply = false) { // see http://tools.ietf.org/html/rfc4254#section-5.3 $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); if (!$want_reply) { $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); } $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE; $this->curTimeout = 0; while (!is_bool($this->_get_channel_packet($client_channel))) { } if ($want_reply) { $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); } if ($this->bitmap & NET_SSH2_MASK_SHELL) { $this->bitmap&= ~NET_SSH2_MASK_SHELL; } } /** * Disconnect * * @param int $reason * @return bool * @access private */ function _disconnect($reason) { if ($this->bitmap & NET_SSH2_MASK_CONNECTED) { $data = pack('CNNa*Na*', NET_SSH2_MSG_DISCONNECT, $reason, 0, '', 0, ''); $this->_send_binary_packet($data); } $this->bitmap = 0; if (is_resource($this->fsock) && get_resource_type($this->fsock) == 'stream') { fclose($this->fsock); } return false; } /** * String Shift * * Inspired by array_shift * * @param string $string * @param int $index * @return string * @access private */ function _string_shift(&$string, $index = 1) { $substr = substr($string, 0, $index); $string = substr($string, $index); return $substr; } /** * Define Array * * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of * named constants from it, using the value as the name of the constant and the index as the value of the constant. * If any of the constants that would be defined already exists, none of the constants will be defined. * * @param array $array * @access private */ function _define_array() { $args = func_get_args(); foreach ($args as $arg) { foreach ($arg as $key => $value) { if (!defined($value)) { define($value, $key); } else { break 2; } } } } /** * Returns a log of the packets that have been sent and received. * * Returns a string if NET_SSH2_LOGGING == NET_SSH2_LOG_COMPLEX, an array if NET_SSH2_LOGGING == NET_SSH2_LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING') * * @access public * @return array|false|string */ function getLog() { if (!defined('NET_SSH2_LOGGING')) { return false; } switch (NET_SSH2_LOGGING) { case NET_SSH2_LOG_SIMPLE: return $this->message_number_log; case NET_SSH2_LOG_COMPLEX: $log = $this->_format_log($this->message_log, $this->message_number_log); return PHP_SAPI == 'cli' ? $log : '<pre>' . $log . '</pre>'; default: return false; } } /** * Formats a log for printing * * @param array $message_log * @param array $message_number_log * @access private * @return string */ function _format_log($message_log, $message_number_log) { $output = ''; for ($i = 0; $i < count($message_log); $i++) { $output.= $message_number_log[$i] . "\r\n"; $current_log = $message_log[$i]; $j = 0; do { if (strlen($current_log)) { $output.= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; } $fragment = $this->_string_shift($current_log, $this->log_short_width); $hex = substr(preg_replace_callback('#.#s', array($this, '_format_log_helper'), $fragment), strlen($this->log_boundary)); // replace non ASCII printable characters with dots // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters // also replace < with a . since < messes up the output on web browsers $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); $output.= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; $j++; } while (strlen($current_log)); $output.= "\r\n"; } return $output; } /** * Helper function for _format_log * * For use with preg_replace_callback() * * @param array $matches * @access private * @return string */ function _format_log_helper($matches) { return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); } /** * Helper function for agent->_on_channel_open() * * Used when channels are created to inform agent * of said channel opening. Must be called after * channel open confirmation received * * @access private */ function _on_channel_open() { if (isset($this->agent)) { $this->agent->_on_channel_open($this); } } /** * Returns the first value of the intersection of two arrays or false if * the intersection is empty. The order is defined by the first parameter. * * @param array $array1 * @param array $array2 * @return mixed False if intersection is empty, else intersected value. * @access private */ function _array_intersect_first($array1, $array2) { foreach ($array1 as $value) { if (in_array($value, $array2)) { return $value; } } return false; } /** * Returns all errors * * @return string[] * @access public */ function getErrors() { return $this->errors; } /** * Returns the last error * * @return string * @access public */ function getLastError() { $count = count($this->errors); if ($count > 0) { return $this->errors[$count - 1]; } } /** * Return the server identification. * * @return string * @access public */ function getServerIdentification() { $this->_connect(); return $this->server_identifier; } /** * Return a list of the key exchange algorithms the server supports. * * @return array * @access public */ function getKexAlgorithms() { $this->_connect(); return $this->kex_algorithms; } /** * Return a list of the host key (public key) algorithms the server supports. * * @return array * @access public */ function getServerHostKeyAlgorithms() { $this->_connect(); return $this->server_host_key_algorithms; } /** * Return a list of the (symmetric key) encryption algorithms the server supports, when receiving stuff from the client. * * @return array * @access public */ function getEncryptionAlgorithmsClient2Server() { $this->_connect(); return $this->encryption_algorithms_client_to_server; } /** * Return a list of the (symmetric key) encryption algorithms the server supports, when sending stuff to the client. * * @return array * @access public */ function getEncryptionAlgorithmsServer2Client() { $this->_connect(); return $this->encryption_algorithms_server_to_client; } /** * Return a list of the MAC algorithms the server supports, when receiving stuff from the client. * * @return array * @access public */ function getMACAlgorithmsClient2Server() { $this->_connect(); return $this->mac_algorithms_client_to_server; } /** * Return a list of the MAC algorithms the server supports, when sending stuff to the client. * * @return array * @access public */ function getMACAlgorithmsServer2Client() { $this->_connect(); return $this->mac_algorithms_server_to_client; } /** * Return a list of the compression algorithms the server supports, when receiving stuff from the client. * * @return array * @access public */ function getCompressionAlgorithmsClient2Server() { $this->_connect(); return $this->compression_algorithms_client_to_server; } /** * Return a list of the compression algorithms the server supports, when sending stuff to the client. * * @return array * @access public */ function getCompressionAlgorithmsServer2Client() { $this->_connect(); return $this->compression_algorithms_server_to_client; } /** * Return a list of the languages the server supports, when sending stuff to the client. * * @return array * @access public */ function getLanguagesServer2Client() { $this->_connect(); return $this->languages_server_to_client; } /** * Return a list of the languages the server supports, when receiving stuff from the client. * * @return array * @access public */ function getLanguagesClient2Server() { $this->_connect(); return $this->languages_client_to_server; } /** * Returns a list of algorithms the server supports * * @return array * @access public */ public function getServerAlgorithms() { $this->_connect(); return array( 'kex' => $this->kex_algorithms, 'hostkey' => $this->server_host_key_algorithms, 'client_to_server' => array( 'crypt' => $this->encryption_algorithms_client_to_server, 'mac' => $this->mac_algorithms_client_to_server, 'comp' => $this->compression_algorithms_client_to_server, 'lang' => $this->languages_client_to_server ), 'server_to_client' => array( 'crypt' => $this->encryption_algorithms_server_to_client, 'mac' => $this->mac_algorithms_server_to_client, 'comp' => $this->compression_algorithms_server_to_client, 'lang' => $this->languages_server_to_client ) ); } /** * Returns a list of KEX algorithms that phpseclib supports * * @return array * @access public */ function getSupportedKEXAlgorithms() { $kex_algorithms = array( 'diffie-hellman-group-exchange-sha256',// RFC 4419 'diffie-hellman-group-exchange-sha1', // RFC 4419 // Diffie-Hellman Key Agreement (DH) using integer modulo prime // groups. 'diffie-hellman-group14-sha1', // REQUIRED 'diffie-hellman-group1-sha1', // REQUIRED ); return $kex_algorithms; } /** * Returns a list of host key algorithms that phpseclib supports * * @return array * @access public */ function getSupportedHostKeyAlgorithms() { return array( 'rsa-sha2-256', // RFC 8332 'rsa-sha2-512', // RFC 8332 'ssh-rsa', // RECOMMENDED sign Raw RSA Key 'ssh-dss' // REQUIRED sign Raw DSS Key ); } /** * Returns a list of symmetric key algorithms that phpseclib supports * * @return array * @access public */ function getSupportedEncryptionAlgorithms() { $algos = array( // from <http://tools.ietf.org/html/rfc4345#section-4>: 'arcfour256', 'arcfour128', //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key // CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>: 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key 'aes192-ctr', // RECOMMENDED AES with 192-bit key 'aes256-ctr', // RECOMMENDED AES with 256-bit key 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key 'aes128-cbc', // RECOMMENDED AES with a 128-bit key 'aes192-cbc', // OPTIONAL AES with a 192-bit key 'aes256-cbc', // OPTIONAL AES in CBC mode, with a 256-bit key 'twofish128-cbc', // OPTIONAL Twofish with a 128-bit key 'twofish192-cbc', // OPTIONAL Twofish with a 192-bit key 'twofish256-cbc', 'twofish-cbc', // OPTIONAL alias for "twofish256-cbc" // (this is being retained for historical reasons) 'blowfish-ctr', // OPTIONAL Blowfish in SDCTR mode 'blowfish-cbc', // OPTIONAL Blowfish in CBC mode '3des-ctr', // RECOMMENDED Three-key 3DES in SDCTR mode '3des-cbc', // REQUIRED three-key 3DES in CBC mode //'none' // OPTIONAL no encryption; NOT RECOMMENDED ); $engines = array( CRYPT_ENGINE_OPENSSL, CRYPT_ENGINE_MCRYPT, CRYPT_ENGINE_INTERNAL ); $ciphers = array(); foreach ($engines as $engine) { foreach ($algos as $algo) { $obj = $this->_encryption_algorithm_to_crypt_instance($algo); if (strtolower(get_class($obj)) == 'crypt_rijndael') { $obj->setKeyLength(preg_replace('#[^\d]#', '', $algo)); } switch ($algo) { case 'arcfour128': case 'arcfour256': if ($engine == CRYPT_ENGINE_INTERNAL) { $algos = array_diff($algos, array($algo)); $ciphers[] = $algo; } else { continue 2; } } if ($obj->isValidEngine($engine)) { $algos = array_diff($algos, array($algo)); $ciphers[] = $algo; } } } return $ciphers; } /** * Returns a list of MAC algorithms that phpseclib supports * * @return array * @access public */ function getSupportedMACAlgorithms() { return array( // from <http://www.ietf.org/rfc/rfc6668.txt>: 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) 'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20) 'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20) 'hmac-md5-96', // OPTIONAL first 96 bits of HMAC-MD5 (digest length = 12, key length = 16) 'hmac-md5', // OPTIONAL HMAC-MD5 (digest length = key length = 16) //'none' // OPTIONAL no MAC; NOT RECOMMENDED ); } /** * Returns a list of compression algorithms that phpseclib supports * * @return array * @access public */ function getSupportedCompressionAlgorithms() { return array( 'none' // REQUIRED no compression //'zlib' // OPTIONAL ZLIB (LZ77) compression ); } /** * Return list of negotiated algorithms * * Uses the same format as https://www.php.net/ssh2-methods-negotiated * * @return array * @access public */ function getAlgorithmsNegotiated() { $this->_connect(); return array( 'kex' => $this->kex_algorithm, 'hostkey' => $this->signature_format, 'client_to_server' => array( 'crypt' => $this->encrypt->name, 'mac' => $this->hmac_create->name, 'comp' => 'none', ), 'server_to_client' => array( 'crypt' => $this->decrypt->name, 'mac' => $this->hmac_check->name, 'comp' => 'none', ) ); } /** * Accepts an associative array with up to four parameters as described at * <https://www.php.net/manual/en/function.ssh2-connect.php> * * @param array $methods * @access public */ function setPreferredAlgorithms($methods) { $preferred = $methods; if (isset($preferred['kex'])) { $preferred['kex'] = array_intersect( $preferred['kex'], $this->getSupportedKEXAlgorithms() ); } if (isset($preferred['hostkey'])) { $preferred['hostkey'] = array_intersect( $preferred['hostkey'], $this->getSupportedHostKeyAlgorithms() ); } $keys = array('client_to_server', 'server_to_client'); foreach ($keys as $key) { if (isset($preferred[$key])) { $a = &$preferred[$key]; if (isset($a['crypt'])) { $a['crypt'] = array_intersect( $a['crypt'], $this->getSupportedEncryptionAlgorithms() ); } if (isset($a['comp'])) { $a['comp'] = array_intersect( $a['comp'], $this->getSupportedCompressionAlgorithms() ); } if (isset($a['mac'])) { $a['mac'] = array_intersect( $a['mac'], $this->getSupportedMACAlgorithms() ); } } } $keys = array( 'kex', 'hostkey', 'client_to_server/crypt', 'client_to_server/comp', 'client_to_server/mac', 'server_to_client/crypt', 'server_to_client/comp', 'server_to_client/mac', ); foreach ($keys as $key) { $p = $preferred; $m = $methods; $subkeys = explode('/', $key); foreach ($subkeys as $subkey) { if (!isset($p[$subkey])) { continue 2; } $p = $p[$subkey]; $m = $m[$subkey]; } if (count($p) != count($m)) { $diff = array_diff($m, $p); $msg = count($diff) == 1 ? ' is not a supported algorithm' : ' are not supported algorithms'; user_error(implode(', ', $diff) . $msg); return false; } } $this->preferred = $preferred; } /** * Returns the banner message. * * Quoting from the RFC, "in some jurisdictions, sending a warning message before * authentication may be relevant for getting legal protection." * * @return string * @access public */ function getBannerMessage() { return $this->banner_message; } /** * Returns the server public host key. * * Caching this the first time you connect to a server and checking the result on subsequent connections * is recommended. Returns false if the server signature is not signed correctly with the public host key. * * @return mixed * @access public */ function getServerPublicHostKey() { if (!($this->bitmap & NET_SSH2_MASK_CONSTRUCTOR)) { if (!$this->_connect()) { return false; } } $signature = $this->signature; $server_public_host_key = $this->server_public_host_key; if (strlen($server_public_host_key) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($server_public_host_key, 4))); $this->_string_shift($server_public_host_key, $length); if ($this->signature_validated) { return $this->bitmap ? $this->signature_format . ' ' . base64_encode($this->server_public_host_key) : false; } $this->signature_validated = true; switch ($this->signature_format) { case 'ssh-dss': $zero = new Math_BigInteger(); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $p = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $q = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $g = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $y = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); /* The value for 'dss_signature_blob' is encoded as a string containing r, followed by s (which are 160-bit integers, without lengths or padding, unsigned, and in network byte order). */ $temp = unpack('Nlength', $this->_string_shift($signature, 4)); if ($temp['length'] != 40) { user_error('Invalid signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $r = new Math_BigInteger($this->_string_shift($signature, 20), 256); $s = new Math_BigInteger($this->_string_shift($signature, 20), 256); switch (true) { case $r->equals($zero): case $r->compare($q) >= 0: case $s->equals($zero): case $s->compare($q) >= 0: user_error('Invalid signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $w = $s->modInverse($q); $u1 = $w->multiply(new Math_BigInteger(sha1($this->exchange_hash), 16)); list(, $u1) = $u1->divide($q); $u2 = $w->multiply($r); list(, $u2) = $u2->divide($q); $g = $g->modPow($u1, $p); $y = $y->modPow($u2, $p); $v = $g->multiply($y); list(, $v) = $v->divide($p); list(, $v) = $v->divide($q); if (!$v->equals($r)) { user_error('Bad server signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); } break; case 'ssh-rsa': case 'rsa-sha2-256': case 'rsa-sha2-512': if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $e = new Math_BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); if (strlen($server_public_host_key) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); $rawN = $this->_string_shift($server_public_host_key, $temp['length']); $n = new Math_BigInteger($rawN, -256); $nLength = strlen(ltrim($rawN, "\0")); /* if (strlen($signature) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($signature, 4)); $signature = $this->_string_shift($signature, $temp['length']); if (!class_exists('Crypt_RSA')) { include_once 'Crypt/RSA.php'; } $rsa = new Crypt_RSA(); switch ($this->signature_format) { case 'rsa-sha2-512': $hash = 'sha512'; break; case 'rsa-sha2-256': $hash = 'sha256'; break; //case 'ssh-rsa': default: $hash = 'sha1'; } $rsa->setHash($hash); $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1); $rsa->loadKey(array('e' => $e, 'n' => $n), CRYPT_RSA_PUBLIC_FORMAT_RAW); if (!$rsa->verify($this->exchange_hash, $signature)) { user_error('Bad server signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); } */ if (strlen($signature) < 4) { return false; } $temp = unpack('Nlength', $this->_string_shift($signature, 4)); $s = new Math_BigInteger($this->_string_shift($signature, $temp['length']), 256); // validate an RSA signature per "8.2 RSASSA-PKCS1-v1_5", "5.2.2 RSAVP1", and "9.1 EMSA-PSS" in the // following URL: // ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf // also, see SSHRSA.c (rsa2_verifysig) in PuTTy's source. if ($s->compare(new Math_BigInteger()) < 0 || $s->compare($n->subtract(new Math_BigInteger(1))) > 0) { user_error('Invalid signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } $s = $s->modPow($e, $n); $s = $s->toBytes(); switch ($this->signature_format) { case 'rsa-sha2-512': $hash = 'sha512'; break; case 'rsa-sha2-256': $hash = 'sha256'; break; //case 'ssh-rsa': default: $hash = 'sha1'; } $hashObj = new Crypt_Hash($hash); switch ($this->signature_format) { case 'rsa-sha2-512': $h = pack('N5a*', 0x00305130, 0x0D060960, 0x86480165, 0x03040203, 0x05000440, $hashObj->hash($this->exchange_hash)); break; case 'rsa-sha2-256': $h = pack('N5a*', 0x00303130, 0x0D060960, 0x86480165, 0x03040201, 0x05000420, $hashObj->hash($this->exchange_hash)); break; //case 'ssh-rsa': default: $hash = 'sha1'; $h = pack('N4a*', 0x00302130, 0x0906052B, 0x0E03021A, 0x05000414, $hashObj->hash($this->exchange_hash)); } $h = chr(0x01) . str_repeat(chr(0xFF), $nLength - 2 - strlen($h)) . $h; if ($s != $h) { user_error('Bad server signature'); return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); } break; default: user_error('Unsupported signature format'); return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); } return $this->signature_format . ' ' . base64_encode($this->server_public_host_key); } /** * Returns the exit status of an SSH command or false. * * @return false|int * @access public */ function getExitStatus() { if (is_null($this->exit_status)) { return false; } return $this->exit_status; } /** * Returns the number of columns for the terminal window size. * * @return int * @access public */ function getWindowColumns() { return $this->windowColumns; } /** * Returns the number of rows for the terminal window size. * * @return int * @access public */ function getWindowRows() { return $this->windowRows; } /** * Sets the number of columns for the terminal window size. * * @param int $value * @access public */ function setWindowColumns($value) { $this->windowColumns = $value; } /** * Sets the number of rows for the terminal window size. * * @param int $value * @access public */ function setWindowRows($value) { $this->windowRows = $value; } /** * Sets the number of columns and rows for the terminal window size. * * @param int $columns * @param int $rows * @access public */ function setWindowSize($columns = 80, $rows = 24) { $this->windowColumns = $columns; $this->windowRows = $rows; } } PK G�l[#0�O] O] SFTP/Stream.phpnu �[��� <?php /** * SFTP Stream Wrapper * * Creates an sftp:// protocol handler that can be used with, for example, fopen(), dir(), etc. * * PHP version 5 * * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @category Net * @package Net_SFTP_Stream * @author Jim Wigginton <terrafrost@php.net> * @copyright 2013 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ /** * SFTP Stream Wrapper * * @package Net_SFTP_Stream * @author Jim Wigginton <terrafrost@php.net> * @access public */ class Net_SFTP_Stream { /** * SFTP instances * * Rather than re-create the connection we re-use instances if possible * * @var array */ static $instances; /** * SFTP instance * * @var object * @access private */ var $sftp; /** * Path * * @var string * @access private */ var $path; /** * Mode * * @var string * @access private */ var $mode; /** * Position * * @var int * @access private */ var $pos; /** * Size * * @var int * @access private */ var $size; /** * Directory entries * * @var array * @access private */ var $entries; /** * EOF flag * * @var bool * @access private */ var $eof; /** * Context resource * * Technically this needs to be publically accessible so PHP can set it directly * * @var resource * @access public */ var $context; /** * Notification callback function * * @var callable * @access public */ var $notification; /** * Registers this class as a URL wrapper. * * @param string $protocol The wrapper name to be registered. * @return bool True on success, false otherwise. * @access public */ static function register($protocol = 'sftp') { if (in_array($protocol, stream_get_wrappers(), true)) { return false; } $class = function_exists('get_called_class') ? get_called_class() : __CLASS__; return stream_wrapper_register($protocol, $class); } /** * The Constructor * * @access public */ function __construct() { if (defined('NET_SFTP_STREAM_LOGGING')) { echo "__construct()\r\n"; } if (!class_exists('Net_SFTP')) { include_once 'Net/SFTP.php'; } } /** * Path Parser * * Extract a path from a URI and actually connect to an SSH server if appropriate * * If "notification" is set as a context parameter the message code for successful login is * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE. * * @param string $path * @return string * @access private */ function _parse_path($path) { $orig = $path; extract(parse_url($path) + array('port' => 22)); if (isset($query)) { $path.= '?' . $query; } elseif (preg_match('/(\?|\?#)$/', $orig)) { $path.= '?'; } if (isset($fragment)) { $path.= '#' . $fragment; } elseif ($orig[strlen($orig) - 1] == '#') { $path.= '#'; } if (!isset($host)) { return false; } if (isset($this->context)) { $context = stream_context_get_params($this->context); if (isset($context['notification'])) { $this->notification = $context['notification']; } } if ($host[0] == '$') { $host = substr($host, 1); global ${$host}; if (!is_object($$host) || get_class($$host) != 'Net_SFTP') { return false; } $this->sftp = $$host; } else { if (isset($this->context)) { $context = stream_context_get_options($this->context); } if (isset($context[$scheme]['session'])) { $sftp = $context[$scheme]['session']; } if (isset($context[$scheme]['sftp'])) { $sftp = $context[$scheme]['sftp']; } if (isset($sftp) && is_object($sftp) && get_class($sftp) == 'Net_SFTP') { $this->sftp = $sftp; return $path; } if (isset($context[$scheme]['username'])) { $user = $context[$scheme]['username']; } if (isset($context[$scheme]['password'])) { $pass = $context[$scheme]['password']; } if (isset($context[$scheme]['privkey']) && is_object($context[$scheme]['privkey']) && get_Class($context[$scheme]['privkey']) == 'Crypt_RSA') { $pass = $context[$scheme]['privkey']; } if (!isset($user) || !isset($pass)) { return false; } // casting $pass to a string is necessary in the event that it's a Crypt_RSA object if (isset(self::$instances[$host][$port][$user][(string) $pass])) { $this->sftp = self::$instances[$host][$port][$user][(string) $pass]; } else { $this->sftp = new Net_SFTP($host, $port); $this->sftp->disableStatCache(); if (isset($this->notification) && is_callable($this->notification)) { /* if !is_callable($this->notification) we could do this: user_error('fopen(): failed to call user notifier', E_USER_WARNING); the ftp wrapper gives errors like that when the notifier isn't callable. i've opted not to do that, however, since the ftp wrapper gives the line on which the fopen occurred as the line number - not the line that the user_error is on. */ call_user_func($this->notification, STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); call_user_func($this->notification, STREAM_NOTIFY_AUTH_REQUIRED, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); if (!$this->sftp->login($user, $pass)) { call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_ERR, 'Login Failure', NET_SSH2_MSG_USERAUTH_FAILURE, 0, 0); return false; } call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_INFO, 'Login Success', NET_SSH2_MSG_USERAUTH_SUCCESS, 0, 0); } else { if (!$this->sftp->login($user, $pass)) { return false; } } self::$instances[$host][$port][$user][(string) $pass] = $this->sftp; } } return $path; } /** * Opens file or URL * * @param string $path * @param string $mode * @param int $options * @param string $opened_path * @return bool * @access public */ function _stream_open($path, $mode, $options, &$opened_path) { $path = $this->_parse_path($path); if ($path === false) { return false; } $this->path = $path; $this->size = $this->sftp->size($path); $this->mode = preg_replace('#[bt]$#', '', $mode); $this->eof = false; if ($this->size === false) { if ($this->mode[0] == 'r') { return false; } else { $this->sftp->touch($path); $this->size = 0; } } else { switch ($this->mode[0]) { case 'x': return false; case 'w': $this->sftp->truncate($path, 0); $this->size = 0; } } $this->pos = $this->mode[0] != 'a' ? 0 : $this->size; return true; } /** * Read from stream * * @param int $count * @return mixed * @access public */ function _stream_read($count) { switch ($this->mode) { case 'w': case 'a': case 'x': case 'c': return false; } // commented out because some files - eg. /dev/urandom - will say their size is 0 when in fact it's kinda infinite //if ($this->pos >= $this->size) { // $this->eof = true; // return false; //} $result = $this->sftp->get($this->path, false, $this->pos, $count); if (isset($this->notification) && is_callable($this->notification)) { if ($result === false) { call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); return 0; } // seems that PHP calls stream_read in 8k chunks call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($result), $this->size); } if (empty($result)) { // ie. false or empty string $this->eof = true; return false; } $this->pos+= strlen($result); return $result; } /** * Write to stream * * @param string $data * @return mixed * @access public */ function _stream_write($data) { switch ($this->mode) { case 'r': return false; } $result = $this->sftp->put($this->path, $data, NET_SFTP_STRING, $this->pos); if (isset($this->notification) && is_callable($this->notification)) { if (!$result) { call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); return 0; } // seems that PHP splits up strings into 8k blocks before calling stream_write call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($data), strlen($data)); } if ($result === false) { return false; } $this->pos+= strlen($data); if ($this->pos > $this->size) { $this->size = $this->pos; } $this->eof = false; return strlen($data); } /** * Retrieve the current position of a stream * * @return int * @access public */ function _stream_tell() { return $this->pos; } /** * Tests for end-of-file on a file pointer * * In my testing there are four classes functions that normally effect the pointer: * fseek, fputs / fwrite, fgets / fread and ftruncate. * * Only fgets / fread, however, results in feof() returning true. do fputs($fp, 'aaa') on a blank file and feof() * will return false. do fread($fp, 1) and feof() will then return true. do fseek($fp, 10) on ablank file and feof() * will return false. do fread($fp, 1) and feof() will then return true. * * @return bool * @access public */ function _stream_eof() { return $this->eof; } /** * Seeks to specific location in a stream * * @param int $offset * @param int $whence * @return bool * @access public */ function _stream_seek($offset, $whence) { switch ($whence) { case SEEK_SET: if ($offset >= $this->size || $offset < 0) { return false; } break; case SEEK_CUR: $offset+= $this->pos; break; case SEEK_END: $offset+= $this->size; } $this->pos = $offset; $this->eof = false; return true; } /** * Change stream options * * @param string $path * @param int $option * @param mixed $var * @return bool * @access public */ function _stream_metadata($path, $option, $var) { $path = $this->_parse_path($path); if ($path === false) { return false; } // stream_metadata was introduced in PHP 5.4.0 but as of 5.4.11 the constants haven't been defined // see http://www.php.net/streamwrapper.stream-metadata and https://bugs.php.net/64246 // and https://github.com/php/php-src/blob/master/main/php_streams.h#L592 switch ($option) { case 1: // PHP_STREAM_META_TOUCH return $this->sftp->touch($path, $var[0], $var[1]); case 2: // PHP_STREAM_OWNER_NAME case 3: // PHP_STREAM_GROUP_NAME return false; case 4: // PHP_STREAM_META_OWNER return $this->sftp->chown($path, $var); case 5: // PHP_STREAM_META_GROUP return $this->sftp->chgrp($path, $var); case 6: // PHP_STREAM_META_ACCESS return $this->sftp->chmod($path, $var) !== false; } } /** * Retrieve the underlaying resource * * @param int $cast_as * @return resource * @access public */ function _stream_cast($cast_as) { return $this->sftp->fsock; } /** * Advisory file locking * * @param int $operation * @return bool * @access public */ function _stream_lock($operation) { return false; } /** * Renames a file or directory * * Attempts to rename oldname to newname, moving it between directories if necessary. * If newname exists, it will be overwritten. This is a departure from what Net_SFTP * does. * * @param string $path_from * @param string $path_to * @return bool * @access public */ function _rename($path_from, $path_to) { $path1 = parse_url($path_from); $path2 = parse_url($path_to); unset($path1['path'], $path2['path']); if ($path1 != $path2) { return false; } $path_from = $this->_parse_path($path_from); $path_to = parse_url($path_to); if ($path_from === false) { return false; } $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2 // "It is an error if there already exists a file with the name specified by newpath." // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5 if (!$this->sftp->rename($path_from, $path_to)) { if ($this->sftp->stat($path_to)) { return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to); } return false; } return true; } /** * Open directory handle * * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and * removed in 5.4 I'm just going to ignore it. * * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting * the SFTP specs: * * The SSH_FXP_NAME response has the following format: * * uint32 id * uint32 count * repeats count times: * string filename * string longname * ATTRS attrs * * @param string $path * @param int $options * @return bool * @access public */ function _dir_opendir($path, $options) { $path = $this->_parse_path($path); if ($path === false) { return false; } $this->pos = 0; $this->entries = $this->sftp->nlist($path); return $this->entries !== false; } /** * Read entry from directory handle * * @return mixed * @access public */ function _dir_readdir() { if (isset($this->entries[$this->pos])) { return $this->entries[$this->pos++]; } return false; } /** * Rewind directory handle * * @return bool * @access public */ function _dir_rewinddir() { $this->pos = 0; return true; } /** * Close directory handle * * @return bool * @access public */ function _dir_closedir() { return true; } /** * Create a directory * * Only valid $options is STREAM_MKDIR_RECURSIVE * * @param string $path * @param int $mode * @param int $options * @return bool * @access public */ function _mkdir($path, $mode, $options) { $path = $this->_parse_path($path); if ($path === false) { return false; } return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE); } /** * Removes a directory * * Only valid $options is STREAM_MKDIR_RECURSIVE per <http://php.net/streamwrapper.rmdir>, however, * <http://php.net/rmdir> does not have a $recursive parameter as mkdir() does so I don't know how * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as * $options. What does 8 correspond to? * * @param string $path * @param int $mode * @param int $options * @return bool * @access public */ function _rmdir($path, $options) { $path = $this->_parse_path($path); if ($path === false) { return false; } return $this->sftp->rmdir($path); } /** * Flushes the output * * See <http://php.net/fflush>. Always returns true because Net_SFTP doesn't cache stuff before writing * * @return bool * @access public */ function _stream_flush() { return true; } /** * Retrieve information about a file resource * * @return mixed * @access public */ function _stream_stat() { $results = $this->sftp->stat($this->path); if ($results === false) { return false; } return $results; } /** * Delete a file * * @param string $path * @return bool * @access public */ function _unlink($path) { $path = $this->_parse_path($path); if ($path === false) { return false; } return $this->sftp->delete($path, false); } /** * Retrieve information about a file * * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of Net_SFTP_Stream is quiet by default * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll * cross that bridge when and if it's reached * * @param string $path * @param int $flags * @return mixed * @access public */ function _url_stat($path, $flags) { $path = $this->_parse_path($path); if ($path === false) { return false; } $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path); if ($results === false) { return false; } return $results; } /** * Truncate stream * * @param int $new_size * @return bool * @access public */ function _stream_truncate($new_size) { if (!$this->sftp->truncate($this->path, $new_size)) { return false; } $this->eof = false; $this->size = $new_size; return true; } /** * Change stream options * * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't. * The other two aren't supported because of limitations in Net_SFTP. * * @param int $option * @param int $arg1 * @param int $arg2 * @return bool * @access public */ function _stream_set_option($option, $arg1, $arg2) { return false; } /** * Close an resource * * @access public */ function _stream_close() { } /** * __call Magic Method * * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you. * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function * lets you figure that out. * * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method. * * @param string * @param array * @return mixed * @access public */ function __call($name, $arguments) { if (defined('NET_SFTP_STREAM_LOGGING')) { echo $name . '('; $last = count($arguments) - 1; foreach ($arguments as $i => $argument) { var_export($argument); if ($i != $last) { echo ','; } } echo ")\r\n"; } $name = '_' . $name; if (!method_exists($this, $name)) { return false; } return call_user_func_array(array($this, $name), $arguments); } } Net_SFTP_Stream::register(); PK G�l[ �ǩ� �� SFTP.phpnu �[��� <?php /** * Pure-PHP implementation of SFTP. * * PHP versions 4 and 5 * * Currently only supports SFTPv2 and v3, which, according to wikipedia.org, "is the most widely used version, * implemented by the popular OpenSSH SFTP server". If you want SFTPv4/5/6 support, provide me with access * to an SFTPv4/5/6 server. * * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}. * * Here's a short example of how to use this library: * <code> * <?php * include 'Net/SFTP.php'; * * $sftp = new Net_SFTP('www.domain.tld'); * if (!$sftp->login('username', 'password')) { * exit('Login Failed'); * } * * echo $sftp->pwd() . "\r\n"; * $sftp->put('filename.ext', 'hello, world!'); * print_r($sftp->nlist()); * ?> * </code> * * LICENSE: Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @category Net * @package Net_SFTP * @author Jim Wigginton <terrafrost@php.net> * @copyright 2009 Jim Wigginton * @license http://www.opensource.org/licenses/mit-license.html MIT License * @link http://phpseclib.sourceforge.net */ /** * Include Net_SSH2 */ if (!class_exists('Net_SSH2')) { include_once 'SSH2.php'; } /**#@+ * @access public * @see self::getLog() */ /** * Returns the message numbers */ define('NET_SFTP_LOG_SIMPLE', NET_SSH2_LOG_SIMPLE); /** * Returns the message content */ define('NET_SFTP_LOG_COMPLEX', NET_SSH2_LOG_COMPLEX); /** * Outputs the message content in real-time. */ define('NET_SFTP_LOG_REALTIME', 3); /**#@-*/ /** * SFTP channel constant * * Net_SSH2::exec() uses 0 and Net_SSH2::read() / Net_SSH2::write() use 1. * * @see Net_SSH2::_send_channel_packet() * @see Net_SSH2::_get_channel_packet() * @access private */ define('NET_SFTP_CHANNEL', 0x100); /**#@+ * @access public * @see self::put() */ /** * Reads data from a local file. */ define('NET_SFTP_LOCAL_FILE', 1); /** * Reads data from a string. */ // this value isn't really used anymore but i'm keeping it reserved for historical reasons define('NET_SFTP_STRING', 2); /** * Reads data from callback: * function callback($length) returns string to proceed, null for EOF */ define('NET_SFTP_CALLBACK', 16); /** * Resumes an upload */ define('NET_SFTP_RESUME', 4); /** * Append a local file to an already existing remote file */ define('NET_SFTP_RESUME_START', 8); /**#@-*/ /** * Pure-PHP implementations of SFTP. * * @package Net_SFTP * @author Jim Wigginton <terrafrost@php.net> * @access public */ class Net_SFTP extends Net_SSH2 { /** * Packet Types * * @see self::Net_SFTP() * @var array * @access private */ var $packet_types = array(); /** * Status Codes * * @see self::Net_SFTP() * @var array * @access private */ var $status_codes = array(); /** * The Request ID * * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support * concurrent actions, so it's somewhat academic, here. * * @var boolean * @see self::_send_sftp_packet() * @access private */ var $use_request_id = false; /** * The Packet Type * * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support * concurrent actions, so it's somewhat academic, here. * * @var int * @see self::_get_sftp_packet() * @access private */ var $packet_type = -1; /** * Packet Buffer * * @var string * @see self::_get_sftp_packet() * @access private */ var $packet_buffer = ''; /** * Extensions supported by the server * * @var array * @see self::_initChannel() * @access private */ var $extensions = array(); /** * Server SFTP version * * @var int * @see self::_initChannel() * @access private */ var $version; /** * Current working directory * * @var string * @see self::realpath() * @see self::chdir() * @access private */ var $pwd = false; /** * Packet Type Log * * @see self::getLog() * @var array * @access private */ var $packet_type_log = array(); /** * Packet Log * * @see self::getLog() * @var array * @access private */ var $packet_log = array(); /** * Error information * * @see self::getSFTPErrors() * @see self::getLastSFTPError() * @var array * @access private */ var $sftp_errors = array(); /** * Stat Cache * * Rather than always having to open a directory and close it immediately there after to see if a file is a directory * we'll cache the results. * * @see self::_update_stat_cache() * @see self::_remove_from_stat_cache() * @see self::_query_stat_cache() * @var array * @access private */ var $stat_cache = array(); /** * Max SFTP Packet Size * * @see self::Net_SFTP() * @see self::get() * @var array * @access private */ var $max_sftp_packet; /** * Stat Cache Flag * * @see self::disableStatCache() * @see self::enableStatCache() * @var bool * @access private */ var $use_stat_cache = true; /** * Sort Options * * @see self::_comparator() * @see self::setListOrder() * @var array * @access private */ var $sortOptions = array(); /** * Canonicalization Flag * * Determines whether or not paths should be canonicalized before being * passed on to the remote server. * * @see self::enablePathCanonicalization() * @see self::disablePathCanonicalization() * @see self::realpath() * @var bool * @access private */ var $canonicalize_paths = true; /** * Request Buffers * * @see self::_get_sftp_packet() * @var array * @access private */ var $requestBuffer = array(); /** * Default Constructor. * * Connects to an SFTP server * * @param string $host * @param int $port * @param int $timeout * @return Net_SFTP * @access public */ function __construct($host, $port = 22, $timeout = 10) { parent::__construct($host, $port, $timeout); $this->max_sftp_packet = 1 << 15; $this->packet_types = array( 1 => 'NET_SFTP_INIT', 2 => 'NET_SFTP_VERSION', /* the format of SSH_FXP_OPEN changed between SFTPv4 and SFTPv5+: SFTPv5+: http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.1 pre-SFTPv5 : http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3 */ 3 => 'NET_SFTP_OPEN', 4 => 'NET_SFTP_CLOSE', 5 => 'NET_SFTP_READ', 6 => 'NET_SFTP_WRITE', 7 => 'NET_SFTP_LSTAT', 9 => 'NET_SFTP_SETSTAT', 11 => 'NET_SFTP_OPENDIR', 12 => 'NET_SFTP_READDIR', 13 => 'NET_SFTP_REMOVE', 14 => 'NET_SFTP_MKDIR', 15 => 'NET_SFTP_RMDIR', 16 => 'NET_SFTP_REALPATH', 17 => 'NET_SFTP_STAT', /* the format of SSH_FXP_RENAME changed between SFTPv4 and SFTPv5+: SFTPv5+: http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 pre-SFTPv5 : http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.5 */ 18 => 'NET_SFTP_RENAME', 19 => 'NET_SFTP_READLINK', 20 => 'NET_SFTP_SYMLINK', 101=> 'NET_SFTP_STATUS', 102=> 'NET_SFTP_HANDLE', /* the format of SSH_FXP_NAME changed between SFTPv3 and SFTPv4+: SFTPv4+: http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4 pre-SFTPv4 : http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7 */ 103=> 'NET_SFTP_DATA', 104=> 'NET_SFTP_NAME', 105=> 'NET_SFTP_ATTRS', 200=> 'NET_SFTP_EXTENDED' ); $this->status_codes = array( 0 => 'NET_SFTP_STATUS_OK', 1 => 'NET_SFTP_STATUS_EOF', 2 => 'NET_SFTP_STATUS_NO_SUCH_FILE', 3 => 'NET_SFTP_STATUS_PERMISSION_DENIED', 4 => 'NET_SFTP_STATUS_FAILURE', 5 => 'NET_SFTP_STATUS_BAD_MESSAGE', 6 => 'NET_SFTP_STATUS_NO_CONNECTION', 7 => 'NET_SFTP_STATUS_CONNECTION_LOST', 8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED', 9 => 'NET_SFTP_STATUS_INVALID_HANDLE', 10 => 'NET_SFTP_STATUS_NO_SUCH_PATH', 11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS', 12 => 'NET_SFTP_STATUS_WRITE_PROTECT', 13 => 'NET_SFTP_STATUS_NO_MEDIA', 14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM', 15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED', 16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL', 17 => 'NET_SFTP_STATUS_LOCK_CONFLICT', 18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY', 19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY', 20 => 'NET_SFTP_STATUS_INVALID_FILENAME', 21 => 'NET_SFTP_STATUS_LINK_LOOP', 22 => 'NET_SFTP_STATUS_CANNOT_DELETE', 23 => 'NET_SFTP_STATUS_INVALID_PARAMETER', 24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY', 25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT', 26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED', 27 => 'NET_SFTP_STATUS_DELETE_PENDING', 28 => 'NET_SFTP_STATUS_FILE_CORRUPT', 29 => 'NET_SFTP_STATUS_OWNER_INVALID', 30 => 'NET_SFTP_STATUS_GROUP_INVALID', 31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK' ); // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1 // the order, in this case, matters quite a lot - see Net_SFTP::_parseAttributes() to understand why $this->attributes = array( 0x00000001 => 'NET_SFTP_ATTR_SIZE', 0x00000002 => 'NET_SFTP_ATTR_UIDGID', // defined in SFTPv3, removed in SFTPv4+ 0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS', 0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME', // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers // yields inconsistent behavior depending on how php is compiled. so we left shift -1 (which, in // two's compliment, consists of all 1 bits) by 31. on 64-bit systems this'll yield 0xFFFFFFFF80000000. // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored. (-1 << 31) & 0xFFFFFFFF => 'NET_SFTP_ATTR_EXTENDED' ); // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3 // the flag definitions change somewhat in SFTPv5+. if SFTPv5+ support is added to this library, maybe name // the array for that $this->open5_flags and similarly alter the constant names. $this->open_flags = array( 0x00000001 => 'NET_SFTP_OPEN_READ', 0x00000002 => 'NET_SFTP_OPEN_WRITE', 0x00000004 => 'NET_SFTP_OPEN_APPEND', 0x00000008 => 'NET_SFTP_OPEN_CREATE', 0x00000010 => 'NET_SFTP_OPEN_TRUNCATE', 0x00000020 => 'NET_SFTP_OPEN_EXCL' ); // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 // see Net_SFTP::_parseLongname() for an explanation $this->file_types = array( 1 => 'NET_SFTP_TYPE_REGULAR', 2 => 'NET_SFTP_TYPE_DIRECTORY', 3 => 'NET_SFTP_TYPE_SYMLINK', 4 => 'NET_SFTP_TYPE_SPECIAL', 5 => 'NET_SFTP_TYPE_UNKNOWN', // the followin types were first defined for use in SFTPv5+ // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 6 => 'NET_SFTP_TYPE_SOCKET', 7 => 'NET_SFTP_TYPE_CHAR_DEVICE', 8 => 'NET_SFTP_TYPE_BLOCK_DEVICE', 9 => 'NET_SFTP_TYPE_FIFO' ); $this->_define_array( $this->packet_types, $this->status_codes, $this->attributes, $this->open_flags, $this->file_types ); if (!defined('NET_SFTP_QUEUE_SIZE')) { define('NET_SFTP_QUEUE_SIZE', 32); } } /** * PHP4 compatible Default Constructor. * * @see self::__construct() * @param string $host * @param int $port * @param int $timeout * @access public */ function Net_SFTP($host, $port = 22, $timeout = 10) { $this->__construct($host, $port, $timeout); } /** * Login * * @param string $username * @param string $password * @return bool * @access public */ function login($username) { $args = func_get_args(); $this->auth[] = $args; if (!call_user_func_array(array(&$this, '_login'), $args)) { return false; } $this->window_size_server_to_client[NET_SFTP_CHANNEL] = $this->window_size; $packet = pack( 'CNa*N3', NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SFTP_CHANNEL, $this->window_size, 0x4000 ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_OPEN; $response = $this->_get_channel_packet(NET_SFTP_CHANNEL, true); if ($response === false) { return false; } $packet = pack( 'CNNa*CNa*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SFTP_CHANNEL], strlen('subsystem'), 'subsystem', 1, strlen('sftp'), 'sftp' ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->_get_channel_packet(NET_SFTP_CHANNEL, true); if ($response === false) { // from PuTTY's psftp.exe $command = "test -x /usr/lib/sftp-server && exec /usr/lib/sftp-server\n" . "test -x /usr/local/lib/sftp-server && exec /usr/local/lib/sftp-server\n" . "exec sftp-server"; // we don't do $this->exec($command, false) because exec() operates on a different channel and plus the SSH_MSG_CHANNEL_OPEN that exec() does // is redundant $packet = pack( 'CNNa*CNa*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SFTP_CHANNEL], strlen('exec'), 'exec', 1, strlen($command), $command ); if (!$this->_send_binary_packet($packet)) { return false; } $this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; $response = $this->_get_channel_packet(NET_SFTP_CHANNEL, true); if ($response === false) { return false; } } $this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_DATA; if (!$this->_send_sftp_packet(NET_SFTP_INIT, "\0\0\0\3")) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_VERSION) { user_error('Expected SSH_FXP_VERSION'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nversion', $this->_string_shift($response, 4))); $this->version = $version; while (!empty($response)) { if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $key = $this->_string_shift($response, $length); if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $value = $this->_string_shift($response, $length); $this->extensions[$key] = $value; } /* SFTPv4+ defines a 'newline' extension. SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com', however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that 'newline@vandyke.com' would. */ /* if (isset($this->extensions['newline@vandyke.com'])) { $this->extensions['newline'] = $this->extensions['newline@vandyke.com']; unset($this->extensions['newline@vandyke.com']); } */ $this->use_request_id = true; /* A Note on SFTPv4/5/6 support: <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.1> states the following: "If the client wishes to interoperate with servers that support noncontiguous version numbers it SHOULD send '3'" Given that the server only sends its version number after the client has already done so, the above seems to be suggesting that v3 should be the default version. This makes sense given that v3 is the most popular. <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.5> states the following; "If the server did not send the "versions" extension, or the version-from-list was not included, the server MAY send a status response describing the failure, but MUST then close the channel without processing any further requests." So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4? If it only implements v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what Net_SFTP would do is close the channel and reopen it with a new and updated SSH_FXP_INIT packet. */ switch ($this->version) { case 2: case 3: break; default: return false; } $this->pwd = $this->_realpath('.'); $this->_update_stat_cache($this->pwd, array()); return true; } /** * Disable the stat cache * * @access public */ function disableStatCache() { $this->use_stat_cache = false; } /** * Enable the stat cache * * @access public */ function enableStatCache() { $this->use_stat_cache = true; } /** * Clear the stat cache * * @access public */ function clearStatCache() { $this->stat_cache = array(); } /** * Enable path canonicalization * * @access public */ function enablePathCanonicalization() { $this->canonicalize_paths = true; } /** * Enable path canonicalization * * @access public */ function disablePathCanonicalization() { $this->canonicalize_paths = false; } /** * Returns the current directory name * * @return mixed * @access public */ function pwd() { return $this->pwd; } /** * Logs errors * * @param string $response * @param int $status * @access public */ function _logError($response, $status = -1) { if ($status == -1) { if (strlen($response) < 4) { return; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); } $error = $this->status_codes[$status]; if ($this->version > 2 || strlen($response) < 4) { extract(unpack('Nlength', $this->_string_shift($response, 4))); $this->sftp_errors[] = $error . ': ' . $this->_string_shift($response, $length); } else { $this->sftp_errors[] = $error; } } /** * Returns canonicalized absolute pathname * * realpath() expands all symbolic links and resolves references to '/./', '/../' and extra '/' characters in the input * path and returns the canonicalized absolute pathname. * * @param string $path * @return mixed * @access public */ function realpath($path) { return $this->_realpath($path); } /** * Canonicalize the Server-Side Path Name * * SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it. Returns * the absolute (canonicalized) path. * * If canonicalize_paths has been disabled using disablePathCanonicalization(), $path is returned as-is. * * @see self::chdir() * @see self::disablePathCanonicalization() * @param string $path * @return mixed * @access private */ function _realpath($path) { if (!$this->canonicalize_paths) { return $path; } if ($this->pwd === false) { // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9 if (!$this->_send_sftp_packet(NET_SFTP_REALPATH, pack('Na*', strlen($path), $path))) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: // although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following // should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks // at is the first part and that part is defined the same in SFTP versions 3 through 6. $this->_string_shift($response, 4); // skip over the count - it should be 1, anyway if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); return $this->_string_shift($response, $length); case NET_SFTP_STATUS: $this->_logError($response); return false; default: user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS'); return false; } } if ($path[0] != '/') { $path = $this->pwd . '/' . $path; } $path = explode('/', $path); $new = array(); foreach ($path as $dir) { if (!strlen($dir)) { continue; } switch ($dir) { case '..': array_pop($new); case '.': break; default: $new[] = $dir; } } return '/' . implode('/', $new); } /** * Changes the current directory * * @param string $dir * @return bool * @access public */ function chdir($dir) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } // assume current dir if $dir is empty if ($dir === '') { $dir = './'; // suffix a slash if needed } elseif ($dir[strlen($dir) - 1] != '/') { $dir.= '/'; } $dir = $this->_realpath($dir); // confirm that $dir is, in fact, a valid directory if ($this->use_stat_cache && is_array($this->_query_stat_cache($dir))) { $this->pwd = $dir; return true; } // we could do a stat on the alleged $dir to see if it's a directory but that doesn't tell us // the currently logged in user has the appropriate permissions or not. maybe you could see if // the file's uid / gid match the currently logged in user's uid / gid but how there's no easy // way to get those with SFTP if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) { return false; } // see Net_SFTP::nlist() for a more thorough explanation of the following $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: $this->_logError($response); return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } if (!$this->_close_handle($handle)) { return false; } $this->_update_stat_cache($dir, array()); $this->pwd = $dir; return true; } /** * Returns a list of files in the given directory * * @param string $dir * @param bool $recursive * @return mixed * @access public */ function nlist($dir = '.', $recursive = false) { return $this->_nlist_helper($dir, $recursive, ''); } /** * Helper method for nlist * * @param string $dir * @param bool $recursive * @param string $relativeDir * @return mixed * @access private */ function _nlist_helper($dir, $recursive, $relativeDir) { $files = $this->_list($dir, false); if (!$recursive || $files === false) { return $files; } $result = array(); foreach ($files as $value) { if ($value == '.' || $value == '..') { if ($relativeDir == '') { $result[] = $value; } continue; } if (is_array($this->_query_stat_cache($this->_realpath($dir . '/' . $value)))) { $temp = $this->_nlist_helper($dir . '/' . $value, true, $relativeDir . $value . '/'); $temp = is_array($temp) ? $temp : array(); $result = array_merge($result, $temp); } else { $result[] = $relativeDir . $value; } } return $result; } /** * Returns a detailed list of files in the given directory * * @param string $dir * @param bool $recursive * @return mixed * @access public */ function rawlist($dir = '.', $recursive = false) { $files = $this->_list($dir, true); if (!$recursive || $files === false) { return $files; } static $depth = 0; foreach ($files as $key => $value) { if ($depth != 0 && $key == '..') { unset($files[$key]); continue; } $is_directory = false; if ($key != '.' && $key != '..') { if ($this->use_stat_cache) { $is_directory = is_array($this->_query_stat_cache($this->_realpath($dir . '/' . $key))); } else { $stat = $this->lstat($dir . '/' . $key); $is_directory = $stat && $stat['type'] === NET_SFTP_TYPE_DIRECTORY; } } if ($is_directory) { $depth++; $files[$key] = $this->rawlist($dir . '/' . $key, true); $depth--; } else { $files[$key] = (object) $value; } } return $files; } /** * Reads a list, be it detailed or not, of files in the given directory * * @param string $dir * @param bool $raw * @return mixed * @access private */ function _list($dir, $raw = true) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $dir = $this->_realpath($dir . '/'); if ($dir === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.2 if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.2 // since 'handle' is the last field in the SSH_FXP_HANDLE packet, we'll just remove the first four bytes that // represent the length of the string and leave it at that $handle = substr($response, 4); break; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED $this->_logError($response); return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } $this->_update_stat_cache($dir, array()); $contents = array(); while (true) { // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.2 // why multiple SSH_FXP_READDIR packets would be sent when the response to a single one can span arbitrarily many // SSH_MSG_CHANNEL_DATA messages is not known to me. if (!$this->_send_sftp_packet(NET_SFTP_READDIR, pack('Na*', strlen($handle), $handle))) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: if (strlen($response) < 4) { return false; } extract(unpack('Ncount', $this->_string_shift($response, 4))); for ($i = 0; $i < $count; $i++) { if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $shortname = $this->_string_shift($response, $length); if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $longname = $this->_string_shift($response, $length); $attributes = $this->_parseAttributes($response); if (!isset($attributes['type'])) { $fileType = $this->_parseLongname($longname); if ($fileType) { $attributes['type'] = $fileType; } } $contents[$shortname] = $attributes + array('filename' => $shortname); if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) { $this->_update_stat_cache($dir . '/' . $shortname, array()); } else { if ($shortname == '..') { $temp = $this->_realpath($dir . '/..') . '/.'; } else { $temp = $dir . '/' . $shortname; } $this->_update_stat_cache($temp, (object) array('lstat' => $attributes)); } // SFTPv6 has an optional boolean end-of-list field, but we'll ignore that, since the // final SSH_FXP_STATUS packet should tell us that, already. } break; case NET_SFTP_STATUS: if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_EOF) { $this->_logError($response, $status); return false; } break 2; default: user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS'); return false; } } if (!$this->_close_handle($handle)) { return false; } if (count($this->sortOptions)) { uasort($contents, array(&$this, '_comparator')); } return $raw ? $contents : array_keys($contents); } /** * Compares two rawlist entries using parameters set by setListOrder() * * Intended for use with uasort() * * @param array $a * @param array $b * @return int * @access private */ function _comparator($a, $b) { switch (true) { case $a['filename'] === '.' || $b['filename'] === '.': if ($a['filename'] === $b['filename']) { return 0; } return $a['filename'] === '.' ? -1 : 1; case $a['filename'] === '..' || $b['filename'] === '..': if ($a['filename'] === $b['filename']) { return 0; } return $a['filename'] === '..' ? -1 : 1; case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY: if (!isset($b['type'])) { return 1; } if ($b['type'] !== $a['type']) { return -1; } break; case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY: return 1; } foreach ($this->sortOptions as $sort => $order) { if (!isset($a[$sort]) || !isset($b[$sort])) { if (isset($a[$sort])) { return -1; } if (isset($b[$sort])) { return 1; } return 0; } switch ($sort) { case 'filename': $result = strcasecmp($a['filename'], $b['filename']); if ($result) { return $order === SORT_DESC ? -$result : $result; } break; case 'permissions': case 'mode': $a[$sort]&= 07777; $b[$sort]&= 07777; default: if ($a[$sort] === $b[$sort]) { break; } return $order === SORT_ASC ? $a[$sort] - $b[$sort] : $b[$sort] - $a[$sort]; } } } /** * Defines how nlist() and rawlist() will be sorted - if at all. * * If sorting is enabled directories and files will be sorted independently with * directories appearing before files in the resultant array that is returned. * * Any parameter returned by stat is a valid sort parameter for this function. * Filename comparisons are case insensitive. * * Examples: * * $sftp->setListOrder('filename', SORT_ASC); * $sftp->setListOrder('size', SORT_DESC, 'filename', SORT_ASC); * $sftp->setListOrder(true); * Separates directories from files but doesn't do any sorting beyond that * $sftp->setListOrder(); * Don't do any sort of sorting * * @access public */ function setListOrder() { $this->sortOptions = array(); $args = func_get_args(); if (empty($args)) { return; } $len = count($args) & 0x7FFFFFFE; for ($i = 0; $i < $len; $i+=2) { $this->sortOptions[$args[$i]] = $args[$i + 1]; } if (!count($this->sortOptions)) { $this->sortOptions = array('bogus' => true); } } /** * Returns the file size, in bytes, or false, on failure * * Files larger than 4GB will show up as being exactly 4GB. * * @param string $filename * @return mixed * @access public */ function size($filename) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $result = $this->stat($filename); if ($result === false) { return false; } return isset($result['size']) ? $result['size'] : -1; } /** * Save files / directories to cache * * @param string $path * @param mixed $value * @access private */ function _update_stat_cache($path, $value) { if ($this->use_stat_cache === false) { return; } // preg_replace('#^/|/(?=/)|/$#', '', $dir) == str_replace('//', '/', trim($path, '/')) $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; $max = count($dirs) - 1; foreach ($dirs as $i => $dir) { // if $temp is an object that means one of two things. // 1. a file was deleted and changed to a directory behind phpseclib's back // 2. it's a symlink. when lstat is done it's unclear what it's a symlink to if (is_object($temp)) { $temp = array(); } if (!isset($temp[$dir])) { $temp[$dir] = array(); } if ($i === $max) { if (is_object($temp[$dir]) && is_object($value)) { if (!isset($value->stat) && isset($temp[$dir]->stat)) { $value->stat = $temp[$dir]->stat; } if (!isset($value->lstat) && isset($temp[$dir]->lstat)) { $value->lstat = $temp[$dir]->lstat; } } $temp[$dir] = $value; break; } $temp = &$temp[$dir]; } } /** * Remove files / directories from cache * * @param string $path * @return bool * @access private */ function _remove_from_stat_cache($path) { $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; $max = count($dirs) - 1; foreach ($dirs as $i => $dir) { if ($i === $max) { unset($temp[$dir]); return true; } if (!isset($temp[$dir])) { return false; } $temp = &$temp[$dir]; } } /** * Checks cache for path * * Mainly used by file_exists * * @param string $dir * @return mixed * @access private */ function _query_stat_cache($path) { $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); $temp = &$this->stat_cache; foreach ($dirs as $dir) { if (!isset($temp[$dir])) { return null; } $temp = &$temp[$dir]; } return $temp; } /** * Returns general information about a file. * * Returns an array on success and false otherwise. * * @param string $filename * @return mixed * @access public */ function stat($filename) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $filename = $this->_realpath($filename); if ($filename === false) { return false; } if ($this->use_stat_cache) { $result = $this->_query_stat_cache($filename); if (is_array($result) && isset($result['.']) && isset($result['.']->stat)) { return $result['.']->stat; } if (is_object($result) && isset($result->stat)) { return $result->stat; } } $stat = $this->_stat($filename, NET_SFTP_STAT); if ($stat === false) { $this->_remove_from_stat_cache($filename); return false; } if (isset($stat['type'])) { if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename.= '/.'; } $this->_update_stat_cache($filename, (object) array('stat' => $stat)); return $stat; } $pwd = $this->pwd; $stat['type'] = $this->chdir($filename) ? NET_SFTP_TYPE_DIRECTORY : NET_SFTP_TYPE_REGULAR; $this->pwd = $pwd; if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename.= '/.'; } $this->_update_stat_cache($filename, (object) array('stat' => $stat)); return $stat; } /** * Returns general information about a file or symbolic link. * * Returns an array on success and false otherwise. * * @param string $filename * @return mixed * @access public */ function lstat($filename) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $filename = $this->_realpath($filename); if ($filename === false) { return false; } if ($this->use_stat_cache) { $result = $this->_query_stat_cache($filename); if (is_array($result) && isset($result['.']) && isset($result['.']->lstat)) { return $result['.']->lstat; } if (is_object($result) && isset($result->lstat)) { return $result->lstat; } } $lstat = $this->_stat($filename, NET_SFTP_LSTAT); if ($lstat === false) { $this->_remove_from_stat_cache($filename); return false; } if (isset($lstat['type'])) { if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename.= '/.'; } $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); return $lstat; } $stat = $this->_stat($filename, NET_SFTP_STAT); if ($lstat != $stat) { $lstat = array_merge($lstat, array('type' => NET_SFTP_TYPE_SYMLINK)); $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); return $stat; } $pwd = $this->pwd; $lstat['type'] = $this->chdir($filename) ? NET_SFTP_TYPE_DIRECTORY : NET_SFTP_TYPE_REGULAR; $this->pwd = $pwd; if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { $filename.= '/.'; } $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); return $lstat; } /** * Returns general information about a file or symbolic link * * Determines information without calling Net_SFTP::realpath(). * The second parameter can be either NET_SFTP_STAT or NET_SFTP_LSTAT. * * @param string $filename * @param int $type * @return mixed * @access private */ function _stat($filename, $type) { // SFTPv4+ adds an additional 32-bit integer field - flags - to the following: $packet = pack('Na*', strlen($filename), $filename); if (!$this->_send_sftp_packet($type, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_ATTRS: return $this->_parseAttributes($response); case NET_SFTP_STATUS: $this->_logError($response); return false; } user_error('Expected SSH_FXP_ATTRS or SSH_FXP_STATUS'); return false; } /** * Truncates a file to a given length * * @param string $filename * @param int $new_size * @return bool * @access public */ function truncate($filename, $new_size) { $attr = pack('N3', NET_SFTP_ATTR_SIZE, $new_size / 4294967296, $new_size); // 4294967296 == 0x100000000 == 1<<32 return $this->_setstat($filename, $attr, false); } /** * Sets access and modification time of file. * * If the file does not exist, it will be created. * * @param string $filename * @param int $time * @param int $atime * @return bool * @access public */ function touch($filename, $time = null, $atime = null) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $filename = $this->_realpath($filename); if ($filename === false) { return false; } if (!isset($time)) { $time = time(); } if (!isset($atime)) { $atime = $time; } $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_EXCL; $attr = pack('N3', NET_SFTP_ATTR_ACCESSTIME, $time, $atime); $packet = pack('Na*Na*', strlen($filename), $filename, $flags, $attr); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return $this->_close_handle(substr($response, 4)); case NET_SFTP_STATUS: $this->_logError($response); break; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } return $this->_setstat($filename, $attr, false); } /** * Changes file or directory owner * * Returns true on success or false on error. * * @param string $filename * @param int $uid * @param bool $recursive * @return bool * @access public */ function chown($filename, $uid, $recursive = false) { // quoting from <http://www.kernel.org/doc/man-pages/online/pages/man2/chown.2.html>, // "if the owner or group is specified as -1, then that ID is not changed" $attr = pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1); return $this->_setstat($filename, $attr, $recursive); } /** * Changes file or directory group * * Returns true on success or false on error. * * @param string $filename * @param int $gid * @param bool $recursive * @return bool * @access public */ function chgrp($filename, $gid, $recursive = false) { $attr = pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid); return $this->_setstat($filename, $attr, $recursive); } /** * Set permissions on a file. * * Returns the new file permissions on success or false on error. * If $recursive is true than this just returns true or false. * * @param int $mode * @param string $filename * @param bool $recursive * @return mixed * @access public */ function chmod($mode, $filename, $recursive = false) { if (is_string($mode) && is_int($filename)) { $temp = $mode; $mode = $filename; $filename = $temp; } $attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777); if (!$this->_setstat($filename, $attr, $recursive)) { return false; } if ($recursive) { return true; } $filename = $this->realpath($filename); // rather than return what the permissions *should* be, we'll return what they actually are. this will also // tell us if the file actually exists. // incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following: $packet = pack('Na*', strlen($filename), $filename); if (!$this->_send_sftp_packet(NET_SFTP_STAT, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_ATTRS: $attrs = $this->_parseAttributes($response); return $attrs['permissions']; case NET_SFTP_STATUS: $this->_logError($response); return false; } user_error('Expected SSH_FXP_ATTRS or SSH_FXP_STATUS'); return false; } /** * Sets information about a file * * @param string $filename * @param string $attr * @param bool $recursive * @return bool * @access private */ function _setstat($filename, $attr, $recursive) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $filename = $this->_realpath($filename); if ($filename === false) { return false; } $this->_remove_from_stat_cache($filename); if ($recursive) { $i = 0; $result = $this->_setstat_recursive($filename, $attr, $i); $this->_read_put_responses($i); return $result; } // SFTPv4+ has an additional byte field - type - that would need to be sent, as well. setting it to // SSH_FILEXFER_TYPE_UNKNOWN might work. if not, we'd have to do an SSH_FXP_STAT before doing an SSH_FXP_SETSTAT. if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($filename), $filename, $attr))) { return false; } /* "Because some systems must use separate system calls to set various attributes, it is possible that a failure response will be returned, but yet some of the attributes may be have been successfully modified. If possible, servers SHOULD avoid this situation; however, clients MUST be aware that this is possible." -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6 */ $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); return false; } return true; } /** * Recursively sets information on directories on the SFTP server * * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. * * @param string $path * @param string $attr * @param int $i * @return bool * @access private */ function _setstat_recursive($path, $attr, &$i) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; $entries = $this->_list($path, true); if ($entries === false) { return $this->_setstat($path, $attr, false); } // normally $entries would have at least . and .. but it might not if the directories // permissions didn't allow reading if (empty($entries)) { return false; } unset($entries['.'], $entries['..']); foreach ($entries as $filename => $props) { if (!isset($props['type'])) { return false; } $temp = $path . '/' . $filename; if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { if (!$this->_setstat_recursive($temp, $attr, $i)) { return false; } } else { if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($temp), $temp, $attr))) { return false; } $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; } } } if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($path), $path, $attr))) { return false; } $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; } return true; } /** * Return the target of a symbolic link * * @param string $link * @return mixed * @access public */ function readlink($link) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $link = $this->_realpath($link); if (!$this->_send_sftp_packet(NET_SFTP_READLINK, pack('Na*', strlen($link), $link))) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_NAME: break; case NET_SFTP_STATUS: $this->_logError($response); return false; default: user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Ncount', $this->_string_shift($response, 4))); // the file isn't a symlink if (!$count) { return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($response, 4))); return $this->_string_shift($response, $length); } /** * Create a symlink * * symlink() creates a symbolic link to the existing target with the specified name link. * * @param string $target * @param string $link * @return bool * @access public */ function symlink($target, $link) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } //$target = $this->_realpath($target); $link = $this->_realpath($link); $packet = pack('Na*Na*', strlen($target), $target, strlen($link), $link); if (!$this->_send_sftp_packet(NET_SFTP_SYMLINK, $packet)) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); return false; } return true; } /** * Creates a directory. * * @param string $dir * @return bool * @access public */ function mkdir($dir, $mode = -1, $recursive = false) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $dir = $this->_realpath($dir); // by not providing any permissions, hopefully the server will use the logged in users umask - their // default permissions. $attr = $mode == -1 ? "\0\0\0\0" : pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777); if ($recursive) { $dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir)); if (empty($dirs[0])) { array_shift($dirs); $dirs[0] = '/' . $dirs[0]; } for ($i = 0; $i < count($dirs); $i++) { $temp = array_slice($dirs, 0, $i + 1); $temp = implode('/', $temp); $result = $this->_mkdir_helper($temp, $attr); } return $result; } return $this->_mkdir_helper($dir, $attr); } /** * Helper function for directory creation * * @param string $dir * @return bool * @access private */ function _mkdir_helper($dir, $attr) { if (!$this->_send_sftp_packet(NET_SFTP_MKDIR, pack('Na*a*', strlen($dir), $dir, $attr))) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); return false; } return true; } /** * Removes a directory. * * @param string $dir * @return bool * @access public */ function rmdir($dir) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $dir = $this->_realpath($dir); if ($dir === false) { return false; } if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($dir), $dir))) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED? $this->_logError($response, $status); return false; } $this->_remove_from_stat_cache($dir); // the following will do a soft delete, which would be useful if you deleted a file // and then tried to do a stat on the deleted file. the above, in contrast, does // a hard delete //$this->_update_stat_cache($dir, false); return true; } /** * Uploads a file to the SFTP server. * * By default, Net_SFTP::put() does not read from the local filesystem. $data is dumped directly into $remote_file. * So, for example, if you set $data to 'filename.ext' and then do Net_SFTP::get(), you will get a file, twelve bytes * long, containing 'filename.ext' as its contents. * * Setting $mode to NET_SFTP_LOCAL_FILE will change the above behavior. With NET_SFTP_LOCAL_FILE, $remote_file will * contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how * large $remote_file will be, as well. * * If $data is a resource then it'll be used as a resource instead. * * * Setting $mode to NET_SFTP_CALLBACK will use $data as callback function, which gets only one parameter -- number * of bytes to return, and returns a string if there is some data or null if there is no more data * * Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take * care of that, yourself. * * $mode can take an additional two parameters - NET_SFTP_RESUME and NET_SFTP_RESUME_START. These are bitwise AND'd with * $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following: * * NET_SFTP_LOCAL_FILE | NET_SFTP_RESUME * * If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace * NET_SFTP_RESUME with NET_SFTP_RESUME_START. * * If $mode & (NET_SFTP_RESUME | NET_SFTP_RESUME_START) then NET_SFTP_RESUME_START will be assumed. * * $start and $local_start give you more fine grained control over this process and take precident over NET_SFTP_RESUME * when they're non-negative. ie. $start could let you write at the end of a file (like NET_SFTP_RESUME) or in the middle * of one. $local_start could let you start your reading from the end of a file (like NET_SFTP_RESUME_START) or in the * middle of one. * * Setting $local_start to > 0 or $mode | NET_SFTP_RESUME_START doesn't do anything unless $mode | NET_SFTP_LOCAL_FILE. * * @param string $remote_file * @param string|resource $data * @param int $mode * @param int $start * @param int $local_start * @param callable|null $progressCallback * @return bool * @access public * @internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - Net_SFTP::setMode(). */ function put($remote_file, $data, $mode = NET_SFTP_STRING, $start = -1, $local_start = -1, $progressCallback = null) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $remote_file = $this->_realpath($remote_file); if ($remote_file === false) { return false; } $this->_remove_from_stat_cache($remote_file); $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE; // according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file." // in practice, it doesn't seem to do that. //$flags|= ($mode & NET_SFTP_RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; if ($start >= 0) { $offset = $start; } elseif ($mode & NET_SFTP_RESUME) { // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called $size = $this->size($remote_file); $offset = $size !== false ? $size : 0; } else { $offset = 0; $flags|= NET_SFTP_OPEN_TRUNCATE; } $packet = pack('Na*N2', strlen($remote_file), $remote_file, $flags, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: $this->_logError($response); return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3 $dataCallback = false; switch (true) { case $mode & NET_SFTP_CALLBACK: if (!is_callable($data)) { user_error("\$data should be is_callable if you set NET_SFTP_CALLBACK flag"); } $dataCallback = $data; // do nothing break; case is_resource($data): $mode = $mode & ~NET_SFTP_LOCAL_FILE; $info = stream_get_meta_data($data); if ($info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') { $fp = fopen('php://memory', 'w+'); stream_copy_to_stream($data, $fp); rewind($fp); } else { $fp = $data; } break; case $mode & NET_SFTP_LOCAL_FILE: if (!is_file($data)) { user_error("$data is not a valid file"); return false; } $fp = @fopen($data, 'rb'); if (!$fp) { return false; } } if (isset($fp)) { $stat = fstat($fp); $size = !empty($stat) ? $stat['size'] : 0; if ($local_start >= 0) { fseek($fp, $local_start); $size-= $local_start; } } elseif ($dataCallback) { $size = 0; } else { $size = strlen($data); } $sent = 0; $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size; $sftp_packet_size = 4096; // PuTTY uses 4096 // make the SFTP packet be exactly 4096 bytes by including the bytes in the NET_SFTP_WRITE packets "header" $sftp_packet_size-= strlen($handle) + 25; $i = 0; while ($dataCallback || ($size === 0 || $sent < $size)) { if ($dataCallback) { $temp = call_user_func($dataCallback, $sftp_packet_size); if (is_null($temp)) { break; } } else { $temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size); if ($temp === false || $temp === '') { break; } } $subtemp = $offset + $sent; $packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp); if (!$this->_send_sftp_packet(NET_SFTP_WRITE, $packet)) { if ($mode & NET_SFTP_LOCAL_FILE) { fclose($fp); } return false; } $sent+= strlen($temp); if (is_callable($progressCallback)) { call_user_func($progressCallback, $sent); } $i++; if ($i == NET_SFTP_QUEUE_SIZE) { if (!$this->_read_put_responses($i)) { $i = 0; break; } $i = 0; } } if (!$this->_read_put_responses($i)) { if ($mode & NET_SFTP_LOCAL_FILE) { fclose($fp); } $this->_close_handle($handle); return false; } if ($mode & NET_SFTP_LOCAL_FILE) { fclose($fp); } return $this->_close_handle($handle); } /** * Reads multiple successive SSH_FXP_WRITE responses * * Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i * SSH_FXP_WRITEs, in succession, and then reading $i responses. * * @param int $i * @return bool * @access private */ function _read_put_responses($i) { while ($i--) { $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); break; } } return $i < 0; } /** * Close handle * * @param string $handle * @return bool * @access private */ function _close_handle($handle) { if (!$this->_send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle))) { return false; } // "The client MUST release all resources associated with the handle regardless of the status." // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3 $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); return false; } return true; } /** * Downloads a file from the SFTP server. * * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if * the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the * operation. * * $offset and $length can be used to download files in chunks. * * @param string $remote_file * @param string $local_file * @param int $offset * @param int $length * @param callable|null $progressCallback * @return mixed * @access public */ function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $remote_file = $this->_realpath($remote_file); if ($remote_file === false) { return false; } $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: $handle = substr($response, 4); break; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED $this->_logError($response); return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } if (is_resource($local_file)) { $fp = $local_file; $stat = fstat($fp); $res_offset = $stat['size']; } else { $res_offset = 0; if ($local_file !== false) { $fp = fopen($local_file, 'wb'); if (!$fp) { return false; } } else { $content = ''; } } $fclose_check = $local_file !== false && !is_resource($local_file); $start = $offset; $read = 0; while (true) { $i = 0; while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) { $tempoffset = $start + $read; $packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet; $packet = pack('Na*N3', strlen($handle), $handle, $tempoffset / 4294967296, $tempoffset, $packet_size); if (!$this->_send_sftp_packet(NET_SFTP_READ, $packet, $i)) { if ($fclose_check) { fclose($fp); } return false; } $packet = null; $read+= $packet_size; if (is_callable($progressCallback)) { call_user_func($progressCallback, $read); } $i++; } if (!$i) { break; } $packets_sent = $i - 1; $clear_responses = false; while ($i > 0) { $i--; if ($clear_responses) { $this->_get_sftp_packet($packets_sent - $i); continue; } else { $response = $this->_get_sftp_packet($packets_sent - $i); } switch ($this->packet_type) { case NET_SFTP_DATA: $temp = substr($response, 4); $offset+= strlen($temp); if ($local_file === false) { $content.= $temp; } else { fputs($fp, $temp); } $temp = null; break; case NET_SFTP_STATUS: // could, in theory, return false if !strlen($content) but we'll hold off for the time being $this->_logError($response); $clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses break; default: if ($fclose_check) { fclose($fp); } user_error('Expected SSH_FX_DATA or SSH_FXP_STATUS'); } $response = null; } if ($clear_responses) { break; } } if ($length > 0 && $length <= $offset - $start) { if ($local_file === false) { $content = substr($content, 0, $length); } else { ftruncate($fp, $length + $res_offset); } } if ($fclose_check) { fclose($fp); } if (!$this->_close_handle($handle)) { return false; } // if $content isn't set that means a file was written to return isset($content) ? $content : true; } /** * Deletes a file on the SFTP server. * * @param string $path * @param bool $recursive * @return bool * @access public */ function delete($path, $recursive = true) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } if (is_object($path)) { // It's an object. Cast it as string before we check anything else. $path = (string) $path; } if (!is_string($path) || $path == '') { return false; } $path = $this->_realpath($path); if ($path === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path))) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); if (!$recursive) { return false; } $i = 0; $result = $this->_delete_recursive($path, $i); $this->_read_put_responses($i); return $result; } $this->_remove_from_stat_cache($path); return true; } /** * Recursively deletes directories on the SFTP server * * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. * * @param string $path * @param int $i * @return bool * @access private */ function _delete_recursive($path, &$i) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; $entries = $this->_list($path, true); // normally $entries would have at least . and .. but it might not if the directories // permissions didn't allow reading if (empty($entries)) { return false; } unset($entries['.'], $entries['..']); foreach ($entries as $filename => $props) { if (!isset($props['type'])) { return false; } $temp = $path . '/' . $filename; if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { if (!$this->_delete_recursive($temp, $i)) { return false; } } else { if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($temp), $temp))) { return false; } $this->_remove_from_stat_cache($temp); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; } } } if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($path), $path))) { return false; } $this->_remove_from_stat_cache($path); $i++; if ($i >= NET_SFTP_QUEUE_SIZE) { if (!$this->_read_put_responses($i)) { return false; } $i = 0; } return true; } /** * Checks whether a file or directory exists * * @param string $path * @return bool * @access public */ function file_exists($path) { if ($this->use_stat_cache) { $path = $this->_realpath($path); $result = $this->_query_stat_cache($path); if (isset($result)) { // return true if $result is an array or if it's an stdClass object return $result !== false; } } return $this->stat($path) !== false; } /** * Tells whether the filename is a directory * * @param string $path * @return bool * @access public */ function is_dir($path) { $result = $this->_get_stat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_DIRECTORY; } /** * Tells whether the filename is a regular file * * @param string $path * @return bool * @access public */ function is_file($path) { $result = $this->_get_stat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_REGULAR; } /** * Tells whether the filename is a symbolic link * * @param string $path * @return bool * @access public */ function is_link($path) { $result = $this->_get_lstat_cache_prop($path, 'type'); if ($result === false) { return false; } return $result === NET_SFTP_TYPE_SYMLINK; } /** * Tells whether a file exists and is readable * * @param string $path * @return bool * @access public */ function is_readable($path) { $path = $this->_realpath($path); $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_READ, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return true; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } } /** * Tells whether the filename is writable * * @param string $path * @return bool * @access public */ function is_writable($path) { $path = $this->_realpath($path); $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_WRITE, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } $response = $this->_get_sftp_packet(); switch ($this->packet_type) { case NET_SFTP_HANDLE: return true; case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED return false; default: user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); return false; } } /** * Tells whether the filename is writeable * * Alias of is_writable * * @param string $path * @return bool * @access public */ function is_writeable($path) { return $this->is_writable($path); } /** * Gets last access time of file * * @param string $path * @return mixed * @access public */ function fileatime($path) { return $this->_get_stat_cache_prop($path, 'atime'); } /** * Gets file modification time * * @param string $path * @return mixed * @access public */ function filemtime($path) { return $this->_get_stat_cache_prop($path, 'mtime'); } /** * Gets file permissions * * @param string $path * @return mixed * @access public */ function fileperms($path) { return $this->_get_stat_cache_prop($path, 'permissions'); } /** * Gets file owner * * @param string $path * @return mixed * @access public */ function fileowner($path) { return $this->_get_stat_cache_prop($path, 'uid'); } /** * Gets file group * * @param string $path * @return mixed * @access public */ function filegroup($path) { return $this->_get_stat_cache_prop($path, 'gid'); } /** * Gets file size * * @param string $path * @return mixed * @access public */ function filesize($path) { return $this->_get_stat_cache_prop($path, 'size'); } /** * Gets file type * * @param string $path * @return mixed * @access public */ function filetype($path) { $type = $this->_get_stat_cache_prop($path, 'type'); if ($type === false) { return false; } switch ($type) { case NET_SFTP_TYPE_BLOCK_DEVICE: return 'block'; case NET_SFTP_TYPE_CHAR_DEVICE: return 'char'; case NET_SFTP_TYPE_DIRECTORY: return 'dir'; case NET_SFTP_TYPE_FIFO: return 'fifo'; case NET_SFTP_TYPE_REGULAR: return 'file'; case NET_SFTP_TYPE_SYMLINK: return 'link'; default: return false; } } /** * Return a stat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @return mixed * @access private */ function _get_stat_cache_prop($path, $prop) { return $this->_get_xstat_cache_prop($path, $prop, 'stat'); } /** * Return an lstat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @return mixed * @access private */ function _get_lstat_cache_prop($path, $prop) { return $this->_get_xstat_cache_prop($path, $prop, 'lstat'); } /** * Return a stat or lstat properity * * Uses cache if appropriate. * * @param string $path * @param string $prop * @return mixed * @access private */ function _get_xstat_cache_prop($path, $prop, $type) { if ($this->use_stat_cache) { $path = $this->_realpath($path); $result = $this->_query_stat_cache($path); if (is_object($result) && isset($result->$type)) { return $result->{$type}[$prop]; } } $result = $this->$type($path); if ($result === false || !isset($result[$prop])) { return false; } return $result[$prop]; } /** * Renames a file or a directory on the SFTP server * * @param string $oldname * @param string $newname * @return bool * @access public */ function rename($oldname, $newname) { if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { return false; } $oldname = $this->_realpath($oldname); $newname = $this->_realpath($newname); if ($oldname === false || $newname === false) { return false; } // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 $packet = pack('Na*Na*', strlen($oldname), $oldname, strlen($newname), $newname); if (!$this->_send_sftp_packet(NET_SFTP_RENAME, $packet)) { return false; } $response = $this->_get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { user_error('Expected SSH_FXP_STATUS'); return false; } // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED if (strlen($response) < 4) { return false; } extract(unpack('Nstatus', $this->_string_shift($response, 4))); if ($status != NET_SFTP_STATUS_OK) { $this->_logError($response, $status); return false; } // don't move the stat cache entry over since this operation could very well change the // atime and mtime attributes //$this->_update_stat_cache($newname, $this->_query_stat_cache($oldname)); $this->_remove_from_stat_cache($oldname); $this->_remove_from_stat_cache($newname); return true; } /** * Parse Attributes * * See '7. File Attributes' of draft-ietf-secsh-filexfer-13 for more info. * * @param string $response * @return array * @access private */ function _parseAttributes(&$response) { $attr = array(); if (strlen($response) < 4) { user_error('Malformed file attributes'); return array(); } extract(unpack('Nflags', $this->_string_shift($response, 4))); // SFTPv4+ have a type field (a byte) that follows the above flag field foreach ($this->attributes as $key => $value) { switch ($flags & $key) { case NET_SFTP_ATTR_SIZE: // 0x00000001 // The size attribute is defined as an unsigned 64-bit integer. // The following will use floats on 32-bit platforms, if necessary. // As can be seen in the BigInteger class, floats are generally // IEEE 754 binary64 "double precision" on such platforms and // as such can represent integers of at least 2^50 without loss // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB. $attr['size'] = hexdec(bin2hex($this->_string_shift($response, 8))); break; case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only) if (strlen($response) < 8) { user_error('Malformed file attributes'); return $attr; } $attr+= unpack('Nuid/Ngid', $this->_string_shift($response, 8)); break; case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; } $attr+= unpack('Npermissions', $this->_string_shift($response, 4)); // mode == permissions; permissions was the original array key and is retained for bc purposes. // mode was added because that's the more industry standard terminology $attr+= array('mode' => $attr['permissions']); $fileType = $this->_parseMode($attr['permissions']); if ($fileType !== false) { $attr+= array('type' => $fileType); } break; case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 if (strlen($response) < 8) { user_error('Malformed file attributes'); return $attr; } $attr+= unpack('Natime/Nmtime', $this->_string_shift($response, 8)); break; case NET_SFTP_ATTR_EXTENDED: // 0x80000000 if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; } extract(unpack('Ncount', $this->_string_shift($response, 4))); for ($i = 0; $i < $count; $i++) { if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $key = $this->_string_shift($response, $length); if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; } extract(unpack('Nlength', $this->_string_shift($response, 4))); $attr[$key] = $this->_string_shift($response, $length); } } } return $attr; } /** * Attempt to identify the file type * * Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway * * @param int $mode * @return int * @access private */ function _parseMode($mode) { // values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12 // see, also, http://linux.die.net/man/2/stat switch ($mode & 0170000) {// ie. 1111 0000 0000 0000 case 0000000: // no file type specified - figure out the file type using alternative means return false; case 0040000: return NET_SFTP_TYPE_DIRECTORY; case 0100000: return NET_SFTP_TYPE_REGULAR; case 0120000: return NET_SFTP_TYPE_SYMLINK; // new types introduced in SFTPv5+ // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 case 0010000: // named pipe (fifo) return NET_SFTP_TYPE_FIFO; case 0020000: // character special return NET_SFTP_TYPE_CHAR_DEVICE; case 0060000: // block special return NET_SFTP_TYPE_BLOCK_DEVICE; case 0140000: // socket return NET_SFTP_TYPE_SOCKET; case 0160000: // whiteout // "SPECIAL should be used for files that are of // a known type which cannot be expressed in the protocol" return NET_SFTP_TYPE_SPECIAL; default: return NET_SFTP_TYPE_UNKNOWN; } } /** * Parse Longname * * SFTPv3 doesn't provide any easy way of identifying a file type. You could try to open * a file as a directory and see if an error is returned or you could try to parse the * SFTPv3-specific longname field of the SSH_FXP_NAME packet. That's what this function does. * The result is returned using the * {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}. * * If the longname is in an unrecognized format bool(false) is returned. * * @param string $longname * @return mixed * @access private */ function _parseLongname($longname) { // http://en.wikipedia.org/wiki/Unix_file_types // http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) { switch ($longname[0]) { case '-': return NET_SFTP_TYPE_REGULAR; case 'd': return NET_SFTP_TYPE_DIRECTORY; case 'l': return NET_SFTP_TYPE_SYMLINK; default: return NET_SFTP_TYPE_SPECIAL; } } return false; } /** * Sends SFTP Packets * * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. * * @param int $type * @param string $data * @see self::_get_sftp_packet() * @see Net_SSH2::_send_channel_packet() * @return bool * @access private */ function _send_sftp_packet($type, $data, $request_id = 1) { $packet = $this->use_request_id ? pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) : pack('NCa*', strlen($data) + 1, $type, $data); $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $result = $this->_send_channel_packet(NET_SFTP_CHANNEL, $packet); $stop = strtok(microtime(), ' ') + strtok(''); if (defined('NET_SFTP_LOGGING')) { $packet_type = '-> ' . $this->packet_types[$type] . ' (' . round($stop - $start, 4) . 's)'; if (NET_SFTP_LOGGING == NET_SFTP_LOG_REALTIME) { echo "<pre>\r\n" . $this->_format_log(array($data), array($packet_type)) . "\r\n</pre>\r\n"; flush(); ob_flush(); } else { $this->packet_type_log[] = $packet_type; if (NET_SFTP_LOGGING == NET_SFTP_LOG_COMPLEX) { $this->packet_log[] = $data; } } } return $result; } /** * Resets a connection for re-use * * @param int $reason * @access private */ function _reset_connection($reason) { parent::_reset_connection($reason); $this->use_request_id = false; $this->pwd = false; $this->requestBuffer = array(); } /** * Receives SFTP Packets * * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. * * Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present. * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA * messages containing one SFTP packet. * * @see self::_send_sftp_packet() * @return string * @access private */ function _get_sftp_packet($request_id = null) { if (isset($request_id) && isset($this->requestBuffer[$request_id])) { $this->packet_type = $this->requestBuffer[$request_id]['packet_type']; $temp = $this->requestBuffer[$request_id]['packet']; unset($this->requestBuffer[$request_id]); return $temp; } // in SSH2.php the timeout is cumulative per function call. eg. exec() will // timeout after 10s. but for SFTP.php it's cumulative per packet $this->curTimeout = $this->timeout; $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 // SFTP packet length while (strlen($this->packet_buffer) < 4) { $temp = $this->_get_channel_packet(NET_SFTP_CHANNEL, true); if (is_bool($temp)) { $this->packet_type = false; $this->packet_buffer = ''; return false; } $this->packet_buffer.= $temp; } if (strlen($this->packet_buffer) < 4) { return false; } extract(unpack('Nlength', $this->_string_shift($this->packet_buffer, 4))); $tempLength = $length; $tempLength-= strlen($this->packet_buffer); // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h if ($tempLength > 256 * 1024) { user_error('Invalid SFTP packet size'); return false; } // SFTP packet type and data payload while ($tempLength > 0) { $temp = $this->_get_channel_packet(NET_SFTP_CHANNEL, true); if (is_bool($temp)) { $this->packet_type = false; $this->packet_buffer = ''; return false; } $this->packet_buffer.= $temp; $tempLength-= strlen($temp); } $stop = strtok(microtime(), ' ') + strtok(''); $this->packet_type = ord($this->_string_shift($this->packet_buffer)); if ($this->use_request_id) { extract(unpack('Npacket_id', $this->_string_shift($this->packet_buffer, 4))); // remove the request id $length-= 5; // account for the request id and the packet type } else { $length-= 1; // account for the packet type } $packet = $this->_string_shift($this->packet_buffer, $length); if (defined('NET_SFTP_LOGGING')) { $packet_type = '<- ' . $this->packet_types[$this->packet_type] . ' (' . round($stop - $start, 4) . 's)'; if (NET_SFTP_LOGGING == NET_SFTP_LOG_REALTIME) { echo "<pre>\r\n" . $this->_format_log(array($packet), array($packet_type)) . "\r\n</pre>\r\n"; flush(); ob_flush(); } else { $this->packet_type_log[] = $packet_type; if (NET_SFTP_LOGGING == NET_SFTP_LOG_COMPLEX) { $this->packet_log[] = $packet; } } } if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) { $this->requestBuffer[$packet_id] = array( 'packet_type' => $this->packet_type, 'packet' => $packet ); return $this->_get_sftp_packet($request_id); } return $packet; } /** * Returns a log of the packets that have been sent and received. * * Returns a string if NET_SFTP_LOGGING == NET_SFTP_LOG_COMPLEX, an array if NET_SFTP_LOGGING == NET_SFTP_LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING') * * @access public * @return string or Array */ function getSFTPLog() { if (!defined('NET_SFTP_LOGGING')) { return false; } switch (NET_SFTP_LOGGING) { case NET_SFTP_LOG_COMPLEX: return $this->_format_log($this->packet_log, $this->packet_type_log); break; //case NET_SFTP_LOG_SIMPLE: default: return $this->packet_type_log; } } /** * Returns all errors * * @return array * @access public */ function getSFTPErrors() { return $this->sftp_errors; } /** * Returns the last error * * @return string * @access public */ function getLastSFTPError() { return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : ''; } /** * Get supported SFTP versions * * @return array * @access public */ function getSupportedVersions() { $temp = array('version' => $this->version); if (isset($this->extensions['versions'])) { $temp['extensions'] = $this->extensions['versions']; } return $temp; } /** * Disconnect * * @param int $reason * @return bool * @access private */ function _disconnect($reason) { $this->pwd = false; parent::_disconnect($reason); } } PK �:m[��B!"