diff --git a/README b/README index b5f06d5..35a6512 100644 --- a/README +++ b/README @@ -27,7 +27,7 @@ should have come with this archive. *** REQUIREMENTS python 2.3 - (python 2.2 may work with some pain) + (python 2.2 is also supported, but not recommended) pyCrypt PyCrypt compiled for Win32 can be downloaded from the HashTar homepage: diff --git a/paramiko/__init__.py b/paramiko/__init__.py index 3bfc90c..67bc666 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -66,7 +66,8 @@ __version__ = "0.9-horsea" __license__ = "GNU Lesser General Public License (LGPL)" -import transport, auth_transport, channel, rsakey, dsskey, message, ssh_exception, sftp +import transport, auth_transport, channel, rsakey, dsskey, message, ssh_exception +import sftp, sftp_client Transport = auth_transport.Transport Channel = channel.Channel @@ -75,9 +76,12 @@ DSSKey = dsskey.DSSKey SSHException = ssh_exception.SSHException Message = message.Message PasswordRequiredException = ssh_exception.PasswordRequiredException -SFTP = sftp.SFTP +SFTP = sftp_client.SFTP +SFTPError = sftp_client.SFTPError +SFTPAttributes = sftp_client.SFTPAttributes ServerInterface = server.ServerInterface SecurityOptions = transport.SecurityOptions +SubsystemHandler = transport.SubsystemHandler from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, OPEN_FAILED_CONNECT_FAILED, \ @@ -85,6 +89,7 @@ from common import AUTH_SUCCESSFUL, AUTH_PARTIALLY_SUCCESSFUL, AUTH_FAILED, \ __all__ = [ 'Transport', 'SecurityOptions', + 'SubsystemHandler', 'Channel', 'RSAKey', 'DSSKey', @@ -92,6 +97,9 @@ __all__ = [ 'Transport', 'SSHException', 'PasswordRequiredException', 'SFTP', + 'SFTPClient', + 'SFTPError', + 'SFTPAttributes', 'ServerInterface', 'transport', 'auth_transport', @@ -101,6 +109,6 @@ __all__ = [ 'Transport', 'pkey', 'message', 'ssh_exception', - 'sftp', + 'sftp_client', 'server', 'util' ] diff --git a/paramiko/sftp.py b/paramiko/sftp.py index 95214f1..6a44b2d 100644 --- a/paramiko/sftp.py +++ b/paramiko/sftp.py @@ -25,16 +25,23 @@ from channel import Channel from message import Message from file import BufferedFile -_CMD_INIT, _CMD_VERSION, _CMD_OPEN, _CMD_CLOSE, _CMD_READ, _CMD_WRITE, _CMD_LSTAT, _CMD_FSTAT, \ - _CMD_SETSTAT, _CMD_FSETSTAT, _CMD_OPENDIR, _CMD_READDIR, _CMD_REMOVE, _CMD_MKDIR, \ - _CMD_RMDIR, _CMD_REALPATH, _CMD_STAT, _CMD_RENAME, _CMD_READLINK, _CMD_SYMLINK \ +CMD_INIT, CMD_VERSION, CMD_OPEN, CMD_CLOSE, CMD_READ, CMD_WRITE, CMD_LSTAT, CMD_FSTAT, \ + CMD_SETSTAT, CMD_FSETSTAT, CMD_OPENDIR, CMD_READDIR, CMD_REMOVE, CMD_MKDIR, \ + CMD_RMDIR, CMD_REALPATH, CMD_STAT, CMD_RENAME, CMD_READLINK, CMD_SYMLINK \ = range(1, 21) -_CMD_STATUS, _CMD_HANDLE, _CMD_DATA, _CMD_NAME, _CMD_ATTRS = range(101, 106) -_CMD_EXTENDED, _CMD_EXTENDED_REPLY = range(200, 202) +CMD_STATUS, CMD_HANDLE, CMD_DATA, CMD_NAME, CMD_ATTRS = range(101, 106) +CMD_EXTENDED, CMD_EXTENDED_REPLY = range(200, 202) -_FX_OK = 0 -_FX_EOF, _FX_NO_SUCH_FILE, _FX_PERMISSION_DENIED, _FX_FAILURE, _FX_BAD_MESSAGE, \ - _FX_NO_CONNECTION, _FX_CONNECTION_LOST, _FX_OP_UNSUPPORTED = range(1, 9) +FX_OK = 0 +FX_EOF, FX_NO_SUCH_FILE, FX_PERMISSION_DENIED, FX_FAILURE, FX_BAD_MESSAGE, \ + FX_NO_CONNECTION, FX_CONNECTION_LOST, FX_OP_UNSUPPORTED = range(1, 9) + +FXF_READ = 0x1 +FXF_WRITE = 0x2 +FXF_APPEND = 0x4 +FXF_CREATE = 0x8 +FXF_TRUNC = 0x10 +FXF_EXCL = 0x20 _VERSION = 3 @@ -136,8 +143,8 @@ class SFTPFile (BufferedFile): BufferedFile._set_mode(self, mode, bufsize) def _get_size(self): - t, msg = self.sftp._request(_CMD_FSTAT, self.handle) - if t != _CMD_ATTRS: + t, msg = self.sftp._request(CMD_FSTAT, self.handle) + if t != CMD_ATTRS: raise SFTPError('Expected attrs') attr = SFTPAttributes() attr.unpack(msg) @@ -148,12 +155,12 @@ class SFTPFile (BufferedFile): def close(self): BufferedFile.close(self) - self.sftp._request(_CMD_CLOSE, self.handle) + self.sftp._request(CMD_CLOSE, self.handle) def _read(self, size): size = min(size, self.MAX_REQUEST_SIZE) - t, msg = self.sftp._request(_CMD_READ, self.handle, long(self._realpos), int(size)) - if t != _CMD_DATA: + t, msg = self.sftp._request(CMD_READ, self.handle, long(self._realpos), int(size)) + if t != CMD_DATA: raise SFTPError('Expected data') return msg.get_string() @@ -161,7 +168,7 @@ class SFTPFile (BufferedFile): offset = 0 while offset < len(data): chunk = min(len(data) - offset, self.MAX_REQUEST_SIZE) - t, msg = self.sftp._request(_CMD_WRITE, self.handle, long(self._realpos + offset), + t, msg = self.sftp._request(CMD_WRITE, self.handle, long(self._realpos + offset), str(data[offset : offset + chunk])) offset += chunk return len(data) @@ -220,322 +227,29 @@ class SFTPFile (BufferedFile): @return: an object containing attributes about this file. @rtype: SFTPAttributes """ - t, msg = self.sftp._request(_CMD_FSTAT, self.handle) - if t != _CMD_ATTRS: + t, msg = self.sftp._request(CMD_FSTAT, self.handle) + if t != CMD_ATTRS: raise SFTPError('Expected attributes') attr = SFTPAttributes(msg) attr._pythonize() return attr -class SFTP (object): - def __init__(self, sock): - self.sock = sock - self.ultra_debug = False - self.request_number = 1 - if type(sock) is Channel: - self.logger = logging.getLogger('paramiko.chan.' + sock.get_name() + '.sftp') - else: - self.logger = logging.getLogger('paramiko.sftp') - # protocol: (maybe should move to a different method) - self._send_packet(_CMD_INIT, struct.pack('>I', _VERSION)) +class BaseSFTP (object): + def _send_version(self): + self._send_packet(CMD_INIT, struct.pack('>I', _VERSION)) t, data = self._read_packet() - if t != _CMD_VERSION: + if t != CMD_VERSION: raise SFTPError('Incompatible sftp protocol') version = struct.unpack('>I', data[:4])[0] -# if version != _VERSION: -# raise SFTPError('Incompatible sftp protocol') - - def from_transport(selfclass, t): - """ - Create an SFTP client channel from an open L{Transport}. - - @param t: an open L{Transport} which is already authenticated. - @type t: L{Transport} - @return: a new L{SFTP} object, referring to an sftp session (channel) - across the transport. - @rtype: L{SFTP} - """ - chan = t.open_session() - if chan is None: - return None - if not chan.invoke_subsystem('sftp'): - raise SFTPError('Failed to invoke sftp subsystem') - return selfclass(chan) - from_transport = classmethod(from_transport) - - def listdir(self, path): - """ - Return a list containing the names of the entries in the given C{path}. - The list is in arbitrary order. It does not include the special - entries C{'.'} and C{'..'} even if they are present in the folder. - - @param path: path to list. - @type path: string - @return: list of filenames. - @rtype: list of string - """ - t, msg = self._request(_CMD_OPENDIR, path) - if t != _CMD_HANDLE: - raise SFTPError('Expected handle') - handle = msg.get_string() - filelist = [] - while 1: - try: - t, msg = self._request(_CMD_READDIR, handle) - except EOFError, e: - # done with handle - break - if t != _CMD_NAME: - raise SFTPError('Expected name response') - count = msg.get_int() - for i in range(count): - filename = msg.get_string() - longname = msg.get_string() - attr = SFTPAttributes(msg) - if (filename != '.') and (filename != '..'): - filelist.append(filename) - # currently we ignore the rest - self._request(_CMD_CLOSE, handle) - return filelist - - def open(self, filename, mode='r', bufsize=-1): - """ - Open a file on the remote server. The arguments are the same as for - python's built-in C{open} (aka C{file}). A file-like object is - returned, which closely mimics the behavior of a normal python file - object. - - The mode indicates how the file is to be opened: C{'r'} for reading, - C{'w'} for writing (truncating an existing file), C{'a'} for appending, - C{'r+'} for reading/writing, C{'w+'} for reading/writing (truncating an - existing file), C{'a+'} for reading/appending. The python C{'b'} flag - is ignored, since SSH treats all files as binary. The C{'U'} flag is - supported in a compatible way. - - @param filename: name of the file to open. - @type filename: string - @param mode: mode (python-style) to open in. - @type mode: string - @param bufsize: desired buffering (-1 = default buffer size, 0 = - unbuffered, 1 = line buffered, >1 = requested buffer size). - @type bufsize: int - @return: a file object representing the open file. - @rtype: SFTPFile - - @raise IOError: if the file could not be opened. - """ - imode = 0 - if ('r' in mode) or ('+' in mode): - imode |= self._FXF_READ - if ('w' in mode) or ('+' in mode): - imode |= self._FXF_WRITE - if ('w' in mode): - imode |= self._FXF_CREATE | self._FXF_TRUNC - if ('a' in mode): - imode |= self._FXF_APPEND - attrblock = SFTPAttributes() - t, msg = self._request(_CMD_OPEN, filename, imode, attrblock) - if t != _CMD_HANDLE: - raise SFTPError('Expected handle') - handle = msg.get_string() - return SFTPFile(self, handle, mode, bufsize) - - def remove(self, path): - """ - Remove the file at the given path. - - @param path: path (absolute or relative) of the file to remove. - @type path: string - - @raise IOError: if the path refers to a folder (directory). Use - L{rmdir} to remove a folder. - """ - self._request(_CMD_REMOVE, path) - - unlink = remove - - def rename(self, oldpath, newpath): - """ - Rename a file or folder from C{oldpath} to C{newpath}. - - @param oldpath: existing name of the file or folder. - @type oldpath: string - @param newpath: new name for the file or folder. - @type newpath: string - - @raise IOError: if C{newpath} is a folder, or something else goes - wrong. - """ - self._request(_CMD_RENAME, oldpath, newpath) - - def mkdir(self, path, mode=0777): - """ - Create a folder (directory) named C{path} with numeric mode C{mode}. - The default mode is 0777 (octal). On some systems, mode is ignored. - Where it is used, the current umask value is first masked out. - - @param path: name of the folder to create. - @type path: string - @param mode: permissions (posix-style) for the newly-created folder. - @type mode: int - """ - attr = SFTPAttributes() - attr.permissions = mode - self._request(_CMD_MKDIR, path, attr) - - def rmdir(self, path): - """ - Remove the folder named C{path}. - - @param path: name of the folder to remove. - @type path: string - """ - self._request(_CMD_RMDIR, path) - - def stat(self, path): - """ - Retrieve information about a file on the remote system. The return - value is an object whose attributes correspond to the attributes of - python's C{stat} structure as returned by C{os.stat}, except that it - contains fewer fields. An SFTP server may return as much or as little - info as it wants, so the results may vary from server to server. - - Unlike a python C{stat} object, the result may not be accessed as a - tuple. This is mostly due to the author's slack factor. - - The fields supported are: C{st_mode}, C{st_size}, C{st_uid}, C{st_gid}, - C{st_atime}, and C{st_mtime}. - - @param path: the filename to stat. - @type path: string - @return: an object containing attributes about the given file. - @rtype: SFTPAttributes - """ - t, msg = self._request(_CMD_STAT, path) - if t != _CMD_ATTRS: - raise SFTPError('Expected attributes') - attr = SFTPAttributes(msg) - attr._pythonize() - return attr - - def lstat(self, path): - """ - Retrieve information about a file on the remote system, without - following symbolic links (shortcuts). This otherwise behaves exactly - the same as L{stat}. - - @param path: the filename to stat. - @type path: string - @return: an object containing attributes about the given file. - @rtype: SFTPAttributes - """ - t, msg = self._request(_CMD_LSTAT, path) - if t != _CMD_ATTRS: - raise SFTPError('Expected attributes') - attr = SFTPAttributes(msg) - attr._pythonize() - return attr - - def symlink(self, source, dest): - """ - Create a symbolic link (shortcut) of the C{source} path at - C{destination}. - - @param source: path of the original file. - @type source: string - @param dest: path of the newly created symlink. - @type dest: string - """ - self._request(_CMD_SYMLINK, source, dest) - - def chmod(self, path, mode): - """ - Change the mode (permissions) of a file. The permissions are - unix-style and identical to those used by python's C{os.chmod} - function. - - @param path: path of the file to change the permissions of. - @type path: string - @param mode: new permissions. - @type mode: int - """ - attr = SFTPAttributes() - attr.permissions = mode - self._request(_CMD_SETSTAT, path, attr) - - def chown(self, path, uid, gid): - """ - Change the owner (C{uid}) and group (C{gid}) of a file. As with - python's C{os.chown} function, you must pass both arguments, so if you - only want to change one, use L{stat} first to retrieve the current - owner and group. - - @param path: path of the file to change the owner and group of. - @type path: string - @param uid: new owner's uid - @type uid: int - @param gid: new group id - @type gid: int - """ - attr = SFTPAttributes() - attr.uid, attr.gid = uid, gid - self._request(_CMD_SETSTAT, path, attr) - - def utime(self, path, times): - """ - Set the access and modified times of the file specified by C{path}. If - C{times} is C{None}, then the file's access and modified times are set - to the current time. Otherwise, C{times} must be a 2-tuple of numbers, - of the form C{(atime, mtime)}, which is used to set the access and - modified times, respectively. This bizarre API is mimicked from python - for the sake of consistency -- I apologize. - - @param path: path of the file to modify. - @type path: string - @param times: C{None} or a tuple of (access time, modified time) in - standard internet epoch time (seconds since 01 January 1970 GMT). - @type times: tuple of int - """ - if times is None: - times = (time.time(), time.time()) - attr = SFTPAttributes() - attr.atime, attr.mtime = times - self._request(_CMD_SETSTAT, path, attr) - - def readlink(self, path): - """ - Return the target of a symbolic link (shortcut). You can use - L{symlink} to create these. The result may be either an absolute or - relative pathname. - - @param path: path of the symbolic link file. - @type path: string - @return: target path. - @rtype: string - """ - t, msg = self._request(_CMD_READLINK, path) - if t != _CMD_NAME: - raise SFTPError('Expected name response') - count = msg.get_int() - if count == 0: - return None - if count != 1: - raise SFTPError('Readlink returned %d results' % count) - return msg.get_string() + # if version != _VERSION: + # raise SFTPError('Incompatible sftp protocol') + return version ### internals... - _FXF_READ = 0x1 - _FXF_WRITE = 0x2 - _FXF_APPEND = 0x4 - _FXF_CREATE = 0x8 - _FXF_TRUNC = 0x10 - _FXF_EXCL = 0x20 - - def _log(self, level, msg): if type(msg) == type([]): for m in msg: @@ -577,42 +291,3 @@ class SFTP (object): if size > 0: return ord(data[0]), data[1:] return 0, '' - - def _request(self, t, *arg): - msg = Message() - msg.add_int(self.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)) - t, data = self._read_packet() - msg = Message(data) - num = msg.get_int() - if num != self.request_number: - raise SFTPError('Expected response #%d, got response #%d' % (self.request_number, num)) - self.request_number += 1 - if t == _CMD_STATUS: - self._convert_status(msg) - return t, msg - - def _convert_status(self, msg): - """ - Raises EOFError or IOError on error status; otherwise does nothing. - """ - code = msg.get_int() - text = msg.get_string() - if code == _FX_OK: - return - elif code == _FX_EOF: - raise EOFError(text) - else: - raise IOError(text) -