diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 969af2d9..c5f7b3af 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -5,9 +5,7 @@ * * PHP version 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. + * Supports SFTPv2/3/4/5/6. Defaults to v3. * * The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}. * @@ -154,6 +152,24 @@ class SFTP extends SSH2 */ var $version; + /** + * Default Server SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + var $defaultVersion; + + /** + * Preferred SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + var $preferredVersion = 3; + /** * Current working directory * @@ -294,6 +310,14 @@ class SFTP extends SSH2 */ var $channel_close = false; + /** + * Has the SFTP channel been partially negotiated? + * + * @var bool + * @access private + */ + var $partial_init = false; + /** * Default Constructor. * @@ -314,15 +338,13 @@ class SFTP extends SSH2 $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', + 10 => 'NET_SFTP_FSETSTAT', 11 => 'NET_SFTP_OPENDIR', 12 => 'NET_SFTP_READDIR', 13 => 'NET_SFTP_REMOVE', @@ -330,18 +352,13 @@ class SFTP extends SSH2 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', + 21 => 'NET_SFTP_LINK', 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', @@ -386,25 +403,59 @@ class SFTP extends SSH2 // the order, in this case, matters quite a lot - see \phpseclib\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+ + 0x00000002 => 'NET_SFTP_ATTR_UIDGID', // defined in SFTPv3, removed in SFTPv4+ + 0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP', // defined in SFTPv4+ 0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS', 0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME', + 0x00000010 => 'NET_SFTP_ATTR_CREATETIME', // SFTPv4+ + 0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME', + 0x00000040 => 'NET_SFTP_ATTR_ACL', + 0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES', + 0x00000200 => 'NET_SFTP_ATTR_BITS', // SFTPv5+ + 0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+ + 0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT', + 0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE', + 0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT', + 0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME', + 0x00008000 => 'NET_SFTP_ATTR_CTIME', // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers // yields inconsistent behavior depending on how php is compiled. so we left shift -1 (which, in // two's compliment, consists of all 1 bits) by 31. on 64-bit systems this'll yield 0xFFFFFFFF80000000. // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored. (-1 << 31) & 0xFFFFFFFF => 'NET_SFTP_ATTR_EXTENDED' ); - // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.3 - // the flag definitions change somewhat in SFTPv5+. if SFTPv5+ support is added to this library, maybe name - // the array for that $this->open5_flags and similarly alter the constant names. $this->open_flags = array( 0x00000001 => 'NET_SFTP_OPEN_READ', 0x00000002 => 'NET_SFTP_OPEN_WRITE', 0x00000004 => 'NET_SFTP_OPEN_APPEND', 0x00000008 => 'NET_SFTP_OPEN_CREATE', 0x00000010 => 'NET_SFTP_OPEN_TRUNCATE', - 0x00000020 => 'NET_SFTP_OPEN_EXCL' + 0x00000020 => 'NET_SFTP_OPEN_EXCL', + 0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4 + ); + // SFTPv5+ changed the flags up: + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3 + $this->open_flags5 = array( + // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened + 0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW', + 0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE', + 0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING', + 0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE', + 0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING', + // the rest of the flags are not supported + 0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored" + 0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC', + 0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE', + 0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ', + 0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE', + 0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE', + 0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY', + 0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW', + 0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE', + 0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO', + 0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP', + 0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM', + 0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER', ); // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 // see \phpseclib\Net\SFTP::_parseLongname() for an explanation @@ -426,6 +477,7 @@ class SFTP extends SSH2 $this->status_codes, $this->attributes, $this->open_flags, + $this->open_flags5, $this->file_types ); @@ -438,28 +490,31 @@ class SFTP extends SSH2 } /** - * Login + * Check a few things before SFTP functions are called * - * @param string $username * @return bool * @access public */ - function login($username) + function _precheck() { - if (!call_user_func_array('parent::login', func_get_args())) { + if (!($this->bitmap & SSH2::MASK_LOGIN)) { return false; } - return $this->_init_sftp_connection(); + if ($this->pwd === false) { + return $this->_init_sftp_connection(); + } + + return true; } /** - * (Re)initializes the SFTP channel + * Partially initialize an SFTP connection * * @return bool - * @access private + * @access public */ - function _init_sftp_connection() + function _partial_init_sftp_connection() { $this->window_size_server_to_client[self::CHANNEL] = $this->window_size; @@ -546,11 +601,13 @@ class SFTP extends SSH2 return false; } + $this->use_request_id = true; + if (strlen($response) < 4) { return false; } extract(unpack('Nversion', $this->_string_shift($response, 4))); - $this->version = $version; + $this->defaultVersion = $version; while (!empty($response)) { if (strlen($response) < 4) { return false; @@ -565,21 +622,22 @@ class SFTP extends SSH2 $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->partial_init = true; - $this->use_request_id = true; + return true; + } + + /** + * (Re)initializes the SFTP channel + * + * @return bool + * @access private + */ + function _init_sftp_connection() + { + if (!$this->partial_init && !$this->_partial_init_sftp_connection()) { + return false; + } /* A Note on SFTPv4/5/6 support: @@ -604,12 +662,60 @@ class SFTP extends SSH2 in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what \phpseclib\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->version = $this->defaultVersion; + if (isset($this->extensions['versions']) && (!$this->preferredVersion || $this->preferredVersion != $this->version)) { + $versions = explode(',', $this->extensions['versions']); + $supported = array(6, 5, 4); + if ($this->preferredVersion) { + $supported = array_diff($supported, array($this->preferredVersion)); + array_unshift($supported, $this->preferredVersion); + } + foreach ($supported as $ver) { + if (in_array($ver, $versions)) { + if ($ver === $this->version) { + break; + } + $this->version = (int) $ver; + $packet = pack('Na*Na*', strlen('version-select'), 'version-select', strlen($ver), $ver); + if (!$this->_send_sftp_packet(NET_SFTP_EXTENDED, $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; + } + + break; + } + } + } + + /* + 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']); + } + */ + + if ($this->version < 2 || $this->version > 6) { + return false; } $this->pwd = $this->_realpath('.'); @@ -697,6 +803,10 @@ class SFTP extends SSH2 */ function pwd() { + if (!$this->_precheck()) { + return false; + } + return $this->pwd; } @@ -738,6 +848,10 @@ class SFTP extends SSH2 */ function realpath($path) { + if (!$this->_precheck()) { + return false; + } + return $this->_realpath($path); } @@ -820,7 +934,7 @@ class SFTP extends SSH2 */ function chdir($dir) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -977,7 +1091,7 @@ class SFTP extends SSH2 */ function _list($dir, $raw = true) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1032,13 +1146,17 @@ class SFTP extends SSH2 } extract(unpack('Nlength', $this->_string_shift($response, 4))); $shortname = $this->_string_shift($response, $length); - if (strlen($response) < 4) { - return false; + // SFTPv4 "removed the long filename from the names structure-- it can now be + // built from information available in the attrs structure." + if ($this->version < 4) { + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $longname = $this->_string_shift($response, $length); } - extract(unpack('Nlength', $this->_string_shift($response, 4))); - $longname = $this->_string_shift($response, $length); $attributes = $this->_parseAttributes($response); - if (!isset($attributes['type'])) { + if (!isset($attributes['type']) && $this->version < 4) { $fileType = $this->_parseLongname($longname); if ($fileType) { $attributes['type'] = $fileType; @@ -1198,10 +1316,6 @@ class SFTP extends SSH2 */ function size($filename) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { - return false; - } - $result = $this->stat($filename); if ($result === false) { return false; @@ -1318,7 +1432,7 @@ class SFTP extends SSH2 */ function stat($filename) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1375,7 +1489,7 @@ class SFTP extends SSH2 */ function lstat($filename) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1489,7 +1603,7 @@ class SFTP extends SSH2 */ function touch($filename, $time = null, $atime = null) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1505,9 +1619,22 @@ class SFTP extends SSH2 $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->version < 4) { + $attr = pack('N3', NET_SFTP_ATTR_ACCESSTIME, $atime, $time); + } else { + $attr = pack('N5', + NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, + $atime / 4294967296, $atime, + $time / 4294967296, $time + ); + } + + $packet = pack('Na*', strlen($filename), $filename); + $packet.= $this->version >= 5 ? + pack('N2', 0, NET_SFTP_OPEN_OPEN_EXISTING) : + pack('N', NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_EXCL); + $packet.= $attr; + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } @@ -1530,19 +1657,47 @@ class SFTP extends SSH2 /** * Changes file or directory owner * + * $uid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string + * would be of the form "user@dns_domain" but it does not need to be. + * `$sftp->getSupportedVersions()['version']` will return the specific version + * that's being used. + * * Returns true on success or false on error. * * @param string $filename - * @param int $uid + * @param int|string $uid * @param bool $recursive * @return bool * @access public */ function chown($filename, $uid, $recursive = false) { - // quoting from , - // "if the owner or group is specified as -1, then that ID is not changed" - $attr = pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1); + /* + quoting , + + "To avoid a representation that is tied to a particular underlying + implementation at the client or server, the use of UTF-8 strings has + been chosen. The string should be of the form "user@dns_domain". + This will allow for a client and server that do not use the same + local representation the ability to translate to a common syntax that + can be interpreted by both. In the case where there is no + translation available to the client or server, the attribute value + must be constructed without the "@"." + + phpseclib _could_ auto append the dns_domain to $uid BUT what if it shouldn't + have one? phpseclib would have no way of knowing so rather than guess phpseclib + will just use whatever value the user provided + */ + + $attr = $this->version < 4 ? + // quoting , + // "if the owner or group is specified as -1, then that ID is not changed" + pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1) : + // quoting , + // "If either the owner or group field is zero length, the field should be + // considered absent, and no change should be made to that specific field + // during a modification operation" + pack('NNa*Na*', NET_SFTP_ATTR_OWNERGROUP, strlen($uid), $uid, 0, ''); return $this->_setstat($filename, $attr, $recursive); } @@ -1550,17 +1705,24 @@ class SFTP extends SSH2 /** * Changes file or directory group * + * $gid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string + * would be of the form "user@dns_domain" but it does not need to be. + * `$sftp->getSupportedVersions()['version']` will return the specific version + * that's being used. + * * Returns true on success or false on error. * * @param string $filename - * @param int $gid + * @param int|string $gid * @param bool $recursive * @return bool * @access public */ function chgrp($filename, $gid, $recursive = false) { - $attr = pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid); + $attr = $this->version < 4 ? + pack('N3', NET_SFTP_ATTR_UIDGID, $gid, -1) : + pack('NNa*Na*', NET_SFTP_ATTR_OWNERGROUP, 0, '', strlen($gid), $gid); return $this->_setstat($filename, $attr, $recursive); } @@ -1627,7 +1789,7 @@ class SFTP extends SSH2 */ function _setstat($filename, $attr, $recursive) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1645,9 +1807,10 @@ class SFTP extends SSH2 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))) { + $packet = $this->version >= 4 ? + pack('Na*a*Ca*', strlen($filename), $filename, substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) : + pack('Na*a*', strlen($filename), $filename, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { return false; } @@ -1717,7 +1880,10 @@ class SFTP extends SSH2 return false; } } else { - if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($temp), $temp, $attr))) { + $packet = $this->version >= 4 ? + pack('Na*Ca*', strlen($temp), $temp, NET_SFTP_TYPE_UNKNOWN, $attr) : + pack('Na*a*', strlen($temp), $temp, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { return false; } @@ -1732,7 +1898,10 @@ class SFTP extends SSH2 } } - if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, pack('Na*a*', strlen($path), $path, $attr))) { + $packet = $this->version >= 4 ? + pack('Na*Ca*', strlen($temp), $temp, NET_SFTP_TYPE_UNKNOWN, $attr) : + pack('Na*a*', strlen($temp), $temp, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { return false; } @@ -1757,7 +1926,7 @@ class SFTP extends SSH2 */ function readlink($link) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1807,15 +1976,44 @@ class SFTP extends SSH2 */ function symlink($target, $link) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { 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)) { + /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-12.1 : + + Changed the SYMLINK packet to be LINK and give it the ability to + create hard links. Also change it's packet number because many + implementation implemented SYMLINK with the arguments reversed. + Hopefully the new argument names make it clear which way is which. + */ + if ($this->version == 6) { + $type = NET_SFTP_LINK; + $packet = pack('Na*Na*C', strlen($link), $link, strlen($target), $target, 1); + } else { + $type = NET_SFTP_SYMLINK; + /* quoting http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL#347 : + + 3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK + + When OpenSSH's sftp-server was implemented, the order of the arguments + to the SSH_FXP_SYMLINK method was inadvertently reversed. Unfortunately, + the reversal was not noticed until the server was widely deployed. Since + fixing this to follow the specification would cause incompatibility, the + current order was retained. For correct operation, clients should send + SSH_FXP_SYMLINK as follows: + + uint32 id + string targetpath + string linkpath */ + $packet = substr($this->server_identifier, 0, 15) == 'SSH-2.0-OpenSSH' ? + pack('Na*Na*', strlen($target), $target, strlen($link), $link) : + pack('Na*Na*', strlen($link), $link, strlen($target), $target); + } + if (!$this->_send_sftp_packet($type, $packet)) { return false; } @@ -1848,7 +2046,7 @@ class SFTP extends SSH2 */ function mkdir($dir, $mode = -1, $recursive = false) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1917,7 +2115,7 @@ class SFTP extends SSH2 */ function rmdir($dir) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -1966,7 +2164,8 @@ class SFTP extends SSH2 * 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. * - * Setting $mode to self::SOURCE_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 + * Setting $mode to self::SOURCE_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 * * If $data is a resource then it'll be used as a resource instead. * @@ -2002,7 +2201,7 @@ class SFTP extends SSH2 */ function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -2013,10 +2212,14 @@ class SFTP extends SSH2 $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 & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; + if ($this->version >= 5) { + $flags = NET_SFTP_OPEN_OPEN_OR_CREATE; + } else { + $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 & SFTP::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; + } if ($start >= 0) { $offset = $start; @@ -2026,10 +2229,17 @@ class SFTP extends SSH2 $offset = $size !== false ? $size : 0; } else { $offset = 0; - $flags|= NET_SFTP_OPEN_TRUNCATE; + if ($this->version >= 5) { + $flags = NET_SFTP_OPEN_CREATE_TRUNCATE; + } else { + $flags|= NET_SFTP_OPEN_TRUNCATE; + } } - $packet = pack('Na*N2', strlen($remote_file), $remote_file, $flags, 0); + $packet = pack('Na*', strlen($remote_file), $remote_file); + $packet.= $this->version >= 5 ? + pack('N3', 0, $flags, 0) : + pack('N2', $flags, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } @@ -2138,6 +2348,8 @@ class SFTP extends SSH2 } } + $result = $this->_close_handle($handle); + if (!$this->_read_put_responses($i)) { if ($mode & self::SOURCE_LOCAL_FILE) { fclose($fp); @@ -2146,18 +2358,30 @@ class SFTP extends SSH2 return false; } - if ($mode & self::SOURCE_LOCAL_FILE) { - if ($this->preserveTime) { - $stat = fstat($fp); - $this->touch($remote_file, $stat['mtime'], $stat['atime']); - } - + if ($mode & SFTP::SOURCE_LOCAL_FILE) { if (isset($fp) && is_resource($fp)) { fclose($fp); } + + if ($this->preserveTime) { + $stat = stat($data); + if ($this->version < 4) { + $attr = pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['mtime']); + } else { + $attr = pack('N5', + NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, + $stat['atime'] / 4294967296, $stat['atime'], + $stat['mtime'] / 4294967296, $stat['mtime'] + ); + } + + if (!$this->_setstat($remote_file, $attr, false)) { + user_error('Error setting file time'); + } + } } - return $this->_close_handle($handle); + return $result; } /** @@ -2244,7 +2468,7 @@ class SFTP extends SSH2 */ function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -2253,7 +2477,10 @@ class SFTP extends SSH2 return false; } - $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0); + $packet = pack('Na*', strlen($remote_file), $remote_file); + $packet.= $this->version >= 5 ? + pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) : + pack('N2', NET_SFTP_OPEN_READ, 0); if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { return false; } @@ -2355,6 +2582,7 @@ class SFTP extends SSH2 } // maybe the file was successfully transferred, maybe it wasn't if ($this->channel_close) { + $this->partial_init = false; $this->_init_sftp_connection(); return false; } else { @@ -2404,7 +2632,7 @@ class SFTP extends SSH2 */ function delete($path, $recursive = true) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -2533,6 +2761,10 @@ class SFTP extends SSH2 function file_exists($path) { if ($this->use_stat_cache) { + if (!$this->_precheck()) { + return false; + } + $path = $this->_realpath($path); $result = $this->_query_stat_cache($path); @@ -2603,6 +2835,10 @@ class SFTP extends SSH2 */ function is_readable($path) { + if (!$this->_precheck()) { + return false; + } + $path = $this->_realpath($path); $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_READ, 0); @@ -2631,6 +2867,10 @@ class SFTP extends SSH2 */ function is_writable($path) { + if (!$this->_precheck()) { + return false; + } + $path = $this->_realpath($path); $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_WRITE, 0); @@ -2811,6 +3051,10 @@ class SFTP extends SSH2 */ function _get_xstat_cache_prop($path, $prop, $type) { + if (!$this->_precheck()) { + return false; + } + if ($this->use_stat_cache) { $path = $this->_realpath($path); @@ -2831,7 +3075,9 @@ class SFTP extends SSH2 } /** - * Renames a file or a directory on the SFTP server + * Renames a file or a directory on the SFTP server. + * + * If the file already exists this will return false * * @param string $oldname * @param string $newname @@ -2840,7 +3086,7 @@ class SFTP extends SSH2 */ function rename($oldname, $newname) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->_precheck()) { return false; } @@ -2852,6 +3098,18 @@ class SFTP extends SSH2 // 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->version >= 5) { + /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-6.5 , + + 'flags' is 0 or a combination of: + + SSH_FXP_RENAME_OVERWRITE 0x00000001 + SSH_FXP_RENAME_ATOMIC 0x00000002 + SSH_FXP_RENAME_NATIVE 0x00000004 + + (none of these are currently supported) */ + $packet.= "\0\0\0\0"; + } if (!$this->_send_sftp_packet(NET_SFTP_RENAME, $packet)) { return false; } @@ -2881,6 +3139,31 @@ class SFTP extends SSH2 return true; } + /** + * Parse Time + * + * See '7.7. Times' of draft-ietf-secsh-filexfer-13 for more info. + * + * @param string $key + * @param int $flags + * @param string $response + * @return array + * @access private + */ + function _parseTime($key, $flags, &$response) + { + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return array(); + } + $attr = array(); + $attr[$key] = hexdec(bin2hex($this->_string_shift($response, 8))); + if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) { + $attr+= extract(unpack('N' . $key . '_nseconds', $this->_string_shift($response, 4))); + } + return $attr; + } + /** * Parse Attributes * @@ -2892,16 +3175,26 @@ class SFTP extends SSH2 */ function _parseAttributes(&$response) { + if ($this->version >= 4) { + $length = 5; + $format = 'Nflags/Ctype'; + } else { + $length = 4; + $format = 'Nflags'; + } + $attr = array(); - if (strlen($response) < 4) { + if (strlen($response) < $length) { 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 + extract(unpack($format, $this->_string_shift($response, $length))); + if (isset($type)) { + $attr['type'] = $type; + } foreach ($this->attributes as $key => $value) { switch ($flags & $key) { - case NET_SFTP_ATTR_SIZE: // 0x00000001 + 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 @@ -2910,14 +3203,14 @@ class SFTP extends SSH2 // 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) + case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 or earlier) 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 + case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; @@ -2931,14 +3224,134 @@ class SFTP extends SSH2 $attr+= array('type' => $fileType); } break; - case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 + case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 + if ($this->version >= 4) { + $attr+= $this->_parseTime('atime', $flags, $response); + break; + } 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 + case NET_SFTP_ATTR_CREATETIME: // 0x00000010 (SFTPv4+) + $attr+= $this->_parseTime('createtime', $flags, $response); + break; + case NET_SFTP_ATTR_MODIFYTIME: // 0x00000020 + $attr+= $this->_parseTime('mtime', $flags, $response); + break; + case NET_SFTP_ATTR_ACL: // 0x00000040 + // access control list + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-04#section-5.7 + // currently unsupported + 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) < 16) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Ntype/Nflag/Nmask/Nlength', $this->_string_shift($response, 16))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $this->_string_shift($response, $length); // who + } + break; + case NET_SFTP_ATTR_OWNERGROUP: // 0x00000080 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['owner'] = $this->_string_shift($response, $length); + + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['group'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_SUBSECOND_TIMES: // 0x00000100 + break; + case NET_SFTP_ATTR_BITS: // 0x00000200 (SFTPv5+) + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-5.8 + // currently unsupported + // tells if you file is: + // readonly, system, hidden, case inensitive, archive, encrypted, compressed, sparse + // append only, immutable, sync + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nattrib-bits/Nattrib-bits-valid', $this->_string_shift($response, 8))); + break; + case NET_SFTP_ATTR_ALLOCATION_SIZE: // 0x00000400 (SFTPv6+) + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.4 + // represents the number of bytes htat the file consumes on the disk. will + // usually be larger than the 'size' field + $attr['allocation-size'] = hexdec(bin2hex($this->_string_shift($response, 8))); + break; + case NET_SFTP_ATTR_TEXT_HINT: // 0x00000800 + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.10 + // currently unsupported + // tells if file is "known text", "guessed text", "known binary", "guessed binary" + extract(unpack('Ctext-hint', $this->_string_shift($response))); + break; + case NET_SFTP_ATTR_MIME_TYPE: // 0x00001000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['mime-type'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_LINK_COUNT: // 0x00002000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + $attr+= unpack('Nlink-count', $this->_string_shift($response, 4)); + break; + case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['untranslated-name'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_CTIME: // 0x00008000 + // 'ctime' contains the last time the file attributes were changed. The + // exact meaning of this field depends on the server. + $attr+= $this->_parseTime('ctime', $flags, $response); + break; + case NET_SFTP_ATTR_EXTENDED: // 0x80000000 if (strlen($response) < 4) { user_error('Malformed file attributes'); return $attr; @@ -3159,7 +3572,6 @@ class SFTP extends SSH2 $tempLength = $length; $tempLength-= strlen($this->packet_buffer); - // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h if (!$this->allow_arbitrary_length_packets && !$this->use_request_id && $tempLength > 256 * 1024) { user_error('Invalid SFTP packet size'); @@ -3279,13 +3691,51 @@ class SFTP extends SSH2 */ function getSupportedVersions() { - $temp = array('version' => $this->version); + if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { + return false; + } + + if (!$this->partial_init) { + $this->_partial_init_sftp_connection(); + } + + $temp = array('version' => $this->defaultVersion); if (isset($this->extensions['versions'])) { $temp['extensions'] = $this->extensions['versions']; } return $temp; } + /** + * Get supported SFTP versions + * + * @return array + * @access public + */ + function getNegotiatedVersion() + { + if (!$this->_precheck()) { + return false; + } + + return $this->version; + } + + /** + * Set preferred version + * + * If you're preferred version isn't supported then the highest supported + * version of SFTP will be utilized. Set to null or false or int(0) to + * unset the preferred version + * + * @param int $version + * @access public + */ + function setPreferredVersion($version) + { + $this->preferredVersion = $version; + } + /** * Disconnect *