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