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