]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/sftpd.py
ftpd/sftpd: stop using RuntimeError, for #639
[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         assert pathstring[0] == "/"
318         pathstring = pathstring.strip("/")
319         if pathstring == "":
320             path = []
321         else:
322             path = pathstring.split("/")
323         print "CONVERT", pathstring, path
324         path = [unicode(p) for p in path]
325         return path
326
327     def _get_node_and_metadata_for_path(self, path):
328         d = self._get_root(path)
329         def _got_root((root,path)):
330             print "ROOT", root
331             print "PATH", path
332             if path:
333                 return root.get_child_and_metadata_at_path(path)
334             else:
335                 return (root,{})
336         d.addCallback(_got_root)
337         return d
338
339     def _get_root(self, path):
340         # return (root, remaining_path)
341         path = [unicode(p) for p in path]
342         if path and path[0] == "uri":
343             d = defer.maybeDeferred(self.client.create_node_from_uri,
344                                     str(path[1]))
345             d.addCallback(lambda root: (root, path[2:]))
346         else:
347             d = defer.succeed((self.root,path))
348         return d
349
350     def _populate_attrs(self, childnode, metadata):
351         attrs = {}
352         attrs["uid"] = 1000
353         attrs["gid"] = 1000
354         attrs["atime"] = 0
355         attrs["mtime"] = int(metadata.get("mtime", 0))
356         isdir = bool(IDirectoryNode.providedBy(childnode))
357         if isdir:
358             attrs["size"] = 1
359             # the permissions must have the extra bits (040000 or 0100000),
360             # otherwise the client will not call openDirectory
361             attrs["permissions"] = 040700 # S_IFDIR
362         else:
363             attrs["size"] = childnode.get_size()
364             attrs["permissions"] = 0100600 # S_IFREG
365         return attrs
366
367     def _convert_error(self, f):
368         if f.check(NoSuchChildError):
369             childname = f.value.args[0].encode("utf-8")
370             raise SFTPError(FX_NO_SUCH_FILE, childname)
371         if f.check(ExistingChildError):
372             msg = f.value.args[0].encode("utf-8")
373             raise SFTPError(FX_FILE_ALREADY_EXISTS, msg)
374         if f.check(PermissionError):
375             raise SFTPError(FX_PERMISSION_DENIED, str(f.value))
376         if f.check(NotImplementedError):
377             raise SFTPError(FX_OP_UNSUPPORTED, str(f.value))
378         return f
379
380
381     def setAttrs(self, path, attrs):
382         print "SETATTRS", path, attrs
383         # ignored
384         return None
385
386     def readLink(self, path):
387         print "READLINK", path
388         raise NotImplementedError
389
390     def makeLink(self, linkPath, targetPath):
391         print "MAKELINK", linkPath, targetPath
392         raise NotImplementedError
393
394     def extendedRequest(self, extendedName, extendedData):
395         print "EXTENDEDREQUEST", extendedName, extendedData
396         # client 'df' command requires 'statvfs@openssh.com' extension
397         raise NotImplementedError
398     def realPath(self, path):
399         print "REALPATH", path
400         if path == ".":
401             return "/"
402         return path
403
404
405     def _get_parent(self, path):
406         # fire with (parentnode, childname)
407         path = [unicode(p) for p in path]
408         if not path:
409             raise NoParentError
410         childname = path[-1]
411         d = self._get_root(path)
412         def _got_root((root, path)):
413             if not path:
414                 raise NoParentError
415             return root.get_child_at_path(path[:-1])
416         d.addCallback(_got_root)
417         def _got_parent(parent):
418             return (parent, childname)
419         d.addCallback(_got_parent)
420         return d
421
422
423 # if you have an SFTPUser, and you want something that provides ISFTPServer,
424 # then you get SFTPHandler(user)
425 components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
426
427 from auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
428
429 class Dispatcher:
430     implements(portal.IRealm)
431     def __init__(self, client):
432         self.client = client
433
434     def requestAvatar(self, avatarID, mind, interface):
435         assert interface == IConchUser
436         rootnode = self.client.create_node_from_uri(avatarID.rootcap)
437         convergence = self.client.convergence
438         s = SFTPUser(self.client, rootnode, avatarID.username, convergence)
439         def logout(): pass
440         return (interface, s, logout)
441
442 class SFTPServer(service.MultiService):
443     def __init__(self, client, accountfile, accounturl,
444                  sftp_portstr, pubkey_file, privkey_file):
445         service.MultiService.__init__(self)
446
447         r = Dispatcher(client)
448         p = portal.Portal(r)
449
450         if accountfile:
451             c = AccountFileChecker(self, accountfile)
452             p.registerChecker(c)
453         if accounturl:
454             c = AccountURLChecker(self, accounturl)
455             p.registerChecker(c)
456         if not accountfile and not accounturl:
457             # we could leave this anonymous, with just the /uri/CAP form
458             raise NeedRootcapLookupScheme("must provide some translation")
459
460         pubkey = keys.Key.fromFile(pubkey_file)
461         privkey = keys.Key.fromFile(privkey_file)
462         class SSHFactory(factory.SSHFactory):
463             publicKeys = {pubkey.sshType(): pubkey}
464             privateKeys = {privkey.sshType(): privkey}
465             def getPrimes(self):
466                 try:
467                     # if present, this enables diffie-hellman-group-exchange
468                     return primes.parseModuliFile("/etc/ssh/moduli")
469                 except IOError:
470                     return None
471
472         f = SSHFactory()
473         f.portal = p
474
475         s = strports.service(sftp_portstr, f)
476         s.setServiceParent(self)
477