paramiko.sftp_server

Package paramiko :: Module sftp_server |
[frames] | no frames]

# Source Code for Module paramiko.sftp_server

      1  # Copyright (C) 2003-2007  Robey Pointer <robey@lag.net>
      2  #
      3  # This file is part of paramiko.
      4  #
      5  # Paramiko is free software; you can redistribute it and/or modify it under the
      6  # terms of the GNU Lesser General Public License as published by the Free
      7  # Software Foundation; either version 2.1 of the License, or (at your option)
      8  # any later version.
      9  #
     10  # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
     11  # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     12  # A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
     13  # details.
     14  #
     15  # You should have received a copy of the GNU Lesser General Public License
     16  # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
     17  # 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA.
     18
     19  """
     20  Server-mode SFTP support.
     21  """
     22
     23  import os
     24  import errno
     25
     26  from Crypto.Hash import MD5, SHA
     27  from paramiko.common import *
     28  from paramiko.server import SubsystemHandler
     29  from paramiko.sftp import *
     30  from paramiko.sftp_si import *
     31  from paramiko.sftp_attr import *
     32
     33
     34  # known hash algorithms for the "check-file" extension
     35  _hash_class = {
     36      'sha1': SHA,
     37      'md5': MD5,
     38  }
     39
     40
     41 -class SFTPServer (BaseSFTP, SubsystemHandler):
     42      """
     43      Server-side SFTP subsystem support.  Since this is a L{SubsystemHandler},
     44      it can be (and is meant to be) set as the handler for C{"sftp"} requests.
     45      Use L{Transport.set_subsystem_handler} to activate this class.
     46      """
     47
     48 -    def __init__(self, channel, name, server, sftp_si=SFTPServerInterface, *largs, **kwargs):
     49          """
     50          The constructor for SFTPServer is meant to be called from within the
     51          L{Transport} as a subsystem handler.  C{server} and any additional
     52          parameters or keyword parameters are passed from the original call to
     53          L{Transport.set_subsystem_handler}.
     54
     55          @param channel: channel passed from the L{Transport}.
     56          @type channel: L{Channel}
     57          @param name: name of the requested subsystem.
     58          @type name: str
     59          @param server: the server object associated with this channel and
     60              subsystem
     61          @type server: L{ServerInterface}
     62          @param sftp_si: a subclass of L{SFTPServerInterface} to use for handling
     63              individual requests.
     64          @type sftp_si: class
     65          """
     66          BaseSFTP.__init__(self)
     67          SubsystemHandler.__init__(self, channel, name, server)
     68          transport = channel.get_transport()
     69          self.logger = util.get_logger(transport.get_log_channel() + '.sftp')
     70          self.ultra_debug = transport.get_hexdump()
     71          self.next_handle = 1
     72          # map of handle-string to SFTPHandle for files & folders:
     73          self.file_table = { }
     74          self.folder_table = { }
     75          self.server = sftp_si(server, *largs, **kwargs)
     76
     77 -    def _log(self, level, msg):
     78          super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
     79
     80 -    def start_subsystem(self, name, transport, channel):
     81          self.sock = channel
     82          self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel))
     83          self._send_server_version()
     84          self.server.session_started()
     85          while True:
     86              try:
     87                  t, data = self._read_packet()
     88              except EOFError:
     89                  self._log(DEBUG, 'EOF -- end of session')
     90                  return
     91              except Exception, e:
     92                  self._log(DEBUG, 'Exception on channel: ' + str(e))
     93                  self._log(DEBUG, util.tb_strings())
     94                  return
     95              msg = Message(data)
     96              request_number = msg.get_int()
     97              try:
     98                  self._process(t, request_number, msg)
     99              except Exception, e:
    100                  self._log(DEBUG, 'Exception in server processing: ' + str(e))
    101                  self._log(DEBUG, util.tb_strings())
    102                  # send some kind of failure message, at least
    103                  try:
    104                      self._send_status(request_number, SFTP_FAILURE)
    105                  except:
    106                      pass
    107
    108 -    def finish_subsystem(self):
    109          self.server.session_ended()
    110          super(SFTPServer, self).finish_subsystem()
    111          # close any file handles that were left open (so we can return them to the OS quickly)
    112          for f in self.file_table.itervalues():
    113              f.close()
    114          for f in self.folder_table.itervalues():
    115              f.close()
    116          self.file_table = {}
    117          self.folder_table = {}
    118
    119 -    def convert_errno(e):
    120          """
    121          Convert an errno value (as from an C{OSError} or C{IOError}) into a
    122          standard SFTP result code.  This is a convenience function for trapping
    123          exceptions in server code and returning an appropriate result.
    124
    125          @param e: an errno code, as from C{OSError.errno}.
    126          @type e: int
    127          @return: an SFTP error code like L{SFTP_NO_SUCH_FILE}.
    128          @rtype: int
    129          """
    130          if e == errno.EACCES:
    131              # permission denied
    132              return SFTP_PERMISSION_DENIED
    133          elif (e == errno.ENOENT) or (e == errno.ENOTDIR):
    134              # no such file
    135              return SFTP_NO_SUCH_FILE
    136          else:
    137              return SFTP_FAILURE
    138      convert_errno = staticmethod(convert_errno)
    139
    140 -    def set_file_attr(filename, attr):
    141          """
    142          Change a file's attributes on the local filesystem.  The contents of
    143          C{attr} are used to change the permissions, owner, group ownership,
    144          and/or modification & access time of the file, depending on which
    145          attributes are present in C{attr}.
    146
    147          This is meant to be a handy helper function for translating SFTP file
    148          requests into local file operations.
    149
    150          @param filename: name of the file to alter (should usually be an
    151              absolute path).
    152          @type filename: str
    153          @param attr: attributes to change.
    154          @type attr: L{SFTPAttributes}
    155          """
    156          if sys.platform != 'win32':
    157              # mode operations are meaningless on win32
    158              if attr._flags & attr.FLAG_PERMISSIONS:
    159                  os.chmod(filename, attr.st_mode)
    160              if attr._flags & attr.FLAG_UIDGID:
    161                  os.chown(filename, attr.st_uid, attr.st_gid)
    162          if attr._flags & attr.FLAG_AMTIME:
    163              os.utime(filename, (attr.st_atime, attr.st_mtime))
    164          if attr._flags & attr.FLAG_SIZE:
    165              open(filename, 'w+').truncate(attr.st_size)
    166      set_file_attr = staticmethod(set_file_attr)
    167
    168
    169      ###  internals...
    170
    171
    172 -    def _response(self, request_number, t, *arg):
    173          msg = Message()
    174          msg.add_int(request_number)
    175          for item in arg:
    176              if type(item) is int:
    177                  msg.add_int(item)
    178              elif type(item) is long:
    179                  msg.add_int64(item)
    180              elif type(item) is str:
    181                  msg.add_string(item)
    182              elif type(item) is SFTPAttributes:
    183                  item._pack(msg)
    184              else:
    185                  raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item)))
    186          self._send_packet(t, str(msg))
    187
    188 -    def _send_handle_response(self, request_number, handle, folder=False):
    189          if not issubclass(type(handle), SFTPHandle):
    190              # must be error code
    191              self._send_status(request_number, handle)
    192              return
    193          handle._set_name('hx%d' % self.next_handle)
    194          self.next_handle += 1
    195          if folder:
    196              self.folder_table[handle._get_name()] = handle
    197          else:
    198              self.file_table[handle._get_name()] = handle
    199          self._response(request_number, CMD_HANDLE, handle._get_name())
    200
    201 -    def _send_status(self, request_number, code, desc=None):
    202          if desc is None:
    203              try:
    204                  desc = SFTP_DESC[code]
    205              except IndexError:
    206                  desc = 'Unknown'
    207          self._response(request_number, CMD_STATUS, code, desc)
    208
    209 -    def _open_folder(self, request_number, path):
    210          resp = self.server.list_folder(path)
    211          if issubclass(type(resp), list):
    212              # got an actual list of filenames in the folder
    213              folder = SFTPHandle()
    214              folder._set_files(resp)
    215              self._send_handle_response(request_number, folder, True)
    216              return
    217          # must be an error code
    218          self._send_status(request_number, resp)
    219
    220 -    def _read_folder(self, request_number, folder):
    221          flist = folder._get_next_files()
    222          if len(flist) == 0:
    223              self._send_status(request_number, SFTP_EOF)
    224              return
    225          msg = Message()
    226          msg.add_int(request_number)
    227          msg.add_int(len(flist))
    228          for attr in flist:
    229              msg.add_string(attr.filename)
    230              msg.add_string(str(attr))
    231              attr._pack(msg)
    232          self._send_packet(CMD_NAME, str(msg))
    233
    234 -    def _check_file(self, request_number, msg):
    235          # this extension actually comes from v6 protocol, but since it's an
    236          # extension, i feel like we can reasonably support it backported.
    237          # it's very useful for verifying uploaded files or checking for
    238          # rsync-like differences between local and remote files.
    239          handle = msg.get_string()
    240          alg_list = msg.get_list()
    241          start = msg.get_int64()
    242          length = msg.get_int64()
    243          block_size = msg.get_int()
    244          if handle not in self.file_table:
    245              self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    246              return
    247          f = self.file_table[handle]
    248          for x in alg_list:
    249              if x in _hash_class:
    250                  algname = x
    251                  alg = _hash_class[x]
    252                  break
    253          else:
    254              self._send_status(request_number, SFTP_FAILURE, 'No supported hash types found')
    255              return
    256          if length == 0:
    257              st = f.stat()
    258              if not issubclass(type(st), SFTPAttributes):
    259                  self._send_status(request_number, st, 'Unable to stat file')
    260                  return
    261              length = st.st_size - start
    262          if block_size == 0:
    263              block_size = length
    264          if block_size < 256:
    265              self._send_status(request_number, SFTP_FAILURE, 'Block size too small')
    266              return
    267
    268          sum_out = ''
    269          offset = start
    270          while offset < start + length:
    271              blocklen = min(block_size, start + length - offset)
    272              # don't try to read more than about 64KB at a time
    273              chunklen = min(blocklen, 65536)
    274              count = 0
    275              hash_obj = alg.new()
    276              while count < blocklen:
    277                  data = f.read(offset, chunklen)
    278                  if not type(data) is str:
    279                      self._send_status(request_number, data, 'Unable to hash file')
    280                      return
    281                  hash_obj.update(data)
    282                  count += len(data)
    283                  offset += count
    284              sum_out += hash_obj.digest()
    285
    286          msg = Message()
    287          msg.add_int(request_number)
    288          msg.add_string('check-file')
    289          msg.add_string(algname)
    290          msg.add_bytes(sum_out)
    291          self._send_packet(CMD_EXTENDED_REPLY, str(msg))
    292
    293 -    def _convert_pflags(self, pflags):
    294          "convert SFTP-style open() flags to python's os.open() flags"
    295          if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE):
    296              flags = os.O_RDWR
    297          elif pflags & SFTP_FLAG_WRITE:
    298              flags = os.O_WRONLY
    299          else:
    300              flags = os.O_RDONLY
    301          if pflags & SFTP_FLAG_APPEND:
    302              flags |= os.O_APPEND
    303          if pflags & SFTP_FLAG_CREATE:
    304              flags |= os.O_CREAT
    305          if pflags & SFTP_FLAG_TRUNC:
    306              flags |= os.O_TRUNC
    307          if pflags & SFTP_FLAG_EXCL:
    308              flags |= os.O_EXCL
    309          return flags
    310
    311 -    def _process(self, t, request_number, msg):
    312          self._log(DEBUG, 'Request: %s' % CMD_NAMES[t])
    313          if t == CMD_OPEN:
    314              path = msg.get_string()
    315              flags = self._convert_pflags(msg.get_int())
    316              attr = SFTPAttributes._from_msg(msg)
    317              self._send_handle_response(request_number, self.server.open(path, flags, attr))
    318          elif t == CMD_CLOSE:
    319              handle = msg.get_string()
    320              if handle in self.folder_table:
    321                  del self.folder_table[handle]
    322                  self._send_status(request_number, SFTP_OK)
    323                  return
    324              if handle in self.file_table:
    325                  self.file_table[handle].close()
    326                  del self.file_table[handle]
    327                  self._send_status(request_number, SFTP_OK)
    328                  return
    329              self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    330          elif t == CMD_READ:
    331              handle = msg.get_string()
    332              offset = msg.get_int64()
    333              length = msg.get_int()
    334              if handle not in self.file_table:
    335                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    336                  return
    337              data = self.file_table[handle].read(offset, length)
    338              if type(data) is str:
    339                  if len(data) == 0:
    340                      self._send_status(request_number, SFTP_EOF)
    341                  else:
    342                      self._response(request_number, CMD_DATA, data)
    343              else:
    344                  self._send_status(request_number, data)
    345          elif t == CMD_WRITE:
    346              handle = msg.get_string()
    347              offset = msg.get_int64()
    348              data = msg.get_string()
    349              if handle not in self.file_table:
    350                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    351                  return
    352              self._send_status(request_number, self.file_table[handle].write(offset, data))
    353          elif t == CMD_REMOVE:
    354              path = msg.get_string()
    355              self._send_status(request_number, self.server.remove(path))
    356          elif t == CMD_RENAME:
    357              oldpath = msg.get_string()
    358              newpath = msg.get_string()
    359              self._send_status(request_number, self.server.rename(oldpath, newpath))
    360          elif t == CMD_MKDIR:
    361              path = msg.get_string()
    362              attr = SFTPAttributes._from_msg(msg)
    363              self._send_status(request_number, self.server.mkdir(path, attr))
    364          elif t == CMD_RMDIR:
    365              path = msg.get_string()
    366              self._send_status(request_number, self.server.rmdir(path))
    367          elif t == CMD_OPENDIR:
    368              path = msg.get_string()
    369              self._open_folder(request_number, path)
    370              return
    371          elif t == CMD_READDIR:
    372              handle = msg.get_string()
    373              if handle not in self.folder_table:
    374                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    375                  return
    376              folder = self.folder_table[handle]
    377              self._read_folder(request_number, folder)
    378          elif t == CMD_STAT:
    379              path = msg.get_string()
    380              resp = self.server.stat(path)
    381              if issubclass(type(resp), SFTPAttributes):
    382                  self._response(request_number, CMD_ATTRS, resp)
    383              else:
    384                  self._send_status(request_number, resp)
    385          elif t == CMD_LSTAT:
    386              path = msg.get_string()
    387              resp = self.server.lstat(path)
    388              if issubclass(type(resp), SFTPAttributes):
    389                  self._response(request_number, CMD_ATTRS, resp)
    390              else:
    391                  self._send_status(request_number, resp)
    392          elif t == CMD_FSTAT:
    393              handle = msg.get_string()
    394              if handle not in self.file_table:
    395                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    396                  return
    397              resp = self.file_table[handle].stat()
    398              if issubclass(type(resp), SFTPAttributes):
    399                  self._response(request_number, CMD_ATTRS, resp)
    400              else:
    401                  self._send_status(request_number, resp)
    402          elif t == CMD_SETSTAT:
    403              path = msg.get_string()
    404              attr = SFTPAttributes._from_msg(msg)
    405              self._send_status(request_number, self.server.chattr(path, attr))
    406          elif t == CMD_FSETSTAT:
    407              handle = msg.get_string()
    408              attr = SFTPAttributes._from_msg(msg)
    409              if handle not in self.file_table:
    410                  self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    411                  return
    412              self._send_status(request_number, self.file_table[handle].chattr(attr))
    413          elif t == CMD_READLINK:
    414              path = msg.get_string()
    415              resp = self.server.readlink(path)
    416              if type(resp) is str:
    417                  self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes())
    418              else:
    419                  self._send_status(request_number, resp)
    420          elif t == CMD_SYMLINK:
    421              # the sftp 2 draft is incorrect here!  path always follows target_path
    422              target_path = msg.get_string()
    423              path = msg.get_string()
    424              self._send_status(request_number, self.server.symlink(target_path, path))
    425          elif t == CMD_REALPATH:
    426              path = msg.get_string()
    427              rpath = self.server.canonicalize(path)
    428              self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes())
    429          elif t == CMD_EXTENDED:
    430              tag = msg.get_string()
    431              if tag == 'check-file':
    432                  self._check_file(request_number, msg)
    433              else:
    434                  self._send_status(request_number, SFTP_OP_UNSUPPORTED)
    435          else:
    436              self._send_status(request_number, SFTP_OP_UNSUPPORTED)
    437
    438
    439  from paramiko.sftp_handle import SFTPHandle
    440