sftp_server.py 19 KB

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