[project @ Arch-1:robey@lag.net--2003-public%secsh--dev--1.0--patch-113]
sftp server support! finally check in sftp_handle (file handle abstraction), sftp_si (server interface), and sftp_server (server implementation) -- all of which make a roughly 90% implementation of server-side sftp.
This commit is contained in:
parent
611d66428e
commit
a8a023a243
2
README
2
README
|
@ -204,4 +204,4 @@ v0.9 FEAROW
|
||||||
* multi-part auth not supported (ie, need username AND pk)
|
* multi-part auth not supported (ie, need username AND pk)
|
||||||
* server mode needs better documentation
|
* server mode needs better documentation
|
||||||
* sftp server mode
|
* sftp server mode
|
||||||
|
* figure out if there's a way to put stdout/stderr on different channels?
|
||||||
|
|
|
@ -67,7 +67,7 @@ __license__ = "GNU Lesser General Public License (LGPL)"
|
||||||
|
|
||||||
|
|
||||||
import transport, auth_transport, channel, rsakey, dsskey, message, ssh_exception, file
|
import transport, auth_transport, channel, rsakey, dsskey, message, ssh_exception, file
|
||||||
import sftp, sftp_client, sftp_attr, sftp_file
|
import sftp, sftp_client, sftp_attr, sftp_file, sftp_handle, sftp_server, sftp_si
|
||||||
|
|
||||||
randpool = transport.randpool
|
randpool = transport.randpool
|
||||||
Transport = auth_transport.Transport
|
Transport = auth_transport.Transport
|
||||||
|
@ -79,8 +79,11 @@ Message = message.Message
|
||||||
PasswordRequiredException = ssh_exception.PasswordRequiredException
|
PasswordRequiredException = ssh_exception.PasswordRequiredException
|
||||||
SFTP = sftp_client.SFTP
|
SFTP = sftp_client.SFTP
|
||||||
SFTPClient = sftp_client.SFTPClient
|
SFTPClient = sftp_client.SFTPClient
|
||||||
|
SFTPServer = sftp_server.SFTPServer
|
||||||
SFTPError = sftp.SFTPError
|
SFTPError = sftp.SFTPError
|
||||||
SFTPAttributes = sftp_attr.SFTPAttributes
|
SFTPAttributes = sftp_attr.SFTPAttributes
|
||||||
|
SFTPHandle = sftp_handle.SFTPHandle
|
||||||
|
SFTPServerInterface = sftp_si.SFTPServerInterface
|
||||||
ServerInterface = server.ServerInterface
|
ServerInterface = server.ServerInterface
|
||||||
SubsystemHandler = server.SubsystemHandler
|
SubsystemHandler = server.SubsystemHandler
|
||||||
SecurityOptions = transport.SecurityOptions
|
SecurityOptions = transport.SecurityOptions
|
||||||
|
@ -103,9 +106,12 @@ __all__ = [ 'Transport',
|
||||||
'SSHException',
|
'SSHException',
|
||||||
'PasswordRequiredException',
|
'PasswordRequiredException',
|
||||||
'SFTP',
|
'SFTP',
|
||||||
|
'SFTPHandle',
|
||||||
'SFTPClient',
|
'SFTPClient',
|
||||||
|
'SFTPServer',
|
||||||
'SFTPError',
|
'SFTPError',
|
||||||
'SFTPAttributes',
|
'SFTPAttributes',
|
||||||
|
'SFTPServerInterface'
|
||||||
'ServerInterface',
|
'ServerInterface',
|
||||||
'BufferedFile',
|
'BufferedFile',
|
||||||
'transport',
|
'transport',
|
||||||
|
@ -117,8 +123,11 @@ __all__ = [ 'Transport',
|
||||||
'message',
|
'message',
|
||||||
'ssh_exception',
|
'ssh_exception',
|
||||||
'sftp_client',
|
'sftp_client',
|
||||||
|
'sftp_server',
|
||||||
'sftp_attr',
|
'sftp_attr',
|
||||||
'sftp_file',
|
'sftp_file',
|
||||||
|
'sftp_si',
|
||||||
|
'sftp_handle',
|
||||||
'server',
|
'server',
|
||||||
'file',
|
'file',
|
||||||
'util' ]
|
'util' ]
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
|
||||||
|
#
|
||||||
|
# This file is part of paramiko.
|
||||||
|
#
|
||||||
|
# Paramiko 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.
|
||||||
|
#
|
||||||
|
# Paramiko is distrubuted 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 Paramiko; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Abstraction of an SFTP file handle (for server mode).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from common import *
|
||||||
|
from sftp import *
|
||||||
|
|
||||||
|
|
||||||
|
class SFTPHandle (object):
|
||||||
|
"""
|
||||||
|
Abstract object representing a handle to an open file (or folder) on
|
||||||
|
the server. Each handle has a string representation used by the client
|
||||||
|
to refer to the underlying file.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.__name = None
|
||||||
|
# only for handles to folders:
|
||||||
|
self.__files = { }
|
||||||
|
self.__tell = None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""
|
||||||
|
When a client closes a file, this method is called on the handle.
|
||||||
|
Normally you would use this method to close the underlying OS level
|
||||||
|
file object(s).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read(self, offset, length):
|
||||||
|
"""
|
||||||
|
Read up to C{length} bytes from this file, starting at position
|
||||||
|
C{offset}. The offset may be a python long, since SFTP allows it
|
||||||
|
to be 64 bits.
|
||||||
|
|
||||||
|
If the end of the file has been reached, this method may return an
|
||||||
|
empty string to signify EOF, or it may also return L{SFTP_EOF}.
|
||||||
|
|
||||||
|
The default implementation checks for an attribute on C{self} named
|
||||||
|
C{readfile}, and if present, performs the read operation on the python
|
||||||
|
file-like object found there. (This is meant as a time saver for the
|
||||||
|
common case where you are wrapping a python file object.)
|
||||||
|
|
||||||
|
@param offset: position in the file to start reading from.
|
||||||
|
@type offset: int or long
|
||||||
|
@param length: number of bytes to attempt to read.
|
||||||
|
@type length: int
|
||||||
|
@return: data read from the file, or an SFTP error code.
|
||||||
|
@rtype: str
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'readfile') or (self.readfile is None):
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
try:
|
||||||
|
if self.__tell is None:
|
||||||
|
self.__tell = self.readfile.tell()
|
||||||
|
if offset != self.__tell:
|
||||||
|
self.readfile.seek(offset)
|
||||||
|
self.__tell = offset
|
||||||
|
data = self.readfile.read(length)
|
||||||
|
except IOError, e:
|
||||||
|
self.__tell = None
|
||||||
|
return SFTPServer.convert_errno(e.errno)
|
||||||
|
self.__tell += len(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def write(self, offset, data):
|
||||||
|
"""
|
||||||
|
Write C{data} into this file at position C{offset}. Extending the
|
||||||
|
file past its original end is expected. Unlike python's normal
|
||||||
|
C{write()} methods, this method cannot do a partial write: it must
|
||||||
|
write all of C{data} or else return an error.
|
||||||
|
|
||||||
|
The default implementation checks for an attribute on C{self} named
|
||||||
|
C{writefile}, and if present, performs the write operation on the
|
||||||
|
python file-like object found there. The attribute is named
|
||||||
|
differently from C{readfile} to make it easy to implement read-only
|
||||||
|
(or write-only) files, but if both attributes are present, they should
|
||||||
|
refer to the same file.
|
||||||
|
|
||||||
|
@param offset: position in the file to start reading from.
|
||||||
|
@type offset: int or long
|
||||||
|
@param data: data to write into the file.
|
||||||
|
@type data: str
|
||||||
|
@return: an SFTP error code like L{SFTP_OK}.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'writefile') or (self.writefile is None):
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
try:
|
||||||
|
if self.__tell is None:
|
||||||
|
self.__tell = self.writefile.tell()
|
||||||
|
if offset != self.__tell:
|
||||||
|
self.writefile.seek(offset)
|
||||||
|
self.__tell = offset
|
||||||
|
self.writefile.write(data)
|
||||||
|
except IOError, e:
|
||||||
|
self.__tell = None
|
||||||
|
return SFTPServer.convert_errno(e.errno)
|
||||||
|
self.__tell += len(data)
|
||||||
|
return SFTP_OK
|
||||||
|
|
||||||
|
def stat(self):
|
||||||
|
"""
|
||||||
|
Return an L{SFTPAttributes} object referring to this open file, or an
|
||||||
|
error code. This is equivalent to L{SFTPServerInterface.stat}, except
|
||||||
|
it's called on an open file instead of a path.
|
||||||
|
|
||||||
|
@return: an attributes object for the given file, or an SFTP error
|
||||||
|
code (like L{SFTP_PERMISSION_DENIED}).
|
||||||
|
@rtype: L{SFTPAttributes} I{or error code}
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def chattr(self, attr):
|
||||||
|
"""
|
||||||
|
Change the attributes of this file. The C{attr} object will contain
|
||||||
|
only those fields provided by the client in its request, so you should
|
||||||
|
check for the presence of fields before using them.
|
||||||
|
|
||||||
|
@param attr: the attributes to change on this file.
|
||||||
|
@type attr: L{SFTPAttributes}
|
||||||
|
@return: an error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
|
||||||
|
### internals...
|
||||||
|
|
||||||
|
|
||||||
|
def _set_files(self, files):
|
||||||
|
"""
|
||||||
|
Used by the SFTP server code to cache a directory listing. (In
|
||||||
|
the SFTP protocol, listing a directory is a multi-stage process
|
||||||
|
requiring a temporary handle.)
|
||||||
|
"""
|
||||||
|
self.__files = files
|
||||||
|
|
||||||
|
def _get_next_files(self):
|
||||||
|
"""
|
||||||
|
Used by the SFTP server code to retreive a cached directory
|
||||||
|
listing.
|
||||||
|
"""
|
||||||
|
fnlist = self.__files[:16]
|
||||||
|
self.__files = self.__files[16:]
|
||||||
|
return fnlist
|
||||||
|
|
||||||
|
def _get_name(self):
|
||||||
|
return self.__name
|
||||||
|
|
||||||
|
def _set_name(self, name):
|
||||||
|
self.__name = name
|
||||||
|
|
||||||
|
|
||||||
|
from sftp_server import SFTPServer
|
|
@ -0,0 +1,335 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
|
||||||
|
#
|
||||||
|
# This file is part of paramiko.
|
||||||
|
#
|
||||||
|
# Paramiko 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.
|
||||||
|
#
|
||||||
|
# Paramiko is distrubuted 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 Paramiko; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Server-mode SFTP support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os, errno
|
||||||
|
from common import *
|
||||||
|
from server import SubsystemHandler
|
||||||
|
from sftp import *
|
||||||
|
from sftp_si import *
|
||||||
|
from sftp_attr import *
|
||||||
|
|
||||||
|
|
||||||
|
class SFTPServer (BaseSFTP, SubsystemHandler):
|
||||||
|
"""
|
||||||
|
Server-side SFTP subsystem support. Since this is a L{SubsystemHandler},
|
||||||
|
it can be (and is meant to be) set as the handler for C{"sftp"} requests.
|
||||||
|
Use L{Transport.set_subsystem_handler} to activate this class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel, name, server=SFTPServerInterface, server_args=None):
|
||||||
|
"""
|
||||||
|
The constructor for SFTPServer is meant to be called from within the
|
||||||
|
L{Transport} as a subsystem handler. The C{server} and C{server_args}
|
||||||
|
parameters are passed from the original call to
|
||||||
|
L{Transport.set_subsystem_handler}.
|
||||||
|
|
||||||
|
@param channel: channel passed from the L{Transport}.
|
||||||
|
@type channel: L{Channel}
|
||||||
|
@param name: name of the requested subsystem.
|
||||||
|
@type name: str
|
||||||
|
@param server: a subclass of L{SFTPServerInterface} to use for handling
|
||||||
|
individual requests.
|
||||||
|
@type server: class
|
||||||
|
@param server_args: keyword parameters to pass to C{server} when it's
|
||||||
|
constructed.
|
||||||
|
@type server_args: dict
|
||||||
|
"""
|
||||||
|
BaseSFTP.__init__(self)
|
||||||
|
SubsystemHandler.__init__(self, channel, name)
|
||||||
|
self.ultra_debug = True
|
||||||
|
self.logger = logging.getLogger('paramiko.chan.' + channel.get_name() + '.sftp')
|
||||||
|
self.next_handle = 1
|
||||||
|
# map of handle-string to SFTPHandle for files & folders:
|
||||||
|
self.file_table = { }
|
||||||
|
self.folder_table = { }
|
||||||
|
if server_args is None:
|
||||||
|
server_args = {}
|
||||||
|
self.server = server(**server_args)
|
||||||
|
|
||||||
|
def start_subsystem(self, name, transport, channel):
|
||||||
|
self.sock = channel
|
||||||
|
self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel))
|
||||||
|
self._send_server_version()
|
||||||
|
self.server.session_started()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
t, data = self._read_packet()
|
||||||
|
except EOFError:
|
||||||
|
self._log(DEBUG, 'EOF -- end of session')
|
||||||
|
return
|
||||||
|
except Exception, e:
|
||||||
|
self._log(DEBUG, 'Exception on channel: ' + str(e))
|
||||||
|
self._log(DEBUG, util.tb_strings())
|
||||||
|
return
|
||||||
|
msg = Message(data)
|
||||||
|
request_number = msg.get_int()
|
||||||
|
self._process(t, request_number, msg)
|
||||||
|
|
||||||
|
def finish_subsystem(self):
|
||||||
|
self.server.session_ended()
|
||||||
|
|
||||||
|
def convert_errno(e):
|
||||||
|
"""
|
||||||
|
Convert an errno value (as from an C{OSError} or C{IOError} into a
|
||||||
|
standard SFTP result code. This is a convenience function for trapping
|
||||||
|
exceptions in server code and returning an appropriate result.
|
||||||
|
|
||||||
|
@param e: an errno code, as from C{OSError.errno}.
|
||||||
|
@type e: int
|
||||||
|
@return: an SFTP error code like L{SFTP_NO_SUCH_fILE}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
if e == errno.EACCES:
|
||||||
|
# permission denied
|
||||||
|
return SFTP_PERMISSION_DENIED
|
||||||
|
elif e == errno.ENOENT:
|
||||||
|
# no such file
|
||||||
|
return SFTP_NO_SUCH_FILE
|
||||||
|
else:
|
||||||
|
return SFTP_FAILURE
|
||||||
|
convert_errno = staticmethod(convert_errno)
|
||||||
|
|
||||||
|
def set_file_attr(filename, attr):
|
||||||
|
"""
|
||||||
|
Change a file's attributes on the local filesystem. The contents of
|
||||||
|
C{attr} are used to change the permissions, owner, group ownership,
|
||||||
|
and/or modification & access time of the file, depending on which
|
||||||
|
attributes are present in C{attr}.
|
||||||
|
|
||||||
|
This is meant to be a handy helper function for translating SFTP file
|
||||||
|
requests into local file operations.
|
||||||
|
|
||||||
|
@param filename: name of the file to alter (should usually be an
|
||||||
|
absolute path).
|
||||||
|
@type filename: str
|
||||||
|
@param attr: attributes to change.
|
||||||
|
@type attr: L{SFTPAttributes}
|
||||||
|
"""
|
||||||
|
if attr._flags & attr.FLAG_PERMISSIONS:
|
||||||
|
os.chmod(filename, attr.st_mode)
|
||||||
|
if attr._flags & attr.FLAG_UIDGID:
|
||||||
|
os.chown(filename, attr.st_uid, attr.st_gid)
|
||||||
|
if attr._flags & attr.FLAG_AMTIME:
|
||||||
|
os.utime(filename, (attr.st_atime, attr.st_mtime))
|
||||||
|
set_file_attr = staticmethod(set_file_attr)
|
||||||
|
|
||||||
|
|
||||||
|
### internals...
|
||||||
|
|
||||||
|
|
||||||
|
def _response(self, request_number, t, *arg):
|
||||||
|
msg = Message()
|
||||||
|
msg.add_int(request_number)
|
||||||
|
for item in arg:
|
||||||
|
if type(item) is int:
|
||||||
|
msg.add_int(item)
|
||||||
|
elif type(item) is long:
|
||||||
|
msg.add_int64(item)
|
||||||
|
elif type(item) is str:
|
||||||
|
msg.add_string(item)
|
||||||
|
elif type(item) is SFTPAttributes:
|
||||||
|
item._pack(msg)
|
||||||
|
else:
|
||||||
|
raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item)))
|
||||||
|
self._send_packet(t, str(msg))
|
||||||
|
|
||||||
|
def _send_handle_response(self, request_number, handle, folder=False):
|
||||||
|
if not issubclass(type(handle), SFTPHandle):
|
||||||
|
# must be error code
|
||||||
|
self._send_status(request_number, handle)
|
||||||
|
return
|
||||||
|
handle._set_name('hx%d' % self.next_handle)
|
||||||
|
self.next_handle += 1
|
||||||
|
if folder:
|
||||||
|
self.folder_table[handle._get_name()] = handle
|
||||||
|
else:
|
||||||
|
self.file_table[handle._get_name()] = handle
|
||||||
|
self._response(request_number, CMD_HANDLE, handle._get_name())
|
||||||
|
|
||||||
|
def _send_status(self, request_number, code, desc=None):
|
||||||
|
if desc is None:
|
||||||
|
desc = SFTP_DESC[code]
|
||||||
|
self._response(request_number, CMD_STATUS, code, desc)
|
||||||
|
|
||||||
|
def _open_folder(self, request_number, path):
|
||||||
|
resp = self.server.list_folder(path)
|
||||||
|
if issubclass(type(resp), list):
|
||||||
|
# got an actual list of filenames in the folder
|
||||||
|
folder = SFTPHandle()
|
||||||
|
folder._set_files(resp)
|
||||||
|
self._send_handle_response(request_number, folder, True)
|
||||||
|
return
|
||||||
|
# must be an error code
|
||||||
|
self._send_status(request_number, resp)
|
||||||
|
|
||||||
|
def _read_folder(self, request_number, folder):
|
||||||
|
flist = folder._get_next_files()
|
||||||
|
if len(flist) == 0:
|
||||||
|
self._send_status(request_number, SFTP_EOF)
|
||||||
|
return
|
||||||
|
msg = Message()
|
||||||
|
msg.add_int(request_number)
|
||||||
|
msg.add_int(len(flist))
|
||||||
|
for attr in flist:
|
||||||
|
msg.add_string(attr.filename)
|
||||||
|
msg.add_string(str(attr))
|
||||||
|
attr._pack(msg)
|
||||||
|
self._send_packet(CMD_NAME, str(msg))
|
||||||
|
|
||||||
|
def _convert_pflags(self, pflags):
|
||||||
|
"convert SFTP-style open() flags to python's os.open() flags"
|
||||||
|
if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE):
|
||||||
|
flags = os.O_RDWR
|
||||||
|
elif pflags & SFTP_FLAG_WRITE:
|
||||||
|
flags = os.O_WRONLY
|
||||||
|
else:
|
||||||
|
flags = os.O_RDONLY
|
||||||
|
if pflags & SFTP_FLAG_APPEND:
|
||||||
|
flags |= os.O_APPEND
|
||||||
|
if pflags & SFTP_FLAG_CREATE:
|
||||||
|
flags |= os.O_CREAT
|
||||||
|
if pflags & SFTP_FLAG_TRUNC:
|
||||||
|
flags |= os.O_TRUNC
|
||||||
|
if pflags & SFTP_FLAG_EXCL:
|
||||||
|
flags |= os.O_EXCL
|
||||||
|
return flags
|
||||||
|
|
||||||
|
def _process(self, t, request_number, msg):
|
||||||
|
self._log(DEBUG, 'Request: %s' % CMD_NAMES[t])
|
||||||
|
if t == CMD_OPEN:
|
||||||
|
path = msg.get_string()
|
||||||
|
flags = self._convert_pflags(msg.get_int())
|
||||||
|
attr = SFTPAttributes._from_msg(msg)
|
||||||
|
self._send_handle_response(request_number, self.server.open(path, flags, attr))
|
||||||
|
elif t == CMD_CLOSE:
|
||||||
|
handle = msg.get_string()
|
||||||
|
if self.folder_table.has_key(handle):
|
||||||
|
del self.folder_table[handle]
|
||||||
|
self._send_status(request_number, SFTP_OK)
|
||||||
|
return
|
||||||
|
if self.file_table.has_key(handle):
|
||||||
|
self.file_table[handle].close()
|
||||||
|
del self.file_table[handle]
|
||||||
|
self._send_status(request_number, SFTP_OK)
|
||||||
|
return
|
||||||
|
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
elif t == CMD_READ:
|
||||||
|
handle = msg.get_string()
|
||||||
|
offset = msg.get_int64()
|
||||||
|
length = msg.get_int()
|
||||||
|
if not self.file_table.has_key(handle):
|
||||||
|
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
return
|
||||||
|
data = self.file_table[handle].read(offset, length)
|
||||||
|
if type(data) is str:
|
||||||
|
if len(data) == 0:
|
||||||
|
self._send_status(request_number, SFTP_EOF)
|
||||||
|
else:
|
||||||
|
self._response(request_number, CMD_DATA, data)
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, data)
|
||||||
|
elif t == CMD_WRITE:
|
||||||
|
handle = msg.get_string()
|
||||||
|
offset = msg.get_int64()
|
||||||
|
data = msg.get_string()
|
||||||
|
if not self.file_table.has_key(handle):
|
||||||
|
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
return
|
||||||
|
self._send_status(request_number, self.file_table[handle].write(offset, data))
|
||||||
|
elif t == CMD_REMOVE:
|
||||||
|
path = msg.get_string()
|
||||||
|
self._send_status(request_number, self.server.remove(path))
|
||||||
|
elif t == CMD_RENAME:
|
||||||
|
oldpath = msg.get_string()
|
||||||
|
newpath = msg.get_string()
|
||||||
|
self._send_status(request_number, self.server.rename(oldpath, newpath))
|
||||||
|
elif t == CMD_MKDIR:
|
||||||
|
path = msg.get_string()
|
||||||
|
attr = SFTPAttributes._from_msg(msg)
|
||||||
|
self._send_status(request_number, self.server.mkdir(path, attr))
|
||||||
|
elif t == CMD_RMDIR:
|
||||||
|
path = msg.get_string()
|
||||||
|
self._send_status(request_number, self.server.rmdir(path))
|
||||||
|
elif t == CMD_OPENDIR:
|
||||||
|
path = msg.get_string()
|
||||||
|
self._open_folder(request_number, path)
|
||||||
|
return
|
||||||
|
elif t == CMD_READDIR:
|
||||||
|
handle = msg.get_string()
|
||||||
|
if not self.folder_table.has_key(handle):
|
||||||
|
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
return
|
||||||
|
folder = self.folder_table[handle]
|
||||||
|
self._read_folder(request_number, folder)
|
||||||
|
elif t == CMD_STAT:
|
||||||
|
path = msg.get_string()
|
||||||
|
resp = self.server.stat(path)
|
||||||
|
if issubclass(type(resp), SFTPAttributes):
|
||||||
|
self._response(request_number, CMD_ATTRS, resp)
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, resp)
|
||||||
|
elif t == CMD_LSTAT:
|
||||||
|
path = msg.get_string()
|
||||||
|
resp = self.server.lstat(path)
|
||||||
|
if issubclass(type(resp), SFTPAttributes):
|
||||||
|
self._response(request_number, CMD_ATTRS, resp)
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, resp)
|
||||||
|
elif t == CMD_FSTAT:
|
||||||
|
handle = msg.get_string()
|
||||||
|
if not self.file_table.has_key(handle):
|
||||||
|
self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
return
|
||||||
|
resp = self.file_table[handle].stat()
|
||||||
|
if issubclass(type(resp), SFTPAttributes):
|
||||||
|
self._response(request_number, CMD_ATTRS, resp)
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, resp)
|
||||||
|
elif t == CMD_SETSTAT:
|
||||||
|
path = msg.get_string()
|
||||||
|
attr = SFTPAttributes._from_msg(msg)
|
||||||
|
self._send_status(request_number, self.server.chattr(path, attr))
|
||||||
|
elif t == CMD_FSETSTAT:
|
||||||
|
handle = msg.get_string()
|
||||||
|
attr = SFTPAttributes._from_msg(msg)
|
||||||
|
if not self.file_table.has_key(handle):
|
||||||
|
self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
|
||||||
|
return
|
||||||
|
self._send_status(request_number, self.file_table[handle].chattr(attr))
|
||||||
|
elif t == CMD_READLINK:
|
||||||
|
path = msg.get_string()
|
||||||
|
resp = self.server.readlink(path)
|
||||||
|
if type(resp) is str:
|
||||||
|
self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes())
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, resp)
|
||||||
|
elif t == CMD_REALPATH:
|
||||||
|
path = msg.get_string()
|
||||||
|
rpath = self.server.canonicalize(path)
|
||||||
|
self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes())
|
||||||
|
else:
|
||||||
|
self._send_status(request_number, SFTP_OP_UNSUPPORTED)
|
||||||
|
|
||||||
|
|
||||||
|
from sftp_handle import SFTPHandle
|
|
@ -0,0 +1,263 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Copyright (C) 2003-2004 Robey Pointer <robey@lag.net>
|
||||||
|
#
|
||||||
|
# This file is part of paramiko.
|
||||||
|
#
|
||||||
|
# Paramiko 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.
|
||||||
|
#
|
||||||
|
# Paramiko is distrubuted 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 Paramiko; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
|
||||||
|
|
||||||
|
"""
|
||||||
|
L{SFTPServerInterface} is an interface to override for SFTP server support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from common import *
|
||||||
|
from sftp import *
|
||||||
|
|
||||||
|
class SFTPServerInterface (object):
|
||||||
|
"""
|
||||||
|
This class defines an interface for controlling the behavior of paramiko
|
||||||
|
when using the L{SFTPServer} subsystem to provide an SFTP server.
|
||||||
|
|
||||||
|
Methods on this class are called from the SFTP session's thread, so you can
|
||||||
|
block as long as necessary without affecting other sessions (even other
|
||||||
|
SFTP sessions). However, raising an exception will usually cause the SFTP
|
||||||
|
session to abruptly end, so you will usually want to catch exceptions and
|
||||||
|
return an appropriate error code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def session_started(self):
|
||||||
|
"""
|
||||||
|
The SFTP server session has just started. This method is meant to be
|
||||||
|
overridden to perform any necessary setup before handling callbacks
|
||||||
|
from SFTP operations.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def session_ended(self):
|
||||||
|
"""
|
||||||
|
The SFTP server session has just ended, either cleanly or via an
|
||||||
|
exception. This method is meant to be overridden to perform any
|
||||||
|
necessary cleanup before this C{SFTPServerInterface} object is
|
||||||
|
destroyed.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def open(self, path, flags, attr):
|
||||||
|
"""
|
||||||
|
Open a file on the server and create a handle for future operations
|
||||||
|
on that file. On success, a new object subclassed from L{SFTPHandle}
|
||||||
|
should be returned. This handle will be used for future operations
|
||||||
|
on the file (read, write, etc). On failure, an error code such as
|
||||||
|
L{SFTP_PERMISSION_DENIED} should be returned.
|
||||||
|
|
||||||
|
C{flags} contains the requested mode for opening (read-only,
|
||||||
|
write-append, etc) as a bitset of flags from the C{os} module:
|
||||||
|
- C{os.O_RDONLY}
|
||||||
|
- C{os.O_WRONLY}
|
||||||
|
- C{os.O_RDWR}
|
||||||
|
- C{os.O_APPEND}
|
||||||
|
- C{os.O_CREAT}
|
||||||
|
- C{os.O_TRUNC}
|
||||||
|
- C{os.O_EXCL}
|
||||||
|
(One of C{os.O_RDONLY}, C{os.O_WRONLY}, or C{os.O_RDWR} will always
|
||||||
|
be set.)
|
||||||
|
|
||||||
|
The C{attr} object contains requested attributes of the file if it
|
||||||
|
has to be created. Some or all attribute fields may be missing if
|
||||||
|
the client didn't specify them.
|
||||||
|
|
||||||
|
@note: The SFTP protocol defines all files to be in "binary" mode.
|
||||||
|
There is no equivalent to python's "text" mode.
|
||||||
|
|
||||||
|
@param path: the requested path (relative or absolute) of the file
|
||||||
|
to be opened.
|
||||||
|
@type path: str
|
||||||
|
@param flags: flags or'd together from the C{os} module indicating the
|
||||||
|
requested mode for opening the file.
|
||||||
|
@type flags: int
|
||||||
|
@param attr: requested attributes of the file if it is newly created.
|
||||||
|
@type attr: L{SFTPAttributes}
|
||||||
|
@return: a new L{SFTPHandle} I{or error code}.
|
||||||
|
@rtype L{SFTPHandle}
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def list_folder(self, path):
|
||||||
|
"""
|
||||||
|
Return a list of files within a given folder. The C{path} will use
|
||||||
|
posix notation (C{"/"} separates folder names) and may be an absolute
|
||||||
|
or relative path.
|
||||||
|
|
||||||
|
The list of files is expected to be a list of L{SFTPAttributes}
|
||||||
|
objects, which are similar in structure to the objects returned by
|
||||||
|
C{os.stat}. In addition, each object should have its C{filename}
|
||||||
|
field filled in, since this is important to a directory listing and
|
||||||
|
not normally present in C{os.stat} results. The method
|
||||||
|
L{SFTPAttributes.from_stat} will usually do what you want.
|
||||||
|
|
||||||
|
In case of an error, you should return one of the C{SFTP_*} error
|
||||||
|
codes, such as L{SFTP_PERMISSION_DENIED}.
|
||||||
|
|
||||||
|
@param path: the requested path (relative or absolute) to be listed.
|
||||||
|
@type path: str
|
||||||
|
@return: a list of the files in the given folder, using
|
||||||
|
L{SFTPAttributes} objects.
|
||||||
|
@rtype: list of L{SFTPAttributes} I{or error code}
|
||||||
|
|
||||||
|
@note: You should normalize the given C{path} first (see the
|
||||||
|
C{os.path} module) and check appropriate permissions before returning
|
||||||
|
the list of files. Be careful of malicious clients attempting to use
|
||||||
|
relative paths to escape restricted folders, if you're doing a direct
|
||||||
|
translation from the SFTP server path to your local filesystem.
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def stat(self, path):
|
||||||
|
"""
|
||||||
|
Return an L{SFTPAttributes} object for a path on the server, or an
|
||||||
|
error code. If your server supports symbolic links (also known as
|
||||||
|
"aliases"), you should follow them. (L{lstat} is the corresponding
|
||||||
|
call that doesn't follow symlinks/aliases.)
|
||||||
|
|
||||||
|
@param path: the requested path (relative or absolute) to fetch
|
||||||
|
file statistics for.
|
||||||
|
@type path: str
|
||||||
|
@return: an attributes object for the given file, or an SFTP error
|
||||||
|
code (like L{SFTP_PERMISSION_DENIED}).
|
||||||
|
@rtype: L{SFTPAttributes} I{or error code}
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def lstat(self, path):
|
||||||
|
"""
|
||||||
|
Return an L{SFTPAttributes} object for a path on the server, or an
|
||||||
|
error code. If your server supports symbolic links (also known as
|
||||||
|
"aliases"), you should I{not} follow them -- instead, you should
|
||||||
|
return data on the symlink or alias itself. (L{stat} is the
|
||||||
|
corresponding call that follows symlinks/aliases.)
|
||||||
|
|
||||||
|
@param path: the requested path (relative or absolute) to fetch
|
||||||
|
file statistics for.
|
||||||
|
@type path: str
|
||||||
|
@return: an attributes object for the given file, or an SFTP error
|
||||||
|
code (like L{SFTP_PERMISSION_DENIED}).
|
||||||
|
@rtype: L{SFTPAttributes} I{or error code}
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def remove(self, path):
|
||||||
|
"""
|
||||||
|
Delete a file, if possible.
|
||||||
|
|
||||||
|
@param path: the requested path (relative or absolute) of the file
|
||||||
|
to delete.
|
||||||
|
@type path: str
|
||||||
|
@return: an SFTP error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def rename(self, oldpath, newpath):
|
||||||
|
"""
|
||||||
|
Rename (or move) a file. The SFTP specification implies that this
|
||||||
|
method can be used to move an existing file into a different folder,
|
||||||
|
and since there's no other (easy) way to move files via SFTP, it's
|
||||||
|
probably a good idea to implement "move" in this method too, even for
|
||||||
|
files that cross disk partition boundaries, if at all possible.
|
||||||
|
|
||||||
|
@note: You should return an error if a file with the same name as
|
||||||
|
C{newpath} already exists. (The rename operation should be
|
||||||
|
non-desctructive.)
|
||||||
|
|
||||||
|
@param oldpath: the requested path (relative or absolute) of the
|
||||||
|
existing file.
|
||||||
|
@type oldpath: str
|
||||||
|
@param newpath: the requested new path of the file.
|
||||||
|
@type newpath: str
|
||||||
|
@return: an SFTP error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def mkdir(self, path, attr):
|
||||||
|
"""
|
||||||
|
Create a new directory with the given attributes. The C{attr}
|
||||||
|
object may be considered a "hint" and ignored.
|
||||||
|
|
||||||
|
The C{attr} object will contain only those fields provided by the
|
||||||
|
client in its request, so you should use C{hasattr} to check for
|
||||||
|
the presense of fields before using them. In some cases, the C{attr}
|
||||||
|
object may be completely empty.
|
||||||
|
|
||||||
|
@param path: requested path (relative or absolute) of the new
|
||||||
|
folder.
|
||||||
|
@type path: str
|
||||||
|
@param attr: requested attributes of the new folder.
|
||||||
|
@type attr: L{SFTPAttributes}
|
||||||
|
@return: an SFTP error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def rmdir(self, path):
|
||||||
|
"""
|
||||||
|
Remove a directory if it exists. The C{path} should refer to an
|
||||||
|
existing, empty folder -- otherwise this method should return an
|
||||||
|
error.
|
||||||
|
|
||||||
|
@param path: requested path (relative or absolute) of the folder
|
||||||
|
to remove.
|
||||||
|
@type path: str
|
||||||
|
@return: an SFTP error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def chattr(self, path, attr):
|
||||||
|
"""
|
||||||
|
Change the attributes of a file. The C{attr} object will contain
|
||||||
|
only those fields provided by the client in its request, so you
|
||||||
|
should check for the presence of fields before using them.
|
||||||
|
|
||||||
|
@param path: requested path (relative or absolute) of the file to
|
||||||
|
change.
|
||||||
|
@type path: str
|
||||||
|
@param attr: requested attributes to change on the file.
|
||||||
|
@type attr: L{SFTPAttributes}
|
||||||
|
@return: an error code like L{SFTP_OK}.
|
||||||
|
@rtype: int
|
||||||
|
"""
|
||||||
|
return SFTP_OP_UNSUPPORTED
|
||||||
|
|
||||||
|
def canonicalize(self, path):
|
||||||
|
"""
|
||||||
|
Return the canonical form of a path on the server. For example,
|
||||||
|
if the server's home folder is C{/home/foo}, the path
|
||||||
|
C{"../betty"} would be canonicalized to C{"/home/betty"}. Note
|
||||||
|
the obvious security issues: if you're serving files only from a
|
||||||
|
specific folder, you probably don't want this method to reveal path
|
||||||
|
names outside that folder.
|
||||||
|
|
||||||
|
You may find the python methods in C{os.path} useful, especially
|
||||||
|
C{os.path.normpath} and C{os.path.realpath}.
|
||||||
|
|
||||||
|
The default implementation returns C{os.path.normpath('/' + path)}.
|
||||||
|
"""
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return os.path.normpath(path)
|
||||||
|
else:
|
||||||
|
return os.path.normpath('/' + path)
|
||||||
|
|
Loading…
Reference in New Issue