root/trunk/twisted/conch/ssh/filetransfer.py

Revision 32523, 32.7 KB (checked in by exarkun, 8 months ago)

Merge sftp-listing-eof-3009

Author: mt, jcollie
Reviewer: exarkun
Fixes: #3009

Change twisted.conch.ssh.filetransfer to handle short STATUS packets,
rather than raising an unhandled exception while trying to parse them. This
improves interoperability with other SFTP implementations which may leave the
message and/or language values out of the packet.

Line 
1# -*- test-case-name: twisted.conch.test.test_filetransfer -*-
2#
3# Copyright (c) Twisted Matrix Laboratories.
4# See LICENSE for details.
5
6
7import struct, errno
8
9from twisted.internet import defer, protocol
10from twisted.python import failure, log
11
12from common import NS, getNS
13from twisted.conch.interfaces import ISFTPServer, ISFTPFile
14
15from zope import interface
16
17
18
19class FileTransferBase(protocol.Protocol):
20
21    versions = (3, )
22
23    packetTypes = {}
24
25    def __init__(self):
26        self.buf = ''
27        self.otherVersion = None # this gets set
28
29    def sendPacket(self, kind, data):
30        self.transport.write(struct.pack('!LB', len(data)+1, kind) + data)
31
32    def dataReceived(self, data):
33        self.buf += data
34        while len(self.buf) > 5:
35            length, kind = struct.unpack('!LB', self.buf[:5])
36            if len(self.buf) < 4 + length:
37                return
38            data, self.buf = self.buf[5:4+length], self.buf[4+length:]
39            packetType = self.packetTypes.get(kind, None)
40            if not packetType:
41                log.msg('no packet type for', kind)
42                continue
43            f = getattr(self, 'packet_%s' % packetType, None)
44            if not f:
45                log.msg('not implemented: %s' % packetType)
46                log.msg(repr(data[4:]))
47                reqId, = struct.unpack('!L', data[:4])
48                self._sendStatus(reqId, FX_OP_UNSUPPORTED,
49                                 "don't understand %s" % packetType)
50                #XXX not implemented
51                continue
52            try:
53                f(data)
54            except:
55                log.err()
56                continue
57                reqId ,= struct.unpack('!L', data[:4])
58                self._ebStatus(failure.Failure(e), reqId)
59
60    def _parseAttributes(self, data):
61        flags ,= struct.unpack('!L', data[:4])
62        attrs = {}
63        data = data[4:]
64        if flags & FILEXFER_ATTR_SIZE == FILEXFER_ATTR_SIZE:
65            size ,= struct.unpack('!Q', data[:8])
66            attrs['size'] = size
67            data = data[8:]
68        if flags & FILEXFER_ATTR_OWNERGROUP == FILEXFER_ATTR_OWNERGROUP:
69            uid, gid = struct.unpack('!2L', data[:8])
70            attrs['uid'] = uid
71            attrs['gid'] = gid
72            data = data[8:]
73        if flags & FILEXFER_ATTR_PERMISSIONS == FILEXFER_ATTR_PERMISSIONS:
74            perms ,= struct.unpack('!L', data[:4])
75            attrs['permissions'] = perms
76            data = data[4:]
77        if flags & FILEXFER_ATTR_ACMODTIME == FILEXFER_ATTR_ACMODTIME:
78            atime, mtime = struct.unpack('!2L', data[:8])
79            attrs['atime'] = atime
80            attrs['mtime'] = mtime
81            data = data[8:]
82        if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED:
83            extended_count ,= struct.unpack('!L', data[:4])
84            data = data[4:]
85            for i in xrange(extended_count):
86                extended_type, data = getNS(data)
87                extended_data, data = getNS(data)
88                attrs['ext_%s' % extended_type] = extended_data
89        return attrs, data
90
91    def _packAttributes(self, attrs):
92        flags = 0
93        data = ''
94        if 'size' in attrs:
95            data += struct.pack('!Q', attrs['size'])
96            flags |= FILEXFER_ATTR_SIZE
97        if 'uid' in attrs and 'gid' in attrs:
98            data += struct.pack('!2L', attrs['uid'], attrs['gid'])
99            flags |= FILEXFER_ATTR_OWNERGROUP
100        if 'permissions' in attrs:
101            data += struct.pack('!L', attrs['permissions'])
102            flags |= FILEXFER_ATTR_PERMISSIONS
103        if 'atime' in attrs and 'mtime' in attrs:
104            data += struct.pack('!2L', attrs['atime'], attrs['mtime'])
105            flags |= FILEXFER_ATTR_ACMODTIME
106        extended = []
107        for k in attrs:
108            if k.startswith('ext_'):
109                ext_type = NS(k[4:])
110                ext_data = NS(attrs[k])
111                extended.append(ext_type+ext_data)
112        if extended:
113            data += struct.pack('!L', len(extended))
114            data += ''.join(extended)
115            flags |= FILEXFER_ATTR_EXTENDED
116        return struct.pack('!L', flags) + data
117
118class FileTransferServer(FileTransferBase):
119
120    def __init__(self, data=None, avatar=None):
121        FileTransferBase.__init__(self)
122        self.client = ISFTPServer(avatar) # yay interfaces
123        self.openFiles = {}
124        self.openDirs = {}
125
126    def packet_INIT(self, data):
127        version ,= struct.unpack('!L', data[:4])
128        self.version = min(list(self.versions) + [version])
129        data = data[4:]
130        ext = {}
131        while data:
132            ext_name, data = getNS(data)
133            ext_data, data = getNS(data)
134            ext[ext_name] = ext_data
135        our_ext = self.client.gotVersion(version, ext)
136        our_ext_data = ""
137        for (k,v) in our_ext.items():
138            our_ext_data += NS(k) + NS(v)
139        self.sendPacket(FXP_VERSION, struct.pack('!L', self.version) + \
140                                     our_ext_data)
141
142    def packet_OPEN(self, data):
143        requestId = data[:4]
144        data = data[4:]
145        filename, data = getNS(data)
146        flags ,= struct.unpack('!L', data[:4])
147        data = data[4:]
148        attrs, data = self._parseAttributes(data)
149        assert data == '', 'still have data in OPEN: %s' % repr(data)
150        d = defer.maybeDeferred(self.client.openFile, filename, flags, attrs)
151        d.addCallback(self._cbOpenFile, requestId)
152        d.addErrback(self._ebStatus, requestId, "open failed")
153
154    def _cbOpenFile(self, fileObj, requestId):
155        fileId = str(hash(fileObj))
156        if fileId in self.openFiles:
157            raise KeyError, 'id already open'
158        self.openFiles[fileId] = fileObj
159        self.sendPacket(FXP_HANDLE, requestId + NS(fileId))
160
161    def packet_CLOSE(self, data):
162        requestId = data[:4]
163        data = data[4:]
164        handle, data = getNS(data)
165        assert data == '', 'still have data in CLOSE: %s' % repr(data)
166        if handle in self.openFiles:
167            fileObj = self.openFiles[handle]
168            d = defer.maybeDeferred(fileObj.close)
169            d.addCallback(self._cbClose, handle, requestId)
170            d.addErrback(self._ebStatus, requestId, "close failed")
171        elif handle in self.openDirs:
172            dirObj = self.openDirs[handle][0]
173            d = defer.maybeDeferred(dirObj.close)
174            d.addCallback(self._cbClose, handle, requestId, 1)
175            d.addErrback(self._ebStatus, requestId, "close failed")
176        else:
177            self._ebClose(failure.Failure(KeyError()), requestId)
178
179    def _cbClose(self, result, handle, requestId, isDir = 0):
180        if isDir:
181            del self.openDirs[handle]
182        else:
183            del self.openFiles[handle]
184        self._sendStatus(requestId, FX_OK, 'file closed')
185
186    def packet_READ(self, data):
187        requestId = data[:4]
188        data = data[4:]
189        handle, data = getNS(data)
190        (offset, length), data = struct.unpack('!QL', data[:12]), data[12:]
191        assert data == '', 'still have data in READ: %s' % repr(data)
192        if handle not in self.openFiles:
193            self._ebRead(failure.Failure(KeyError()), requestId)
194        else:
195            fileObj = self.openFiles[handle]
196            d = defer.maybeDeferred(fileObj.readChunk, offset, length)
197            d.addCallback(self._cbRead, requestId)
198            d.addErrback(self._ebStatus, requestId, "read failed")
199
200    def _cbRead(self, result, requestId):
201        if result == '': # python's read will return this for EOF
202            raise EOFError()
203        self.sendPacket(FXP_DATA, requestId + NS(result))
204
205    def packet_WRITE(self, data):
206        requestId = data[:4]
207        data = data[4:]
208        handle, data = getNS(data)
209        offset, = struct.unpack('!Q', data[:8])
210        data = data[8:]
211        writeData, data = getNS(data)
212        assert data == '', 'still have data in WRITE: %s' % repr(data)
213        if handle not in self.openFiles:
214            self._ebWrite(failure.Failure(KeyError()), requestId)
215        else:
216            fileObj = self.openFiles[handle]
217            d = defer.maybeDeferred(fileObj.writeChunk, offset, writeData)
218            d.addCallback(self._cbStatus, requestId, "write succeeded")
219            d.addErrback(self._ebStatus, requestId, "write failed")
220
221    def packet_REMOVE(self, data):
222        requestId = data[:4]
223        data = data[4:]
224        filename, data = getNS(data)
225        assert data == '', 'still have data in REMOVE: %s' % repr(data)
226        d = defer.maybeDeferred(self.client.removeFile, filename)
227        d.addCallback(self._cbStatus, requestId, "remove succeeded")
228        d.addErrback(self._ebStatus, requestId, "remove failed")
229
230    def packet_RENAME(self, data):
231        requestId = data[:4]
232        data = data[4:]
233        oldPath, data = getNS(data)
234        newPath, data = getNS(data)
235        assert data == '', 'still have data in RENAME: %s' % repr(data)
236        d = defer.maybeDeferred(self.client.renameFile, oldPath, newPath)
237        d.addCallback(self._cbStatus, requestId, "rename succeeded")
238        d.addErrback(self._ebStatus, requestId, "rename failed")
239
240    def packet_MKDIR(self, data):
241        requestId = data[:4]
242        data = data[4:]
243        path, data = getNS(data)
244        attrs, data = self._parseAttributes(data)
245        assert data == '', 'still have data in MKDIR: %s' % repr(data)
246        d = defer.maybeDeferred(self.client.makeDirectory, path, attrs)
247        d.addCallback(self._cbStatus, requestId, "mkdir succeeded")
248        d.addErrback(self._ebStatus, requestId, "mkdir failed")
249
250    def packet_RMDIR(self, data):
251        requestId = data[:4]
252        data = data[4:]
253        path, data = getNS(data)
254        assert data == '', 'still have data in RMDIR: %s' % repr(data)
255        d = defer.maybeDeferred(self.client.removeDirectory, path)
256        d.addCallback(self._cbStatus, requestId, "rmdir succeeded")
257        d.addErrback(self._ebStatus, requestId, "rmdir failed")
258
259    def packet_OPENDIR(self, data):
260        requestId = data[:4]
261        data = data[4:]
262        path, data = getNS(data)
263        assert data == '', 'still have data in OPENDIR: %s' % repr(data)
264        d = defer.maybeDeferred(self.client.openDirectory, path)
265        d.addCallback(self._cbOpenDirectory, requestId)
266        d.addErrback(self._ebStatus, requestId, "opendir failed")
267
268    def _cbOpenDirectory(self, dirObj, requestId):
269        handle = str(hash(dirObj))
270        if handle in self.openDirs:
271            raise KeyError, "already opened this directory"
272        self.openDirs[handle] = [dirObj, iter(dirObj)]
273        self.sendPacket(FXP_HANDLE, requestId + NS(handle))
274
275    def packet_READDIR(self, data):
276        requestId = data[:4]
277        data = data[4:]
278        handle, data = getNS(data)
279        assert data == '', 'still have data in READDIR: %s' % repr(data)
280        if handle not in self.openDirs:
281            self._ebStatus(failure.Failure(KeyError()), requestId)
282        else:
283            dirObj, dirIter = self.openDirs[handle]
284            d = defer.maybeDeferred(self._scanDirectory, dirIter, [])
285            d.addCallback(self._cbSendDirectory, requestId)
286            d.addErrback(self._ebStatus, requestId, "scan directory failed")
287
288    def _scanDirectory(self, dirIter, f):
289        while len(f) < 250:
290            try:
291                info = dirIter.next()
292            except StopIteration:
293                if not f:
294                    raise EOFError
295                return f
296            if isinstance(info, defer.Deferred):
297                info.addCallback(self._cbScanDirectory, dirIter, f)
298                return
299            else:
300                f.append(info)
301        return f
302
303    def _cbScanDirectory(self, result, dirIter, f):
304        f.append(result)
305        return self._scanDirectory(dirIter, f)
306
307    def _cbSendDirectory(self, result, requestId):
308        data = ''
309        for (filename, longname, attrs) in result:
310            data += NS(filename)
311            data += NS(longname)
312            data += self._packAttributes(attrs)
313        self.sendPacket(FXP_NAME, requestId +
314                        struct.pack('!L', len(result))+data)
315
316    def packet_STAT(self, data, followLinks = 1):
317        requestId = data[:4]
318        data = data[4:]
319        path, data = getNS(data)
320        assert data == '', 'still have data in STAT/LSTAT: %s' % repr(data)
321        d = defer.maybeDeferred(self.client.getAttrs, path, followLinks)
322        d.addCallback(self._cbStat, requestId)
323        d.addErrback(self._ebStatus, requestId, 'stat/lstat failed')
324
325    def packet_LSTAT(self, data):
326        self.packet_STAT(data, 0)
327
328    def packet_FSTAT(self, data):
329        requestId = data[:4]
330        data = data[4:]
331        handle, data = getNS(data)
332        assert data == '', 'still have data in FSTAT: %s' % repr(data)
333        if handle not in self.openFiles:
334            self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles'
335                                        % handle)), requestId)
336        else:
337            fileObj = self.openFiles[handle]
338            d = defer.maybeDeferred(fileObj.getAttrs)
339            d.addCallback(self._cbStat, requestId)
340            d.addErrback(self._ebStatus, requestId, 'fstat failed')
341
342    def _cbStat(self, result, requestId):
343        data = requestId + self._packAttributes(result)
344        self.sendPacket(FXP_ATTRS, data)
345
346    def packet_SETSTAT(self, data):
347        requestId = data[:4]
348        data = data[4:]
349        path, data = getNS(data)
350        attrs, data = self._parseAttributes(data)
351        if data != '':
352            log.msg('WARN: still have data in SETSTAT: %s' % repr(data))
353        d = defer.maybeDeferred(self.client.setAttrs, path, attrs)
354        d.addCallback(self._cbStatus, requestId, 'setstat succeeded')
355        d.addErrback(self._ebStatus, requestId, 'setstat failed')
356
357    def packet_FSETSTAT(self, data):
358        requestId = data[:4]
359        data = data[4:]
360        handle, data = getNS(data)
361        attrs, data = self._parseAttributes(data)
362        assert data == '', 'still have data in FSETSTAT: %s' % repr(data)
363        if handle not in self.openFiles:
364            self._ebStatus(failure.Failure(KeyError()), requestId)
365        else:
366            fileObj = self.openFiles[handle]
367            d = defer.maybeDeferred(fileObj.setAttrs, attrs)
368            d.addCallback(self._cbStatus, requestId, 'fsetstat succeeded')
369            d.addErrback(self._ebStatus, requestId, 'fsetstat failed')
370
371    def packet_READLINK(self, data):
372        requestId = data[:4]
373        data = data[4:]
374        path, data = getNS(data)
375        assert data == '', 'still have data in READLINK: %s' % repr(data)
376        d = defer.maybeDeferred(self.client.readLink, path)
377        d.addCallback(self._cbReadLink, requestId)
378        d.addErrback(self._ebStatus, requestId, 'readlink failed')
379
380    def _cbReadLink(self, result, requestId):
381        self._cbSendDirectory([(result, '', {})], requestId)
382
383    def packet_SYMLINK(self, data):
384        requestId = data[:4]
385        data = data[4:]
386        linkPath, data = getNS(data)
387        targetPath, data = getNS(data)
388        d = defer.maybeDeferred(self.client.makeLink, linkPath, targetPath)
389        d.addCallback(self._cbStatus, requestId, 'symlink succeeded')
390        d.addErrback(self._ebStatus, requestId, 'symlink failed')
391
392    def packet_REALPATH(self, data):
393        requestId = data[:4]
394        data = data[4:]
395        path, data = getNS(data)
396        assert data == '', 'still have data in REALPATH: %s' % repr(data)
397        d = defer.maybeDeferred(self.client.realPath, path)
398        d.addCallback(self._cbReadLink, requestId) # same return format
399        d.addErrback(self._ebStatus, requestId, 'realpath failed')
400
401    def packet_EXTENDED(self, data):
402        requestId = data[:4]
403        data = data[4:]
404        extName, extData = getNS(data)
405        d = defer.maybeDeferred(self.client.extendedRequest, extName, extData)
406        d.addCallback(self._cbExtended, requestId)
407        d.addErrback(self._ebStatus, requestId, 'extended %s failed' % extName)
408
409    def _cbExtended(self, data, requestId):
410        self.sendPacket(FXP_EXTENDED_REPLY, requestId + data)
411
412    def _cbStatus(self, result, requestId, msg = "request succeeded"):
413        self._sendStatus(requestId, FX_OK, msg)
414
415    def _ebStatus(self, reason, requestId, msg = "request failed"):
416        code = FX_FAILURE
417        message = msg
418        if reason.type in (IOError, OSError):
419            if reason.value.errno == errno.ENOENT: # no such file
420                code = FX_NO_SUCH_FILE
421                message = reason.value.strerror
422            elif reason.value.errno == errno.EACCES: # permission denied
423                code = FX_PERMISSION_DENIED
424                message = reason.value.strerror
425            elif reason.value.errno == errno.EEXIST:
426                code = FX_FILE_ALREADY_EXISTS
427            else:
428                log.err(reason)
429        elif reason.type == EOFError: # EOF
430            code = FX_EOF
431            if reason.value.args:
432                message = reason.value.args[0]
433        elif reason.type == NotImplementedError:
434            code = FX_OP_UNSUPPORTED
435            if reason.value.args:
436                message = reason.value.args[0]
437        elif reason.type == SFTPError:
438            code = reason.value.code
439            message = reason.value.message
440        else:
441            log.err(reason)
442        self._sendStatus(requestId, code, message)
443
444    def _sendStatus(self, requestId, code, message, lang = ''):
445        """
446        Helper method to send a FXP_STATUS message.
447        """
448        data = requestId + struct.pack('!L', code)
449        data += NS(message)
450        data += NS(lang)
451        self.sendPacket(FXP_STATUS, data)
452
453
454    def connectionLost(self, reason):
455        """
456        Clean all opened files and directories.
457        """
458        for fileObj in self.openFiles.values():
459            fileObj.close()
460        self.openFiles = {}
461        for (dirObj, dirIter) in self.openDirs.values():
462            dirObj.close()
463        self.openDirs = {}
464
465
466
467class FileTransferClient(FileTransferBase):
468
469    def __init__(self, extData = {}):
470        """
471        @param extData: a dict of extended_name : extended_data items
472        to be sent to the server.
473        """
474        FileTransferBase.__init__(self)
475        self.extData = {}
476        self.counter = 0
477        self.openRequests = {} # id -> Deferred
478        self.wasAFile = {} # Deferred -> 1 TERRIBLE HACK
479
480    def connectionMade(self):
481        data = struct.pack('!L', max(self.versions))
482        for k,v in self.extData.itervalues():
483            data += NS(k) + NS(v)
484        self.sendPacket(FXP_INIT, data)
485
486    def _sendRequest(self, msg, data):
487        data = struct.pack('!L', self.counter) + data
488        d = defer.Deferred()
489        self.openRequests[self.counter] = d
490        self.counter += 1
491        self.sendPacket(msg, data)
492        return d
493
494    def _parseRequest(self, data):
495        (id,) = struct.unpack('!L', data[:4])
496        d = self.openRequests[id]
497        del self.openRequests[id]
498        return d, data[4:]
499
500    def openFile(self, filename, flags, attrs):
501        """
502        Open a file.
503
504        This method returns a L{Deferred} that is called back with an object
505        that provides the L{ISFTPFile} interface.
506
507        @param filename: a string representing the file to open.
508
509        @param flags: a integer of the flags to open the file with, ORed together.
510        The flags and their values are listed at the bottom of this file.
511
512        @param attrs: a list of attributes to open the file with.  It is a
513        dictionary, consisting of 0 or more keys.  The possible keys are::
514
515            size: the size of the file in bytes
516            uid: the user ID of the file as an integer
517            gid: the group ID of the file as an integer
518            permissions: the permissions of the file with as an integer.
519            the bit representation of this field is defined by POSIX.
520            atime: the access time of the file as seconds since the epoch.
521            mtime: the modification time of the file as seconds since the epoch.
522            ext_*: extended attributes.  The server is not required to
523            understand this, but it may.
524
525        NOTE: there is no way to indicate text or binary files.  it is up
526        to the SFTP client to deal with this.
527        """
528        data = NS(filename) + struct.pack('!L', flags) + self._packAttributes(attrs)
529        d = self._sendRequest(FXP_OPEN, data)
530        self.wasAFile[d] = (1, filename) # HACK
531        return d
532
533    def removeFile(self, filename):
534        """
535        Remove the given file.
536
537        This method returns a Deferred that is called back when it succeeds.
538
539        @param filename: the name of the file as a string.
540        """
541        return self._sendRequest(FXP_REMOVE, NS(filename))
542
543    def renameFile(self, oldpath, newpath):
544        """
545        Rename the given file.
546
547        This method returns a Deferred that is called back when it succeeds.
548
549        @param oldpath: the current location of the file.
550        @param newpath: the new file name.
551        """
552        return self._sendRequest(FXP_RENAME, NS(oldpath)+NS(newpath))
553
554    def makeDirectory(self, path, attrs):
555        """
556        Make a directory.
557
558        This method returns a Deferred that is called back when it is
559        created.
560
561        @param path: the name of the directory to create as a string.
562
563        @param attrs: a dictionary of attributes to create the directory
564        with.  Its meaning is the same as the attrs in the openFile method.
565        """
566        return self._sendRequest(FXP_MKDIR, NS(path)+self._packAttributes(attrs))
567
568    def removeDirectory(self, path):
569        """
570        Remove a directory (non-recursively)
571
572        It is an error to remove a directory that has files or directories in
573        it.
574
575        This method returns a Deferred that is called back when it is removed.
576
577        @param path: the directory to remove.
578        """
579        return self._sendRequest(FXP_RMDIR, NS(path))
580
581    def openDirectory(self, path):
582        """
583        Open a directory for scanning.
584
585        This method returns a Deferred that is called back with an iterable
586        object that has a close() method.
587
588        The close() method is called when the client is finished reading
589        from the directory.  At this point, the iterable will no longer
590        be used.
591
592        The iterable returns triples of the form (filename, longname, attrs)
593        or a Deferred that returns the same.  The sequence must support
594        __getitem__, but otherwise may be any 'sequence-like' object.
595
596        filename is the name of the file relative to the directory.
597        logname is an expanded format of the filename.  The recommended format
598        is:
599        -rwxr-xr-x   1 mjos     staff      348911 Mar 25 14:29 t-filexfer
600        1234567890 123 12345678 12345678 12345678 123456789012
601
602        The first line is sample output, the second is the length of the field.
603        The fields are: permissions, link count, user owner, group owner,
604        size in bytes, modification time.
605
606        attrs is a dictionary in the format of the attrs argument to openFile.
607
608        @param path: the directory to open.
609        """
610        d = self._sendRequest(FXP_OPENDIR, NS(path))
611        self.wasAFile[d] = (0, path)
612        return d
613
614    def getAttrs(self, path, followLinks=0):
615        """
616        Return the attributes for the given path.
617
618        This method returns a dictionary in the same format as the attrs
619        argument to openFile or a Deferred that is called back with same.
620
621        @param path: the path to return attributes for as a string.
622        @param followLinks: a boolean.  if it is True, follow symbolic links
623        and return attributes for the real path at the base.  if it is False,
624        return attributes for the specified path.
625        """
626        if followLinks: m = FXP_STAT
627        else: m = FXP_LSTAT
628        return self._sendRequest(m, NS(path))
629
630    def setAttrs(self, path, attrs):
631        """
632        Set the attributes for the path.
633
634        This method returns when the attributes are set or a Deferred that is
635        called back when they are.
636
637        @param path: the path to set attributes for as a string.
638        @param attrs: a dictionary in the same format as the attrs argument to
639        openFile.
640        """
641        data = NS(path) + self._packAttributes(attrs)
642        return self._sendRequest(FXP_SETSTAT, data)
643
644    def readLink(self, path):
645        """
646        Find the root of a set of symbolic links.
647
648        This method returns the target of the link, or a Deferred that
649        returns the same.
650
651        @param path: the path of the symlink to read.
652        """
653        d = self._sendRequest(FXP_READLINK, NS(path))
654        return d.addCallback(self._cbRealPath)
655
656    def makeLink(self, linkPath, targetPath):
657        """
658        Create a symbolic link.
659
660        This method returns when the link is made, or a Deferred that
661        returns the same.
662
663        @param linkPath: the pathname of the symlink as a string
664        @param targetPath: the path of the target of the link as a string.
665        """
666        return self._sendRequest(FXP_SYMLINK, NS(linkPath)+NS(targetPath))
667
668    def realPath(self, path):
669        """
670        Convert any path to an absolute path.
671
672        This method returns the absolute path as a string, or a Deferred
673        that returns the same.
674
675        @param path: the path to convert as a string.
676        """
677        d = self._sendRequest(FXP_REALPATH, NS(path))
678        return d.addCallback(self._cbRealPath)
679
680    def _cbRealPath(self, result):
681        name, longname, attrs = result[0]
682        return name
683
684    def extendedRequest(self, request, data):
685        """
686        Make an extended request of the server.
687
688        The method returns a Deferred that is called back with
689        the result of the extended request.
690
691        @param request: the name of the extended request to make.
692        @param data: any other data that goes along with the request.
693        """
694        return self._sendRequest(FXP_EXTENDED, NS(request) + data)
695
696    def packet_VERSION(self, data):
697        version, = struct.unpack('!L', data[:4])
698        data = data[4:]
699        d = {}
700        while data:
701            k, data = getNS(data)
702            v, data = getNS(data)
703            d[k]=v
704        self.version = version
705        self.gotServerVersion(version, d)
706
707    def packet_STATUS(self, data):
708        d, data = self._parseRequest(data)
709        code, = struct.unpack('!L', data[:4])
710        data = data[4:]
711        if len(data) >= 4:
712            msg, data = getNS(data)
713            if len(data) >= 4:
714                lang, data = getNS(data)
715            else:
716                lang = ''
717        else:
718            msg = ''
719            lang = ''
720        if code == FX_OK:
721            d.callback((msg, lang))
722        elif code == FX_EOF:
723            d.errback(EOFError(msg))
724        elif code == FX_OP_UNSUPPORTED:
725            d.errback(NotImplementedError(msg))
726        else:
727            d.errback(SFTPError(code, msg, lang))
728
729    def packet_HANDLE(self, data):
730        d, data = self._parseRequest(data)
731        isFile, name = self.wasAFile.pop(d)
732        if isFile:
733            cb = ClientFile(self, getNS(data)[0])
734        else:
735            cb = ClientDirectory(self, getNS(data)[0])
736        cb.name = name
737        d.callback(cb)
738
739    def packet_DATA(self, data):
740        d, data = self._parseRequest(data)
741        d.callback(getNS(data)[0])
742
743    def packet_NAME(self, data):
744        d, data = self._parseRequest(data)
745        count, = struct.unpack('!L', data[:4])
746        data = data[4:]
747        files = []
748        for i in range(count):
749            filename, data = getNS(data)
750            longname, data = getNS(data)
751            attrs, data = self._parseAttributes(data)
752            files.append((filename, longname, attrs))
753        d.callback(files)
754
755    def packet_ATTRS(self, data):
756        d, data = self._parseRequest(data)
757        d.callback(self._parseAttributes(data)[0])
758
759    def packet_EXTENDED_REPLY(self, data):
760        d, data = self._parseRequest(data)
761        d.callback(data)
762
763    def gotServerVersion(self, serverVersion, extData):
764        """
765        Called when the client sends their version info.
766
767        @param otherVersion: an integer representing the version of the SFTP
768        protocol they are claiming.
769        @param extData: a dictionary of extended_name : extended_data items.
770        These items are sent by the client to indicate additional features.
771        """
772
773class ClientFile:
774
775    interface.implements(ISFTPFile)
776
777    def __init__(self, parent, handle):
778        self.parent = parent
779        self.handle = NS(handle)
780
781    def close(self):
782        return self.parent._sendRequest(FXP_CLOSE, self.handle)
783
784    def readChunk(self, offset, length):
785        data = self.handle + struct.pack("!QL", offset, length)
786        return self.parent._sendRequest(FXP_READ, data)
787
788    def writeChunk(self, offset, chunk):
789        data = self.handle + struct.pack("!Q", offset) + NS(chunk)
790        return self.parent._sendRequest(FXP_WRITE, data)
791
792    def getAttrs(self):
793        return self.parent._sendRequest(FXP_FSTAT, self.handle)
794
795    def setAttrs(self, attrs):
796        data = self.handle + self.parent._packAttributes(attrs)
797        return self.parent._sendRequest(FXP_FSTAT, data)
798
799class ClientDirectory:
800
801    def __init__(self, parent, handle):
802        self.parent = parent
803        self.handle = NS(handle)
804        self.filesCache = []
805
806    def read(self):
807        d = self.parent._sendRequest(FXP_READDIR, self.handle)
808        return d
809
810    def close(self):
811        return self.parent._sendRequest(FXP_CLOSE, self.handle)
812
813    def __iter__(self):
814        return self
815
816    def next(self):
817        if self.filesCache:
818            return self.filesCache.pop(0)
819        d = self.read()
820        d.addCallback(self._cbReadDir)
821        d.addErrback(self._ebReadDir)
822        return d
823
824    def _cbReadDir(self, names):
825        self.filesCache = names[1:]
826        return names[0]
827
828    def _ebReadDir(self, reason):
829        reason.trap(EOFError)
830        def _():
831            raise StopIteration
832        self.next = _
833        return reason
834
835
836class SFTPError(Exception):
837
838    def __init__(self, errorCode, errorMessage, lang = ''):
839        Exception.__init__(self)
840        self.code = errorCode
841        self._message = errorMessage
842        self.lang = lang
843
844
845    def message(self):
846        """
847        A string received over the network that explains the error to a human.
848        """
849        # Python 2.6 deprecates assigning to the 'message' attribute of an
850        # exception. We define this read-only property here in order to
851        # prevent the warning about deprecation while maintaining backwards
852        # compatibility with object clients that rely on the 'message'
853        # attribute being set correctly. See bug #3897.
854        return self._message
855    message = property(message)
856
857
858    def __str__(self):
859        return 'SFTPError %s: %s' % (self.code, self.message)
860
861FXP_INIT            =   1
862FXP_VERSION         =   2
863FXP_OPEN            =   3
864FXP_CLOSE           =   4
865FXP_READ            =   5
866FXP_WRITE           =   6
867FXP_LSTAT           =   7
868FXP_FSTAT           =   8
869FXP_SETSTAT         =   9
870FXP_FSETSTAT        =  10
871FXP_OPENDIR         =  11
872FXP_READDIR         =  12
873FXP_REMOVE          =  13
874FXP_MKDIR           =  14
875FXP_RMDIR           =  15
876FXP_REALPATH        =  16
877FXP_STAT            =  17
878FXP_RENAME          =  18
879FXP_READLINK        =  19
880FXP_SYMLINK         =  20
881FXP_STATUS          = 101
882FXP_HANDLE          = 102
883FXP_DATA            = 103
884FXP_NAME            = 104
885FXP_ATTRS           = 105
886FXP_EXTENDED        = 200
887FXP_EXTENDED_REPLY  = 201
888
889FILEXFER_ATTR_SIZE        = 0x00000001
890FILEXFER_ATTR_UIDGID      = 0x00000002
891FILEXFER_ATTR_OWNERGROUP  = FILEXFER_ATTR_UIDGID
892FILEXFER_ATTR_PERMISSIONS = 0x00000004
893FILEXFER_ATTR_ACMODTIME   = 0x00000008
894FILEXFER_ATTR_EXTENDED    = 0x80000000L
895
896FILEXFER_TYPE_REGULAR        = 1
897FILEXFER_TYPE_DIRECTORY      = 2
898FILEXFER_TYPE_SYMLINK        = 3
899FILEXFER_TYPE_SPECIAL        = 4
900FILEXFER_TYPE_UNKNOWN        = 5
901
902FXF_READ          = 0x00000001
903FXF_WRITE         = 0x00000002
904FXF_APPEND        = 0x00000004
905FXF_CREAT         = 0x00000008
906FXF_TRUNC         = 0x00000010
907FXF_EXCL          = 0x00000020
908FXF_TEXT          = 0x00000040
909
910FX_OK                          = 0
911FX_EOF                         = 1
912FX_NO_SUCH_FILE                = 2
913FX_PERMISSION_DENIED           = 3
914FX_FAILURE                     = 4
915FX_BAD_MESSAGE                 = 5
916FX_NO_CONNECTION               = 6
917FX_CONNECTION_LOST             = 7
918FX_OP_UNSUPPORTED              = 8
919FX_FILE_ALREADY_EXISTS         = 11
920# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more
921# useful error codes, but so far OpenSSH doesn't implement them.  We use them
922# internally for clarity, but for now define them all as FX_FAILURE to be
923# compatible with existing software.
924FX_NOT_A_DIRECTORY             = FX_FAILURE
925FX_FILE_IS_A_DIRECTORY         = FX_FAILURE
926
927
928# initialize FileTransferBase.packetTypes:
929g = globals()
930for name in g.keys():
931    if name.startswith('FXP_'):
932        value = g[name]
933        FileTransferBase.packetTypes[value] = name[4:]
934del g, name, value
Note: See TracBrowser for help on using the browser.