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 <robeypointer@gmail.com>
      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          if issubclass(type(msg), list):
     79              for m in msg:
     80                  super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + m)
     81          else:
     82              super(SFTPServer, self)._log(level, "[chan " + self.sock.get_name() + "] " + msg)
     83
     84 -    def start_subsystem(self, name, transport, channel):
     85          self.sock = channel
     86          self._log(DEBUG, 'Started sftp server on channel %s' % repr(channel))
     87          self._send_server_version()
     88          self.server.session_started()
     89          while True:
     90              try:
     91                  t, data = self._read_packet()
     92              except EOFError:
     93                  self._log(DEBUG, 'EOF -- end of session')
     94                  return
     95              except Exception, e:
     96                  self._log(DEBUG, 'Exception on channel: ' + str(e))
     97                  self._log(DEBUG, util.tb_strings())
     98                  return
     99              msg = Message(data)
    100              request_number = msg.get_int()
    101              try:
    102                  self._process(t, request_number, msg)
    103              except Exception, e:
    104                  self._log(DEBUG, 'Exception in server processing: ' + str(e))
    105                  self._log(DEBUG, util.tb_strings())
    106                  # send some kind of failure message, at least
    107                  try:
    108                      self._send_status(request_number, SFTP_FAILURE)
    109                  except:
    110                      pass
    111
    112 -    def finish_subsystem(self):
    113          self.server.session_ended()
    114          super(SFTPServer, self).finish_subsystem()
    115          # close any file handles that were left open (so we can return them to the OS quickly)
    116          for f in self.file_table.itervalues():
    117              f.close()
    118          for f in self.folder_table.itervalues():
    119              f.close()
    120          self.file_table = {}
    121          self.folder_table = {}
    122
    123 -    def convert_errno(e):
    124          """
    125          Convert an errno value (as from an C{OSError} or C{IOError}) into a
    126          standard SFTP result code.  This is a convenience function for trapping
    127          exceptions in server code and returning an appropriate result.
    128
    129          @param e: an errno code, as from C{OSError.errno}.
    130          @type e: int
    131          @return: an SFTP error code like L{SFTP_NO_SUCH_FILE}.
    132          @rtype: int
    133          """
    134          if e == errno.EACCES:
    135              # permission denied
    136              return SFTP_PERMISSION_DENIED
    137          elif (e == errno.ENOENT) or (e == errno.ENOTDIR):
    138              # no such file
    139              return SFTP_NO_SUCH_FILE
    140          else:
    141              return SFTP_FAILURE
    142      convert_errno = staticmethod(convert_errno)
    143
    144 -    def set_file_attr(filename, attr):
    145          """
    146          Change a file's attributes on the local filesystem.  The contents of
    147          C{attr} are used to change the permissions, owner, group ownership,
    148          and/or modification & access time of the file, depending on which
    149          attributes are present in C{attr}.
    150
    151          This is meant to be a handy helper function for translating SFTP file
    152          requests into local file operations.
    153
    154          @param filename: name of the file to alter (should usually be an
    155              absolute path).
    156          @type filename: str
    157          @param attr: attributes to change.
    158          @type attr: L{SFTPAttributes}
    159          """
    160          if sys.platform != 'win32':
    161              # mode operations are meaningless on win32
    162              if attr._flags & attr.FLAG_PERMISSIONS:
    163                  os.chmod(filename, attr.st_mode)
    164              if attr._flags & attr.FLAG_UIDGID:
    165                  os.chown(filename, attr.st_uid, attr.st_gid)
    166          if attr._flags & attr.FLAG_AMTIME:
    167              os.utime(filename, (attr.st_atime, attr.st_mtime))
    168          if attr._flags & attr.FLAG_SIZE:
    169              open(filename, 'w+').truncate(attr.st_size)
    170      set_file_attr = staticmethod(set_file_attr)
    171
    172
    173      ###  internals...
    174
    175
    176 -    def _response(self, request_number, t, *arg):
    177          msg = Message()
    178          msg.add_int(request_number)
    179          for item in arg:
    180              if type(item) is int:
    181                  msg.add_int(item)
    182              elif type(item) is long:
    183                  msg.add_int64(item)
    184              elif type(item) is str:
    185                  msg.add_string(item)
    186              elif type(item) is SFTPAttributes:
    187                  item._pack(msg)
    188              else:
    189                  raise Exception('unknown type for ' + repr(item) + ' type ' + repr(type(item)))
    190          self._send_packet(t, str(msg))
    191
    192 -    def _send_handle_response(self, request_number, handle, folder=False):
    193          if not issubclass(type(handle), SFTPHandle):
    194              # must be error code
    195              self._send_status(request_number, handle)
    196              return
    197          handle._set_name('hx%d' % self.next_handle)
    198          self.next_handle += 1
    199          if folder:
    200              self.folder_table[handle._get_name()] = handle
    201          else:
    202              self.file_table[handle._get_name()] = handle
    203          self._response(request_number, CMD_HANDLE, handle._get_name())
    204
    205 -    def _send_status(self, request_number, code, desc=None):
    206          if desc is None:
    207              try:
    208                  desc = SFTP_DESC[code]
    209              except IndexError:
    210                  desc = 'Unknown'
    211          # some clients expect a "langauge" tag at the end (but don't mind it being blank)
    212          self._response(request_number, CMD_STATUS, code, desc, '')
    213
    214 -    def _open_folder(self, request_number, path):
    215          resp = self.server.list_folder(path)
    216          if issubclass(type(resp), list):
    217              # got an actual list of filenames in the folder
    218              folder = SFTPHandle()
    219              folder._set_files(resp)
    220              self._send_handle_response(request_number, folder, True)
    221              return
    222          # must be an error code
    223          self._send_status(request_number, resp)
    224
    225 -    def _read_folder(self, request_number, folder):
    226          flist = folder._get_next_files()
    227          if len(flist) == 0:
    228              self._send_status(request_number, SFTP_EOF)
    229              return
    230          msg = Message()
    231          msg.add_int(request_number)
    232          msg.add_int(len(flist))
    233          for attr in flist:
    234              msg.add_string(attr.filename)
    235              msg.add_string(str(attr))
    236              attr._pack(msg)
    237          self._send_packet(CMD_NAME, str(msg))
    238
    239 -    def _check_file(self, request_number, msg):
    240          # this extension actually comes from v6 protocol, but since it's an
    241          # extension, i feel like we can reasonably support it backported.
    242          # it's very useful for verifying uploaded files or checking for
    243          # rsync-like differences between local and remote files.
    244          handle = msg.get_string()
    245          alg_list = msg.get_list()
    246          start = msg.get_int64()
    247          length = msg.get_int64()
    248          block_size = msg.get_int()
    249          if handle not in self.file_table:
    250              self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    251              return
    252          f = self.file_table[handle]
    253          for x in alg_list:
    254              if x in _hash_class:
    255                  algname = x
    256                  alg = _hash_class[x]
    257                  break
    258          else:
    259              self._send_status(request_number, SFTP_FAILURE, 'No supported hash types found')
    260              return
    261          if length == 0:
    262              st = f.stat()
    263              if not issubclass(type(st), SFTPAttributes):
    264                  self._send_status(request_number, st, 'Unable to stat file')
    265                  return
    266              length = st.st_size - start
    267          if block_size == 0:
    268              block_size = length
    269          if block_size < 256:
    270              self._send_status(request_number, SFTP_FAILURE, 'Block size too small')
    271              return
    272
    273          sum_out = ''
    274          offset = start
    275          while offset < start + length:
    276              blocklen = min(block_size, start + length - offset)
    277              # don't try to read more than about 64KB at a time
    278              chunklen = min(blocklen, 65536)
    279              count = 0
    280              hash_obj = alg.new()
    281              while count < blocklen:
    282                  data = f.read(offset, chunklen)
    283                  if not type(data) is str:
    284                      self._send_status(request_number, data, 'Unable to hash file')
    285                      return
    286                  hash_obj.update(data)
    287                  count += len(data)
    288                  offset += count
    289              sum_out += hash_obj.digest()
    290
    291          msg = Message()
    292          msg.add_int(request_number)
    293          msg.add_string('check-file')
    294          msg.add_string(algname)
    295          msg.add_bytes(sum_out)
    296          self._send_packet(CMD_EXTENDED_REPLY, str(msg))
    297
    298 -    def _convert_pflags(self, pflags):
    299          "convert SFTP-style open() flags to python's os.open() flags"
    300          if (pflags & SFTP_FLAG_READ) and (pflags & SFTP_FLAG_WRITE):
    301              flags = os.O_RDWR
    302          elif pflags & SFTP_FLAG_WRITE:
    303              flags = os.O_WRONLY
    304          else:
    305              flags = os.O_RDONLY
    306          if pflags & SFTP_FLAG_APPEND:
    307              flags |= os.O_APPEND
    308          if pflags & SFTP_FLAG_CREATE:
    309              flags |= os.O_CREAT
    310          if pflags & SFTP_FLAG_TRUNC:
    311              flags |= os.O_TRUNC
    312          if pflags & SFTP_FLAG_EXCL:
    313              flags |= os.O_EXCL
    314          return flags
    315
    316 -    def _process(self, t, request_number, msg):
    317          self._log(DEBUG, 'Request: %s' % CMD_NAMES[t])
    318          if t == CMD_OPEN:
    319              path = msg.get_string()
    320              flags = self._convert_pflags(msg.get_int())
    321              attr = SFTPAttributes._from_msg(msg)
    322              self._send_handle_response(request_number, self.server.open(path, flags, attr))
    323          elif t == CMD_CLOSE:
    324              handle = msg.get_string()
    325              if handle in self.folder_table:
    326                  del self.folder_table[handle]
    327                  self._send_status(request_number, SFTP_OK)
    328                  return
    329              if handle in self.file_table:
    330                  self.file_table[handle].close()
    331                  del self.file_table[handle]
    332                  self._send_status(request_number, SFTP_OK)
    333                  return
    334              self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    335          elif t == CMD_READ:
    336              handle = msg.get_string()
    337              offset = msg.get_int64()
    338              length = msg.get_int()
    339              if handle not in self.file_table:
    340                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    341                  return
    342              data = self.file_table[handle].read(offset, length)
    343              if type(data) is str:
    344                  if len(data) == 0:
    345                      self._send_status(request_number, SFTP_EOF)
    346                  else:
    347                      self._response(request_number, CMD_DATA, data)
    348              else:
    349                  self._send_status(request_number, data)
    350          elif t == CMD_WRITE:
    351              handle = msg.get_string()
    352              offset = msg.get_int64()
    353              data = msg.get_string()
    354              if handle not in self.file_table:
    355                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    356                  return
    357              self._send_status(request_number, self.file_table[handle].write(offset, data))
    358          elif t == CMD_REMOVE:
    359              path = msg.get_string()
    360              self._send_status(request_number, self.server.remove(path))
    361          elif t == CMD_RENAME:
    362              oldpath = msg.get_string()
    363              newpath = msg.get_string()
    364              self._send_status(request_number, self.server.rename(oldpath, newpath))
    365          elif t == CMD_MKDIR:
    366              path = msg.get_string()
    367              attr = SFTPAttributes._from_msg(msg)
    368              self._send_status(request_number, self.server.mkdir(path, attr))
    369          elif t == CMD_RMDIR:
    370              path = msg.get_string()
    371              self._send_status(request_number, self.server.rmdir(path))
    372          elif t == CMD_OPENDIR:
    373              path = msg.get_string()
    374              self._open_folder(request_number, path)
    375              return
    376          elif t == CMD_READDIR:
    377              handle = msg.get_string()
    378              if handle not in self.folder_table:
    379                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    380                  return
    381              folder = self.folder_table[handle]
    382              self._read_folder(request_number, folder)
    383          elif t == CMD_STAT:
    384              path = msg.get_string()
    385              resp = self.server.stat(path)
    386              if issubclass(type(resp), SFTPAttributes):
    387                  self._response(request_number, CMD_ATTRS, resp)
    388              else:
    389                  self._send_status(request_number, resp)
    390          elif t == CMD_LSTAT:
    391              path = msg.get_string()
    392              resp = self.server.lstat(path)
    393              if issubclass(type(resp), SFTPAttributes):
    394                  self._response(request_number, CMD_ATTRS, resp)
    395              else:
    396                  self._send_status(request_number, resp)
    397          elif t == CMD_FSTAT:
    398              handle = msg.get_string()
    399              if handle not in self.file_table:
    400                  self._send_status(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    401                  return
    402              resp = self.file_table[handle].stat()
    403              if issubclass(type(resp), SFTPAttributes):
    404                  self._response(request_number, CMD_ATTRS, resp)
    405              else:
    406                  self._send_status(request_number, resp)
    407          elif t == CMD_SETSTAT:
    408              path = msg.get_string()
    409              attr = SFTPAttributes._from_msg(msg)
    410              self._send_status(request_number, self.server.chattr(path, attr))
    411          elif t == CMD_FSETSTAT:
    412              handle = msg.get_string()
    413              attr = SFTPAttributes._from_msg(msg)
    414              if handle not in self.file_table:
    415                  self._response(request_number, SFTP_BAD_MESSAGE, 'Invalid handle')
    416                  return
    417              self._send_status(request_number, self.file_table[handle].chattr(attr))
    418          elif t == CMD_READLINK:
    419              path = msg.get_string()
    420              resp = self.server.readlink(path)
    421              if type(resp) is str:
    422                  self._response(request_number, CMD_NAME, 1, resp, '', SFTPAttributes())
    423              else:
    424                  self._send_status(request_number, resp)
    425          elif t == CMD_SYMLINK:
    426              # the sftp 2 draft is incorrect here!  path always follows target_path
    427              target_path = msg.get_string()
    428              path = msg.get_string()
    429              self._send_status(request_number, self.server.symlink(target_path, path))
    430          elif t == CMD_REALPATH:
    431              path = msg.get_string()
    432              rpath = self.server.canonicalize(path)
    433              self._response(request_number, CMD_NAME, 1, rpath, '', SFTPAttributes())
    434          elif t == CMD_EXTENDED:
    435              tag = msg.get_string()
    436              if tag == 'check-file':
    437                  self._check_file(request_number, msg)
    438              else:
    439                  self._send_status(request_number, SFTP_OP_UNSUPPORTED)
    440          else:
    441              self._send_status(request_number, SFTP_OP_UNSUPPORTED)
    442
    443
    444  from paramiko.sftp_handle import SFTPHandle
    445