]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/frontends/ftpd.py
bump Twisted dep to 11.1.0, thus simplify IntishPermissions
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / frontends / ftpd.py
1
2 from types import NoneType
3
4 from zope.interface import implements
5 from twisted.application import service, strports
6 from twisted.internet import defer
7 from twisted.internet.interfaces import IConsumer
8 from twisted.cred import portal
9 from twisted.python import filepath
10 from twisted.protocols import ftp
11
12 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
13      NoSuchChildError
14 from allmydata.immutable.upload import FileHandle
15 from allmydata.util.fileutil import EncryptedTemporaryFile
16 from allmydata.util.assertutil import precondition
17
18 class ReadFile:
19     implements(ftp.IReadFile)
20     def __init__(self, node):
21         self.node = node
22     def send(self, consumer):
23         d = self.node.read(consumer)
24         return d # when consumed
25
26 class FileWriter:
27     implements(IConsumer)
28
29     def registerProducer(self, producer, streaming):
30         if not streaming:
31             raise NotImplementedError("Non-streaming producer not supported.")
32         # we write the data to a temporary file, since Tahoe can't do
33         # streaming upload yet.
34         self.f = EncryptedTemporaryFile()
35         return None
36
37     def unregisterProducer(self):
38         # the upload actually happens in WriteFile.close()
39         pass
40
41     def write(self, data):
42         self.f.write(data)
43
44 class WriteFile:
45     implements(ftp.IWriteFile)
46
47     def __init__(self, parent, childname, convergence):
48         self.parent = parent
49         self.childname = childname
50         self.convergence = convergence
51
52     def receive(self):
53         self.c = FileWriter()
54         return defer.succeed(self.c)
55
56     def close(self):
57         u = FileHandle(self.c.f, self.convergence)
58         d = self.parent.add_file(self.childname, u)
59         return d
60
61
62 class NoParentError(Exception):
63     pass
64
65 # filepath.Permissions was added in Twisted-11.1.0, which we require. Twisted
66 # <15.0.0 expected an int, and only does '&' on it. Twisted >=15.0.0 expects
67 # a filepath.Permissions. This satisfies both.
68
69 class IntishPermissions(filepath.Permissions):
70     def __init__(self, statModeInt):
71         self._tahoe_statModeInt = statModeInt
72         filepath.Permissions.__init__(self, statModeInt)
73     def __and__(self, other):
74         return self._tahoe_statModeInt & other
75
76 class Handler:
77     implements(ftp.IFTPShell)
78     def __init__(self, client, rootnode, username, convergence):
79         self.client = client
80         self.root = rootnode
81         self.username = username
82         self.convergence = convergence
83
84     def makeDirectory(self, path):
85         d = self._get_root(path)
86         d.addCallback(lambda (root,path):
87                       self._get_or_create_directories(root, path))
88         return d
89
90     def _get_or_create_directories(self, node, path):
91         if not IDirectoryNode.providedBy(node):
92             # unfortunately it is too late to provide the name of the
93             # blocking directory in the error message.
94             raise ftp.FileExistsError("cannot create directory because there "
95                                       "is a file in the way")
96         if not path:
97             return defer.succeed(node)
98         d = node.get(path[0])
99         def _maybe_create(f):
100             f.trap(NoSuchChildError)
101             return node.create_subdirectory(path[0])
102         d.addErrback(_maybe_create)
103         d.addCallback(self._get_or_create_directories, path[1:])
104         return d
105
106     def _get_parent(self, path):
107         # fire with (parentnode, childname)
108         path = [unicode(p) for p in path]
109         if not path:
110             raise NoParentError
111         childname = path[-1]
112         d = self._get_root(path)
113         def _got_root((root, path)):
114             if not path:
115                 raise NoParentError
116             return root.get_child_at_path(path[:-1])
117         d.addCallback(_got_root)
118         def _got_parent(parent):
119             return (parent, childname)
120         d.addCallback(_got_parent)
121         return d
122
123     def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
124         d = defer.maybeDeferred(self._get_parent, path)
125         def _convert_error(f):
126             f.trap(NoParentError)
127             raise ftp.PermissionDeniedError("cannot delete root directory")
128         d.addErrback(_convert_error)
129         def _got_parent( (parent, childname) ):
130             d = parent.get(childname)
131             def _got_child(child):
132                 if must_be_directory and not IDirectoryNode.providedBy(child):
133                     raise ftp.IsNotADirectoryError("rmdir called on a file")
134                 if must_be_file and IDirectoryNode.providedBy(child):
135                     raise ftp.IsADirectoryError("rmfile called on a directory")
136                 return parent.delete(childname)
137             d.addCallback(_got_child)
138             d.addErrback(self._convert_error)
139             return d
140         d.addCallback(_got_parent)
141         return d
142
143     def removeDirectory(self, path):
144         return self._remove_thing(path, must_be_directory=True)
145
146     def removeFile(self, path):
147         return self._remove_thing(path, must_be_file=True)
148
149     def rename(self, fromPath, toPath):
150         # the target directory must already exist
151         d = self._get_parent(fromPath)
152         def _got_from_parent( (fromparent, childname) ):
153             d = self._get_parent(toPath)
154             d.addCallback(lambda (toparent, tochildname):
155                           fromparent.move_child_to(childname,
156                                                    toparent, tochildname,
157                                                    overwrite=False))
158             return d
159         d.addCallback(_got_from_parent)
160         d.addErrback(self._convert_error)
161         return d
162
163     def access(self, path):
164         # we allow access to everything that exists. We are required to raise
165         # an error for paths that don't exist: FTP clients (at least ncftp)
166         # uses this to decide whether to mkdir or not.
167         d = self._get_node_and_metadata_for_path(path)
168         d.addErrback(self._convert_error)
169         d.addCallback(lambda res: None)
170         return d
171
172     def _convert_error(self, f):
173         if f.check(NoSuchChildError):
174             childname = f.value.args[0].encode("utf-8")
175             msg = "'%s' doesn't exist" % childname
176             raise ftp.FileNotFoundError(msg)
177         if f.check(ExistingChildError):
178             msg = f.value.args[0].encode("utf-8")
179             raise ftp.FileExistsError(msg)
180         return f
181
182     def _get_root(self, path):
183         # return (root, remaining_path)
184         path = [unicode(p) for p in path]
185         if path and path[0] == "uri":
186             d = defer.maybeDeferred(self.client.create_node_from_uri,
187                                     str(path[1]))
188             d.addCallback(lambda root: (root, path[2:]))
189         else:
190             d = defer.succeed((self.root,path))
191         return d
192
193     def _get_node_and_metadata_for_path(self, path):
194         d = self._get_root(path)
195         def _got_root((root,path)):
196             if path:
197                 return root.get_child_and_metadata_at_path(path)
198             else:
199                 return (root,{})
200         d.addCallback(_got_root)
201         return d
202
203     def _populate_row(self, keys, (childnode, metadata)):
204         values = []
205         isdir = bool(IDirectoryNode.providedBy(childnode))
206         for key in keys:
207             if key == "size":
208                 if isdir:
209                     value = 0
210                 else:
211                     value = childnode.get_size() or 0
212             elif key == "directory":
213                 value = isdir
214             elif key == "permissions":
215                 # Twisted-14.0.2 (and earlier) expected an int, and used it
216                 # in a rendering function that did (mode & NUMBER).
217                 # Twisted-15.0.0 expects a
218                 # twisted.python.filepath.Permissions , and calls its
219                 # .shorthand() method. This provides both both.
220                 value = IntishPermissions(0600)
221             elif key == "hardlinks":
222                 value = 1
223             elif key == "modified":
224                 # follow sftpd convention (i.e. linkmotime in preference to mtime)
225                 if "linkmotime" in metadata.get("tahoe", {}):
226                     value = metadata["tahoe"]["linkmotime"]
227                 else:
228                     value = metadata.get("mtime", 0)
229             elif key == "owner":
230                 value = self.username
231             elif key == "group":
232                 value = self.username
233             else:
234                 value = "??"
235             values.append(value)
236         return values
237
238     def stat(self, path, keys=()):
239         # for files only, I think
240         d = self._get_node_and_metadata_for_path(path)
241         def _render((node,metadata)):
242             assert not IDirectoryNode.providedBy(node)
243             return self._populate_row(keys, (node,metadata))
244         d.addCallback(_render)
245         d.addErrback(self._convert_error)
246         return d
247
248     def list(self, path, keys=()):
249         # the interface claims that path is a list of unicodes, but in
250         # practice it is not
251         d = self._get_node_and_metadata_for_path(path)
252         def _list((node, metadata)):
253             if IDirectoryNode.providedBy(node):
254                 return node.list()
255             return { path[-1]: (node, metadata) } # need last-edge metadata
256         d.addCallback(_list)
257         def _render(children):
258             results = []
259             for (name, childnode) in children.iteritems():
260                 # the interface claims that the result should have a unicode
261                 # object as the name, but it fails unless you give it a
262                 # bytestring
263                 results.append( (name.encode("utf-8"),
264                                  self._populate_row(keys, childnode) ) )
265             return results
266         d.addCallback(_render)
267         d.addErrback(self._convert_error)
268         return d
269
270     def openForReading(self, path):
271         d = self._get_node_and_metadata_for_path(path)
272         d.addCallback(lambda (node,metadata): ReadFile(node))
273         d.addErrback(self._convert_error)
274         return d
275
276     def openForWriting(self, path):
277         path = [unicode(p) for p in path]
278         if not path:
279             raise ftp.PermissionDeniedError("cannot STOR to root directory")
280         childname = path[-1]
281         d = self._get_root(path)
282         def _got_root((root, path)):
283             if not path:
284                 raise ftp.PermissionDeniedError("cannot STOR to root directory")
285             return root.get_child_at_path(path[:-1])
286         d.addCallback(_got_root)
287         def _got_parent(parent):
288             return WriteFile(parent, childname, self.convergence)
289         d.addCallback(_got_parent)
290         return d
291
292 from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
293
294
295 class Dispatcher:
296     implements(portal.IRealm)
297     def __init__(self, client):
298         self.client = client
299
300     def requestAvatar(self, avatarID, mind, interface):
301         assert interface == ftp.IFTPShell
302         rootnode = self.client.create_node_from_uri(avatarID.rootcap)
303         convergence = self.client.convergence
304         s = Handler(self.client, rootnode, avatarID.username, convergence)
305         def logout(): pass
306         return (interface, s, None)
307
308
309 class FTPServer(service.MultiService):
310     def __init__(self, client, accountfile, accounturl, ftp_portstr):
311         precondition(isinstance(accountfile, (unicode, NoneType)), accountfile)
312         service.MultiService.__init__(self)
313
314         r = Dispatcher(client)
315         p = portal.Portal(r)
316
317         if accountfile:
318             c = AccountFileChecker(self, accountfile)
319             p.registerChecker(c)
320         if accounturl:
321             c = AccountURLChecker(self, accounturl)
322             p.registerChecker(c)
323         if not accountfile and not accounturl:
324             # we could leave this anonymous, with just the /uri/CAP form
325             raise NeedRootcapLookupScheme("must provide some translation")
326
327         f = ftp.FTPFactory(p)
328         s = strports.service(ftp_portstr, f)
329         s.setServiceParent(self)