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