Merge pull request #1697 from terrafrost/sftpv456-3.0

add SFTP v4/5/6 support to phpseclib v3
This commit is contained in:
terrafrost 2021-09-28 20:39:40 -05:00 committed by GitHub
commit d8ea63dbdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 517 additions and 116 deletions

View File

@ -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
extract(unpack('Nupper/Nlower', self::shift($data, 8)));
$temp = $upper ? 4294967296 * $upper : 0;
$temp+= $lower < 0 ? ($lower & 0x7FFFFFFFF) + 0x80000000 : $lower;
// $temp = hexdec(bin2hex(self::shift($data, 8)));
$result[] = $temp;
continue 2;
}
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;

View File

@ -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',
@ -402,8 +419,20 @@ class SFTP extends SSH2
$this->attributes = [
0x00000001 => 'NET_SFTP_ATTR_SIZE',
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,14 +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:
$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;
default:
}
$this->version = (int) $ver;
$packet = Strings::packSSH2('ss', 'version-select', "$ver");
$this->send_sftp_packet(NET_SFTP_EXTENDED, $packet);
$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 = true;
$this->pwd = $this->realpath('.');
$this->update_stat_cache($this->pwd, []);
@ -686,6 +787,10 @@ class SFTP extends SSH2
*/
public function pwd()
{
if (!$this->precheck()) {
return false;
}
return $this->pwd;
}
@ -729,11 +834,15 @@ class SFTP extends SSH2
*/
public function realpath($path)
{
if ($this->precheck() === false) {
return false;
}
if (!$this->canonicalize_paths) {
return $path;
}
if ($this->pwd === false) {
if ($this->pwd === true) {
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
$this->send_sftp_packet(NET_SFTP_REALPATH, Strings::packSSH2('s', $path));
@ -787,7 +896,7 @@ class SFTP extends SSH2
*/
public function chdir($dir)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -941,7 +1050,7 @@ class SFTP extends SSH2
*/
private function readlist($dir, $raw = true)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -984,9 +1093,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 +1354,7 @@ class SFTP extends SSH2
*/
public function stat($filename)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -1297,7 +1411,7 @@ class SFTP extends SSH2
*/
public function lstat($filename)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -1392,7 +1506,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('NQ', NET_SFTP_ATTR_SIZE, $new_size);
return $this->setstat($filename, $attr, false);
}
@ -1411,7 +1525,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 +1541,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 +1571,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 <http://www.kernel.org/doc/man-pages/online/pages/man2/chown.2.html>,
/*
quoting <https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.5>,
"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 <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);
pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1) :
// quoting <https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.5>,
// "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"
Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, $uid, '');
return $this->setstat($filename, $attr, $recursive);
}
@ -1470,17 +1619,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) :
Strings::packSSH2('Nss', NET_SFTP_ATTR_OWNERGROUP, '', $gid);
return $this->setstat($filename, $attr, $recursive);
}
@ -1547,7 +1703,7 @@ class SFTP extends SSH2
*/
private function setstat($filename, $attr, $recursive)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -1565,9 +1721,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 +1790,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 +1807,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 +1835,7 @@ class SFTP extends SSH2
*/
public function readlink($link)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -1713,15 +1879,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 +1944,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 +2009,7 @@ class SFTP extends SSH2
*/
public function rmdir($dir)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -1858,7 +2053,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 +2094,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 +2103,16 @@ class SFTP extends SSH2
return false;
}
$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 +2122,19 @@ class SFTP extends SSH2
$offset = $size !== false ? $size : 0;
} else {
$offset = 0;
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_file);
$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 +2241,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 +2251,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 +2349,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 +2358,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 +2462,7 @@ class SFTP extends SSH2
fclose($fp);
}
if ($this->channel_close) {
$this->partial_init = false;
$this->init_sftp_connection();
return false;
} else {
@ -2294,7 +2514,7 @@ class SFTP extends SSH2
*/
public function delete($path, $recursive = true)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -2415,6 +2635,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 +2709,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 +2737,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 +2917,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 +2941,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 +2953,7 @@ class SFTP extends SSH2
*/
public function rename($oldname, $newname)
{
if (!($this->bitmap & SSH2::MASK_LOGIN)) {
if (!$this->precheck()) {
return false;
}
@ -2727,6 +2965,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 +3001,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,10 +3033,12 @@ class SFTP extends SSH2
*/
protected function parseAttributes(&$response)
{
$attr = [];
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
@ -2775,9 +3048,7 @@ class SFTP extends SSH2
// 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 +3056,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 ($this->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 +3452,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
*

View File

@ -216,7 +216,7 @@ class SSH2
* @var array|false
* @access private
*/
private $server_identifier = false;
protected $server_identifier = false;
/**
* Key Exchange Algorithms