]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/sftpd.py
SFTP: ignore permissions when opening a file (needed for sshfs interoperability).
[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=os.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             return len(files) > 0
1166         d.addBoth(_done)
1167         return d
1168
1169     def _sync_heisenfiles(self, userpath, direntry, ignore=None):
1170         request = "._sync_heisenfiles(%r, %r, ignore=%r)" % (userpath, direntry, ignore)
1171         self.log(request, level=OPERATIONAL)
1172
1173         files = []
1174         if direntry in all_heisenfiles:
1175             files = all_heisenfiles[direntry]
1176         if userpath in self._heisenfiles:
1177             files += self._heisenfiles[userpath]
1178
1179         if noisy: self.log("files = %r in %r" % (files, request), level=NOISY)
1180
1181         d = defer.succeed(None)
1182         for f in files:
1183             if f is not ignore:
1184                 d.addBoth(f.sync)
1185
1186         def _done(ign):
1187             self.log("done %r" % (request,), level=OPERATIONAL)
1188             return None
1189         d.addBoth(_done)
1190         return d
1191
1192     def _remove_heisenfile(self, userpath, parent, childname, file_to_remove):
1193         if noisy: self.log("._remove_heisenfile(%r, %r, %r, %r)" % (userpath, parent, childname, file_to_remove), level=NOISY)
1194
1195         direntry = _direntry_for(parent, childname)
1196         if direntry in all_heisenfiles:
1197             all_old_files = all_heisenfiles[direntry]
1198             all_new_files = [f for f in all_old_files if f is not file_to_remove]
1199             if len(all_new_files) > 0:
1200                 all_heisenfiles[direntry] = all_new_files
1201             else:
1202                 del all_heisenfiles[direntry]
1203
1204         if userpath in self._heisenfiles:
1205             old_files = self._heisenfiles[userpath]
1206             new_files = [f for f in old_files if f is not file_to_remove]
1207             if len(new_files) > 0:
1208                 self._heisenfiles[userpath] = new_files
1209             else:
1210                 del self._heisenfiles[userpath]
1211
1212     def _make_file(self, existing_file, userpath, flags, parent=None, childname=None, filenode=None, metadata=None):
1213         if noisy: self.log("._make_file(%r, %r, %r = %r, parent=%r, childname=%r, filenode=%r, metadata=%r)" %
1214                            (existing_file, userpath, flags, _repr_flags(flags), parent, childname, filenode, metadata),
1215                            level=NOISY)
1216
1217         assert metadata is None or 'no-write' in metadata, metadata
1218
1219         writing = (flags & (FXF_WRITE | FXF_CREAT)) != 0
1220         direntry = _direntry_for(parent, childname, filenode)
1221
1222         d = self._sync_heisenfiles(userpath, direntry, ignore=existing_file)
1223
1224         if not writing and (flags & FXF_READ) and filenode and not filenode.is_mutable() and filenode.get_size() <= SIZE_THRESHOLD:
1225             d.addCallback(lambda ign: ShortReadOnlySFTPFile(userpath, filenode, metadata))
1226         else:
1227             close_notify = None
1228             if writing:
1229                 close_notify = self._remove_heisenfile
1230
1231             d.addCallback(lambda ign: existing_file or GeneralSFTPFile(userpath, flags, close_notify, self._convergence))
1232             def _got_file(file):
1233                 file.open(parent=parent, childname=childname, filenode=filenode, metadata=metadata)
1234                 if writing:
1235                     self._add_heisenfiles_by_direntry(direntry, [file])
1236                 return file
1237             d.addCallback(_got_file)
1238         return d
1239
1240     def openFile(self, pathstring, flags, attrs, delay=None):
1241         request = ".openFile(%r, %r = %r, %r, delay=%r)" % (pathstring, flags, _repr_flags(flags), attrs, delay)
1242         self.log(request, level=OPERATIONAL)
1243
1244         # This is used for both reading and writing.
1245         # First exclude invalid combinations of flags, and empty paths.
1246
1247         if not (flags & (FXF_READ | FXF_WRITE)):
1248             def _bad_readwrite():
1249                 raise SFTPError(FX_BAD_MESSAGE, "invalid file open flags: at least one of FXF_READ and FXF_WRITE must be set")
1250             return defer.execute(_bad_readwrite)
1251
1252         if (flags & FXF_EXCL) and not (flags & FXF_CREAT):
1253             def _bad_exclcreat():
1254                 raise SFTPError(FX_BAD_MESSAGE, "invalid file open flags: FXF_EXCL cannot be set without FXF_CREAT")
1255             return defer.execute(_bad_exclcreat)
1256
1257         path = self._path_from_string(pathstring)
1258         if not path:
1259             def _emptypath(): raise SFTPError(FX_NO_SUCH_FILE, "path cannot be empty")
1260             return defer.execute(_emptypath)
1261
1262         # The combination of flags is potentially valid.
1263
1264         # To work around clients that have race condition bugs, a getAttr, rename, or
1265         # remove request following an 'open' request with FXF_WRITE or FXF_CREAT flags,
1266         # should succeed even if the 'open' request has not yet completed. So we now
1267         # synchronously add a file object into the self._heisenfiles dict, indexed
1268         # by its UTF-8 userpath. (We can't yet add it to the all_heisenfiles dict,
1269         # because we don't yet have a user-independent path for the file.) The file
1270         # object does not know its filenode, parent, or childname at this point.
1271
1272         userpath = self._path_to_utf8(path)
1273
1274         if flags & (FXF_WRITE | FXF_CREAT):
1275             file = GeneralSFTPFile(userpath, flags, self._remove_heisenfile, self._convergence)
1276             self._add_heisenfiles_by_path(userpath, [file])
1277         else:
1278             # We haven't decided which file implementation to use yet.
1279             file = None
1280
1281         desired_metadata = _attrs_to_metadata(attrs)
1282
1283         # Now there are two major cases:
1284         #
1285         #  1. The path is specified as /uri/FILECAP, with no parent directory.
1286         #     If the FILECAP is mutable and writeable, then we can open it in write-only
1287         #     or read/write mode (non-exclusively), otherwise we can only open it in
1288         #     read-only mode. The open should succeed immediately as long as FILECAP is
1289         #     a valid known filecap that grants the required permission.
1290         #
1291         #  2. The path is specified relative to a parent. We find the parent dirnode and
1292         #     get the child's URI and metadata if it exists. There are four subcases:
1293         #       a. the child does not exist: FXF_CREAT must be set, and we must be able
1294         #          to write to the parent directory.
1295         #       b. the child exists but is not a valid known filecap: fail
1296         #       c. the child is mutable: if we are trying to open it write-only or
1297         #          read/write, then we must be able to write to the file.
1298         #       d. the child is immutable: if we are trying to open it write-only or
1299         #          read/write, then we must be able to write to the parent directory.
1300         #
1301         # To reduce latency, open normally succeeds as soon as these conditions are
1302         # met, even though there might be a failure in downloading the existing file
1303         # or uploading a new one. However, there is an exception: if a file has been
1304         # written, then closed, and is now being reopened, then we have to delay the
1305         # open until the previous upload/publish has completed. This is necessary
1306         # because sshfs does not wait for the result of an FXF_CLOSE message before
1307         # reporting to the client that a file has been closed. It applies both to
1308         # mutable files, and to directory entries linked to an immutable file.
1309         #
1310         # Note that the permission checks below are for more precise error reporting on
1311         # the open call; later operations would fail even if we did not make these checks.
1312
1313         d = delay or defer.succeed(None)
1314         d.addCallback(lambda ign: self._get_root(path))
1315         def _got_root( (root, path) ):
1316             if root.is_unknown():
1317                 raise SFTPError(FX_PERMISSION_DENIED,
1318                                 "cannot open an unknown cap (or child of an unknown object). "
1319                                 "Upgrading the gateway to a later Tahoe-LAFS version may help")
1320             if not path:
1321                 # case 1
1322                 if noisy: self.log("case 1: root = %r, path[:-1] = %r" % (root, path[:-1]), level=NOISY)
1323                 if not IFileNode.providedBy(root):
1324                     raise SFTPError(FX_PERMISSION_DENIED,
1325                                     "cannot open a directory cap")
1326                 if (flags & FXF_WRITE) and root.is_readonly():
1327                     raise SFTPError(FX_PERMISSION_DENIED,
1328                                     "cannot write to a non-writeable filecap without a parent directory")
1329                 if flags & FXF_EXCL:
1330                     raise SFTPError(FX_FAILURE,
1331                                     "cannot create a file exclusively when it already exists")
1332
1333                 # The file does not need to be added to all_heisenfiles, because it is not
1334                 # associated with a directory entry that needs to be updated.
1335
1336                 metadata = update_metadata(None, desired_metadata, time())
1337
1338                 # We have to decide what to pass for the 'parent_readonly' argument to _no_write,
1339                 # given that we don't actually have a parent. This only affects the permissions
1340                 # reported by a getAttrs on this file handle in the case of an immutable file.
1341                 # We choose 'parent_readonly=True' since that will cause the permissions to be
1342                 # reported as r--r--r--, which is appropriate because an immutable file can't be
1343                 # written via this path.
1344
1345                 metadata['no-write'] = _no_write(True, root)
1346                 return self._make_file(file, userpath, flags, filenode=root, metadata=metadata)
1347             else:
1348                 # case 2
1349                 childname = path[-1]
1350
1351                 if noisy: self.log("case 2: root = %r, childname = %r, desired_metadata = %r, path[:-1] = %r" %
1352                                    (root, childname, desired_metadata, path[:-1]), level=NOISY)
1353                 d2 = root.get_child_at_path(path[:-1])
1354                 def _got_parent(parent):
1355                     if noisy: self.log("_got_parent(%r)" % (parent,), level=NOISY)
1356                     if parent.is_unknown():
1357                         raise SFTPError(FX_PERMISSION_DENIED,
1358                                         "cannot open a child of an unknown object. "
1359                                         "Upgrading the gateway to a later Tahoe-LAFS version may help")
1360
1361                     parent_readonly = parent.is_readonly()
1362                     d3 = defer.succeed(None)
1363                     if flags & FXF_EXCL:
1364                         # FXF_EXCL means that the link to the file (not the file itself) must
1365                         # be created atomically wrt updates by this storage client.
1366                         # That is, we need to create the link before returning success to the
1367                         # SFTP open request (and not just on close, as would normally be the
1368                         # case). We make the link initially point to a zero-length LIT file,
1369                         # which is consistent with what might happen on a POSIX filesystem.
1370
1371                         if parent_readonly:
1372                             raise SFTPError(FX_FAILURE,
1373                                             "cannot create a file exclusively when the parent directory is read-only")
1374
1375                         # 'overwrite=False' ensures failure if the link already exists.
1376                         # FIXME: should use a single call to set_uri and return (child, metadata) (#1035)
1377
1378                         zero_length_lit = "URI:LIT:"
1379                         if noisy: self.log("%r.set_uri(%r, None, readcap=%r, overwrite=False)" %
1380                                            (parent, zero_length_lit, childname), level=NOISY)
1381                         d3.addCallback(lambda ign: parent.set_uri(childname, None, readcap=zero_length_lit,
1382                                                                   metadata=desired_metadata, overwrite=False))
1383                         def _seturi_done(child):
1384                             if noisy: self.log("%r.get_metadata_for(%r)" % (parent, childname), level=NOISY)
1385                             d4 = parent.get_metadata_for(childname)
1386                             d4.addCallback(lambda metadata: (child, metadata))
1387                             return d4
1388                         d3.addCallback(_seturi_done)
1389                     else:
1390                         if noisy: self.log("%r.get_child_and_metadata(%r)" % (parent, childname), level=NOISY)
1391                         d3.addCallback(lambda ign: parent.get_child_and_metadata(childname))
1392
1393                     def _got_child( (filenode, current_metadata) ):
1394                         if noisy: self.log("_got_child( (%r, %r) )" % (filenode, current_metadata), level=NOISY)
1395
1396                         metadata = update_metadata(current_metadata, desired_metadata, time())
1397
1398                         # Ignore the permissions of the desired_metadata in an open call. The permissions
1399                         # can only be set by setAttrs.
1400                         metadata['no-write'] = _no_write(parent_readonly, filenode, current_metadata)
1401
1402                         if filenode.is_unknown():
1403                             raise SFTPError(FX_PERMISSION_DENIED,
1404                                             "cannot open an unknown cap. Upgrading the gateway "
1405                                             "to a later Tahoe-LAFS version may help")
1406                         if not IFileNode.providedBy(filenode):
1407                             raise SFTPError(FX_PERMISSION_DENIED,
1408                                             "cannot open a directory as if it were a file")
1409                         if (flags & FXF_WRITE) and metadata['no-write']:
1410                             raise SFTPError(FX_PERMISSION_DENIED,
1411                                             "cannot open a non-writeable file for writing")
1412
1413                         return self._make_file(file, userpath, flags, parent=parent, childname=childname,
1414                                                filenode=filenode, metadata=metadata)
1415                     def _no_child(f):
1416                         if noisy: self.log("_no_child(%r)" % (f,), level=NOISY)
1417                         f.trap(NoSuchChildError)
1418
1419                         if not (flags & FXF_CREAT):
1420                             raise SFTPError(FX_NO_SUCH_FILE,
1421                                             "the file does not exist, and was not opened with the creation (CREAT) flag")
1422                         if parent_readonly:
1423                             raise SFTPError(FX_PERMISSION_DENIED,
1424                                             "cannot create a file when the parent directory is read-only")
1425
1426                         return self._make_file(file, userpath, flags, parent=parent, childname=childname)
1427                     d3.addCallbacks(_got_child, _no_child)
1428                     return d3
1429
1430                 d2.addCallback(_got_parent)
1431                 return d2
1432
1433         d.addCallback(_got_root)
1434         def _remove_on_error(err):
1435             if file:
1436                 self._remove_heisenfile(userpath, None, None, file)
1437             return err
1438         d.addErrback(_remove_on_error)
1439         d.addBoth(_convert_error, request)
1440         return d
1441
1442     def renameFile(self, from_pathstring, to_pathstring, overwrite=False):
1443         request = ".renameFile(%r, %r)" % (from_pathstring, to_pathstring)
1444         self.log(request, level=OPERATIONAL)
1445
1446         from_path = self._path_from_string(from_pathstring)
1447         to_path = self._path_from_string(to_pathstring)
1448         from_userpath = self._path_to_utf8(from_path)
1449         to_userpath = self._path_to_utf8(to_path)
1450
1451         # the target directory must already exist
1452         d = deferredutil.gatherResults([self._get_parent_or_node(from_path),
1453                                         self._get_parent_or_node(to_path)])
1454         def _got( (from_pair, to_pair) ):
1455             if noisy: self.log("_got( (%r, %r) ) in .renameFile(%r, %r, overwrite=%r)" %
1456                                (from_pair, to_pair, from_pathstring, to_pathstring, overwrite), level=NOISY)
1457             (from_parent, from_childname) = from_pair
1458             (to_parent, to_childname) = to_pair
1459
1460             if from_childname is None:
1461                 raise SFTPError(FX_NO_SUCH_FILE, "cannot rename a source object specified by URI")
1462             if to_childname is None:
1463                 raise SFTPError(FX_NO_SUCH_FILE, "cannot rename to a destination specified by URI")
1464
1465             # <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5>
1466             # "It is an error if there already exists a file with the name specified
1467             #  by newpath."
1468             # OpenSSH's SFTP server returns FX_PERMISSION_DENIED for this error.
1469             #
1470             # For the standard SSH_FXP_RENAME operation, overwrite=False.
1471             # We also support the posix-rename@openssh.com extension, which uses overwrite=True.
1472
1473             d2 = defer.fail(NoSuchChildError())
1474             if not overwrite:
1475                 d2.addCallback(lambda ign: to_parent.get(to_childname))
1476             def _expect_fail(res):
1477                 if not isinstance(res, Failure):
1478                     raise SFTPError(FX_PERMISSION_DENIED, "cannot rename to existing path " + to_userpath)
1479
1480                 # It is OK if we fail for errors other than NoSuchChildError, since that probably
1481                 # indicates some problem accessing the destination directory.
1482                 res.trap(NoSuchChildError)
1483             d2.addBoth(_expect_fail)
1484
1485             # If there are heisenfiles to be written at the 'from' direntry, then ensure
1486             # they will now be written at the 'to' direntry instead.
1487             d2.addCallback(lambda ign:
1488                            self._rename_heisenfiles(from_userpath, from_parent, from_childname,
1489                                                     to_userpath, to_parent, to_childname, overwrite=overwrite))
1490
1491             def _move(renamed):
1492                 # FIXME: use move_child_to_path to avoid possible data loss due to #943
1493                 #d3 = from_parent.move_child_to_path(from_childname, to_root, to_path, overwrite=overwrite)
1494
1495                 d3 = from_parent.move_child_to(from_childname, to_parent, to_childname, overwrite=overwrite)
1496                 def _check(err):
1497                     if noisy: self.log("_check(%r) in .renameFile(%r, %r, overwrite=%r)" %
1498                                        (err, from_pathstring, to_pathstring, overwrite), level=NOISY)
1499
1500                     if not isinstance(err, Failure) or (renamed and err.check(NoSuchChildError)):
1501                         return None
1502                     if not overwrite and err.check(ExistingChildError):
1503                         raise SFTPError(FX_PERMISSION_DENIED, "cannot rename to existing path " + to_userpath)
1504
1505                     return err
1506                 d3.addBoth(_check)
1507                 return d3
1508             d2.addCallback(_move)
1509             return d2
1510         d.addCallback(_got)
1511         d.addBoth(_convert_error, request)
1512         return d
1513
1514     def makeDirectory(self, pathstring, attrs):
1515         request = ".makeDirectory(%r, %r)" % (pathstring, attrs)
1516         self.log(request, level=OPERATIONAL)
1517
1518         path = self._path_from_string(pathstring)
1519         metadata = _attrs_to_metadata(attrs)
1520         if 'no-write' in metadata:
1521             def _denied(): raise SFTPError(FX_PERMISSION_DENIED, "cannot create a directory that is initially read-only")
1522             return defer.execute(_denied)
1523
1524         d = self._get_root(path)
1525         d.addCallback(lambda (root, path):
1526                       self._get_or_create_directories(root, path, metadata))
1527         d.addBoth(_convert_error, request)
1528         return d
1529
1530     def _get_or_create_directories(self, node, path, metadata):
1531         if not IDirectoryNode.providedBy(node):
1532             # TODO: provide the name of the blocking file in the error message.
1533             def _blocked(): raise SFTPError(FX_FAILURE, "cannot create directory because there "
1534                                                         "is a file in the way") # close enough
1535             return defer.execute(_blocked)
1536
1537         if not path:
1538             return defer.succeed(node)
1539         d = node.get(path[0])
1540         def _maybe_create(f):
1541             f.trap(NoSuchChildError)
1542             return node.create_subdirectory(path[0])
1543         d.addErrback(_maybe_create)
1544         d.addCallback(self._get_or_create_directories, path[1:], metadata)
1545         return d
1546
1547     def removeFile(self, pathstring):
1548         request = ".removeFile(%r)" % (pathstring,)
1549         self.log(request, level=OPERATIONAL)
1550
1551         path = self._path_from_string(pathstring)
1552         d = self._remove_object(path, must_be_file=True)
1553         d.addBoth(_convert_error, request)
1554         return d
1555
1556     def removeDirectory(self, pathstring):
1557         request = ".removeDirectory(%r)" % (pathstring,)
1558         self.log(request, level=OPERATIONAL)
1559
1560         path = self._path_from_string(pathstring)
1561         d = self._remove_object(path, must_be_directory=True)
1562         d.addBoth(_convert_error, request)
1563         return d
1564
1565     def _remove_object(self, path, must_be_directory=False, must_be_file=False):
1566         userpath = self._path_to_utf8(path)
1567         d = self._get_parent_or_node(path)
1568         def _got_parent( (parent, childname) ):
1569             if childname is None:
1570                 raise SFTPError(FX_NO_SUCH_FILE, "cannot remove an object specified by URI")
1571
1572             direntry = _direntry_for(parent, childname)
1573             d2 = defer.succeed(False)
1574             if not must_be_directory:
1575                 d2.addCallback(lambda ign: self._abandon_any_heisenfiles(userpath, direntry))
1576
1577             d2.addCallback(lambda abandoned:
1578                            parent.delete(childname, must_exist=not abandoned,
1579                                          must_be_directory=must_be_directory, must_be_file=must_be_file))
1580             return d2
1581         d.addCallback(_got_parent)
1582         return d
1583
1584     def openDirectory(self, pathstring):
1585         request = ".openDirectory(%r)" % (pathstring,)
1586         self.log(request, level=OPERATIONAL)
1587
1588         path = self._path_from_string(pathstring)
1589         d = self._get_parent_or_node(path)
1590         def _got_parent_or_node( (parent_or_node, childname) ):
1591             if noisy: self.log("_got_parent_or_node( (%r, %r) ) in openDirectory(%r)" %
1592                                (parent_or_node, childname, pathstring), level=NOISY)
1593             if childname is None:
1594                 return parent_or_node
1595             else:
1596                 return parent_or_node.get(childname)
1597         d.addCallback(_got_parent_or_node)
1598         def _list(dirnode):
1599             if dirnode.is_unknown():
1600                 raise SFTPError(FX_PERMISSION_DENIED,
1601                                 "cannot list an unknown cap as a directory. Upgrading the gateway "
1602                                 "to a later Tahoe-LAFS version may help")
1603             if not IDirectoryNode.providedBy(dirnode):
1604                 raise SFTPError(FX_PERMISSION_DENIED,
1605                                 "cannot list a file as if it were a directory")
1606
1607             d2 = dirnode.list()
1608             def _render(children):
1609                 parent_readonly = dirnode.is_readonly()
1610                 results = []
1611                 for filename, (child, metadata) in children.iteritems():
1612                     # The file size may be cached or absent.
1613                     metadata['no-write'] = _no_write(parent_readonly, child, metadata)
1614                     attrs = _populate_attrs(child, metadata)
1615                     filename_utf8 = filename.encode('utf-8')
1616                     longname = _lsLine(filename_utf8, attrs)
1617                     results.append( (filename_utf8, longname, attrs) )
1618                 return StoppableList(results)
1619             d2.addCallback(_render)
1620             return d2
1621         d.addCallback(_list)
1622         d.addBoth(_convert_error, request)
1623         return d
1624
1625     def getAttrs(self, pathstring, followLinks):
1626         request = ".getAttrs(%r, followLinks=%r)" % (pathstring, followLinks)
1627         self.log(request, level=OPERATIONAL)
1628
1629         # When asked about a specific file, report its current size.
1630         # TODO: the modification time for a mutable file should be
1631         # reported as the update time of the best version. But that
1632         # information isn't currently stored in mutable shares, I think.
1633
1634         path = self._path_from_string(pathstring)
1635         userpath = self._path_to_utf8(path)
1636         d = self._get_parent_or_node(path)
1637         def _got_parent_or_node( (parent_or_node, childname) ):
1638             if noisy: self.log("_got_parent_or_node( (%r, %r) )" % (parent_or_node, childname), level=NOISY)
1639
1640             # Some clients will incorrectly try to get the attributes
1641             # of a file immediately after opening it, before it has been put
1642             # into the all_heisenfiles table. This is a race condition bug in
1643             # the client, but we handle it anyway by calling .sync() on all
1644             # files matching either the path or the direntry.
1645
1646             direntry = _direntry_for(parent_or_node, childname)
1647             d2 = self._sync_heisenfiles(userpath, direntry)
1648
1649             if childname is None:
1650                 node = parent_or_node
1651                 d2.addCallback(lambda ign: node.get_current_size())
1652                 d2.addCallback(lambda size:
1653                                _populate_attrs(node, {'no-write': node.is_unknown() or node.is_readonly()}, size=size))
1654             else:
1655                 parent = parent_or_node
1656                 d2.addCallback(lambda ign: parent.get_child_and_metadata_at_path([childname]))
1657                 def _got( (child, metadata) ):
1658                     if noisy: self.log("_got( (%r, %r) )" % (child, metadata), level=NOISY)
1659                     assert IDirectoryNode.providedBy(parent), parent
1660                     metadata['no-write'] = _no_write(parent.is_readonly(), child, metadata)
1661                     d3 = child.get_current_size()
1662                     d3.addCallback(lambda size: _populate_attrs(child, metadata, size=size))
1663                     return d3
1664                 def _nosuch(err):
1665                     if noisy: self.log("_nosuch(%r)" % (err,), level=NOISY)
1666                     err.trap(NoSuchChildError)
1667                     if noisy: self.log("checking open files:\nself._heisenfiles = %r\nall_heisenfiles = %r\ndirentry=%r" %
1668                                        (self._heisenfiles, all_heisenfiles, direntry), level=NOISY)
1669                     if direntry in all_heisenfiles:
1670                         files = all_heisenfiles[direntry]
1671                         if len(files) == 0:  # pragma: no cover
1672                             return err
1673                         # use the heisenfile that was most recently opened
1674                         return files[-1].getAttrs()
1675                     return err
1676                 d2.addCallbacks(_got, _nosuch)
1677             return d2
1678         d.addCallback(_got_parent_or_node)
1679         d.addBoth(_convert_error, request)
1680         return d
1681
1682     def setAttrs(self, pathstring, attrs):
1683         request = ".setAttrs(%r, %r)" % (pathstring, attrs)
1684         self.log(request, level=OPERATIONAL)
1685
1686         if "size" in attrs:
1687             # this would require us to download and re-upload the truncated/extended
1688             # file contents
1689             def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "setAttrs wth size attribute unsupported")
1690             return defer.execute(_unsupported)
1691
1692         path = self._path_from_string(pathstring)
1693         userpath = self._path_to_utf8(path)
1694         d = self._get_parent_or_node(path)
1695         def _got_parent_or_node( (parent_or_node, childname) ):
1696             if noisy: self.log("_got_parent_or_node( (%r, %r) )" % (parent_or_node, childname), level=NOISY)
1697
1698             direntry = _direntry_for(parent_or_node, childname)
1699             d2 = self._update_attrs_for_heisenfiles(userpath, direntry, attrs)
1700
1701             def _update(updated_heisenfiles):
1702                 if childname is None:
1703                     if updated_heisenfiles:
1704                         return None
1705                     raise SFTPError(FX_NO_SUCH_FILE, userpath)
1706                 else:
1707                     desired_metadata = _attrs_to_metadata(attrs)
1708                     if noisy: self.log("desired_metadata = %r" % (desired_metadata,), level=NOISY)
1709
1710                     return parent_or_node.set_metadata_for(childname, desired_metadata)
1711             d2.addCallback(_update)
1712             d2.addCallback(lambda ign: None)
1713             return d2
1714         d.addCallback(_got_parent_or_node)
1715         d.addBoth(_convert_error, request)
1716         return d
1717
1718     def readLink(self, pathstring):
1719         self.log(".readLink(%r)" % (pathstring,), level=OPERATIONAL)
1720
1721         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "readLink")
1722         return defer.execute(_unsupported)
1723
1724     def makeLink(self, linkPathstring, targetPathstring):
1725         self.log(".makeLink(%r, %r)" % (linkPathstring, targetPathstring), level=OPERATIONAL)
1726
1727         # If this is implemented, note the reversal of arguments described in point 7 of
1728         # <http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.15>.
1729
1730         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "makeLink")
1731         return defer.execute(_unsupported)
1732
1733     def extendedRequest(self, extensionName, extensionData):
1734         self.log(".extendedRequest(%r, <data of length %r>)" % (extensionName, len(extensionData)), level=OPERATIONAL)
1735
1736         # We implement the three main OpenSSH SFTP extensions; see
1737         # <http://www.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL?rev=1.15>
1738
1739         if extensionName == 'posix-rename@openssh.com':
1740             def _bad(): raise SFTPError(FX_BAD_MESSAGE, "could not parse posix-rename@openssh.com request")
1741
1742             (fromPathLen,) = struct.unpack('>L', extensionData[0:4])
1743             if 8 + fromPathLen > len(extensionData): return defer.execute(_bad)
1744
1745             (toPathLen,) = struct.unpack('>L', extensionData[(4 + fromPathLen):(8 + fromPathLen)])
1746             if 8 + fromPathLen + toPathLen != len(extensionData): return defer.execute(_bad)
1747
1748             fromPathstring = extensionData[4:(4 + fromPathLen)]
1749             toPathstring = extensionData[(8 + fromPathLen):]
1750             d = self.renameFile(fromPathstring, toPathstring, overwrite=True)
1751
1752             # Twisted conch assumes that the response from an extended request is either
1753             # an error, or an FXP_EXTENDED_REPLY. But it happens to do the right thing
1754             # (respond with an FXP_STATUS message) if we return a Failure with code FX_OK.
1755             def _succeeded(ign):
1756                 raise SFTPError(FX_OK, "request succeeded")
1757             d.addCallback(_succeeded)
1758             return d
1759
1760         if extensionName == 'statvfs@openssh.com' or extensionName == 'fstatvfs@openssh.com':
1761             # f_bsize and f_frsize should be the same to avoid a bug in 'df'
1762             return defer.succeed(struct.pack('>11Q',
1763                 1024,         # uint64  f_bsize     /* file system block size */
1764                 1024,         # uint64  f_frsize    /* fundamental fs block size */
1765                 628318530,    # uint64  f_blocks    /* number of blocks (unit f_frsize) */
1766                 314159265,    # uint64  f_bfree     /* free blocks in file system */
1767                 314159265,    # uint64  f_bavail    /* free blocks for non-root */
1768                 200000000,    # uint64  f_files     /* total file inodes */
1769                 100000000,    # uint64  f_ffree     /* free file inodes */
1770                 100000000,    # uint64  f_favail    /* free file inodes for non-root */
1771                 0x1AF5,       # uint64  f_fsid      /* file system id */
1772                 2,            # uint64  f_flag      /* bit mask = ST_NOSUID; not ST_RDONLY */
1773                 65535,        # uint64  f_namemax   /* maximum filename length */
1774                 ))
1775
1776         def _unsupported(): raise SFTPError(FX_OP_UNSUPPORTED, "unsupported %r request <data of length %r>" %
1777                                                                (extensionName, len(extensionData)))
1778         return defer.execute(_unsupported)
1779
1780     def realPath(self, pathstring):
1781         self.log(".realPath(%r)" % (pathstring,), level=OPERATIONAL)
1782
1783         return self._path_to_utf8(self._path_from_string(pathstring))
1784
1785     def _path_to_utf8(self, path):
1786         return (u"/" + u"/".join(path)).encode('utf-8')
1787
1788     def _path_from_string(self, pathstring):
1789         if noisy: self.log("CONVERT %r" % (pathstring,), level=NOISY)
1790
1791         # The home directory is the root directory.
1792         pathstring = pathstring.strip("/")
1793         if pathstring == "" or pathstring == ".":
1794             path_utf8 = []
1795         else:
1796             path_utf8 = pathstring.split("/")
1797
1798         # <http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.2>
1799         # "Servers SHOULD interpret a path name component ".." as referring to
1800         #  the parent directory, and "." as referring to the current directory."
1801         path = []
1802         for p_utf8 in path_utf8:
1803             if p_utf8 == "..":
1804                 # ignore excess .. components at the root
1805                 if len(path) > 0:
1806                     path = path[:-1]
1807             elif p_utf8 != ".":
1808                 try:
1809                     p = p_utf8.decode('utf-8', 'strict')
1810                 except UnicodeError:
1811                     raise SFTPError(FX_NO_SUCH_FILE, "path could not be decoded as UTF-8")
1812                 path.append(p)
1813
1814         if noisy: self.log(" PATH %r" % (path,), level=NOISY)
1815         return path
1816
1817     def _get_root(self, path):
1818         # return Deferred (root, remaining_path)
1819         d = defer.succeed(None)
1820         if path and path[0] == u"uri":
1821             d.addCallback(lambda ign: self._client.create_node_from_uri(path[1].encode('utf-8')))
1822             d.addCallback(lambda root: (root, path[2:]))
1823         else:
1824             d.addCallback(lambda ign: (self._root, path))
1825         return d
1826
1827     def _get_parent_or_node(self, path):
1828         # return Deferred (parent, childname) or (node, None)
1829         d = self._get_root(path)
1830         def _got_root( (root, remaining_path) ):
1831             if not remaining_path:
1832                 return (root, None)
1833             else:
1834                 d2 = root.get_child_at_path(remaining_path[:-1])
1835                 d2.addCallback(lambda parent: (parent, remaining_path[-1]))
1836                 return d2
1837         d.addCallback(_got_root)
1838         return d
1839
1840
1841 class FakeTransport:
1842     implements(ITransport)
1843     def write(self, data):
1844         logmsg("FakeTransport.write(<data of length %r>)" % (len(data),), level=NOISY)
1845
1846     def writeSequence(self, data):
1847         logmsg("FakeTransport.writeSequence(...)", level=NOISY)
1848
1849     def loseConnection(self):
1850         logmsg("FakeTransport.loseConnection()", level=NOISY)
1851
1852     # getPeer and getHost can just raise errors, since we don't know what to return
1853
1854
1855 class ShellSession(PrefixingLogMixin):
1856     implements(ISession)
1857     def __init__(self, userHandler):
1858         PrefixingLogMixin.__init__(self, facility="tahoe.sftp")
1859         if noisy: self.log(".__init__(%r)" % (userHandler), level=NOISY)
1860
1861     def getPty(self, terminal, windowSize, attrs):
1862         self.log(".getPty(%r, %r, %r)" % (terminal, windowSize, attrs), level=OPERATIONAL)
1863
1864     def openShell(self, protocol):
1865         self.log(".openShell(%r)" % (protocol,), level=OPERATIONAL)
1866         if hasattr(protocol, 'transport') and protocol.transport is None:
1867             protocol.transport = FakeTransport()  # work around Twisted bug
1868
1869         d = defer.succeed(None)
1870         d.addCallback(lambda ign: protocol.write("This server supports only SFTP, not shell sessions.\n"))
1871         d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessTerminated(exitCode=1))))
1872         return d
1873
1874     def execCommand(self, protocol, cmd):
1875         self.log(".execCommand(%r, %r)" % (protocol, cmd), level=OPERATIONAL)
1876         if hasattr(protocol, 'transport') and protocol.transport is None:
1877             protocol.transport = FakeTransport()  # work around Twisted bug
1878
1879         d = defer.succeed(None)
1880         if cmd == "df -P -k /":
1881             d.addCallback(lambda ign: protocol.write(
1882                           "Filesystem         1024-blocks      Used Available Capacity Mounted on\n"
1883                           "tahoe                628318530 314159265 314159265      50% /\n"))
1884             d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessDone(None))))
1885         else:
1886             d.addCallback(lambda ign: protocol.processEnded(Reason(ProcessTerminated(exitCode=1))))
1887         return d
1888
1889     def windowChanged(self, newWindowSize):
1890         self.log(".windowChanged(%r)" % (newWindowSize,), level=OPERATIONAL)
1891
1892     def eofReceived(self):
1893         self.log(".eofReceived()", level=OPERATIONAL)
1894
1895     def closed(self):
1896         self.log(".closed()", level=OPERATIONAL)
1897
1898
1899 # If you have an SFTPUserHandler and want something that provides ISession, you get
1900 # ShellSession(userHandler).
1901 # We use adaptation because this must be a different object to the SFTPUserHandler.
1902 components.registerAdapter(ShellSession, SFTPUserHandler, ISession)
1903
1904
1905 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
1906
1907 class Dispatcher:
1908     implements(portal.IRealm)
1909     def __init__(self, client):
1910         self._client = client
1911
1912     def requestAvatar(self, avatarID, mind, interface):
1913         assert interface == IConchUser, interface
1914         rootnode = self._client.create_node_from_uri(avatarID.rootcap)
1915         handler = SFTPUserHandler(self._client, rootnode, avatarID.username)
1916         return (interface, handler, handler.logout)
1917
1918
1919 class SFTPServer(service.MultiService):
1920     def __init__(self, client, accountfile, accounturl,
1921                  sftp_portstr, pubkey_file, privkey_file):
1922         service.MultiService.__init__(self)
1923
1924         r = Dispatcher(client)
1925         p = portal.Portal(r)
1926
1927         if accountfile:
1928             c = AccountFileChecker(self, accountfile)
1929             p.registerChecker(c)
1930         if accounturl:
1931             c = AccountURLChecker(self, accounturl)
1932             p.registerChecker(c)
1933         if not accountfile and not accounturl:
1934             # we could leave this anonymous, with just the /uri/CAP form
1935             raise NeedRootcapLookupScheme("must provide an account file or URL")
1936
1937         pubkey = keys.Key.fromFile(pubkey_file)
1938         privkey = keys.Key.fromFile(privkey_file)
1939         class SSHFactory(factory.SSHFactory):
1940             publicKeys = {pubkey.sshType(): pubkey}
1941             privateKeys = {privkey.sshType(): privkey}
1942             def getPrimes(self):
1943                 try:
1944                     # if present, this enables diffie-hellman-group-exchange
1945                     return primes.parseModuliFile("/etc/ssh/moduli")
1946                 except IOError:
1947                     return None
1948
1949         f = SSHFactory()
1950         f.portal = p
1951
1952         s = strports.service(sftp_portstr, f)
1953         s.setServiceParent(self)