]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/sftpd.py
stringutils.py, sftpd.py: Portability fixes for Python <= 2.5.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / sftpd.py
1
2 import os, tempfile, heapq, binascii, traceback, array, stat, struct
3 from stat import S_IFREG, S_IFDIR
4 from time import time, strftime, localtime
5
6 from zope.interface import implements
7 from twisted.python import components
8 from twisted.application import service, strports
9 from twisted.conch.ssh import factory, keys, session
10 from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
11      FX_NO_SUCH_FILE, FX_OP_UNSUPPORTED, FX_PERMISSION_DENIED, FX_EOF, \
12      FX_BAD_MESSAGE, FX_FAILURE, FX_OK
13 from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, \
14      FXF_CREAT, FXF_TRUNC, FXF_EXCL
15 from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser, ISession
16 from twisted.conch.avatar import ConchUser
17 from twisted.conch.openssh_compat import primes
18 from twisted.cred import portal
19 from twisted.internet.error import ProcessDone, ProcessTerminated
20 from twisted.python.failure import Failure
21 from twisted.internet.interfaces import ITransport
22
23 from twisted.internet import defer
24 from twisted.internet.interfaces import IFinishableConsumer
25 from foolscap.api import eventually
26 from allmydata.util import deferredutil
27
28 from allmydata.util.consumer import download_to_data
29 from allmydata.interfaces import IFileNode, IDirectoryNode, ExistingChildError, \
30      NoSuchChildError, ChildOfWrongTypeError
31 from allmydata.mutable.common import NotWriteableError
32 from allmydata.immutable.upload import FileHandle
33 from allmydata.dirnode import update_metadata
34
35 from pycryptopp.cipher.aes import AES
36
37 noisy = True
38 use_foolscap_logging = True
39
40 from allmydata.util.log import NOISY, OPERATIONAL, WEIRD, \
41     msg as _msg, err as _err, PrefixingLogMixin as _PrefixingLogMixin
42
43 if use_foolscap_logging:
44     (logmsg, logerr, PrefixingLogMixin) = (_msg, _err, _PrefixingLogMixin)
45 else:  # pragma: no cover
46     def logmsg(s, level=None):
47         print s
48     def logerr(s, level=None):
49         print s
50     class PrefixingLogMixin:
51         def __init__(self, facility=None, prefix=''):
52             self.prefix = prefix
53         def log(self, s, level=None):
54             print "%r %s" % (self.prefix, s)
55
56
57 def eventually_callback(d):
58     return lambda res: eventually(d.callback, res)
59
60 def eventually_errback(d):
61     return lambda err: eventually(d.errback, err)
62
63
64 def _utf8(x):
65     if isinstance(x, unicode):
66         return x.encode('utf-8')
67     if isinstance(x, str):
68         return x
69     return repr(x)
70
71
72 def _to_sftp_time(t):
73     """SFTP times are unsigned 32-bit integers representing UTC seconds
74     (ignoring leap seconds) since the Unix epoch, January 1 1970 00:00 UTC.
75     A Tahoe time is the corresponding float."""
76     return long(t) & 0xFFFFFFFFL
77
78
79 def _convert_error(res, request):
80     if not isinstance(res, Failure):
81         logged_res = res
82         if isinstance(res, str): logged_res = "<data of length %r>" % (len(res),)
83         logmsg("SUCCESS %r %r" % (request, logged_res,), level=OPERATIONAL)
84         return res
85
86     err = res
87     logmsg("RAISE %r %r" % (request, err.value), level=OPERATIONAL)
88     try:
89         if noisy: logmsg(traceback.format_exc(err.value), level=NOISY)
90     except:  # pragma: no cover
91         pass
92
93     # The message argument to SFTPError must not reveal information that
94     # might compromise anonymity.
95
96     if err.check(SFTPError):
97         # original raiser of SFTPError has responsibility to ensure anonymity
98         raise err
99     if err.check(NoSuchChildError):
100         childname = _utf8(err.value.args[0])
101         raise SFTPError(FX_NO_SUCH_FILE, childname)
102     if err.check(NotWriteableError) or err.check(ChildOfWrongTypeError):
103         msg = _utf8(err.value.args[0])
104         raise SFTPError(FX_PERMISSION_DENIED, msg)
105     if err.check(ExistingChildError):
106         # Versions of SFTP after v3 (which is what twisted.conch implements)
107         # define a specific error code for this case: FX_FILE_ALREADY_EXISTS.
108         # However v3 doesn't; instead, other servers such as sshd return
109         # FX_FAILURE. The gvfs SFTP backend, for example, depends on this
110         # to translate the error to the equivalent of POSIX EEXIST, which is
111         # necessary for some picky programs (such as gedit).
112         msg = _utf8(err.value.args[0])
113         raise SFTPError(FX_FAILURE, msg)
114     if err.check(NotImplementedError):
115         raise SFTPError(FX_OP_UNSUPPORTED, _utf8(err.value))
116     if err.check(EOFError):
117         raise SFTPError(FX_EOF, "end of file reached")
118     if err.check(defer.FirstError):
119         _convert_error(err.value.subFailure, request)
120
121     # We assume that the error message is not anonymity-sensitive.
122     raise SFTPError(FX_FAILURE, _utf8(err.value))
123
124
125 def _repr_flags(flags):
126     return "|".join([f for f in
127                      [(flags & FXF_READ)   and "FXF_READ"   or None,
128                       (flags & FXF_WRITE)  and "FXF_WRITE"  or None,
129                       (flags & FXF_APPEND) and "FXF_APPEND" or None,
130                       (flags & FXF_CREAT)  and "FXF_CREAT"  or None,
131                       (flags & FXF_TRUNC)  and "FXF_TRUNC"  or None,
132                       (flags & FXF_EXCL)   and "FXF_EXCL"   or None,
133                      ]
134                      if f])
135
136
137 def _lsLine(name, attrs):
138     st_uid = "tahoe"
139     st_gid = "tahoe"
140     st_mtime = attrs.get("mtime", 0)
141     st_mode = attrs["permissions"]
142     # TODO: check that clients are okay with this being a "?".
143     # (They should be because the longname is intended for human
144     # consumption.)
145     st_size = attrs.get("size", "?")
146     # We don't know how many links there really are to this object.
147     st_nlink = 1
148
149     # Based on <http://twistedmatrix.com/trac/browser/trunk/twisted/conch/ls.py?rev=25412>.
150     # We can't call the version in Twisted because we might have a version earlier than
151     # <http://twistedmatrix.com/trac/changeset/25412> (released in Twisted 8.2).
152
153     mode = st_mode
154     perms = array.array('c', '-'*10)
155     ft = stat.S_IFMT(mode)
156     if   stat.S_ISDIR(ft): perms[0] = 'd'
157     elif stat.S_ISREG(ft): perms[0] = '-'
158     else: perms[0] = '?'
159     # user
160     if mode&stat.S_IRUSR: perms[1] = 'r'
161     if mode&stat.S_IWUSR: perms[2] = 'w'
162     if mode&stat.S_IXUSR: perms[3] = 'x'
163     # group
164     if mode&stat.S_IRGRP: perms[4] = 'r'
165     if mode&stat.S_IWGRP: perms[5] = 'w'
166     if mode&stat.S_IXGRP: perms[6] = 'x'
167     # other
168     if mode&stat.S_IROTH: perms[7] = 'r'
169     if mode&stat.S_IWOTH: perms[8] = 'w'
170     if mode&stat.S_IXOTH: perms[9] = 'x'
171     # suid/sgid never set
172
173     l = perms.tostring()
174     l += str(st_nlink).rjust(5) + ' '
175     un = str(st_uid)
176     l += un.ljust(9)
177     gr = str(st_gid)
178     l += gr.ljust(9)
179     sz = str(st_size)
180     l += sz.rjust(8)
181     l += ' '
182     day = 60 * 60 * 24
183     sixmo = day * 7 * 26
184     now = time()
185     if st_mtime + sixmo < now or st_mtime > now + day:
186         # mtime is more than 6 months ago, or more than one day in the future
187         l += strftime("%b %d  %Y ", localtime(st_mtime))
188     else:
189         l += strftime("%b %d %H:%M ", localtime(st_mtime))
190     l += name
191     return l
192
193
194 def _no_write(parent_readonly, child, metadata=None):
195     """Whether child should be listed as having read-only permissions in parent."""
196
197     if child.is_unknown():
198         return True
199     elif child.is_mutable():
200         return child.is_readonly()
201     elif parent_readonly or IDirectoryNode.providedBy(child):
202         return True
203     else:
204         return metadata is not None and metadata.get('no-write', False)
205
206
207 def _populate_attrs(childnode, metadata, size=None):
208     attrs = {}
209
210     # The permissions must have the S_IFDIR (040000) or S_IFREG (0100000)
211     # bits, otherwise the client may refuse to open a directory.
212     # Also, sshfs run as a non-root user requires files and directories
213     # to be world-readable/writeable.
214     # It is important that we never set the executable bits on files.
215     #
216     # Directories and unknown nodes have no size, and SFTP doesn't
217     # require us to make one up.
218     #
219     # childnode might be None, meaning that the file doesn't exist yet,
220     # but we're going to write it later.
221
222     if childnode and childnode.is_unknown():
223         perms = 0
224     elif childnode and IDirectoryNode.providedBy(childnode):
225         perms = S_IFDIR | 0777
226     else:
227         # For files, omit the size if we don't immediately know it.
228         if childnode and size is None:
229             size = childnode.get_size()
230         if size is not None:
231             assert isinstance(size, (int, long)) and not isinstance(size, bool), repr(size)
232             attrs['size'] = size
233         perms = S_IFREG | 0666
234
235     if metadata:
236         if metadata.get('no-write', False):
237             perms &= S_IFDIR | S_IFREG | 0555  # clear 'w' bits
238
239         # See webapi.txt for what these times mean.
240         # We would prefer to omit atime, but SFTP version 3 can only
241         # accept mtime if atime is also set.
242         if 'linkmotime' in metadata.get('tahoe', {}):
243             attrs['mtime'] = attrs['atime'] = _to_sftp_time(metadata['tahoe']['linkmotime'])
244         elif 'mtime' in metadata:
245             attrs['mtime'] = attrs['atime'] = _to_sftp_time(metadata['mtime'])
246
247         if 'linkcrtime' in metadata.get('tahoe', {}):
248             attrs['createtime'] = _to_sftp_time(metadata['tahoe']['linkcrtime'])
249
250         if 'ctime' in metadata:
251             attrs['ctime'] = _to_sftp_time(metadata['ctime'])
252
253     attrs['permissions'] = perms
254
255     # twisted.conch.ssh.filetransfer only implements SFTP version 3,
256     # which doesn't include SSH_FILEXFER_ATTR_FLAGS.
257
258     return attrs
259
260
261 def _attrs_to_metadata(attrs):
262     metadata = {}
263
264     for key in attrs:
265         if key == "mtime" or key == "ctime" or key == "createtime":
266             metadata[key] = long(attrs[key])
267         elif key.startswith("ext_"):
268             metadata[key] = str(attrs[key])
269
270     perms = attrs.get('permissions', stat.S_IWUSR)
271     if not (perms & stat.S_IWUSR):
272         metadata['no-write'] = True
273
274     return metadata
275
276
277 def _direntry_for(filenode_or_parent, childname, filenode=None):
278     if childname is None:
279         filenode_or_parent = filenode
280
281     if filenode_or_parent:
282         rw_uri = filenode_or_parent.get_write_uri()
283         if rw_uri and childname:
284             return rw_uri + "/" + childname.encode('utf-8')
285         else:
286             return rw_uri
287
288     return None
289
290
291 class EncryptedTemporaryFile(PrefixingLogMixin):
292     # not implemented: next, readline, readlines, xreadlines, writelines
293
294     def __init__(self):
295         PrefixingLogMixin.__init__(self, facility="tahoe.sftp")
296         self.file = tempfile.TemporaryFile()
297         self.key = os.urandom(16)  # AES-128
298
299     def _crypt(self, offset, data):
300         # TODO: use random-access AES (pycryptopp ticket #18)
301         offset_big = offset // 16
302         offset_small = offset % 16
303         iv = binascii.unhexlify("%032x" % offset_big)
304         cipher = AES(self.key, iv=iv)
305         cipher.process("\x00"*offset_small)
306         return cipher.process(data)
307
308     def close(self):
309         self.file.close()
310
311     def flush(self):
312         self.file.flush()
313
314     def seek(self, offset, whence=0):  # 0 = SEEK_SET
315         if noisy: self.log(".seek(%r, %r)" % (offset, whence), level=NOISY)
316         self.file.seek(offset, whence)
317
318     def tell(self):
319         offset = self.file.tell()
320         if noisy: self.log(".tell() = %r" % (offset,), level=NOISY)
321         return offset
322
323     def read(self, size=-1):
324         if noisy: self.log(".read(%r)" % (size,), level=NOISY)
325         index = self.file.tell()
326         ciphertext = self.file.read(size)
327         plaintext = self._crypt(index, ciphertext)
328         return plaintext
329
330     def write(self, plaintext):
331         if noisy: self.log(".write(<data of length %r>)" % (len(plaintext),), level=NOISY)
332         index = self.file.tell()
333         ciphertext = self._crypt(index, plaintext)
334         self.file.write(ciphertext)
335
336     def truncate(self, newsize):
337         if noisy: self.log(".truncate(%r)" % (newsize,), level=NOISY)
338         self.file.truncate(newsize)
339
340
341 class OverwriteableFileConsumer(PrefixingLogMixin):
342     implements(IFinishableConsumer)
343     """I act both as a consumer for the download of the original file contents, and as a
344     wrapper for a temporary file that records the downloaded data and any overwrites.
345     I use a priority queue to keep track of which regions of the file have been overwritten
346     but not yet downloaded, so that the download does not clobber overwritten data.
347     I use another priority queue to record milestones at which to make callbacks
348     indicating that a given number of bytes have been downloaded.
349
350     The temporary file reflects the contents of the file that I represent, except that:
351      - regions that have neither been downloaded nor overwritten, if present,
352        contain garbage.
353      - the temporary file may be shorter than the represented file (it is never longer).
354        The latter's current size is stored in self.current_size.
355
356     This abstraction is mostly independent of SFTP. Consider moving it, if it is found
357     useful for other frontends."""
358
359     def __init__(self, download_size, tempfile_maker):
360         PrefixingLogMixin.__init__(self, facility="tahoe.sftp")
361         if noisy: self.log(".__init__(%r, %r)" % (download_size, tempfile_maker), level=NOISY)
362         self.download_size = download_size
363         self.current_size = download_size
364         self.f = tempfile_maker()
365         self.downloaded = 0
366         self.milestones = []  # empty heap of (offset, d)
367         self.overwrites = []  # empty heap of (start, end)
368         self.is_closed = False
369         self.done = self.when_reached(download_size)  # adds a milestone
370         self.is_done = False
371         def _signal_done(ign):
372             if noisy: self.log("DONE", level=NOISY)
373             self.is_done = True
374         self.done.addCallback(_signal_done)
375         self.producer = None
376
377     def get_file(self):
378         return self.f
379
380     def get_current_size(self):
381         return self.current_size
382
383     def set_current_size(self, size):
384         if noisy: self.log(".set_current_size(%r), current_size = %r, downloaded = %r" %
385                            (size, self.current_size, self.downloaded), level=NOISY)
386         if size < self.current_size or size < self.downloaded:
387             self.f.truncate(size)
388         if size > self.current_size:
389             self.overwrite(self.current_size, "\x00" * (size - self.current_size))
390         self.current_size = size
391
392         # invariant: self.download_size <= self.current_size
393         if size < self.download_size:
394             self.download_size = size
395         if self.downloaded >= self.download_size:
396             self.finish()
397
398     def registerProducer(self, p, streaming):
399         if noisy: self.log(".registerProducer(%r, streaming=%r)" % (p, streaming), level=NOISY)
400         self.producer = p
401         if streaming:
402             # call resumeProducing once to start things off
403             p.resumeProducing()
404         else:
405             def _iterate():
406                 if not self.is_done:
407                     p.resumeProducing()
408                     eventually(_iterate)
409             _iterate()
410
411     def write(self, data):
412         if noisy: self.log(".write(<data of length %r>)" % (len(data),), level=NOISY)
413         if self.is_closed:
414             return
415
416         if self.downloaded >= self.download_size:
417             return
418
419         next_downloaded = self.downloaded + len(data)
420         if next_downloaded > self.download_size:
421             data = data[:(self.download_size - self.downloaded)]
422
423         while len(self.overwrites) > 0:
424             (start, end) = self.overwrites[0]
425             if start >= next_downloaded:
426                 # This and all remaining overwrites are after the data we just downloaded.
427                 break
428             if start > self.downloaded:
429                 # The data we just downloaded has been partially overwritten.
430                 # Write the prefix of it that precedes the overwritten region.
431                 self.f.seek(self.downloaded)
432                 self.f.write(data[:(start - self.downloaded)])
433
434             # This merges consecutive overwrites if possible, which allows us to detect the
435             # case where the download can be stopped early because the remaining region
436             # to download has already been fully overwritten.
437             heapq.heappop(self.overwrites)
438             while len(self.overwrites) > 0:
439                 (start1, end1) = self.overwrites[0]
440                 if start1 > end:
441                     break
442                 end = end1
443                 heapq.heappop(self.overwrites)
444
445             if end >= next_downloaded:
446                 # This overwrite extends past the downloaded data, so there is no
447                 # more data to consider on this call.
448                 heapq.heappush(self.overwrites, (next_downloaded, end))
449                 self._update_downloaded(next_downloaded)
450                 return
451             elif end >= self.downloaded:
452                 data = data[(end - self.downloaded):]
453                 self._update_downloaded(end)
454
455         self.f.seek(self.downloaded)
456         self.f.write(data)
457         self._update_downloaded(next_downloaded)
458
459     def _update_downloaded(self, new_downloaded):
460         self.downloaded = new_downloaded
461         milestone = new_downloaded
462         if len(self.overwrites) > 0:
463             (start, end) = self.overwrites[0]
464             if start <= new_downloaded and end > milestone:
465                 milestone = end
466
467         while len(self.milestones) > 0:
468             (next, d) = self.milestones[0]
469             if next > milestone:
470                 return
471             if noisy: self.log("MILESTONE %r %r" % (next, d), level=NOISY)
472             heapq.heappop(self.milestones)
473             eventually_callback(d)(None)
474
475         if milestone >= self.download_size:
476             self.finish()
477
478     def overwrite(self, offset, data):
479         if noisy: self.log(".overwrite(%r, <data of length %r>)" % (offset, len(data)), level=NOISY)
480         if offset > self.current_size:
481             # Normally writing at an offset beyond the current end-of-file
482             # would leave a hole that appears filled with zeroes. However, an
483             # EncryptedTemporaryFile doesn't behave like that (if there is a
484             # hole in the file on disk, the zeroes that are read back will be
485             # XORed with the keystream). So we must explicitly write zeroes in
486             # the gap between the current EOF and the offset.
487
488             self.f.seek(self.current_size)
489             self.f.write("\x00" * (offset - self.current_size))
490             start = self.current_size
491         else:
492             self.f.seek(offset)
493             start = offset
494
495         self.f.write(data)
496         end = offset + len(data)
497         self.current_size = max(self.current_size, end)
498         if end > self.downloaded:
499             heapq.heappush(self.overwrites, (start, end))
500
501     def read(self, offset, length):
502         """When the data has been read, callback the Deferred that we return with this data.
503         Otherwise errback the Deferred that we return.
504         The caller must perform no more overwrites until the Deferred has fired."""
505
506         if noisy: self.log(".read(%r, %r), current_size = %r" % (offset, length, self.current_size), level=NOISY)
507         if offset >= self.current_size:
508             def _eof(): raise EOFError("read past end of file")
509             return defer.execute(_eof)
510
511         if offset + length > self.current_size:
512             length = self.current_size - offset
513             if noisy: self.log("truncating read to %r bytes" % (length,), level=NOISY)
514
515         needed = min(offset + length, self.download_size)
516         d = self.when_reached(needed)
517         def _reached(ign):
518             # It is not necessarily the case that self.downloaded >= needed, because
519             # the file might have been truncated (thus truncating the download) and
520             # then extended.
521
522             assert self.current_size >= offset + length, (self.current_size, offset, length)
523             if noisy: self.log("self.f = %r" % (self.f,), level=NOISY)
524             self.f.seek(offset)
525             return self.f.read(length)
526         d.addCallback(_reached)
527         return d
528
529     def when_reached(self, index):
530         if noisy: self.log(".when_reached(%r)" % (index,), level=NOISY)
531         if index <= self.downloaded:  # already reached
532             if noisy: self.log("already reached %r" % (index,), level=NOISY)
533             return defer.succeed(None)
534         d = defer.Deferred()
535         def _reached(ign):
536             if noisy: self.log("reached %r" % (index,), level=NOISY)
537             return ign
538         d.addCallback(_reached)
539         heapq.heappush(self.milestones, (index, d))
540         return d
541
542     def when_done(self):
543         return self.done
544
545     def finish(self):
546         while len(self.milestones) > 0:
547             (next, d) = self.milestones[0]
548             if noisy: self.log("MILESTONE FINISH %r %r" % (next, d), level=NOISY)
549             heapq.heappop(self.milestones)
550             # The callback means that the milestone has been reached if
551             # it is ever going to be. Note that the file may have been
552             # truncated to before the milestone.
553             eventually_callback(d)(None)
554
555         # FIXME: causes spurious failures
556         #self.unregisterProducer()
557
558     def close(self):
559         if not self.is_closed:
560             self.is_closed = True
561             try:
562                 self.f.close()
563             except BaseException, e:
564                 self.log("suppressed %r from close of temporary file %r" % (e, self.f), level=WEIRD)
565         self.finish()
566
567     def unregisterProducer(self):
568         if self.producer:
569             self.producer.stopProducing()
570             self.producer = None
571
572
573 SIZE_THRESHOLD = 1000
574
575
576 class ShortReadOnlySFTPFile(PrefixingLogMixin):
577     implements(ISFTPFile)
578     """I represent a file handle to a particular file on an SFTP connection.
579     I am used only for short immutable files opened in read-only mode.
580     The file contents are downloaded to memory when I am created."""
581
582     def __init__(self, userpath, filenode, metadata):
583         PrefixingLogMixin.__init__(self, facility="tahoe.sftp", prefix=userpath)
584         if noisy: self.log(".__init__(%r, %r, %r)" % (userpath, filenode, metadata), level=NOISY)
585
586         assert IFileNode.providedBy(filenode), filenode
587         self.filenode = filenode
588         self.metadata = metadata
589         self.async = download_to_data(filenode)
590         self.closed = False
591
592     def readChunk(self, offset, length):
593         request = ".readChunk(%r, %r)" % (offset, length)
594         self.log(request, level=OPERATIONAL)
595
596         if self.closed:
597             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot read from a closed file handle")
598             return defer.execute(_closed)
599
600         d = defer.Deferred()
601         def _read(data):
602             if noisy: self.log("_read(<data of length %r>) in readChunk(%r, %r)" % (len(data), offset, length), level=NOISY)
603
604             # "In response to this request, the server will read as many bytes as it
605             #  can from the file (up to 'len'), and return them in a SSH_FXP_DATA
606             #  message.  If an error occurs or EOF is encountered before reading any
607             #  data, the server will respond with SSH_FXP_STATUS.  For normal disk
608             #  files, it is guaranteed that this will read the specified number of
609             #  bytes, or up to end of file."
610             #
611             # i.e. we respond with an EOF error iff offset is already at EOF.
612
613             if offset >= len(data):
614                 eventually_errback(d)(SFTPError(FX_EOF, "read at or past end of file"))
615             else:
616                 eventually_callback(d)(data[offset:min(offset+length, len(data))])
617             return data
618         self.async.addCallbacks(_read, eventually_errback(d))
619         d.addBoth(_convert_error, request)
620         return d
621
622     def writeChunk(self, offset, data):
623         self.log(".writeChunk(%r, <data of length %r>) denied" % (offset, len(data)), level=OPERATIONAL)
624
625         def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "file handle was not opened for writing")
626         return defer.execute(_denied)
627
628     def close(self):
629         self.log(".close()", level=OPERATIONAL)
630
631         self.closed = True
632         return defer.succeed(None)
633
634     def getAttrs(self):
635         request = ".getAttrs()"
636         self.log(request, level=OPERATIONAL)
637
638         if self.closed:
639             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot get attributes for a closed file handle")
640             return defer.execute(_closed)
641
642         d = defer.execute(_populate_attrs, self.filenode, self.metadata)
643         d.addBoth(_convert_error, request)
644         return d
645
646     def setAttrs(self, attrs):
647         self.log(".setAttrs(%r) denied" % (attrs,), level=OPERATIONAL)
648         def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "file handle was not opened for writing")
649         return defer.execute(_denied)
650
651
652 class GeneralSFTPFile(PrefixingLogMixin):
653     implements(ISFTPFile)
654     """I represent a file handle to a particular file on an SFTP connection.
655     I wrap an instance of OverwriteableFileConsumer, which is responsible for
656     storing the file contents. In order to allow write requests to be satisfied
657     immediately, there is effectively a FIFO queue between requests made to this
658     file handle, and requests to my OverwriteableFileConsumer. This queue is
659     implemented by the callback chain of self.async.
660
661     When first constructed, I am in an 'unopened' state that causes most
662     operations to be delayed until 'open' is called."""
663
664     def __init__(self, userpath, flags, close_notify, convergence):
665         PrefixingLogMixin.__init__(self, facility="tahoe.sftp", prefix=userpath)
666         if noisy: self.log(".__init__(%r, %r = %r, %r, <convergence censored>)" %
667                            (userpath, flags, _repr_flags(flags), close_notify), level=NOISY)
668
669         self.userpath = userpath
670         self.flags = flags
671         self.close_notify = close_notify
672         self.convergence = convergence
673         self.async = defer.Deferred()
674         # Creating or truncating the file is a change, but if FXF_EXCL is set, a zero-length file has already been created.
675         self.has_changed = (flags & (FXF_CREAT | FXF_TRUNC)) and not (flags & FXF_EXCL)
676         self.closed = False
677         self.abandoned = False
678         self.parent = None
679         self.childname = None
680         self.filenode = None
681         self.metadata = None
682
683         # self.consumer should only be relied on in callbacks for self.async, since it might
684         # not be set before then.
685         self.consumer = None
686
687     def open(self, parent=None, childname=None, filenode=None, metadata=None):
688         self.log(".open(parent=%r, childname=%r, filenode=%r, metadata=%r)" %
689                  (parent, childname, filenode, metadata), level=OPERATIONAL)
690
691         # If the file has been renamed, the new (parent, childname) takes precedence.
692         if self.parent is None:
693             self.parent = parent
694         if self.childname is None:
695             self.childname = childname
696         self.filenode = filenode
697         self.metadata = metadata
698
699         assert not self.closed
700         tempfile_maker = EncryptedTemporaryFile
701
702         if (self.flags & FXF_TRUNC) or not filenode:
703             # We're either truncating or creating the file, so we don't need the old contents.
704             self.consumer = OverwriteableFileConsumer(0, tempfile_maker)
705             self.consumer.finish()
706         else:
707             assert IFileNode.providedBy(filenode), filenode
708
709             # TODO: use download interface described in #993 when implemented.
710             if filenode.is_mutable():
711                 self.async.addCallback(lambda ign: filenode.download_best_version())
712                 def _downloaded(data):
713                     self.consumer = OverwriteableFileConsumer(len(data), tempfile_maker)
714                     self.consumer.write(data)
715                     self.consumer.finish()
716                     return None
717                 self.async.addCallback(_downloaded)
718             else:
719                 download_size = filenode.get_size()
720                 assert download_size is not None, "download_size is None"
721                 self.consumer = OverwriteableFileConsumer(download_size, tempfile_maker)
722                 def _read(ign):
723                     if noisy: self.log("_read immutable", level=NOISY)
724                     filenode.read(self.consumer, 0, None)
725                 self.async.addCallback(_read)
726
727         eventually_callback(self.async)(None)
728
729         if noisy: self.log("open done", level=NOISY)
730         return self
731
732     def rename(self, new_userpath, new_parent, new_childname):
733         self.log(".rename(%r, %r, %r)" % (new_userpath, new_parent, new_childname), level=OPERATIONAL)
734
735         self.userpath = new_userpath
736         self.parent = new_parent
737         self.childname = new_childname
738
739     def abandon(self):
740         self.log(".abandon()", level=OPERATIONAL)
741
742         self.abandoned = True
743
744     def sync(self, ign=None):
745         # The ign argument allows some_file.sync to be used as a callback.
746         self.log(".sync()", level=OPERATIONAL)
747
748         d = defer.Deferred()
749         self.async.addBoth(eventually_callback(d))
750         def _done(res):
751             if noisy: self.log("_done(%r) in .sync()" % (res,), level=NOISY)
752             return res
753         d.addBoth(_done)
754         return d
755
756     def get_metadata(self):
757         return self.metadata
758
759     def readChunk(self, offset, length):
760         request = ".readChunk(%r, %r)" % (offset, length)
761         self.log(request, level=OPERATIONAL)
762
763         if not (self.flags & FXF_READ):
764             def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "file handle was not opened for reading")
765             return defer.execute(_denied)
766
767         if self.closed:
768             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot read from a closed file handle")
769             return defer.execute(_closed)
770
771         d = defer.Deferred()
772         def _read(ign):
773             if noisy: self.log("_read in readChunk(%r, %r)" % (offset, length), level=NOISY)
774             d2 = self.consumer.read(offset, length)
775             d2.addCallbacks(eventually_callback(d), eventually_errback(d))
776             # It is correct to drop d2 here.
777             return None
778         self.async.addCallbacks(_read, eventually_errback(d))
779         d.addBoth(_convert_error, request)
780         return d
781
782     def writeChunk(self, offset, data):
783         self.log(".writeChunk(%r, <data of length %r>)" % (offset, len(data)), level=OPERATIONAL)
784
785         if not (self.flags & FXF_WRITE):
786             def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "file handle was not opened for writing")
787             return defer.execute(_denied)
788
789         if self.closed:
790             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot write to a closed file handle")
791             return defer.execute(_closed)
792
793         self.has_changed = True
794
795         # Note that we return without waiting for the write to occur. Reads and
796         # close wait for prior writes, and will fail if any prior operation failed.
797         # This is ok because SFTP makes no guarantee that the write completes
798         # before the request does. In fact it explicitly allows write errors to be
799         # delayed until close:
800         #   "One should note that on some server platforms even a close can fail.
801         #    This can happen e.g. if the server operating system caches writes,
802         #    and an error occurs while flushing cached writes during the close."
803
804         def _write(ign):
805             if noisy: self.log("_write in .writeChunk(%r, <data of length %r>), current_size = %r" %
806                                (offset, len(data), self.consumer.get_current_size()), level=NOISY)
807             # FXF_APPEND means that we should always write at the current end of file.
808             write_offset = offset
809             if self.flags & FXF_APPEND:
810                 write_offset = self.consumer.get_current_size()
811
812             self.consumer.overwrite(write_offset, data)
813             if noisy: self.log("overwrite done", level=NOISY)
814             return None
815         self.async.addCallback(_write)
816         # don't addErrback to self.async, just allow subsequent async ops to fail.
817         return defer.succeed(None)
818
819     def close(self):
820         request = ".close()"
821         self.log(request, level=OPERATIONAL)
822
823         if self.closed:
824             return defer.succeed(None)
825
826         # This means that close has been called, not that the close has succeeded.
827         self.closed = True
828
829         if not (self.flags & (FXF_WRITE | FXF_CREAT)):
830             def _readonly_close():
831                 if self.consumer:
832                     self.consumer.close()
833             return defer.execute(_readonly_close)
834
835         # We must capture the abandoned, parent, and childname variables synchronously
836         # at the close call. This is needed by the correctness arguments in the comments
837         # for _abandon_any_heisenfiles and _rename_heisenfiles.
838         # Note that the file must have been opened before it can be closed.
839         abandoned = self.abandoned
840         parent = self.parent
841         childname = self.childname
842         
843         # has_changed is set when writeChunk is called, not when the write occurs, so
844         # it is correct to optimize out the commit if it is False at the close call.
845         has_changed = self.has_changed
846
847         def _committed(res):
848             if noisy: self.log("_committed(%r)" % (res,), level=NOISY)
849
850             self.consumer.close()
851
852             # We must close_notify before re-firing self.async.
853             if self.close_notify:
854                 self.close_notify(self.userpath, self.parent, self.childname, self)
855             return res
856
857         def _close(ign):
858             d2 = self.consumer.when_done()
859             if self.filenode and self.filenode.is_mutable():
860                 self.log("update mutable file %r childname=%r" % (self.filenode, childname), level=OPERATIONAL)
861                 if self.metadata.get('no-write', False) and not self.filenode.is_readonly():
862                     assert parent and childname, (parent, childname, self.metadata)
863                     d2.addCallback(lambda ign: parent.set_metadata_for(childname, self.metadata))
864
865                 d2.addCallback(lambda ign: self.consumer.get_current_size())
866                 d2.addCallback(lambda size: self.consumer.read(0, size))
867                 d2.addCallback(lambda new_contents: self.filenode.overwrite(new_contents))
868             else:
869                 def _add_file(ign):
870                     self.log("_add_file childname=%r" % (childname,), level=OPERATIONAL)
871                     u = FileHandle(self.consumer.get_file(), self.convergence)
872                     return parent.add_file(childname, u, metadata=self.metadata)
873                 d2.addCallback(_add_file)
874
875             d2.addBoth(_committed)
876             return d2
877
878         d = defer.Deferred()
879
880         # If the file has been abandoned, we don't want the close operation to get "stuck",
881         # even if self.async fails to re-fire. Doing the close independently of self.async
882         # in that case ensures that dropping an ssh connection is sufficient to abandon
883         # any heisenfiles that were not explicitly closed in that connection.
884         if abandoned or not has_changed:
885             d.addCallback(_committed)
886         else:
887             self.async.addCallback(_close)
888
889         self.async.addCallbacks(eventually_callback(d), eventually_errback(d))
890         d.addBoth(_convert_error, request)
891         return d
892
893     def getAttrs(self):
894         request = ".getAttrs()"
895         self.log(request, level=OPERATIONAL)
896
897         if self.closed:
898             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot get attributes for a closed file handle")
899             return defer.execute(_closed)
900
901         # Optimization for read-only handles, when we already know the metadata.
902         if not (self.flags & (FXF_WRITE | FXF_CREAT)) and self.metadata and self.filenode and not self.filenode.is_mutable():
903             return defer.succeed(_populate_attrs(self.filenode, self.metadata))
904
905         d = defer.Deferred()
906         def _get(ign):
907             if noisy: self.log("_get(%r) in %r, filenode = %r, metadata = %r" % (ign, request, self.filenode, self.metadata), level=NOISY)
908
909             # self.filenode might be None, but that's ok.
910             attrs = _populate_attrs(self.filenode, self.metadata, size=self.consumer.get_current_size())
911             eventually_callback(d)(attrs)
912             return None
913         self.async.addCallbacks(_get, eventually_errback(d))
914         d.addBoth(_convert_error, request)
915         return d
916
917     def setAttrs(self, attrs, only_if_at=None):
918         request = ".setAttrs(%r, only_if_at=%r)" % (attrs, only_if_at)
919         self.log(request, level=OPERATIONAL)
920
921         if not (self.flags & FXF_WRITE):
922             def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "file handle was not opened for writing")
923             return defer.execute(_denied)
924
925         if self.closed:
926             def _closed(): raise SFTPError(FX_BAD_MESSAGE, "cannot set attributes for a closed file handle")
927             return defer.execute(_closed)
928
929         size = attrs.get("size", None)
930         if size is not None and (not isinstance(size, (int, long)) or size < 0):
931             def _bad(): raise SFTPError(FX_BAD_MESSAGE, "new size is not a valid nonnegative integer")
932             return defer.execute(_bad)
933
934         d = defer.Deferred()
935         def _set(ign):
936             if noisy: self.log("_set(%r) in %r" % (ign, request), level=NOISY)
937             if only_if_at and only_if_at != _direntry_for(self.parent, self.childname, self.filenode):
938                 return None
939
940             now = time()
941             self.metadata = update_metadata(self.metadata, _attrs_to_metadata(attrs), now)
942             if size is not None:
943                 self.consumer.set_current_size(size)
944             eventually_callback(d)(None)
945             return None
946         self.async.addCallbacks(_set, eventually_errback(d))
947         d.addBoth(_convert_error, request)
948         return d
949
950
951 class StoppableList:
952     def __init__(self, items):
953         self.items = items
954     def __iter__(self):
955         for i in self.items:
956             yield i
957     def close(self):
958         pass
959
960
961 class Reason:
962     def __init__(self, value):
963         self.value = value
964
965
966 # A "heisenfile" is a file that has been opened with write flags
967 # (FXF_WRITE and/or FXF_CREAT) and not yet close-notified.
968 # 'all_heisenfiles' maps from a direntry string to a list of
969 # GeneralSFTPFile.
970 #
971 # A direntry string is parent_write_uri + "/" + childname_utf8 for
972 # an immutable file, or file_write_uri for a mutable file.
973 # Updates to this dict are single-threaded.
974
975 all_heisenfiles = {}
976
977
978 class SFTPUserHandler(ConchUser, PrefixingLogMixin):
979     implements(ISFTPServer)
980     def __init__(self, client, rootnode, username):
981         ConchUser.__init__(self)
982         PrefixingLogMixin.__init__(self, facility="tahoe.sftp", prefix=username)
983         if noisy: self.log(".__init__(%r, %r, %r)" % (client, rootnode, username), level=NOISY)
984
985         self.channelLookup["session"] = session.SSHSession
986         self.subsystemLookup["sftp"] = FileTransferServer
987
988         self._client = client
989         self._root = rootnode
990         self._username = username
991         self._convergence = client.convergence
992
993         # maps from UTF-8 paths for this user, to files written and still open
994         self._heisenfiles = {}
995
996     def gotVersion(self, otherVersion, extData):
997         self.log(".gotVersion(%r, %r)" % (otherVersion, extData), level=OPERATIONAL)
998
999         # advertise the same extensions as the OpenSSH SFTP server
1000         # <http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.15>
1001         return {'posix-rename@openssh.com': '1',
1002                 'statvfs@openssh.com': '2',
1003                 'fstatvfs@openssh.com': '2',
1004                }
1005
1006     def logout(self):
1007         self.log(".logout()", level=OPERATIONAL)
1008
1009         for files in self._heisenfiles.itervalues():
1010             for f in files:
1011                 f.abandon()
1012
1013     def _add_heisenfiles_by_path(self, userpath, files_to_add):
1014         self.log("._add_heisenfiles_by_path(%r, %r)" % (userpath, files_to_add), level=OPERATIONAL)
1015
1016         if userpath in self._heisenfiles:
1017             self._heisenfiles[userpath] += files_to_add
1018         else:
1019             self._heisenfiles[userpath] = files_to_add
1020
1021     def _add_heisenfiles_by_direntry(self, direntry, files_to_add):
1022         self.log("._add_heisenfiles_by_direntry(%r, %r)" % (direntry, files_to_add), level=OPERATIONAL)
1023
1024         if direntry:
1025             if direntry in all_heisenfiles:
1026                 all_heisenfiles[direntry] += files_to_add
1027             else:
1028                 all_heisenfiles[direntry] = files_to_add
1029
1030     def _abandon_any_heisenfiles(self, userpath, direntry):
1031         request = "._abandon_any_heisenfiles(%r, %r)" % (userpath, direntry)
1032         self.log(request, level=OPERATIONAL)
1033
1034         # First we synchronously mark all heisenfiles matching the userpath or direntry
1035         # as abandoned, and remove them from the two heisenfile dicts. Then we .sync()
1036         # each file that we abandoned.
1037         #
1038         # For each file, the call to .abandon() occurs:
1039         #   * before the file is closed, in which case it will never be committed
1040         #     (uploaded+linked or published); or
1041         #   * after it is closed but before it has been close_notified, in which case the
1042         #     .sync() ensures that it has been committed (successfully or not) before we
1043         #     return.
1044         #
1045         # This avoids a race that might otherwise cause the file to be committed after
1046         # the remove operation has completed.
1047         #
1048         # We return a Deferred that fires with True if any files were abandoned (this
1049         # does not mean that they were not committed; it is used to determine whether
1050         # a NoSuchChildError from the attempt to delete the file should be suppressed).
1051
1052         files = []
1053         if direntry in all_heisenfiles:
1054             files = all_heisenfiles[direntry]
1055             del all_heisenfiles[direntry]
1056         if userpath in self._heisenfiles:
1057             files += self._heisenfiles[userpath]
1058             del self._heisenfiles[userpath]
1059
1060         if noisy: self.log("files = %r in %r" % (files, request), level=NOISY)
1061
1062         for f in files:
1063             f.abandon()
1064
1065         d = defer.succeed(None)
1066         for f in files:
1067             d.addBoth(f.sync)
1068
1069         def _done(ign):
1070             self.log("done %r" % (request,), level=OPERATIONAL)
1071             return len(files) > 0
1072         d.addBoth(_done)
1073         return d
1074
1075     def _rename_heisenfiles(self, from_userpath, from_parent, from_childname,
1076                             to_userpath, to_parent, to_childname, overwrite=True):
1077         request = ("._rename_heisenfiles(%r, %r, %r, %r, %r, %r, overwrite=%r)" %
1078                    (from_userpath, from_parent, from_childname, to_userpath, to_parent, to_childname, overwrite))
1079         self.log(request, level=OPERATIONAL)
1080
1081         # First we synchronously rename all heisenfiles matching the userpath or direntry.
1082         # Then we .sync() each file that we renamed.
1083         #
1084         # For each file, the call to .rename occurs:
1085         #   * before the file is closed, in which case it will be committed at the
1086         #     new direntry; or
1087         #   * after it is closed but before it has been close_notified, in which case the
1088         #     .sync() ensures that it has been committed (successfully or not) before we
1089         #     return.
1090         #
1091         # This avoids a race that might otherwise cause the file to be committed at the
1092         # old name after the rename operation has completed.
1093         #
1094         # Note that if overwrite is False, the caller should already have checked
1095         # whether a real direntry exists at the destination. It is possible that another
1096         # direntry (heisen or real) comes to exist at the destination after that check,
1097         # but in that case it is correct for the rename to succeed (and for the commit
1098         # of the heisenfile at the destination to possibly clobber the other entry, since
1099         # that can happen anyway when we have concurrent write handles to the same direntry).
1100         #
1101         # We return a Deferred that fires with True if any files were renamed (this
1102         # does not mean that they were not committed; it is used to determine whether
1103         # a NoSuchChildError from the rename attempt should be suppressed). If overwrite
1104         # is False and there were already heisenfiles at the destination userpath or
1105         # direntry, we return a Deferred that fails with SFTPError(FX_PERMISSION_DENIED).
1106
1107         from_direntry = _direntry_for(from_parent, from_childname)
1108         to_direntry = _direntry_for(to_parent, to_childname)
1109
1110         if not overwrite and (to_userpath in self._heisenfiles or to_direntry in all_heisenfiles):
1111             def _existing(): raise SFTPError(FX_PERMISSION_DENIED, "cannot rename to existing path " + to_userpath)
1112             return defer.execute(_existing)
1113
1114         from_files = []
1115         if from_direntry in all_heisenfiles:
1116             from_files = all_heisenfiles[from_direntry]
1117             del all_heisenfiles[from_direntry]
1118         if from_userpath in self._heisenfiles:
1119             from_files += self._heisenfiles[from_userpath]
1120             del self._heisenfiles[from_userpath]
1121
1122         if noisy: self.log("from_files = %r in %r" % (from_files, request), level=NOISY)
1123
1124         self._add_heisenfiles_by_direntry(to_direntry, from_files)
1125         self._add_heisenfiles_by_path(to_userpath, from_files)
1126
1127         for f in from_files:
1128             f.rename(to_userpath, to_parent, to_childname)
1129
1130         d = defer.succeed(None)
1131         for f in from_files:
1132             d.addBoth(f.sync)
1133
1134         def _done(ign):
1135             self.log("done %r" % (request,), level=OPERATIONAL)
1136             return len(from_files) > 0
1137         d.addBoth(_done)
1138         return d
1139
1140     def _update_attrs_for_heisenfiles(self, userpath, direntry, attrs):
1141         request = "._update_attrs_for_heisenfiles(%r, %r, %r)" % (userpath, direntry, attrs)
1142         self.log(request, level=OPERATIONAL)
1143
1144         files = []
1145         if direntry in all_heisenfiles:
1146             files = all_heisenfiles[direntry]
1147         if userpath in self._heisenfiles:
1148             files += self._heisenfiles[userpath]
1149
1150         if noisy: self.log("files = %r in %r" % (files, request), level=NOISY)
1151
1152         # We set the metadata for all heisenfiles at this path or direntry.
1153         # Since a direntry includes a write URI, we must have authority to
1154         # change the metadata of heisenfiles found in the all_heisenfiles dict.
1155         # However that's not necessarily the case for heisenfiles found by
1156         # path. Therefore we tell the setAttrs method of each file to only
1157         # perform the update if the file is at the correct direntry.
1158
1159         d = defer.succeed(None)
1160         for f in files:
1161             d.addBoth(f.setAttrs, attrs, only_if_at=direntry)
1162
1163         def _done(ign):
1164             self.log("done %r" % (request,), level=OPERATIONAL)
1165             # TODO: this should not return True if only_if_at caused all files to be skipped.
1166             return len(files) > 0
1167         d.addBoth(_done)
1168         return d
1169
1170     def _sync_heisenfiles(self, userpath, direntry, ignore=None):
1171         request = "._sync_heisenfiles(%r, %r, ignore=%r)" % (userpath, direntry, ignore)
1172         self.log(request, level=OPERATIONAL)
1173
1174         files = []
1175         if direntry in all_heisenfiles:
1176             files = all_heisenfiles[direntry]
1177         if userpath in self._heisenfiles:
1178             files += self._heisenfiles[userpath]
1179
1180         if noisy: self.log("files = %r in %r" % (files, request), level=NOISY)
1181
1182         d = defer.succeed(None)
1183         for f in files:
1184             if f is not ignore:
1185                 d.addBoth(f.sync)
1186
1187         def _done(ign):
1188             self.log("done %r" % (request,), level=OPERATIONAL)
1189             return None
1190         d.addBoth(_done)
1191         return d
1192
1193     def _remove_heisenfile(self, userpath, parent, childname, file_to_remove):
1194         if noisy: self.log("._remove_heisenfile(%r, %r, %r, %r)" % (userpath, parent, childname, file_to_remove), level=NOISY)
1195
1196         direntry = _direntry_for(parent, childname)
1197         if direntry in all_heisenfiles:
1198             all_old_files = all_heisenfiles[direntry]
1199             all_new_files = [f for f in all_old_files if f is not file_to_remove]
1200             if len(all_new_files) > 0:
1201                 all_heisenfiles[direntry] = all_new_files
1202             else:
1203                 del all_heisenfiles[direntry]
1204
1205         if userpath in self._heisenfiles:
1206             old_files = self._heisenfiles[userpath]
1207             new_files = [f for f in old_files if f is not file_to_remove]
1208             if len(new_files) > 0:
1209                 self._heisenfiles[userpath] = new_files
1210             else:
1211                 del self._heisenfiles[userpath]
1212
1213     def _make_file(self, existing_file, userpath, flags, parent=None, childname=None, filenode=None, metadata=None):
1214         if noisy: self.log("._make_file(%r, %r, %r = %r, parent=%r, childname=%r, filenode=%r, metadata=%r)" %
1215                            (existing_file, userpath, flags, _repr_flags(flags), parent, childname, filenode, metadata),
1216                            level=NOISY)
1217
1218         assert metadata is None or 'no-write' in metadata, metadata
1219
1220         writing = (flags & (FXF_WRITE | FXF_CREAT)) != 0
1221         direntry = _direntry_for(parent, childname, filenode)
1222
1223         d = self._sync_heisenfiles(userpath, direntry, ignore=existing_file)
1224
1225         if not writing and (flags & FXF_READ) and filenode and not filenode.is_mutable() and filenode.get_size() <= SIZE_THRESHOLD:
1226             d.addCallback(lambda ign: ShortReadOnlySFTPFile(userpath, filenode, metadata))
1227         else:
1228             close_notify = None
1229             if writing:
1230                 close_notify = self._remove_heisenfile
1231
1232             d.addCallback(lambda ign: existing_file or GeneralSFTPFile(userpath, flags, close_notify, self._convergence))
1233             def _got_file(file):
1234                 file.open(parent=parent, childname=childname, filenode=filenode, metadata=metadata)
1235                 if writing:
1236                     self._add_heisenfiles_by_direntry(direntry, [file])
1237                 return file
1238             d.addCallback(_got_file)
1239         return d
1240
1241     def openFile(self, pathstring, flags, attrs, delay=None):
1242         request = ".openFile(%r, %r = %r, %r, delay=%r)" % (pathstring, flags, _repr_flags(flags), attrs, delay)
1243         self.log(request, level=OPERATIONAL)
1244
1245         # This is used for both reading and writing.
1246         # First exclude invalid combinations of flags, and empty paths.
1247
1248         if not (flags & (FXF_READ | FXF_WRITE)):
1249             def _bad_readwrite():
1250                 raise SFTPError(FX_BAD_MESSAGE, "invalid file open flags: at least one of FXF_READ and FXF_WRITE must be set")
1251             return defer.execute(_bad_readwrite)
1252
1253         if (flags & FXF_EXCL) and not (flags & FXF_CREAT):
1254             def _bad_exclcreat():
1255                 raise SFTPError(FX_BAD_MESSAGE, "invalid file open flags: FXF_EXCL cannot be set without FXF_CREAT")
1256             return defer.execute(_bad_exclcreat)
1257
1258         path = self._path_from_string(pathstring)
1259         if not path:
1260             def _emptypath(): raise SFTPError(FX_NO_SUCH_FILE, "path cannot be empty")
1261             return defer.execute(_emptypath)
1262
1263         # The combination of flags is potentially valid.
1264
1265         # To work around clients that have race condition bugs, a getAttr, rename, or
1266         # remove request following an 'open' request with FXF_WRITE or FXF_CREAT flags,
1267         # should succeed even if the 'open' request has not yet completed. So we now
1268         # synchronously add a file object into the self._heisenfiles dict, indexed
1269         # by its UTF-8 userpath. (We can't yet add it to the all_heisenfiles dict,
1270         # because we don't yet have a user-independent path for the file.) The file
1271         # object does not know its filenode, parent, or childname at this point.
1272
1273         userpath = self._path_to_utf8(path)
1274
1275         if flags & (FXF_WRITE | FXF_CREAT):
1276             file = GeneralSFTPFile(userpath, flags, self._remove_heisenfile, self._convergence)
1277             self._add_heisenfiles_by_path(userpath, [file])
1278         else:
1279             # We haven't decided which file implementation to use yet.
1280             file = None
1281
1282         desired_metadata = _attrs_to_metadata(attrs)
1283
1284         # Now there are two major cases:
1285         #
1286         #  1. The path is specified as /uri/FILECAP, with no parent directory.
1287         #     If the FILECAP is mutable and writeable, then we can open it in write-only
1288         #     or read/write mode (non-exclusively), otherwise we can only open it in
1289         #     read-only mode. The open should succeed immediately as long as FILECAP is
1290         #     a valid known filecap that grants the required permission.
1291         #
1292         #  2. The path is specified relative to a parent. We find the parent dirnode and
1293         #     get the child's URI and metadata if it exists. There are four subcases:
1294         #       a. the child does not exist: FXF_CREAT must be set, and we must be able
1295         #          to write to the parent directory.
1296         #       b. the child exists but is not a valid known filecap: fail
1297         #       c. the child is mutable: if we are trying to open it write-only or
1298         #          read/write, then we must be able to write to the file.
1299         #       d. the child is immutable: if we are trying to open it write-only or
1300         #          read/write, then we must be able to write to the parent directory.
1301         #
1302         # To reduce latency, open normally succeeds as soon as these conditions are
1303         # met, even though there might be a failure in downloading the existing file
1304         # or uploading a new one. However, there is an exception: if a file has been
1305         # written, then closed, and is now being reopened, then we have to delay the
1306         # open until the previous upload/publish has completed. This is necessary
1307         # because sshfs does not wait for the result of an FXF_CLOSE message before
1308         # reporting to the client that a file has been closed. It applies both to
1309         # mutable files, and to directory entries linked to an immutable file.
1310         #
1311         # Note that the permission checks below are for more precise error reporting on
1312         # the open call; later operations would fail even if we did not make these checks.
1313
1314         d = delay or defer.succeed(None)
1315         d.addCallback(lambda ign: self._get_root(path))
1316         def _got_root( (root, path) ):
1317             if root.is_unknown():
1318                 raise SFTPError(FX_PERMISSION_DENIED,
1319                                 "cannot open an unknown cap (or child of an unknown object). "
1320                                 "Upgrading the gateway to a later Tahoe-LAFS version may help")
1321             if not path:
1322                 # case 1
1323                 if noisy: self.log("case 1: root = %r, path[:-1] = %r" % (root, path[:-1]), level=NOISY)
1324                 if not IFileNode.providedBy(root):
1325                     raise SFTPError(FX_PERMISSION_DENIED,
1326                                     "cannot open a directory cap")
1327                 if (flags & FXF_WRITE) and root.is_readonly():
1328                     raise SFTPError(FX_PERMISSION_DENIED,
1329                                     "cannot write to a non-writeable filecap without a parent directory")
1330                 if flags & FXF_EXCL:
1331                     raise SFTPError(FX_FAILURE,
1332                                     "cannot create a file exclusively when it already exists")
1333
1334                 # The file does not need to be added to all_heisenfiles, because it is not
1335                 # associated with a directory entry that needs to be updated.
1336
1337                 metadata = update_metadata(None, desired_metadata, time())
1338
1339                 # We have to decide what to pass for the 'parent_readonly' argument to _no_write,
1340                 # given that we don't actually have a parent. This only affects the permissions
1341                 # reported by a getAttrs on this file handle in the case of an immutable file.
1342                 # We choose 'parent_readonly=True' since that will cause the permissions to be
1343                 # reported as r--r--r--, which is appropriate because an immutable file can't be
1344                 # written via this path.
1345
1346                 metadata['no-write'] = _no_write(True, root)
1347                 return self._make_file(file, userpath, flags, filenode=root, metadata=metadata)
1348             else:
1349                 # case 2
1350                 childname = path[-1]
1351
1352                 if noisy: self.log("case 2: root = %r, childname = %r, desired_metadata = %r, path[:-1] = %r" %
1353                                    (root, childname, desired_metadata, path[:-1]), level=NOISY)
1354                 d2 = root.get_child_at_path(path[:-1])
1355                 def _got_parent(parent):
1356                     if noisy: self.log("_got_parent(%r)" % (parent,), level=NOISY)
1357                     if parent.is_unknown():
1358                         raise SFTPError(FX_PERMISSION_DENIED,
1359                                         "cannot open a child of an unknown object. "
1360                                         "Upgrading the gateway to a later Tahoe-LAFS version may help")
1361
1362                     parent_readonly = parent.is_readonly()
1363                     d3 = defer.succeed(None)
1364                     if flags & FXF_EXCL:
1365                         # FXF_EXCL means that the link to the file (not the file itself) must
1366                         # be created atomically wrt updates by this storage client.
1367                         # That is, we need to create the link before returning success to the
1368                         # SFTP open request (and not just on close, as would normally be the
1369                         # case). We make the link initially point to a zero-length LIT file,
1370                         # which is consistent with what might happen on a POSIX filesystem.
1371
1372                         if parent_readonly:
1373                             raise SFTPError(FX_FAILURE,
1374                                             "cannot create a file exclusively when the parent directory is read-only")
1375
1376                         # 'overwrite=False' ensures failure if the link already exists.
1377                         # FIXME: should use a single call to set_uri and return (child, metadata) (#1035)
1378
1379                         zero_length_lit = "URI:LIT:"
1380                         if noisy: self.log("%r.set_uri(%r, None, readcap=%r, overwrite=False)" %
1381                                            (parent, zero_length_lit, childname), level=NOISY)
1382                         d3.addCallback(lambda ign: parent.set_uri(childname, None, readcap=zero_length_lit,
1383                                                                   metadata=desired_metadata, overwrite=False))
1384                         def _seturi_done(child):
1385                             if noisy: self.log("%r.get_metadata_for(%r)" % (parent, childname), level=NOISY)
1386                             d4 = parent.get_metadata_for(childname)
1387                             d4.addCallback(lambda metadata: (child, metadata))
1388                             return d4
1389                         d3.addCallback(_seturi_done)
1390                     else:
1391                         if noisy: self.log("%r.get_child_and_metadata(%r)" % (parent, childname), level=NOISY)
1392                         d3.addCallback(lambda ign: parent.get_child_and_metadata(childname))
1393
1394                     def _got_child( (filenode, current_metadata) ):
1395                         if noisy: self.log("_got_child( (%r, %r) )" % (filenode, current_metadata), level=NOISY)
1396
1397                         metadata = update_metadata(current_metadata, desired_metadata, time())
1398
1399                         # Ignore the permissions of the desired_metadata in an open call. The permissions
1400                         # can only be set by setAttrs.
1401                         metadata['no-write'] = _no_write(parent_readonly, filenode, current_metadata)
1402
1403                         if filenode.is_unknown():
1404                             raise SFTPError(FX_PERMISSION_DENIED,
1405                                             "cannot open an unknown cap. Upgrading the gateway "
1406                                             "to a later Tahoe-LAFS version may help")
1407                         if not IFileNode.providedBy(filenode):
1408                             raise SFTPError(FX_PERMISSION_DENIED,
1409                                             "cannot open a directory as if it were a file")
1410                         if (flags & FXF_WRITE) and metadata['no-write']:
1411                             raise SFTPError(FX_PERMISSION_DENIED,
1412                                             "cannot open a non-writeable file for writing")
1413
1414                         return self._make_file(file, userpath, flags, parent=parent, childname=childname,
1415                                                filenode=filenode, metadata=metadata)
1416                     def _no_child(f):
1417                         if noisy: self.log("_no_child(%r)" % (f,), level=NOISY)
1418                         f.trap(NoSuchChildError)
1419
1420                         if not (flags & FXF_CREAT):
1421                             raise SFTPError(FX_NO_SUCH_FILE,
1422                                             "the file does not exist, and was not opened with the creation (CREAT) flag")
1423                         if parent_readonly:
1424                             raise SFTPError(FX_PERMISSION_DENIED,
1425                                             "cannot create a file when the parent directory is read-only")
1426
1427                         return self._make_file(file, userpath, flags, parent=parent, childname=childname)
1428                     d3.addCallbacks(_got_child, _no_child)
1429                     return d3
1430
1431                 d2.addCallback(_got_parent)
1432                 return d2
1433
1434         d.addCallback(_got_root)
1435         def _remove_on_error(err):
1436             if file:
1437                 self._remove_heisenfile(userpath, None, None, file)
1438             return err
1439         d.addErrback(_remove_on_error)
1440         d.addBoth(_convert_error, request)
1441         return d
1442
1443     def renameFile(self, from_pathstring, to_pathstring, overwrite=False):
1444         request = ".renameFile(%r, %r)" % (from_pathstring, to_pathstring)
1445         self.log(request, level=OPERATIONAL)
1446
1447         from_path = self._path_from_string(from_pathstring)
1448         to_path = self._path_from_string(to_pathstring)
1449         from_userpath = self._path_to_utf8(from_path)
1450         to_userpath = self._path_to_utf8(to_path)
1451
1452         # the target directory must already exist
1453         d = deferredutil.gatherResults([self._get_parent_or_node(from_path),
1454                                         self._get_parent_or_node(to_path)])
1455         def _got( (from_pair, to_pair) ):
1456             if noisy: self.log("_got( (%r, %r) ) in .renameFile(%r, %r, overwrite=%r)" %
1457                                (from_pair, to_pair, from_pathstring, to_pathstring, overwrite), level=NOISY)
1458             (from_parent, from_childname) = from_pair
1459             (to_parent, to_childname) = to_pair
1460
1461             if from_childname is None:
1462                 raise SFTPError(FX_NO_SUCH_FILE, "cannot rename a source object specified by URI")
1463             if to_childname is None:
1464                 raise SFTPError(FX_NO_SUCH_FILE, "cannot rename to a destination specified by URI")
1465
1466             # <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5>
1467             # "It is an error if there already exists a file with the name specified
1468             #  by newpath."
1469             # OpenSSH's SFTP server returns FX_PERMISSION_DENIED for this error.
1470             #
1471             # For the standard SSH_FXP_RENAME operation, overwrite=False.
1472             # We also support the posix-rename@openssh.com extension, which uses overwrite=True.
1473
1474             d2 = defer.fail(NoSuchChildError())
1475             if not overwrite:
1476                 d2.addCallback(lambda ign: to_parent.get(to_childname))
1477             def _expect_fail(res):
1478                 if not isinstance(res, Failure):
1479                     raise SFTPError(FX_PERMISSION_DENIED, "cannot rename to existing path " + to_userpath)
1480
1481                 # It is OK if we fail for errors other than NoSuchChildError, since that probably
1482                 # indicates some problem accessing the destination directory.
1483                 res.trap(NoSuchChildError)
1484             d2.addBoth(_expect_fail)
1485
1486             # If there are heisenfiles to be written at the 'from' direntry, then ensure
1487             # they will now be written at the 'to' direntry instead.
1488             d2.addCallback(lambda ign:
1489                            self._rename_heisenfiles(from_userpath, from_parent, from_childname,
1490                                                     to_userpath, to_parent, to_childname, overwrite=overwrite))
1491
1492             def _move(renamed):
1493                 # FIXME: use move_child_to_path to avoid possible data loss due to #943
1494                 #d3 = from_parent.move_child_to_path(from_childname, to_root, to_path, overwrite=overwrite)
1495
1496                 d3 = from_parent.move_child_to(from_childname, to_parent, to_childname, overwrite=overwrite)
1497                 def _check(err):
1498                     if noisy: self.log("_check(%r) in .renameFile(%r, %r, overwrite=%r)" %
1499                                        (err, from_pathstring, to_pathstring, overwrite), level=NOISY)
1500
1501                     if not isinstance(err, Failure) or (renamed and err.check(NoSuchChildError)):
1502                         return None
1503                     if not overwrite and err.check(ExistingChildError):
1504                         raise SFTPError(FX_PERMISSION_DENIED, "cannot rename to existing path " + to_userpath)
1505
1506                     return err
1507                 d3.addBoth(_check)
1508                 return d3
1509             d2.addCallback(_move)
1510             return d2
1511         d.addCallback(_got)
1512         d.addBoth(_convert_error, request)
1513         return d
1514
1515     def makeDirectory(self, pathstring, attrs):
1516         request = ".makeDirectory(%r, %r)" % (pathstring, attrs)
1517         self.log(request, level=OPERATIONAL)
1518
1519         path = self._path_from_string(pathstring)
1520         metadata = _attrs_to_metadata(attrs)
1521         if 'no-write' in metadata:
1522             def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "cannot create a directory that is initially read-only")
1523             return defer.execute(_denied)
1524
1525         d = self._get_root(path)
1526         d.addCallback(lambda (root, path):
1527                       self._get_or_create_directories(root, path, metadata))
1528         d.addBoth(_convert_error, request)
1529         return d
1530
1531     def _get_or_create_directories(self, node, path, metadata):
1532         if not IDirectoryNode.providedBy(node):
1533             # TODO: provide the name of the blocking file in the error message.
1534             def _blocked(): raise SFTPError(FX_FAILURE, "cannot create directory because there "
1535                                                         "is a file in the way") # close enough
1536             return defer.execute(_blocked)
1537
1538         if not path:
1539             return defer.succeed(node)
1540         d = node.get(path[0])
1541         def _maybe_create(f):
1542             f.trap(NoSuchChildError)
1543             return node.create_subdirectory(path[0])
1544         d.addErrback(_maybe_create)
1545         d.addCallback(self._get_or_create_directories, path[1:], metadata)
1546         return d
1547
1548     def removeFile(self, pathstring):
1549         request = ".removeFile(%r)" % (pathstring,)
1550         self.log(request, level=OPERATIONAL)
1551
1552         path = self._path_from_string(pathstring)
1553         d = self._remove_object(path, must_be_file=True)
1554         d.addBoth(_convert_error, request)
1555         return d
1556
1557     def removeDirectory(self, pathstring):
1558         request = ".removeDirectory(%r)" % (pathstring,)
1559         self.log(request, level=OPERATIONAL)
1560
1561         path = self._path_from_string(pathstring)
1562         d = self._remove_object(path, must_be_directory=True)
1563         d.addBoth(_convert_error, request)
1564         return d
1565
1566     def _remove_object(self, path, must_be_directory=False, must_be_file=False):
1567         userpath = self._path_to_utf8(path)
1568         d = self._get_parent_or_node(path)
1569         def _got_parent( (parent, childname) ):
1570             if childname is None:
1571                 raise SFTPError(FX_NO_SUCH_FILE, "cannot remove an object specified by URI")
1572
1573             direntry = _direntry_for(parent, childname)
1574             d2 = defer.succeed(False)
1575             if not must_be_directory:
1576                 d2.addCallback(lambda ign: self._abandon_any_heisenfiles(userpath, direntry))
1577
1578             d2.addCallback(lambda abandoned:
1579                            parent.delete(childname, must_exist=not abandoned,
1580                                          must_be_directory=must_be_directory, must_be_file=must_be_file))
1581             return d2
1582         d.addCallback(_got_parent)
1583         return d
1584
1585     def openDirectory(self, pathstring):
1586         request = ".openDirectory(%r)" % (pathstring,)
1587         self.log(request, level=OPERATIONAL)
1588
1589         path = self._path_from_string(pathstring)
1590         d = self._get_parent_or_node(path)
1591         def _got_parent_or_node( (parent_or_node, childname) ):
1592             if noisy: self.log("_got_parent_or_node( (%r, %r) ) in openDirectory(%r)" %
1593                                (parent_or_node, childname, pathstring), level=NOISY)
1594             if childname is None:
1595                 return parent_or_node
1596             else:
1597                 return parent_or_node.get(childname)
1598         d.addCallback(_got_parent_or_node)
1599         def _list(dirnode):
1600             if dirnode.is_unknown():
1601                 raise SFTPError(FX_PERMISSION_DENIED,
1602                                 "cannot list an unknown cap as a directory. Upgrading the gateway "
1603                                 "to a later Tahoe-LAFS version may help")
1604             if not IDirectoryNode.providedBy(dirnode):
1605                 raise SFTPError(FX_PERMISSION_DENIED,
1606                                 "cannot list a file as if it were a directory")
1607
1608             d2 = dirnode.list()
1609             def _render(children):
1610                 parent_readonly = dirnode.is_readonly()
1611                 results = []
1612                 for filename, (child, metadata) in children.iteritems():
1613                     # The file size may be cached or absent.
1614                     metadata['no-write'] = _no_write(parent_readonly, child, metadata)
1615                     attrs = _populate_attrs(child, metadata)
1616                     filename_utf8 = filename.encode('utf-8')
1617                     longname = _lsLine(filename_utf8, attrs)
1618                     results.append( (filename_utf8, longname, attrs) )
1619                 return StoppableList(results)
1620             d2.addCallback(_render)
1621             return d2
1622         d.addCallback(_list)
1623         d.addBoth(_convert_error, request)
1624         return d
1625
1626     def getAttrs(self, pathstring, followLinks):
1627         request = ".getAttrs(%r, followLinks=%r)" % (pathstring, followLinks)
1628         self.log(request, level=OPERATIONAL)
1629
1630         # When asked about a specific file, report its current size.
1631         # TODO: the modification time for a mutable file should be
1632         # reported as the update time of the best version. But that
1633         # information isn't currently stored in mutable shares, I think.
1634
1635         path = self._path_from_string(pathstring)
1636         userpath = self._path_to_utf8(path)
1637         d = self._get_parent_or_node(path)
1638         def _got_parent_or_node( (parent_or_node, childname) ):
1639             if noisy: self.log("_got_parent_or_node( (%r, %r) )" % (parent_or_node, childname), level=NOISY)
1640
1641             # Some clients will incorrectly try to get the attributes
1642             # of a file immediately after opening it, before it has been put
1643             # into the all_heisenfiles table. This is a race condition bug in
1644             # the client, but we handle it anyway by calling .sync() on all
1645             # files matching either the path or the direntry.
1646
1647             direntry = _direntry_for(parent_or_node, childname)
1648             d2 = self._sync_heisenfiles(userpath, direntry)
1649
1650             if childname is None:
1651                 node = parent_or_node
1652                 d2.addCallback(lambda ign: node.get_current_size())
1653                 d2.addCallback(lambda size:
1654                                _populate_attrs(node, {'no-write': node.is_unknown() or node.is_readonly()}, size=size))
1655             else:
1656                 parent = parent_or_node
1657                 d2.addCallback(lambda ign: parent.get_child_and_metadata_at_path([childname]))
1658                 def _got( (child, metadata) ):
1659                     if noisy: self.log("_got( (%r, %r) )" % (child, metadata), level=NOISY)
1660                     assert IDirectoryNode.providedBy(parent), parent
1661                     metadata['no-write'] = _no_write(parent.is_readonly(), child, metadata)
1662                     d3 = child.get_current_size()
1663                     d3.addCallback(lambda size: _populate_attrs(child, metadata, size=size))
1664                     return d3
1665                 def _nosuch(err):
1666                     if noisy: self.log("_nosuch(%r)" % (err,), level=NOISY)
1667                     err.trap(NoSuchChildError)
1668                     if noisy: self.log("checking open files:\nself._heisenfiles = %r\nall_heisenfiles = %r\ndirentry=%r" %
1669                                        (self._heisenfiles, all_heisenfiles, direntry), level=NOISY)
1670                     if direntry in all_heisenfiles:
1671                         files = all_heisenfiles[direntry]
1672                         if len(files) == 0:  # pragma: no cover
1673                             return err
1674                         # use the heisenfile that was most recently opened
1675                         return files[-1].getAttrs()
1676                     return err
1677                 d2.addCallbacks(_got, _nosuch)
1678             return d2
1679         d.addCallback(_got_parent_or_node)
1680         d.addBoth(_convert_error, request)
1681         return d
1682
1683     def setAttrs(self, pathstring, attrs):
1684         request = ".setAttrs(%r, %r)" % (pathstring, attrs)
1685         self.log(request, level=OPERATIONAL)
1686
1687         if "size" in attrs:
1688             # this would require us to download and re-upload the truncated/extended
1689             # file contents
1690             def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "setAttrs wth size attribute unsupported")
1691             return defer.execute(_unsupported)
1692
1693         path = self._path_from_string(pathstring)
1694         userpath = self._path_to_utf8(path)
1695         d = self._get_parent_or_node(path)
1696         def _got_parent_or_node( (parent_or_node, childname) ):
1697             if noisy: self.log("_got_parent_or_node( (%r, %r) )" % (parent_or_node, childname), level=NOISY)
1698
1699             direntry = _direntry_for(parent_or_node, childname)
1700             d2 = self._update_attrs_for_heisenfiles(userpath, direntry, attrs)
1701
1702             def _update(updated_heisenfiles):
1703                 if childname is None:
1704                     if updated_heisenfiles:
1705                         return None
1706                     raise SFTPError(FX_NO_SUCH_FILE, userpath)
1707                 else:
1708                     desired_metadata = _attrs_to_metadata(attrs)
1709                     if noisy: self.log("desired_metadata = %r" % (desired_metadata,), level=NOISY)
1710
1711                     d3 = parent_or_node.set_metadata_for(childname, desired_metadata)
1712                     def _nosuch(err):
1713                         if updated_heisenfiles:
1714                             err.trap(NoSuchChildError)
1715                         else:
1716                             return err
1717                     d3.addErrback(_nosuch)
1718                     return d3
1719             d2.addCallback(_update)
1720             d2.addCallback(lambda ign: None)
1721             return d2
1722         d.addCallback(_got_parent_or_node)
1723         d.addBoth(_convert_error, request)
1724         return d
1725
1726     def readLink(self, pathstring):
1727         self.log(".readLink(%r)" % (pathstring,), level=OPERATIONAL)
1728
1729         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "readLink")
1730         return defer.execute(_unsupported)
1731
1732     def makeLink(self, linkPathstring, targetPathstring):
1733         self.log(".makeLink(%r, %r)" % (linkPathstring, targetPathstring), level=OPERATIONAL)
1734
1735         # If this is implemented, note the reversal of arguments described in point 7 of
1736         # <http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.15>.
1737
1738         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "makeLink")
1739         return defer.execute(_unsupported)
1740
1741     def extendedRequest(self, extensionName, extensionData):
1742         self.log(".extendedRequest(%r, <data of length %r>)" % (extensionName, len(extensionData)), level=OPERATIONAL)
1743
1744         # We implement the three main OpenSSH SFTP extensions; see
1745         # <http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.15>
1746
1747         if extensionName == 'posix-rename@openssh.com':
1748             def _bad(): raise SFTPError(FX_BAD_MESSAGE, "could not parse posix-rename@openssh.com request")
1749
1750             (fromPathLen,) = struct.unpack('>L', extensionData[0:4])
1751             if 8 + fromPathLen > len(extensionData): return defer.execute(_bad)
1752
1753             (toPathLen,) = struct.unpack('>L', extensionData[(4 + fromPathLen):(8 + fromPathLen)])
1754             if 8 + fromPathLen + toPathLen != len(extensionData): return defer.execute(_bad)
1755
1756             fromPathstring = extensionData[4:(4 + fromPathLen)]
1757             toPathstring = extensionData[(8 + fromPathLen):]
1758             d = self.renameFile(fromPathstring, toPathstring, overwrite=True)
1759
1760             # Twisted conch assumes that the response from an extended request is either
1761             # an error, or an FXP_EXTENDED_REPLY. But it happens to do the right thing
1762             # (respond with an FXP_STATUS message) if we return a Failure with code FX_OK.
1763             def _succeeded(ign):
1764                 raise SFTPError(FX_OK, "request succeeded")
1765             d.addCallback(_succeeded)
1766             return d
1767
1768         if extensionName == 'statvfs@openssh.com' or extensionName == 'fstatvfs@openssh.com':
1769             # f_bsize and f_frsize should be the same to avoid a bug in 'df'
1770             return defer.succeed(struct.pack('>11Q',
1771                 1024,         # uint64  f_bsize     /* file system block size */
1772                 1024,         # uint64  f_frsize    /* fundamental fs block size */
1773                 628318530,    # uint64  f_blocks    /* number of blocks (unit f_frsize) */
1774                 314159265,    # uint64  f_bfree     /* free blocks in file system */
1775                 314159265,    # uint64  f_bavail    /* free blocks for non-root */
1776                 200000000,    # uint64  f_files     /* total file inodes */
1777                 100000000,    # uint64  f_ffree     /* free file inodes */
1778                 100000000,    # uint64  f_favail    /* free file inodes for non-root */
1779                 0x1AF5,       # uint64  f_fsid      /* file system id */
1780                 2,            # uint64  f_flag      /* bit mask = ST_NOSUID; not ST_RDONLY */
1781                 65535,        # uint64  f_namemax   /* maximum filename length */
1782                 ))
1783
1784         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "unsupported %r request <data of length %r>" %
1785                                                                (extensionName, len(extensionData)))
1786         return defer.execute(_unsupported)
1787
1788     def realPath(self, pathstring):
1789         self.log(".realPath(%r)" % (pathstring,), level=OPERATIONAL)
1790
1791         return self._path_to_utf8(self._path_from_string(pathstring))
1792
1793     def _path_to_utf8(self, path):
1794         return (u"/" + u"/".join(path)).encode('utf-8')
1795
1796     def _path_from_string(self, pathstring):
1797         if noisy: self.log("CONVERT %r" % (pathstring,), level=NOISY)
1798
1799         # The home directory is the root directory.
1800         pathstring = pathstring.strip("/")
1801         if pathstring == "" or pathstring == ".":
1802             path_utf8 = []
1803         else:
1804             path_utf8 = pathstring.split("/")
1805
1806         # <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.2>
1807         # "Servers SHOULD interpret a path name component ".." as referring to
1808         #  the parent directory, and "." as referring to the current directory."
1809         path = []
1810         for p_utf8 in path_utf8:
1811             if p_utf8 == "..":
1812                 # ignore excess .. components at the root
1813                 if len(path) > 0:
1814                     path = path[:-1]
1815             elif p_utf8 != ".":
1816                 try:
1817                     p = p_utf8.decode('utf-8', 'strict')
1818                 except UnicodeError:
1819                     raise SFTPError(FX_NO_SUCH_FILE, "path could not be decoded as UTF-8")
1820                 path.append(p)
1821
1822         if noisy: self.log(" PATH %r" % (path,), level=NOISY)
1823         return path
1824
1825     def _get_root(self, path):
1826         # return Deferred (root, remaining_path)
1827         d = defer.succeed(None)
1828         if path and path[0] == u"uri":
1829             d.addCallback(lambda ign: self._client.create_node_from_uri(path[1].encode('utf-8')))
1830             d.addCallback(lambda root: (root, path[2:]))
1831         else:
1832             d.addCallback(lambda ign: (self._root, path))
1833         return d
1834
1835     def _get_parent_or_node(self, path):
1836         # return Deferred (parent, childname) or (node, None)
1837         d = self._get_root(path)
1838         def _got_root( (root, remaining_path) ):
1839             if not remaining_path:
1840                 return (root, None)
1841             else:
1842                 d2 = root.get_child_at_path(remaining_path[:-1])
1843                 d2.addCallback(lambda parent: (parent, remaining_path[-1]))
1844                 return d2
1845         d.addCallback(_got_root)
1846         return d
1847
1848
1849 class FakeTransport:
1850     implements(ITransport)
1851     def write(self, data):
1852         logmsg("FakeTransport.write(<data of length %r>)" % (len(data),), level=NOISY)
1853
1854     def writeSequence(self, data):
1855         logmsg("FakeTransport.writeSequence(...)", level=NOISY)
1856
1857     def loseConnection(self):
1858         logmsg("FakeTransport.loseConnection()", level=NOISY)
1859
1860     # getPeer and getHost can just raise errors, since we don't know what to return
1861
1862
1863 class ShellSession(PrefixingLogMixin):
1864     implements(ISession)
1865     def __init__(self, userHandler):
1866         PrefixingLogMixin.__init__(self, facility="tahoe.sftp")
1867         if noisy: self.log(".__init__(%r)" % (userHandler), level=NOISY)
1868
1869     def getPty(self, terminal, windowSize, attrs):
1870         self.log(".getPty(%r, %r, %r)" % (terminal, windowSize, attrs), level=OPERATIONAL)
1871
1872     def openShell(self, protocol):
1873         self.log(".openShell(%r)" % (protocol,), level=OPERATIONAL)
1874         if hasattr(protocol, 'transport') and protocol.transport is None:
1875             protocol.transport = FakeTransport()  # work around Twisted bug
1876
1877         d = defer.succeed(None)
1878         d.addCallback(lambda ign: protocol.write("This server supports only SFTP, not shell sessions.\n"))
1879         d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessTerminated(exitCode=1))))
1880         return d
1881
1882     def execCommand(self, protocol, cmd):
1883         self.log(".execCommand(%r, %r)" % (protocol, cmd), level=OPERATIONAL)
1884         if hasattr(protocol, 'transport') and protocol.transport is None:
1885             protocol.transport = FakeTransport()  # work around Twisted bug
1886
1887         d = defer.succeed(None)
1888         if cmd == "df -P -k /":
1889             d.addCallback(lambda ign: protocol.write(
1890                           "Filesystem         1024-blocks      Used Available Capacity Mounted on\n"
1891                           "tahoe                628318530 314159265 314159265      50% /\n"))
1892             d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessDone(None))))
1893         else:
1894             d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessTerminated(exitCode=1))))
1895         return d
1896
1897     def windowChanged(self, newWindowSize):
1898         self.log(".windowChanged(%r)" % (newWindowSize,), level=OPERATIONAL)
1899
1900     def eofReceived(self):
1901         self.log(".eofReceived()", level=OPERATIONAL)
1902
1903     def closed(self):
1904         self.log(".closed()", level=OPERATIONAL)
1905
1906
1907 # If you have an SFTPUserHandler and want something that provides ISession, you get
1908 # ShellSession(userHandler).
1909 # We use adaptation because this must be a different object to the SFTPUserHandler.
1910 components.registerAdapter(ShellSession, SFTPUserHandler, ISession)
1911
1912
1913 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
1914
1915 class Dispatcher:
1916     implements(portal.IRealm)
1917     def __init__(self, client):
1918         self._client = client
1919
1920     def requestAvatar(self, avatarID, mind, interface):
1921         assert interface == IConchUser, interface
1922         rootnode = self._client.create_node_from_uri(avatarID.rootcap)
1923         handler = SFTPUserHandler(self._client, rootnode, avatarID.username)
1924         return (interface, handler, handler.logout)
1925
1926
1927 class SFTPServer(service.MultiService):
1928     def __init__(self, client, accountfile, accounturl,
1929                  sftp_portstr, pubkey_file, privkey_file):
1930         service.MultiService.__init__(self)
1931
1932         r = Dispatcher(client)
1933         p = portal.Portal(r)
1934
1935         if accountfile:
1936             c = AccountFileChecker(self, accountfile)
1937             p.registerChecker(c)
1938         if accounturl:
1939             c = AccountURLChecker(self, accounturl)
1940             p.registerChecker(c)
1941         if not accountfile and not accounturl:
1942             # we could leave this anonymous, with just the /uri/CAP form
1943             raise NeedRootcapLookupScheme("must provide an account file or URL")
1944
1945         pubkey = keys.Key.fromFile(pubkey_file)
1946         privkey = keys.Key.fromFile(privkey_file)
1947         class SSHFactory(factory.SSHFactory):
1948             publicKeys = {pubkey.sshType(): pubkey}
1949             privateKeys = {privkey.sshType(): privkey}
1950             def getPrimes(self):
1951                 try:
1952                     # if present, this enables diffie-hellman-group-exchange
1953                     return primes.parseModuliFile("/etc/ssh/moduli")
1954                 except IOError:
1955                     return None
1956
1957         f = SSHFactory()
1958         f.portal = p
1959
1960         s = strports.service(sftp_portstr, f)
1961         s.setServiceParent(self)