Files
friendica/library/phpsec/Net/SFTP.php
T

1461 lines
52 KiB
PHP

<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* Pure-PHP implementation of SFTP.
*
* PHP versions 4 and 5
*
* Currently only supports SFTPv3, which, according to wikipedia.org, "is the most widely used version,
* implemented by the popular OpenSSH SFTP server". If you want SFTPv4/5/6 support, provide me with access
* to an SFTPv4/5/6 server.
*
* The API for this library is modeled after the API from PHP's {@link http://php.net/book.ftp FTP extension}.
*
* Here's a short example of how to use this library:
* <code>
* <?php
* include('Net/SFTP.php');
*
* $sftp = new Net_SFTP('www.domain.tld');
* if (!$sftp->login('username', 'password')) {
* exit('Login Failed');
* }
*
* echo $sftp->pwd() . "\r\n";
* $sftp->put('filename.ext', 'hello, world!');
* print_r($sftp->nlist());
* ?>
* </code>
*
* LICENSE: This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston,
* MA 02111-1307 USA
*
* @category Net
* @package Net_SFTP
* @author Jim Wigginton <terrafrost@php.net>
* @copyright MMIX Jim Wigginton
* @license http://www.gnu.org/licenses/lgpl.txt
* @version $Id: SFTP.php,v 1.21 2010/04/09 02:31:34 terrafrost Exp $
* @link http://phpseclib.sourceforge.net
*/
/**
* Include Net_SSH2
*/
require_once('Net/SSH2.php');
/**#@+
* @access public
* @see Net_SFTP::getLog()
*/
/**
* Returns the message numbers
*/
define('NET_SFTP_LOG_SIMPLE', NET_SSH2_LOG_SIMPLE);
/**
* Returns the message content
*/
define('NET_SFTP_LOG_COMPLEX', NET_SSH2_LOG_COMPLEX);
/**#@-*/
/**
* SFTP channel constant
*
* Net_SSH2::exec() uses 0 and Net_SSH2::interactiveRead() / Net_SSH2::interactiveWrite() use 1.
*
* @see Net_SSH2::_send_channel_packet()
* @see Net_SSH2::_get_channel_packet()
* @access private
*/
define('NET_SFTP_CHANNEL', 2);
/**#@+
* @access public
* @see Net_SFTP::put()
*/
/**
* Reads data from a local file.
*/
define('NET_SFTP_LOCAL_FILE', 1);
/**
* Reads data from a string.
*/
define('NET_SFTP_STRING', 2);
/**#@-*/
/**
* Pure-PHP implementations of SFTP.
*
* @author Jim Wigginton <terrafrost@php.net>
* @version 0.1.0
* @access public
* @package Net_SFTP
*/
class Net_SFTP extends Net_SSH2 {
/**
* Packet Types
*
* @see Net_SFTP::Net_SFTP()
* @var Array
* @access private
*/
var $packet_types = array();
/**
* Status Codes
*
* @see Net_SFTP::Net_SFTP()
* @var Array
* @access private
*/
var $status_codes = array();
/**
* The Request ID
*
* The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support
* concurrent actions, so it's somewhat academic, here.
*
* @var Integer
* @see Net_SFTP::_send_sftp_packet()
* @access private
*/
var $request_id = false;
/**
* The Packet Type
*
* The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support
* concurrent actions, so it's somewhat academic, here.
*
* @var Integer
* @see Net_SFTP::_get_sftp_packet()
* @access private
*/
var $packet_type = -1;
/**
* Packet Buffer
*
* @var String
* @see Net_SFTP::_get_sftp_packet()
* @access private
*/
var $packet_buffer = '';
/**
* Extensions supported by the server
*
* @var Array
* @see Net_SFTP::_initChannel()
* @access private
*/
var $extensions = array();
/**
* Server SFTP version
*
* @var Integer
* @see Net_SFTP::_initChannel()
* @access private
*/
var $version;
/**
* Current working directory
*
* @var String
* @see Net_SFTP::_realpath()
* @see Net_SFTP::chdir()
* @access private
*/
var $pwd = false;
/**
* Packet Type Log
*
* @see Net_SFTP::getLog()
* @var Array
* @access private
*/
var $packet_type_log = array();
/**
* Packet Log
*
* @see Net_SFTP::getLog()
* @var Array
* @access private
*/
var $packet_log = array();
/**
* Error information
*
* @see Net_SFTP::getSFTPErrors()
* @see Net_SFTP::getLastSFTPError()
* @var String
* @access private
*/
var $errors = array();
/**
* Default Constructor.
*
* Connects to an SFTP server
*
* @param String $host
* @param optional Integer $port
* @param optional Integer $timeout
* @return Net_SFTP
* @access public
*/
function Net_SFTP($host, $port = 22, $timeout = 10)
{
parent::Net_SSH2($host, $port, $timeout);
$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',
8 => 'NET_SFTP_FSTAT',
9 => 'NET_SFTP_SETSTAT',
11 => 'NET_SFTP_OPENDIR',
12 => 'NET_SFTP_READDIR',
13 => 'NET_SFTP_REMOVE',
14 => 'NET_SFTP_MKDIR',
15 => 'NET_SFTP_RMDIR',
16 => 'NET_SFTP_REALPATH',
17 => 'NET_SFTP_STAT',
/* the format of SSH_FXP_RENAME changed between SFTPv4 and SFTPv5+:
SFTPv5+: http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3
pre-SFTPv5 : http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-6.5 */
18 => 'NET_SFTP_RENAME',
101=> 'NET_SFTP_STATUS',
102=> 'NET_SFTP_HANDLE',
/* the format of SSH_FXP_NAME changed between SFTPv3 and SFTPv4+:
SFTPv4+: http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.4
pre-SFTPv4 : http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-7 */
103=> 'NET_SFTP_DATA',
104=> 'NET_SFTP_NAME',
105=> 'NET_SFTP_ATTRS',
200=> 'NET_SFTP_EXTENDED'
);
$this->status_codes = array(
0 => 'NET_SFTP_STATUS_OK',
1 => 'NET_SFTP_STATUS_EOF',
2 => 'NET_SFTP_STATUS_NO_SUCH_FILE',
3 => 'NET_SFTP_STATUS_PERMISSION_DENIED',
4 => 'NET_SFTP_STATUS_FAILURE',
5 => 'NET_SFTP_STATUS_BAD_MESSAGE',
6 => 'NET_SFTP_STATUS_NO_CONNECTION',
7 => 'NET_SFTP_STATUS_CONNECTION_LOST',
8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED'
);
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1
// the order, in this case, matters quite a lot - see Net_SFTP::_parseAttributes() to understand why
$this->attributes = array(
0x00000001 => 'NET_SFTP_ATTR_SIZE',
0x00000002 => 'NET_SFTP_ATTR_UIDGID', // defined in SFTPv3, removed in SFTPv4+
0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS',
0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME',
-1 => 'NET_SFTP_ATTR_EXTENDED' // unpack('N', "\xFF\xFF\xFF\xFF") == array(1 => int(-1))
);
// 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 similarily alter the constant names.
$this->open_flags = array(
0x00000001 => 'NET_SFTP_OPEN_READ',
0x00000002 => 'NET_SFTP_OPEN_WRITE',
0x00000008 => 'NET_SFTP_OPEN_CREATE',
0x00000010 => 'NET_SFTP_OPEN_TRUNCATE'
);
$this->_define_array(
$this->packet_types,
$this->status_codes,
$this->attributes,
$this->open_flags
);
}
/**
* Login
*
* @param String $username
* @param optional String $password
* @return Boolean
* @access public
*/
function login($username, $password = '')
{
if (!parent::login($username, $password)) {
return false;
}
$this->window_size_client_to_server[NET_SFTP_CHANNEL] = $this->window_size;
$packet = pack('CNa*N3',
NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SFTP_CHANNEL, $this->window_size, 0x4000);
if (!$this->_send_binary_packet($packet)) {
return false;
}
$this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_OPEN;
$response = $this->_get_channel_packet(NET_SFTP_CHANNEL);
if ($response === false) {
return false;
}
$packet = pack('CNNa*CNa*',
NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SFTP_CHANNEL], strlen('subsystem'), 'subsystem', 1, strlen('sftp'), 'sftp');
if (!$this->_send_binary_packet($packet)) {
return false;
}
$this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST;
$response = $this->_get_channel_packet(NET_SFTP_CHANNEL);
if ($response === false) {
return false;
}
$this->channel_status[NET_SFTP_CHANNEL] = NET_SSH2_MSG_CHANNEL_DATA;
if (!$this->_send_sftp_packet(NET_SFTP_INIT, "\0\0\0\3")) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_VERSION) {
user_error('Expected SSH_FXP_VERSION', E_USER_NOTICE);
return false;
}
extract(unpack('Nversion', $this->_string_shift($response, 4)));
$this->version = $version;
while (!empty($response)) {
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$key = $this->_string_shift($response, $length);
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$value = $this->_string_shift($response, $length);
$this->extensions[$key] = $value;
}
/*
SFTPv4+ defines a 'newline' extension. SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com',
however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's
not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for
one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that
'newline@vandyke.com' would.
*/
/*
if (isset($this->extensions['newline@vandyke.com'])) {
$this->extensions['newline'] = $this->extensions['newline@vandyke.com'];
unset($this->extensions['newline@vandyke.com']);
}
*/
$this->request_id = 1;
/*
A Note on SFTPv4/5/6 support:
<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.1> states the following:
"If the client wishes to interoperate with servers that support noncontiguous version
numbers it SHOULD send '3'"
Given that the server only sends its version number after the client has already done so, the above
seems to be suggesting that v3 should be the default version. This makes sense given that v3 is the
most popular.
<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-5.5> states the following;
"If the server did not send the "versions" extension, or the version-from-list was not included, the
server MAY send a status response describing the failure, but MUST then close the channel without
processing any further requests."
So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and
a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4? If it only implements
v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed
in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what Net_SFTP would do is close the
channel and reopen it with a new and updated SSH_FXP_INIT packet.
*/
if ($this->version != 3) {
return false;
}
$this->pwd = $this->_realpath('.');
return true;
}
/**
* Returns the current directory name
*
* @return Mixed
* @access public
*/
function pwd()
{
return $this->pwd;
}
/**
* Canonicalize the Server-Side Path Name
*
* SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it. Returns
* the absolute (canonicalized) path. If $mode is set to NET_SFTP_CONFIRM_DIR (as opposed to NET_SFTP_CONFIRM_NONE,
* which is what it is set to by default), false is returned if $dir is not a valid directory.
*
* @see Net_SFTP::chdir()
* @param String $dir
* @param optional Integer $mode
* @return Mixed
* @access private
*/
function _realpath($dir)
{
/*
"This protocol represents file names as strings. File names are
assumed to use the slash ('/') character as a directory separator.
File names starting with a slash are "absolute", and are relative to
the root of the file system. Names starting with any other character
are relative to the user's default directory (home directory). Note
that identifying the user is assumed to take place outside of this
protocol."
-- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-6
*/
$file = '';
if ($this->pwd !== false) {
// if the SFTP server returned the canonicalized path even for non-existant files this wouldn't be necessary
// on OpenSSH it isn't necessary but on other SFTP servers it is. that and since the specs say nothing on
// the subject, we'll go ahead and work around it with the following.
if ($dir[strlen($dir) - 1] != '/') {
$file = basename($dir);
$dir = dirname($dir);
}
if ($dir == '.' || $dir == $this->pwd) {
return $this->pwd . $file;
}
if ($dir[0] != '/') {
$dir = $this->pwd . '/' . $dir;
}
// on the surface it seems like maybe resolving a path beginning with / is unnecessary, but such paths
// can contain .'s and ..'s just like any other. we could parse those out as appropriate or we can let
// the server do it. we'll do the latter.
}
/*
that SSH_FXP_REALPATH returns SSH_FXP_NAME does not necessarily mean that anything actually exists at the
specified path. generally speaking, no attributes are returned with this particular SSH_FXP_NAME packet
regardless of whether or not a file actually exists. and in SFTPv3, the longname field and the filename
field match for this particular SSH_FXP_NAME packet. for other SSH_FXP_NAME packets, this will likely
not be the case, but for this one, it is.
*/
// http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
if (!$this->_send_sftp_packet(NET_SFTP_REALPATH, pack('Na*', strlen($dir), $dir))) {
return false;
}
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_NAME:
// although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following
// should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks
// at is the first part and that part is defined the same in SFTP versions 3 through 6.
$this->_string_shift($response, 4); // skip over the count - it should be 1, anyway
extract(unpack('Nlength', $this->_string_shift($response, 4)));
$realpath = $this->_string_shift($response, $length);
break;
case NET_SFTP_STATUS:
extract(unpack('Nstatus/Nlength', $this->_string_shift($response, 8)));
$this->sftp_errors[] = $this->status_codes[$status] . ': ' . $this->_string_shift($response, $length);
return false;
default:
user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS', E_USER_NOTICE);
return false;
}
// if $this->pwd isn't set than the only thing $realpath could be is for '.', which is pretty much guaranteed to
// be a bonafide directory
return $realpath . '/' . $file;
}
/**
* Changes the current directory
*
* @param String $dir
* @return Boolean
* @access public
*/
function chdir($dir)
{
if (!($this->bitmap & NET_SSH2_MASK_LOGIN)) {
return false;
}
if ($dir[strlen($dir) - 1] != '/') {
$dir.= '/';
}
$dir = $this->_realpath($dir);
// confirm that $dir is, in fact, a valid directory
if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) {
return false;
}
// see Net_SFTP::nlist() for a more thorough explanation of the following
$response = $this->_get_sftp_packet();
switch ($this->packet_type) {
case NET_SFTP_HANDLE:
$handle = substr($response, 4);
break;
case NET_SFTP_STATUS:
extract(unpack('Nstatus/Nlength', $this->_string_shift($response, 8)));
$this->sftp_errors[] = $this->status_codes[$status] . ': ' . $this->_string_shift($response, $length);
return false;
default:
user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS', E_USER_NOTICE);
return false;
}
if (!$this->_send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle))) {
return false;
}
$response = $this->_get_sftp_packet();
if ($this->packet_type != NET_SFTP_STATUS) {
user_error('Expected SSH_FXP_STATUS', E_USER_NOTICE);
return false;
}