diff --git a/phpseclib/Common/Functions/Strings.php b/phpseclib/Common/Functions/Strings.php index 666fce59..b3b9f071 100644 --- a/phpseclib/Common/Functions/Strings.php +++ b/phpseclib/Common/Functions/Strings.php @@ -70,6 +70,7 @@ abstract class Strings * C = byte * b = boolean (true/false) * N = uint32 + * Q = uint64 * s = string * i = mpint * L = name-list @@ -100,6 +101,12 @@ abstract class Strings throw new \LengthException('At least four byte needs to be present for successful N / i / s / L decodes'); } break; + case 'Q': + if (strlen($data) < 8) { + throw new \LengthException('At least eight byte needs to be present for successful N / i / s / L decodes'); + } + break; + default: throw new \InvalidArgumentException('$format contains an invalid character'); } @@ -114,6 +121,19 @@ abstract class Strings list(, $temp) = unpack('N', self::shift($data, 4)); $result[] = $temp; continue 2; + case 'Q': + // pack() added support for Q in PHP 5.6.3 and PHP 5.6 is phpseclib 3's minimum version + // so in theory we could support this BUT, "64-bit format codes are not available for + // 32-bit versions" and phpseclib works on 32-bit installs. on 32-bit installs + // 64-bit floats can be used to get larger numbers then 32-bit signed ints would allow + // for. sure, you're not gonna get the full precision of 64-bit numbers but just because + // you need > 32-bit precision doesn't mean you need the full 64-bit precision + list(, $upper, $lower) = unpack('NN', self::shift($data, 8)); + $temp = $upper ? 4294967296 * $lower : 0; + $temp+= $lower < 0 ? ($temp & 0x7FFFFFFFF) + 0x80000000 : $temp; + // $temp = hexdec(bin2hex(self::shift($data, 8))); + $result[] = $temp; + continue; } list(, $length) = unpack('N', self::shift($data, 4)); if (strlen($data) < $length) { @@ -165,6 +185,13 @@ abstract class Strings } $result.= $element ? "\1" : "\0"; break; + case 'Q': + if (!is_int($element) || !is_float($element)) { + throw new \InvalidArgumentException('An integer was expected.'); + } + // 4294967296 == 1 << 32 + $result.= pack('NN', $element / 4294967296, $element); + break; case 'N': if (is_float($element)) { $element = (int) $element; diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index a6f054a1..86f136ae 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}. * @@ -169,6 +167,24 @@ class SFTP extends SSH2 */ private $version; + /** + * Default Server SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + private $defaultVersion; + + /** + * Preferred SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + private $preferredVersion = 3; + /** * Current working directory * @@ -297,7 +313,7 @@ class SFTP extends SSH2 * @var bool * @access private */ - var $allow_arbitrary_length_packets = false; + private $allow_arbitrary_length_packets = false; /** * Was the last packet due to the channels being closed or not? @@ -309,6 +325,14 @@ class SFTP extends SSH2 */ private $channel_close = false; + /** + * Has the SFTP channel been partially negotiated? + * + * @var bool + * @access private + */ + private $partial_init = false; + /** * Default Constructor. * @@ -329,15 +353,13 @@ class SFTP extends SSH2 $this->packet_types = [ 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', @@ -345,18 +367,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', @@ -401,9 +418,21 @@ class SFTP extends SSH2 // the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why $this->attributes = [ 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. @@ -419,7 +448,32 @@ class SFTP extends SSH2 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 = [ + // 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 \phpseclib3\Net\SFTP::_parseLongname() for an explanation @@ -441,6 +495,7 @@ class SFTP extends SSH2 $this->status_codes, $this->attributes, $this->open_flags, + $this->open_flags5, $this->file_types ); @@ -453,31 +508,32 @@ class SFTP extends SSH2 } /** - * Login + * Check a few things before SFTP functions are called + * + * @return bool + * @access public + */ + private function precheck() + { + if (!($this->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + if ($this->pwd === false) { + return $this->init_sftp_connection(); + } + + return true; + } + + /** + * Partially initialize an SFTP connection * - * @param string $username - * @param string|AsymmetricKey|array[]|Agent|null ...$args * @throws \UnexpectedValueException on receipt of unexpected packets * @return bool * @access public */ - public function login($username, ...$args) - { - if (!parent::login(...func_get_args())) { - return false; - } - - return $this->init_sftp_connection(); - } - - /** - * (Re)initializes the SFTP channel - * - * @throws \UnexpectedValueException on receipt of unexpected packets - * @return bool - * @access private - */ - private function init_sftp_connection() + private function partial_init_sftp_connection() { $this->window_size_server_to_client[self::CHANNEL] = $this->window_size; @@ -548,27 +604,30 @@ class SFTP extends SSH2 . 'Got packet type: ' . $this->packet_type); } - list($this->version) = Strings::unpackSSH2('N', $response); + $this->use_request_id = true; + + list($this->defaultVersion) = Strings::unpackSSH2('N', $response); while (!empty($response)) { list($key, $value) = Strings::unpackSSH2('ss', $response); $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 + */ + private function init_sftp_connection() + { + if (!$this->partial_init && !$this->partial_init_sftp_connection()) { + return false; + } /* A Note on SFTPv4/5/6 support: @@ -593,12 +652,56 @@ class SFTP extends SSH2 in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what \phpseclib3\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 = [6, 5, 4]; + if ($this->preferredVersion) { + $supported = array_diff($supported, [$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 = Strings::packSSH2('ss', 'version-select', $ver); + if (!$this->send_sftp_packet(NET_SFTP_EXTENDED, $packet)) { + return false; + } + $response = $this->get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + throw new \UnexpectedValueException('Expected NET_SFTP_STATUS. ' + . 'Got packet type: ' . $this->packet_type); + } + list($status) = Strings::unpackSSH2('N', $response); + if ($status != NET_SFTP_STATUS_OK) { + $this->logError($response, $status); + throw new \UnexpectedValueException('Expected NET_SFTP_STATUS_OK. ' + . ' Got ' . $status); + } + 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('.'); @@ -686,6 +789,10 @@ class SFTP extends SSH2 */ public function pwd() { + if (!$this->precheck()) { + return false; + } + return $this->pwd; } @@ -729,6 +836,10 @@ class SFTP extends SSH2 */ public function realpath($path) { + if (!$this->precheck()) { + return false; + } + if (!$this->canonicalize_paths) { return $path; } @@ -787,7 +898,7 @@ class SFTP extends SSH2 */ public function chdir($dir) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -941,7 +1052,7 @@ class SFTP extends SSH2 */ private function readlist($dir, $raw = true) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -984,9 +1095,14 @@ class SFTP extends SSH2 case NET_SFTP_NAME: list($count) = Strings::unpackSSH2('N', $response); for ($i = 0; $i < $count; $i++) { - list($shortname, $longname) = Strings::unpackSSH2('ss', $response); + list($shortname) = Strings::unpackSSH2('s', $response); + // 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) { + list($longname) = Strings::unpackSSH2('s', $response); + } $attributes = $this->parseAttributes($response); - if (!isset($attributes['type'])) { + if (!isset($attributes['type']) && $this->version < 4) { $fileType = $this->parseLongname($longname); if ($fileType) { $attributes['type'] = $fileType; @@ -1240,7 +1356,7 @@ class SFTP extends SSH2 */ public function stat($filename) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1297,7 +1413,7 @@ class SFTP extends SSH2 */ public function lstat($filename) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1392,7 +1508,7 @@ class SFTP extends SSH2 */ public function truncate($filename, $new_size) { - $attr = pack('N3', NET_SFTP_ATTR_SIZE, $new_size / 4294967296, $new_size); // 4294967296 == 0x100000000 == 1<<32 + $attr = Strings::packSSH2('Q', NET_SFTP_ATTR_SIZE, $new_size); return $this->setstat($filename, $attr, false); } @@ -1411,7 +1527,7 @@ class SFTP extends SSH2 */ public function touch($filename, $time = null, $atime = null) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1427,9 +1543,16 @@ 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 = Strings::packSSH2('sN', $filename, $flags) . $attr; + $attr = $this->version < 4 ? + pack('N3', NET_SFTP_ATTR_ACCESSTIME, $atime, $time) : + Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $atime, $time); + + $packet = Strings::packSSH2('s', $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; + $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); @@ -1450,19 +1573,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 */ 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); } @@ -1470,17 +1621,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 */ 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); } @@ -1547,7 +1705,7 @@ class SFTP extends SSH2 */ private function setstat($filename, $attr, $recursive) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1565,9 +1723,11 @@ 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. - $this->send_sftp_packet(NET_SFTP_SETSTAT, Strings::packSSH2('s', $filename) . $attr); + $packet = Strings:packSSH2('s', $filename); + $packet.= $this->version >= 4 ? + pack('a*Ca*', substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) : + $attr; + $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); /* "Because some systems must use separate system calls to set various attributes, it is possible that a failure @@ -1632,7 +1792,11 @@ class SFTP extends SSH2 return false; } } else { - $this->send_sftp_packet(NET_SFTP_SETSTAT, Strings::packSSH2('s', $temp) . $attr); + $packet = Strings::packSSH2('s', $temp); + $packet.= $this->version >= 4 ? + pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) : + $attr; + $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); $i++; @@ -1645,7 +1809,11 @@ class SFTP extends SSH2 } } - $this->send_sftp_packet(NET_SFTP_SETSTAT, Strings::packSSH2('s', $path) . $attr); + $packet = Strings::packSSH2('s', $path); + $packet.= $this->version >= 4 ? + pack('Ca*', NET_SFTP_TYPE_UNKNOWN, $attr) : + $atr; + $this->send_sftp_packet(NET_SFTP_SETSTAT, $packet); $i++; @@ -1669,7 +1837,7 @@ class SFTP extends SSH2 */ public function readlink($link) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1713,15 +1881,44 @@ class SFTP extends SSH2 */ public function symlink($target, $link) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } //$target = $this->realpath($target); $link = $this->realpath($link); - $packet = Strings::packSSH2('ss', $target, $link); - $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 = Strings::packSSH2('ssC', $link, $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' ? + Strings::packSSH2('SS', $target, $link) : + Strings::packSSH2('SS', $link, $target); + } + $this->send_sftp_packet($type, $packet); $response = $this->get_sftp_packet(); if ($this->packet_type != NET_SFTP_STATUS) { @@ -1749,7 +1946,7 @@ class SFTP extends SSH2 */ public function mkdir($dir, $mode = -1, $recursive = false) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1814,7 +2011,7 @@ class SFTP extends SSH2 */ public function rmdir($dir) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -1858,7 +2055,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. * @@ -1898,7 +2096,7 @@ class SFTP extends SSH2 */ public 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; } @@ -1907,10 +2105,16 @@ class SFTP extends SSH2 return false; } - $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; + $this->remove_from_stat_cache($remote_file); + + 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 & self::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; + } if ($start >= 0) { $offset = $start; @@ -1920,12 +2124,19 @@ 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; + } } $this->remove_from_stat_cache($remote_file); - $packet = Strings::packSSH2('sNN', $remote_file, $flags, 0); + $packet = Strings::packSSH2('s', $remote_filename); + $packet.= $this->version >= 5 ? + pack('N3', 0, $flags, 0) : + pack('N2', $flags, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); @@ -2032,6 +2243,8 @@ class SFTP extends SSH2 } } + $result = $this->close_handle($handle); + if (!$this->read_put_responses($i)) { if ($mode & self::SOURCE_LOCAL_FILE) { fclose($fp); @@ -2040,18 +2253,23 @@ 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); + $attr = $this->version < 4 ? + pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['time']) : + Strings::packSSH2('NQ2', NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, $stat['atime'], $stat['time']); + if (!$this->setstat($remote_file, $attr, false)) { + throw new \RuntimeException('Error setting file time'); + } + } } - return $this->close_handle($handle); + return $result; } /** @@ -2133,7 +2351,7 @@ class SFTP extends SSH2 */ public function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -2142,7 +2360,10 @@ class SFTP extends SSH2 return false; } - $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0); + $packet = Strings::packSSH2('s', $remote_file); + $packet.= $this->version >= 5 ? + pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) : + pack('N2', NET_SFTP_OPEN_READ, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); $response = $this->get_sftp_packet(); @@ -2243,6 +2464,7 @@ class SFTP extends SSH2 fclose($fp); } if ($this->channel_close) { + $this->partial_init = false; $this->init_sftp_connection(); return false; } else { @@ -2294,7 +2516,7 @@ class SFTP extends SSH2 */ public function delete($path, $recursive = true) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -2415,6 +2637,10 @@ class SFTP extends SSH2 public function file_exists($path) { if ($this->use_stat_cache) { + if (!$this->precheck()) { + return false; + } + $path = $this->realpath($path); $result = $this->query_stat_cache($path); @@ -2485,6 +2711,10 @@ class SFTP extends SSH2 */ public function is_readable($path) { + if (!$this->precheck()) { + return false; + } + $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_READ, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); @@ -2509,6 +2739,10 @@ class SFTP extends SSH2 */ public function is_writable($path) { + if (!$this->precheck()) { + return false; + } + $packet = Strings::packSSH2('sNN', $this->realpath($path), NET_SFTP_OPEN_WRITE, 0); $this->send_sftp_packet(NET_SFTP_OPEN, $packet); @@ -2685,6 +2919,10 @@ class SFTP extends SSH2 */ private function get_xstat_cache_prop($path, $prop, $type) { + if (!$this->precheck()) { + return false; + } + if ($this->use_stat_cache) { $path = $this->realpath($path); @@ -2705,7 +2943,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 @@ -2715,7 +2955,7 @@ class SFTP extends SSH2 */ public function rename($oldname, $newname) { - if (!($this->bitmap & SSH2::MASK_LOGIN)) { + if (!$this->precheck()) { return false; } @@ -2727,6 +2967,18 @@ class SFTP extends SSH2 // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 $packet = Strings::packSSH2('ss', $oldname, $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"; + } $this->send_sftp_packet(NET_SFTP_RENAME, $packet); $response = $this->get_sftp_packet(); @@ -2751,6 +3003,27 @@ 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 + */ + private function parseTime($key, $flags, &$response) + { + $attr = []; + list($attr[$key]) = Strings::unpackSSH2('Q', $response); + if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) { + list($attr[key . '-nseconds']) = Strings::unpackSSH2('N', $response); + } + return $attr; + } + /** * Parse Attributes * @@ -2762,22 +3035,22 @@ class SFTP extends SSH2 */ protected function parseAttributes(&$response) { - $attr = []; - list($flags) = Strings::unpackSSH2('N', $response); + if ($this->version >= 4) { + list($flags, $attr['type']) = Strings::unpackSSH2('NC', $response); + } else { + list($flags) = Strings::unpackSSH2('N', $response); + } - // 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 + 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. - list($upper, $size) = Strings::unpackSSH2('NN', $response); - $attr['size'] = $upper ? 4294967296 * $upper : 0; - $attr['size']+= $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size; + list($attr['size']) = Strings::unpackSSH2('Q', $response); break; case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 only) list($attr['uid'], $attr['gid']) = Strings::unpackSSH2('NN', $response); @@ -2785,13 +3058,78 @@ class SFTP extends SSH2 case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 list($attr['mode']) = Strings::unpackSSH2('N', $response); $fileType = $this->parseMode($attr['mode']); - if ($fileType !== false) { + if ($version < 4 && $fileType !== false) { $attr+= ['type' => $fileType]; } break; case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 + if ($this->version >= 4) { + $attr+= $this->parseTime('atime', $flags, $response); + break; + } list($attr['atime'], $attr['mtime']) = Strings::unpackSSH2('NN', $response); break; + 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 + list($count) = Strings::unpackSSH2('N', $response); + for ($i = 0; $i < $count; $i++) { + list($type, $flag, $mask, $who) = Strings::unpackSSH2('N3s', $result); + } + break; + case NET_SFTP_ATTR_OWNERGROUP: // 0x00000080 + list($attr['owner'], $attr['$group']) = Strings::unpackSSH2('ss', $response); + 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 + list($attrib_bits, $attrib_bits_valid) = Strings::unpackSSH2('N2', $response); + // if we were actually gonna implement the above it ought to be + // $attr['attrib-bits'] and $attr['attrib-bits-valid'] + // eg. - instead of _ + 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 that the file consumes on the disk. will + // usually be larger than the 'size' field + list($attr['allocation-size']) = Strings::unpack('Q', $response); + 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" + list($text_hint) = Strings::unpackSSH2('C', $response); + // the above should be $attr['text-hint'] + break; + case NET_SFTP_ATTR_MIME_TYPE: // 0x00001000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11 + list($attr['mime-type']) = Strings::unpackSSH2('s', $response); + break; + case NET_SFTP_ATTR_LINK_COUNT: // 0x00002000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12 + list($attr['link-count']) = Strings::unpackSS2('N', $response); + break; + case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13 + list($attr['untranslated-name']) = Strings::unpackSSH2('s', $response); + 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 list($count) = Strings::unpackSSH2('N', $response); for ($i = 0; $i < $count; $i++) { @@ -3116,13 +3454,51 @@ class SFTP extends SSH2 */ public function getSupportedVersions() { - $temp = ['version' => $this->version]; + if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) { + return false; + } + + if (!$this->partial_init) { + $this->partial_init_sftp_connection(); + } + + $temp = ['version' => $this->defaultVersion]; if (isset($this->extensions['versions'])) { $temp['extensions'] = $this->extensions['versions']; } return $temp; } + /** + * Get supported SFTP versions + * + * @return array + * @access public + */ + 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 + */ + public function setPreferredVersion($version) + { + $this->preferredVersion = $version; + } + /** * Disconnect *