diff options
Diffstat (limited to 'libs/phpseclib-1.0.6/Net')
-rw-r--r-- | libs/phpseclib-1.0.6/Net/SCP.php | 373 | ||||
-rw-r--r-- | libs/phpseclib-1.0.6/Net/SFTP.php | 3120 | ||||
-rw-r--r-- | libs/phpseclib-1.0.6/Net/SFTP/Stream.php | 815 | ||||
-rw-r--r-- | libs/phpseclib-1.0.6/Net/SSH1.php | 1691 | ||||
-rw-r--r-- | libs/phpseclib-1.0.6/Net/SSH2.php | 4504 |
5 files changed, 10503 insertions, 0 deletions
diff --git a/libs/phpseclib-1.0.6/Net/SCP.php b/libs/phpseclib-1.0.6/Net/SCP.php new file mode 100644 index 0000000..354acea --- /dev/null +++ b/libs/phpseclib-1.0.6/Net/SCP.php @@ -0,0 +1,373 @@ +<?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 (!$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(); + } + } +} diff --git a/libs/phpseclib-1.0.6/Net/SFTP.php b/libs/phpseclib-1.0.6/Net/SFTP.php new file mode 100644 index 0000000..40c97da --- /dev/null +++ b/libs/phpseclib-1.0.6/Net/SFTP.php @@ -0,0 +1,3120 @@ +<?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 int + * @see self::_send_sftp_packet() + * @access private + */ + var $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 string + * @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(); + + /** + * 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 => '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(); + 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); + 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); + 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); + 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->request_id = 1; + + /* + 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(); + } + + /** + * 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. + * + * @see self::chdir() + * @param string $path + * @return mixed + * @access private + */ + function _realpath($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 . '/'); + $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; + } + if ($key != '.' && $key != '..' && is_array($this->_query_stat_cache($this->_realpath($dir . '/' . $key)))) { + $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])) { + 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 = $stat['size']; + + 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 + * @return mixed + * @access public + */ + function get($remote_file, $local_file = false, $offset = 0, $length = -1) + { + 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)) { + if ($fclose_check) { + fclose($fp); + } + return false; + } + $packet = null; + $read+= $packet_size; + $i++; + } + + if (!$i) { + break; + } + + $clear_responses = false; + while ($i > 0) { + $i--; + + if ($clear_responses) { + $this->_get_sftp_packet(); + continue; + } else { + $response = $this->_get_sftp_packet(); + } + + 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) + { + $packet = $this->request_id !== false ? + pack('NCNa*', strlen($data) + 5, $type, $this->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; + } + + /** + * 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() + { + $this->curTimeout = false; + + $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); + 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); + + // SFTP packet type and data payload + while ($tempLength > 0) { + $temp = $this->_get_channel_packet(NET_SFTP_CHANNEL); + 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->request_id !== false) { + $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; + } + } + } + + 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 string + * @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); + } +} diff --git a/libs/phpseclib-1.0.6/Net/SFTP/Stream.php b/libs/phpseclib-1.0.6/Net/SFTP/Stream.php new file mode 100644 index 0000000..a944d7f --- /dev/null +++ b/libs/phpseclib-1.0.6/Net/SFTP/Stream.php @@ -0,0 +1,815 @@ +<?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(); diff --git a/libs/phpseclib-1.0.6/Net/SSH1.php b/libs/phpseclib-1.0.6/Net/SSH1.php new file mode 100644 index 0000000..55473d0 --- /dev/null +++ b/libs/phpseclib-1.0.6/Net/SSH1.php @@ -0,0 +1,1691 @@ +<?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); + } + } +} diff --git a/libs/phpseclib-1.0.6/Net/SSH2.php b/libs/phpseclib-1.0.6/Net/SSH2.php new file mode 100644 index 0000000..d104b81 --- /dev/null +++ b/libs/phpseclib-1.0.6/Net/SSH2.php @@ -0,0 +1,4504 @@ +<?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', 0); // PuTTy uses 0x100 +define('NET_SSH2_CHANNEL_SHELL', 1); +define('NET_SSH2_CHANNEL_SUBSYSTEM', 2); +define('NET_SSH2_CHANNEL_AGENT_FORWARD', 3); +/**#@-*/ + +/**#@+ + * @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); +/**#@-*/ + +/**#@+ + * @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); +/** + * Make sure that the log never gets larger than this + */ +define('NET_SSH2_LOG_MAX_SIZE', 1024 * 1024); +/**#@-*/ + +/** + * 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; + + /** + * 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; + + /** + * 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; + + /** + * 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 private + */ + function setCryptoEngine($engine) + { + $this->crypto_engine = $engine; + } + + /** + * 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; + + $this->curTimeout-= $elapsed; + + if ($this->curTimeout <= 0) { + $this->is_timeout = true; + return false; + } + } + + $this->identifier = $this->_generate_identifier(); + + 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)) { + 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[] = utf8_decode($extra); + } + + if ($matches[1] != '1.99' && $matches[1] != '2.0') { + user_error("Cannot connect to SSH $matches[1] servers"); + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + 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; + } + + $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 + * @access private + */ + function _key_exchange($kexinit_payload_server) + { + static $kex_algorithms = array( + 'diffie-hellman-group1-sha1', // REQUIRED + 'diffie-hellman-group14-sha1', // REQUIRED + 'diffie-hellman-group-exchange-sha1', // RFC 4419 + 'diffie-hellman-group-exchange-sha256', // RFC 4419 + ); + + static $server_host_key_algorithms = array( + 'ssh-rsa', // RECOMMENDED sign Raw RSA Key + 'ssh-dss' // REQUIRED sign Raw DSS Key + ); + + static $encryption_algorithms = false; + if ($encryption_algorithms === false) { + $encryption_algorithms = 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 + ); + + if (extension_loaded('openssl') && !extension_loaded('mcrypt')) { + // OpenSSL does not support arcfour256 in any capacity and arcfour128 / arcfour support is limited to + // instances that do not use continuous buffers + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('arcfour256', 'arcfour128', 'arcfour') + ); + } + + if (phpseclib_resolve_include_path('Crypt/RC4.php') === false) { + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('arcfour256', 'arcfour128', 'arcfour') + ); + } + if (phpseclib_resolve_include_path('Crypt/Rijndael.php') === false) { + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('aes128-ctr', 'aes192-ctr', 'aes256-ctr', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc') + ); + } + if (phpseclib_resolve_include_path('Crypt/Twofish.php') === false) { + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('twofish128-ctr', 'twofish192-ctr', 'twofish256-ctr', 'twofish128-cbc', 'twofish192-cbc', 'twofish256-cbc', 'twofish-cbc') + ); + } + if (phpseclib_resolve_include_path('Crypt/Blowfish.php') === false) { + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('blowfish-ctr', 'blowfish-cbc') + ); + } + if (phpseclib_resolve_include_path('Crypt/TripleDES.php') === false) { + $encryption_algorithms = array_diff( + $encryption_algorithms, + array('3des-ctr', '3des-cbc') + ); + } + $encryption_algorithms = array_values($encryption_algorithms); + } + + $mac_algorithms = 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 + ); + + static $compression_algorithms = array( + 'none' // REQUIRED no compression + //'zlib' // OPTIONAL ZLIB (LZ77) compression + ); + + // some SSH servers have buggy implementations of some of the above algorithms + switch ($this->server_identifier) { + case 'SSH-2.0-SSHD': + $mac_algorithms = array_values(array_diff( + $mac_algorithms, + array('hmac-sha1-96', 'hmac-md5-96') + )); + } + + static $str_kex_algorithms, $str_server_host_key_algorithms, + $encryption_algorithms_server_to_client, $mac_algorithms_server_to_client, $compression_algorithms_server_to_client, + $encryption_algorithms_client_to_server, $mac_algorithms_client_to_server, $compression_algorithms_client_to_server; + + if (empty($str_kex_algorithms)) { + $str_kex_algorithms = implode(',', $kex_algorithms); + $str_server_host_key_algorithms = implode(',', $server_host_key_algorithms); + $encryption_algorithms_server_to_client = $encryption_algorithms_client_to_server = implode(',', $encryption_algorithms); + $mac_algorithms_server_to_client = $mac_algorithms_client_to_server = implode(',', $mac_algorithms); + $compression_algorithms_server_to_client = $compression_algorithms_client_to_server = implode(',', $compression_algorithms); + } + + $client_cookie = crypt_random_string(16); + + $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; + + // the sending of SSH2_MSG_KEXINIT could go in one of two places. this is the second place. + $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_binary_packet($kexinit_payload_client)) { + return false; + } + // here ends the second place. + + // 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($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($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 + $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) { + 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)) { + user_error('Connection closed by server'); + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + 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); + } + + if ($public_key_format != $server_host_key_algorithm || $this->signature_format != $server_host_key_algorithm) { + 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) { + 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; + } + + switch ($encrypt) { + case '3des-cbc': + if (!class_exists('Crypt_TripleDES')) { + include_once 'Crypt/TripleDES.php'; + } + $this->encrypt = new Crypt_TripleDES(); + // $this->encrypt_block_size = 64 / 8 == the default + break; + case '3des-ctr': + if (!class_exists('Crypt_TripleDES')) { + include_once 'Crypt/TripleDES.php'; + } + $this->encrypt = new Crypt_TripleDES(CRYPT_DES_MODE_CTR); + // $this->encrypt_block_size = 64 / 8 == the default + break; + case 'aes256-cbc': + case 'aes192-cbc': + case 'aes128-cbc': + if (!class_exists('Crypt_Rijndael')) { + include_once 'Crypt/Rijndael.php'; + } + $this->encrypt = new Crypt_Rijndael(); + $this->encrypt_block_size = 16; // eg. 128 / 8 + break; + case 'aes256-ctr': + case 'aes192-ctr': + case 'aes128-ctr': + if (!class_exists('Crypt_Rijndael')) { + include_once 'Crypt/Rijndael.php'; + } + $this->encrypt = 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'; + } + $this->encrypt = new Crypt_Blowfish(); + $this->encrypt_block_size = 8; + break; + case 'blowfish-ctr': + if (!class_exists('Crypt_Blowfish')) { + include_once 'Crypt/Blowfish.php'; + } + $this->encrypt = new Crypt_Blowfish(CRYPT_BLOWFISH_MODE_CTR); + $this->encrypt_block_size = 8; + break; + case 'twofish128-cbc': + case 'twofish192-cbc': + case 'twofish256-cbc': + case 'twofish-cbc': + if (!class_exists('Crypt_Twofish')) { + include_once 'Crypt/Twofish.php'; + } + $this->encrypt = new Crypt_Twofish(); + $this->encrypt_block_size = 16; + break; + case 'twofish128-ctr': + case 'twofish192-ctr': + case 'twofish256-ctr': + if (!class_exists('Crypt_Twofish')) { + include_once 'Crypt/Twofish.php'; + } + $this->encrypt = new Crypt_Twofish(CRYPT_TWOFISH_MODE_CTR); + $this->encrypt_block_size = 16; + break; + case 'arcfour': + case 'arcfour128': + case 'arcfour256': + if (!class_exists('Crypt_RC4')) { + include_once 'Crypt/RC4.php'; + } + $this->encrypt = new Crypt_RC4(); + break; + case 'none': + //$this->encrypt = new Crypt_Null(); + } + + switch ($decrypt) { + case '3des-cbc': + if (!class_exists('Crypt_TripleDES')) { + include_once 'Crypt/TripleDES.php'; + } + $this->decrypt = new Crypt_TripleDES(); + break; + case '3des-ctr': + if (!class_exists('Crypt_TripleDES')) { + include_once 'Crypt/TripleDES.php'; + } + $this->decrypt = 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'; + } + $this->decrypt = new Crypt_Rijndael(); + $this->decrypt_block_size = 16; + break; + case 'aes256-ctr': + case 'aes192-ctr': + case 'aes128-ctr': + if (!class_exists('Crypt_Rijndael')) { + include_once 'Crypt/Rijndael.php'; + } + $this->decrypt = new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CTR); + $this->decrypt_block_size = 16; + break; + case 'blowfish-cbc': + if (!class_exists('Crypt_Blowfish')) { + include_once 'Crypt/Blowfish.php'; + } + $this->decrypt = new Crypt_Blowfish(); + $this->decrypt_block_size = 8; + break; + case 'blowfish-ctr': + if (!class_exists('Crypt_Blowfish')) { + include_once 'Crypt/Blowfish.php'; + } + $this->decrypt = new Crypt_Blowfish(CRYPT_BLOWFISH_MODE_CTR); + $this->decrypt_block_size = 8; + break; + case 'twofish128-cbc': + case 'twofish192-cbc': + case 'twofish256-cbc': + case 'twofish-cbc': + if (!class_exists('Crypt_Twofish')) { + include_once 'Crypt/Twofish.php'; + } + $this->decrypt = new Crypt_Twofish(); + $this->decrypt_block_size = 16; + break; + case 'twofish128-ctr': + case 'twofish192-ctr': + case 'twofish256-ctr': + if (!class_exists('Crypt_Twofish')) { + include_once 'Crypt/Twofish.php'; + } + $this->decrypt = new Crypt_Twofish(CRYPT_TWOFISH_MODE_CTR); + $this->decrypt_block_size = 16; + break; + case 'arcfour': + case 'arcfour128': + case 'arcfour256': + if (!class_exists('Crypt_RC4')) { + include_once 'Crypt/RC4.php'; + } + $this->decrypt = new Crypt_RC4(); + break; + case 'none': + //$this->decrypt = new Crypt_Null(); + } + + $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); + + if ($this->encrypt) { + if ($this->crypto_engine) { + $this->encrypt->setEngine($this->crypto_engine); + } + $this->encrypt->enableContinuousBuffer(); + $this->encrypt->disablePadding(); + + $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)); + } + + if ($this->decrypt) { + if ($this->crypto_engine) { + $this->decrypt->setEngine($this->crypto_engine); + } + $this->decrypt->enableContinuousBuffer(); + $this->decrypt->disablePadding(); + + $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)); + } + + /* 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($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; + } + + $mac_algorithm = $this->_array_intersect_first($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; + } + + $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($compression_algorithms, $this->compression_algorithms_server_to_client); + if ($compression_algorithm === false) { + user_error('No compatible server to client compression algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + $this->decompress = $compression_algorithm == 'zlib'; + + $compression_algorithm = $this->_array_intersect_first($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->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) + { + 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; + } + + /** + * 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(); + 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) { + 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) { + 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) { + 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: ' . utf8_decode($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) { + 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'] + ); + + $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('ssh-rsa'), 'ssh-rsa', strlen($publickey), $publickey); + + $packet = $part1 . chr(0) . $part2; + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + 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); + $signature = $privatekey->sign(pack('Na*a*', strlen($this->session_id), $this->session_id, $packet)); + $signature = pack('Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', 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) { + 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) { + 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) { + 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(); + + $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); + } + + /** + * 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() + { + if (!is_resource($this->fsock) || feof($this->fsock)) { + user_error('Connection closed prematurely'); + $this->bitmap = 0; + 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) { + user_error('Invalid size'); + return false; + } + + $buffer = ''; + while ($remaining_length > 0) { + $temp = fread($this->fsock, $remaining_length); + if ($temp === false || feof($this->fsock)) { + user_error('Error reading from socket'); + $this->bitmap = 0; + 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) { + user_error('Error reading socket'); + $this->bitmap = 0; + 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); + } + + /** + * Filter Binary Packets + * + * Because some binary packets need to be ignored... + * + * @see self::_get_binary_packet() + * @return string + * @access private + */ + function _filter($payload) + { + 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" . utf8_decode($this->_string_shift($payload, $length)); + $this->bitmap = 0; + return false; + case NET_SSH2_MSG_IGNORE: + $payload = $this->_get_binary_packet(); + 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: ' . utf8_decode($this->_string_shift($payload, $length)); + $payload = $this->_get_binary_packet(); + break; + case NET_SSH2_MSG_UNIMPLEMENTED: + return false; + case NET_SSH2_MSG_KEXINIT: + if ($this->session_id !== false) { + if (!$this->_key_exchange($payload)) { + $this->bitmap = 0; + return false; + } + $payload = $this->_get_binary_packet(); + } + } + + // 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 = utf8_decode($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_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(); + 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(); + 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(); + } + } + + 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->curTimeout) { + 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; + return true; + } + $elapsed = strtok(microtime(), ' ') + strtok('') - $start; + $this->curTimeout-= $elapsed; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + user_error('Connection closed by server'); + return false; + } + if ($client_channel == -1 && $response === true) { + return true; + } + if (!strlen($response)) { + return ''; + } + + 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 ($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_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) { + 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_REQUEST: + 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; + + break; + 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 + + break; + default: + // "Some systems may not implement signals, in which case they SHOULD ignore this message." + // -- http://tools.ietf.org/html/rfc4254#section-6.9 + break; + } + 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)) { + user_error('Connection closed prematurely'); + $this->bitmap = 0; + 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; + 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 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': + 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(); + $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(); + + $h = pack('N4H*', 0x00302130, 0x0906052B, 0x0E03021A, 0x05000414, sha1($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; + } +} |