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