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.protocols import ftp
9 from twisted.cred import error, portal, checkers, credentials
10 from twisted.web.client import getPage
12 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
14 from allmydata.immutable.download import ConsumerAdapter
15 from allmydata.immutable.upload import FileHandle
16 from allmydata.util import base32
19 implements(ftp.IReadFile)
20 def __init__(self, node):
22 def send(self, consumer):
23 ad = ConsumerAdapter(consumer)
24 d = self.node.download(ad)
25 return d # when consumed
30 def registerProducer(self, producer, streaming):
32 raise NotImplementedError("Non-streaming producer not supported.")
33 # we write the data to a temporary file, since Tahoe can't do
34 # streaming upload yet.
35 self.f = tempfile.TemporaryFile()
38 def unregisterProducer(self):
39 # the upload actually happens in WriteFile.close()
42 def write(self, data):
46 implements(ftp.IWriteFile)
48 def __init__(self, parent, childname, convergence):
50 self.childname = childname
51 self.convergence = convergence
55 return defer.succeed(self.c)
58 u = FileHandle(self.c.f, self.convergence)
59 d = self.parent.add_file(self.childname, u)
63 class NoParentError(Exception):
67 implements(ftp.IFTPShell)
68 def __init__(self, client, rootnode, username, convergence):
71 self.username = username
72 self.convergence = convergence
74 def makeDirectory(self, path):
75 d = self._get_root(path)
76 d.addCallback(lambda (root,path):
77 self._get_or_create_directories(root, path))
80 def _get_or_create_directories(self, node, path):
81 if not IDirectoryNode.providedBy(node):
82 # unfortunately it is too late to provide the name of the
83 # blocking directory in the error message.
84 raise ftp.FileExistsError("cannot create directory because there "
85 "is a file in the way")
87 return defer.succeed(node)
90 f.trap(NoSuchChildError)
91 return node.create_empty_directory(path[0])
92 d.addErrback(_maybe_create)
93 d.addCallback(self._get_or_create_directories, path[1:])
96 def _get_parent(self, path):
97 # fire with (parentnode, childname)
98 path = [unicode(p) for p in path]
102 d = self._get_root(path)
103 def _got_root((root, path)):
106 return root.get_child_at_path(path[:-1])
107 d.addCallback(_got_root)
108 def _got_parent(parent):
109 return (parent, childname)
110 d.addCallback(_got_parent)
113 def _remove_thing(self, path, must_be_directory=False, must_be_file=False):
114 d = defer.maybeDeferred(self._get_parent, path)
115 def _convert_error(f):
116 f.trap(NoParentError)
117 raise ftp.PermissionDeniedError("cannot delete root directory")
118 d.addErrback(_convert_error)
119 def _got_parent( (parent, childname) ):
120 d = parent.get(childname)
121 def _got_child(child):
122 if must_be_directory and not IDirectoryNode.providedBy(child):
123 raise ftp.IsNotADirectoryError("rmdir called on a file")
124 if must_be_file and IDirectoryNode.providedBy(child):
125 raise ftp.IsADirectoryError("rmfile called on a directory")
126 return parent.delete(childname)
127 d.addCallback(_got_child)
128 d.addErrback(self._convert_error)
130 d.addCallback(_got_parent)
133 def removeDirectory(self, path):
134 return self._remove_thing(path, must_be_directory=True)
136 def removeFile(self, path):
137 return self._remove_thing(path, must_be_file=True)
139 def rename(self, fromPath, toPath):
140 # the target directory must already exist
141 d = self._get_parent(fromPath)
142 def _got_from_parent( (fromparent, childname) ):
143 d = self._get_parent(toPath)
144 d.addCallback(lambda (toparent, tochildname):
145 fromparent.move_child_to(childname,
146 toparent, tochildname,
149 d.addCallback(_got_from_parent)
150 d.addErrback(self._convert_error)
153 def access(self, path):
154 # we allow access to everything that exists. We are required to raise
155 # an error for paths that don't exist: FTP clients (at least ncftp)
156 # uses this to decide whether to mkdir or not.
157 d = self._get_node_and_metadata_for_path(path)
158 d.addErrback(self._convert_error)
159 d.addCallback(lambda res: None)
162 def _convert_error(self, f):
163 if f.check(NoSuchChildError):
164 childname = f.value.args[0].encode("utf-8")
165 msg = "'%s' doesn't exist" % childname
166 raise ftp.FileNotFoundError(msg)
167 if f.check(ExistingChildError):
168 msg = f.value.args[0].encode("utf-8")
169 raise ftp.FileExistsError(msg)
172 def _get_root(self, path):
173 # return (root, remaining_path)
174 path = [unicode(p) for p in path]
175 if path and path[0] == "uri":
176 d = defer.maybeDeferred(self.client.create_node_from_uri,
178 d.addCallback(lambda root: (root, path[2:]))
180 d = defer.succeed((self.root,path))
183 def _get_node_and_metadata_for_path(self, path):
184 d = self._get_root(path)
185 def _got_root((root,path)):
187 return root.get_child_and_metadata_at_path(path)
190 d.addCallback(_got_root)
193 def _populate_row(self, keys, (childnode, metadata)):
195 isdir = bool(IDirectoryNode.providedBy(childnode))
201 value = childnode.get_size()
202 elif key == "directory":
204 elif key == "permissions":
206 elif key == "hardlinks":
208 elif key == "modified":
209 value = metadata.get("mtime", 0)
211 value = self.username
213 value = self.username
219 def stat(self, path, keys=()):
220 # for files only, I think
221 d = self._get_node_and_metadata_for_path(path)
222 def _render((node,metadata)):
223 assert not IDirectoryNode.providedBy(node)
224 return self._populate_row(keys, (node,metadata))
225 d.addCallback(_render)
226 d.addErrback(self._convert_error)
229 def list(self, path, keys=()):
230 # the interface claims that path is a list of unicodes, but in
232 d = self._get_node_and_metadata_for_path(path)
233 def _list((node, metadata)):
234 if IDirectoryNode.providedBy(node):
236 return { path[-1]: (node, metadata) } # need last-edge metadata
238 def _render(children):
240 for (name, childnode) in children.iteritems():
241 # the interface claims that the result should have a unicode
242 # object as the name, but it fails unless you give it a
244 results.append( (name.encode("utf-8"),
245 self._populate_row(keys, childnode) ) )
247 d.addCallback(_render)
248 d.addErrback(self._convert_error)
251 def openForReading(self, path):
252 d = self._get_node_and_metadata_for_path(path)
253 d.addCallback(lambda (node,metadata): ReadFile(node))
254 d.addErrback(self._convert_error)
257 def openForWriting(self, path):
258 path = [unicode(p) for p in path]
260 raise ftp.PermissionDeniedError("cannot STOR to root directory")
262 d = self._get_root(path)
263 def _got_root((root, path)):
265 raise ftp.PermissionDeniedError("cannot STOR to root directory")
266 return root.get_child_at_path(path[:-1])
267 d.addCallback(_got_root)
268 def _got_parent(parent):
269 return WriteFile(parent, childname, self.convergence)
270 d.addCallback(_got_parent)
275 def __init__(self, username, rootcap):
276 self.username = username
277 self.rootcap = rootcap
279 class AccountFileChecker:
280 implements(checkers.ICredentialsChecker)
281 credentialInterfaces = (credentials.IUsernamePassword,
282 credentials.IUsernameHashedPassword)
283 def __init__(self, client, accountfile):
287 for line in open(os.path.expanduser(accountfile), "r"):
289 if line.startswith("#") or not line:
291 name, passwd, rootcap = line.split()
292 self.passwords[name] = passwd
293 self.rootcaps[name] = rootcap
295 def _cbPasswordMatch(self, matched, username):
297 return FTPAvatarID(username, self.rootcaps[username])
298 raise error.UnauthorizedLogin
300 def requestAvatarId(self, credentials):
301 if credentials.username in self.passwords:
302 d = defer.maybeDeferred(credentials.checkPassword,
303 self.passwords[credentials.username])
304 d.addCallback(self._cbPasswordMatch, str(credentials.username))
306 return defer.fail(error.UnauthorizedLogin())
308 class AccountURLChecker:
309 implements(checkers.ICredentialsChecker)
310 credentialInterfaces = (credentials.IUsernamePassword,)
312 def __init__(self, client, auth_url):
314 self.auth_url = auth_url
316 def _cbPasswordMatch(self, rootcap, username):
317 return FTPAvatarID(username, rootcap)
319 def post_form(self, username, password):
320 sepbase = base32.b2a(os.urandom(4))
324 fields = {"action": "authenticate",
328 for name, value in fields.iteritems():
329 form.append('Content-Disposition: form-data; name="%s"' % name)
331 assert isinstance(value, str)
335 body = "\r\n".join(form) + "\r\n"
336 headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
338 return getPage(self.auth_url, method="POST",
339 postdata=body, headers=headers,
340 followRedirect=True, timeout=30)
342 def _parse_response(self, res):
343 rootcap = res.strip()
345 raise error.UnauthorizedLogin
348 def requestAvatarId(self, credentials):
349 # construct a POST to the login form. While this could theoretically
350 # be done with something like the stdlib 'email' package, I can't
351 # figure out how, so we just slam together a form manually.
352 d = self.post_form(credentials.username, credentials.password)
353 d.addCallback(self._parse_response)
354 d.addCallback(self._cbPasswordMatch, str(credentials.username))
359 implements(portal.IRealm)
360 def __init__(self, client):
363 def requestAvatar(self, avatarID, mind, interface):
364 assert interface == ftp.IFTPShell
365 rootnode = self.client.create_node_from_uri(avatarID.rootcap)
366 convergence = self.client.convergence
367 s = Handler(self.client, rootnode, avatarID.username, convergence)
369 return (interface, s, None)
372 class FTPServer(service.MultiService):
373 def __init__(self, client, accountfile, accounturl, ftp_portstr):
374 service.MultiService.__init__(self)
377 c = AccountFileChecker(self, accountfile)
379 c = AccountURLChecker(self, accounturl)
381 # we could leave this anonymous, with just the /uri/CAP form
382 raise RuntimeError("must provide some translation")
384 # make sure we're using a patched Twisted that uses IWriteFile.close:
385 # see docs/ftp.txt for details.
386 assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
388 r = Dispatcher(client)
391 f = ftp.FTPFactory(p)
393 s = strports.service(ftp_portstr, f)
394 s.setServiceParent(self)