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