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