sftp_server.py 19 KB

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