]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/sftpd.py
Fix for bug #645, correct path handling logic so that it works from sshfs
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / sftpd.py
1
2 import tempfile
3 from zope.interface import implements
4 from twisted.python import components
5 from twisted.application import service, strports
6 from twisted.internet import defer
7 from twisted.internet.interfaces import IConsumer
8 from twisted.conch.ssh import factory, keys, session
9 from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
10 from twisted.conch.avatar import ConchUser
11 from twisted.conch.openssh_compat import primes
12 from twisted.conch import ls
13 from twisted.cred import portal
14
15 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
16      NoSuchChildError
17 from allmydata.immutable.upload import FileHandle
18
19 class MemoryConsumer:
20     implements(IConsumer)
21     def __init__(self):
22         self.chunks = []
23         self.done = False
24     def registerProducer(self, p, streaming):
25         if streaming:
26             # call resumeProducing once to start things off
27             p.resumeProducing()
28         else:
29             while not self.done:
30                 p.resumeProducing()
31     def write(self, data):
32         self.chunks.append(data)
33     def unregisterProducer(self):
34         self.done = True
35
36 def download_to_data(n, offset=0, size=None):
37     d = n.read(MemoryConsumer(), offset, size)
38     d.addCallback(lambda mc: "".join(mc.chunks))
39     return d
40
41 class ReadFile:
42     implements(ISFTPFile)
43     def __init__(self, node):
44         self.node = node
45     def readChunk(self, offset, length):
46         d = download_to_data(self.node, offset, length)
47         def _got(data):
48             return data
49         d.addCallback(_got)
50         return d
51     def close(self):
52         pass
53     def getAttrs(self):
54         print "GETATTRS(file)"
55         raise NotImplementedError
56     def setAttrs(self, attrs):
57         print "SETATTRS(file)", attrs
58         raise NotImplementedError
59
60 class WriteFile:
61     implements(ISFTPFile)
62
63     def __init__(self, parent, childname, convergence):
64         self.parent = parent
65         self.childname = childname
66         self.convergence = convergence
67         self.f = tempfile.TemporaryFile()
68     def writeChunk(self, offset, data):
69         self.f.seek(offset)
70         self.f.write(data)
71
72     def close(self):
73         u = FileHandle(self.f, self.convergence)
74         d = self.parent.add_file(self.childname, u)
75         return d
76
77     def getAttrs(self):
78         print "GETATTRS(file)"
79         raise NotImplementedError
80     def setAttrs(self, attrs):
81         print "SETATTRS(file)", attrs
82         raise NotImplementedError
83
84
85 class NoParentError(Exception):
86     pass
87
88 class PermissionError(Exception):
89     pass
90
91 from twisted.conch.ssh.filetransfer import FileTransferServer, SFTPError, \
92      FX_NO_SUCH_FILE, FX_FILE_ALREADY_EXISTS, FX_OP_UNSUPPORTED, \
93      FX_PERMISSION_DENIED
94 from twisted.conch.ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL
95
96 class SFTPUser(ConchUser):
97     def __init__(self, client, rootnode, username, convergence):
98         ConchUser.__init__(self)
99         self.channelLookup["session"] = session.SSHSession
100         self.subsystemLookup["sftp"] = FileTransferServer
101
102         self.client = client
103         self.root = rootnode
104         self.username = username
105         self.convergence = convergence
106
107 class StoppableList:
108     def __init__(self, items):
109         self.items = items
110     def __iter__(self):
111         for i in self.items:
112             yield i
113     def close(self):
114         pass
115
116 class FakeStat:
117     pass
118
119 class BadRemoveRequest(Exception):
120     pass
121
122 class SFTPHandler:
123     implements(ISFTPServer)
124     def __init__(self, user):
125         print "Creating SFTPHandler from", user
126         self.client = user.client
127         self.root = user.root
128         self.username = user.username
129         self.convergence = user.convergence
130
131     def gotVersion(self, otherVersion, extData):
132         return {}
133
134     def openFile(self, filename, flags, attrs):
135         f = "|".join([f for f in
136                       [(flags & FXF_READ) and "FXF_READ" or None,
137                        (flags & FXF_WRITE) and "FXF_WRITE" or None,
138                        (flags & FXF_APPEND) and "FXF_APPEND" or None,
139                        (flags & FXF_CREAT) and "FXF_CREAT" or None,
140                        (flags & FXF_TRUNC) and "FXF_TRUNC" or None,
141                        (flags & FXF_EXCL) and "FXF_EXCL" or None,
142                       ]
143                       if f])
144         print "OPENFILE", filename, flags, f, attrs
145         # this is used for both reading and writing.
146
147 #        createPlease = False
148 #        exclusive = False
149 #        openFlags = 0
150 #
151 #        if flags & FXF_READ == FXF_READ and flags & FXF_WRITE == 0:
152 #            openFlags = os.O_RDONLY
153 #        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == 0:
154 #            createPlease = True
155 #            openFlags = os.O_WRONLY
156 #        if flags & FXF_WRITE == FXF_WRITE and flags & FXF_READ == FXF_READ:
157 #            createPlease = True
158 #            openFlags = os.O_RDWR
159 #        if flags & FXF_APPEND == FXF_APPEND:
160 #            createPlease = True
161 #            openFlags |= os.O_APPEND
162 #        if flags & FXF_CREAT == FXF_CREAT:
163 #            createPlease = True
164 #            openFlags |= os.O_CREAT
165 #        if flags & FXF_TRUNC == FXF_TRUNC:
166 #            openFlags |= os.O_TRUNC
167 #        if flags & FXF_EXCL == FXF_EXCL:
168 #            exclusive = True
169
170         # /usr/bin/sftp 'get' gives us FXF_READ, while 'put' on a new file
171         # gives FXF_WRITE,FXF_CREAT,FXF_TRUNC . I'm guessing that 'put' on an
172         # existing file gives the same.
173
174         path = self._convert_sftp_path(filename)
175
176         if flags & FXF_READ:
177             if flags & FXF_WRITE:
178                 raise NotImplementedError
179             d = self._get_node_and_metadata_for_path(path)
180             d.addCallback(lambda (node,metadata): ReadFile(node))
181             d.addErrback(self._convert_error)
182             return d
183
184         if flags & FXF_WRITE:
185             if not (flags & FXF_CREAT) or not (flags & FXF_TRUNC):
186                 raise NotImplementedError
187             if not path:
188                 raise PermissionError("cannot STOR to root directory")
189             childname = path[-1]
190             d = self._get_root(path)
191             def _got_root((root, path)):
192                 if not path:
193                     raise PermissionError("cannot STOR to root directory")
194                 return root.get_child_at_path(path[:-1])
195             d.addCallback(_got_root)
196             def _got_parent(parent):
197                 return WriteFile(parent, childname, self.convergence)
198             d.addCallback(_got_parent)
199             return d
200         raise NotImplementedError
201
202     def removeFile(self, path):
203         print "REMOVEFILE", path
204         path = self._convert_sftp_path(path)
205         return self._remove_thing(path, must_be_file=True)
206
207     def renameFile(self, oldpath, newpath):
208         print "RENAMEFILE", oldpath, newpath
209         fromPath = self._convert_sftp_path(oldpath)
210         toPath = self._convert_sftp_path(newpath)
211         # the target directory must already exist
212         d = self._get_parent(fromPath)
213         def _got_from_parent( (fromparent, childname) ):
214             d = self._get_parent(toPath)
215             d.addCallback(lambda (toparent, tochildname):
216                           fromparent.move_child_to(childname,
217                                                    toparent, tochildname,
218                                                    overwrite=False))
219             return d
220         d.addCallback(_got_from_parent)
221         d.addErrback(self._convert_error)
222         return d
223
224     def makeDirectory(self, path, attrs):
225         print "MAKEDIRECTORY", path, attrs
226         # TODO: extract attrs["mtime"], use it to set the parent metadata.
227         # Maybe also copy attrs["ext_*"] .
228         path = self._convert_sftp_path(path)
229         d = self._get_root(path)
230         d.addCallback(lambda (root,path):
231                       self._get_or_create_directories(root, path))
232         return d
233
234     def _get_or_create_directories(self, node, path):
235         if not IDirectoryNode.providedBy(node):
236             # unfortunately it is too late to provide the name of the
237             # blocking directory in the error message.
238             raise ExistingChildError("cannot create directory because there "
239                                      "is a file in the way") # close enough
240         if not path:
241             return defer.succeed(node)
242         d = node.get(path[0])
243         def _maybe_create(f):
244             f.trap(NoSuchChildError)
245             return node.create_empty_directory(path[0])
246         d.addErrback(_maybe_create)
247         d.addCallback(self._get_or_create_directories, path[1:])
248         return d
249
250     def removeDirectory(self, path):
251         print "REMOVEDIRECTORY", path
252         path = self._convert_sftp_path(path)
253         return self._remove_thing(path, must_be_directory=True)
254
255     def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
256         d = defer.maybeDeferred(self._get_parent, path)
257         def _convert_error(f):
258             f.trap(NoParentError)
259             raise PermissionError("cannot delete root directory")
260         d.addErrback(_convert_error)
261         def _got_parent( (parent, childname) ):
262             d = parent.get(childname)
263             def _got_child(child):
264                 if must_be_directory and not IDirectoryNode.providedBy(child):
265                     raise BadRemoveRequest("rmdir called on a file")
266                 if must_be_file and IDirectoryNode.providedBy(child):
267                     raise BadRemoveRequest("rmfile called on a directory")
268                 return parent.delete(childname)
269             d.addCallback(_got_child)
270             d.addErrback(self._convert_error)
271             return d
272         d.addCallback(_got_parent)
273         return d
274
275
276     def openDirectory(self, path):
277         print "OPENDIRECTORY", path
278         path = self._convert_sftp_path(path)
279         d = self._get_node_and_metadata_for_path(path)
280         d.addCallback(lambda (dirnode,metadata): dirnode.list())
281         def _render(children):
282             results = []
283             for filename, (node, metadata) in children.iteritems():
284                 s = FakeStat()
285                 if IDirectoryNode.providedBy(node):
286                     s.st_mode = 040700
287                     s.st_size = 0
288                 else:
289                     s.st_mode = 0100600
290                     s.st_size = node.get_size()
291                 s.st_nlink = 1
292                 s.st_uid = 0
293                 s.st_gid = 0
294                 s.st_mtime = int(metadata.get("mtime", 0))
295                 longname = ls.lsLine(filename.encode("utf-8"), s)
296                 attrs = self._populate_attrs(node, metadata)
297                 results.append( (filename.encode("utf-8"), longname, attrs) )
298             return StoppableList(results)
299         d.addCallback(_render)
300         return d
301
302     def getAttrs(self, path, followLinks):
303         print "GETATTRS", path, followLinks
304         # from ftp.stat
305         d = self._get_node_and_metadata_for_path(self._convert_sftp_path(path))
306         def _render((node,metadata)):
307             return self._populate_attrs(node, metadata)
308         d.addCallback(_render)
309         d.addErrback(self._convert_error)
310         def _done(res):
311             print " DONE", res
312             return res
313         d.addBoth(_done)
314         return d
315
316     def _convert_sftp_path(self, pathstring):
317         pathstring = pathstring.strip("/")
318         if pathstring == "" or  ".":
319             path = []
320         else:
321             path = pathstring.split("/")
322         print "CONVERT", pathstring, path
323         path = [unicode(p) for p in path]
324         return path
325
326     def _get_node_and_metadata_for_path(self, path):
327         d = self._get_root(path)
328         def _got_root((root,path)):
329             print "ROOT", root
330             print "PATH", path
331             if path:
332                 return root.get_child_and_metadata_at_path(path)
333             else:
334                 return (root,{})
335         d.addCallback(_got_root)
336         return d
337
338     def _get_root(self, path):
339         # return (root, remaining_path)
340         path = [unicode(p) for p in path]
341         if path and path[0] == "uri":
342             d = defer.maybeDeferred(self.client.create_node_from_uri,
343                                     str(path[1]))
344             d.addCallback(lambda root: (root, path[2:]))
345         else:
346             d = defer.succeed((self.root,path))
347         return d
348
349     def _populate_attrs(self, childnode, metadata):
350         attrs = {}
351         attrs["uid"] = 1000
352         attrs["gid"] = 1000
353         attrs["atime"] = 0
354         attrs["mtime"] = int(metadata.get("mtime", 0))
355         isdir = bool(IDirectoryNode.providedBy(childnode))
356         if isdir:
357             attrs["size"] = 1
358             # the permissions must have the extra bits (040000 or 0100000),
359             # otherwise the client will not call openDirectory
360             attrs["permissions"] = 040700 # S_IFDIR
361         else:
362             attrs["size"] = childnode.get_size()
363             attrs["permissions"] = 0100600 # S_IFREG
364         return attrs
365
366     def _convert_error(self, f):
367         if f.check(NoSuchChildError):
368             childname = f.value.args[0].encode("utf-8")
369             raise SFTPError(FX_NO_SUCH_FILE, childname)
370         if f.check(ExistingChildError):
371             msg = f.value.args[0].encode("utf-8")
372             raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
373         if f.check(PermissionError):
374             raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
375         if f.check(NotImplementedError):
376             raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
377         return f
378
379
380     def setAttrs(self, path, attrs):
381         print "SETATTRS", path, attrs
382         # ignored
383         return None
384
385     def readLink(self, path):
386         print "READLINK", path
387         raise NotImplementedError
388
389     def makeLink(self, linkPath, targetPath):
390         print "MAKELINK", linkPath, targetPath
391         raise NotImplementedError
392
393     def extendedRequest(self, extendedName, extendedData):
394         print "EXTENDEDREQUEST", extendedName, extendedData
395         # client 'df' command requires 'statvfs@openssh.com' extension
396         raise NotImplementedError
397     def realPath(self, path):
398         print "REALPATH", path
399         if path == ".":
400             return "/"
401         return path
402
403
404     def _get_parent(self, path):
405         # fire with (parentnode, childname)
406         path = [unicode(p) for p in path]
407         if not path:
408             raise NoParentError
409         childname = path[-1]
410         d = self._get_root(path)
411         def _got_root((root, path)):
412             if not path:
413                 raise NoParentError
414             return root.get_child_at_path(path[:-1])
415         d.addCallback(_got_root)
416         def _got_parent(parent):
417             return (parent, childname)
418         d.addCallback(_got_parent)
419         return d
420
421
422 # if you have an SFTPUser, and you want something that provides ISFTPServer,
423 # then you get SFTPHandler(user)
424 components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
425
426 from auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
427
428 class Dispatcher:
429     implements(portal.IRealm)
430     def __init__(self, client):
431         self.client = client
432
433     def requestAvatar(self, avatarID, mind, interface):
434         assert interface == IConchUser
435         rootnode = self.client.create_node_from_uri(avatarID.rootcap)
436         convergence = self.client.convergence
437         s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
438         def logout(): pass
439         return (interface, s, logout)
440
441 class SFTPServer(service.MultiService):
442     def __init__(self, client, accountfile, accounturl,
443                  sftp_portstr, pubkey_file, privkey_file):
444         service.MultiService.__init__(self)
445
446         r = Dispatcher(client)
447         p = portal.Portal(r)
448
449         if accountfile:
450             c = AccountFileChecker(self, accountfile)
451             p.registerChecker(c)
452         if accounturl:
453             c = AccountURLChecker(self, accounturl)
454             p.registerChecker(c)
455         if not accountfile and not accounturl:
456             # we could leave this anonymous, with just the /uri/CAP form
457             raise NeedRootcapLookupScheme("must provide some translation")
458
459         pubkey = keys.Key.fromFile(pubkey_file)
460         privkey = keys.Key.fromFile(privkey_file)
461         class SSHFactory(factory.SSHFactory):
462             publicKeys = {pubkey.sshType(): pubkey}
463             privateKeys = {privkey.sshType(): privkey}
464             def getPrimes(self):
465                 try:
466                     # if present, this enables diffie-hellman-group-exchange
467                     return primes.parseModuliFile("/etc/ssh/moduli")
468                 except IOError:
469                     return None
470
471         f = SSHFactory()
472         f.portal = p
473
474         s = strports.service(sftp_portstr, f)
475         s.setServiceParent(self)
476