]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - 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
index 5ade0e2084febc9c989608667fe55559771eb22d..b00bd9349ecfb4d3b15486f8637380ded7e3322b 100644 (file)
@@ -1,27 +1,26 @@
 
-import os
-import tempfile
+from types import NoneType
+
 from zope.interface import implements
 from twisted.application import service, strports
 from twisted.internet import defer
 from twisted.internet.interfaces import IConsumer
+from twisted.cred import portal
+from twisted.python import filepath
 from twisted.protocols import ftp
-from twisted.cred import error, portal, checkers, credentials
-from twisted.web.client import getPage
 
 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
      NoSuchChildError
-from allmydata.immutable.download import ConsumerAdapter
 from allmydata.immutable.upload import FileHandle
-from allmydata.util import base32
+from allmydata.util.fileutil import EncryptedTemporaryFile
+from allmydata.util.assertutil import precondition
 
 class ReadFile:
     implements(ftp.IReadFile)
     def __init__(self, node):
         self.node = node
     def send(self, consumer):
-        ad = ConsumerAdapter(consumer)
-        d = self.node.download(ad)
+        d = self.node.read(consumer)
         return d # when consumed
 
 class FileWriter:
@@ -32,7 +31,7 @@ class FileWriter:
             raise NotImplementedError("Non-streaming producer not supported.")
         # we write the data to a temporary file, since Tahoe can't do
         # streaming upload yet.
-        self.f = tempfile.TemporaryFile()
+        self.f = EncryptedTemporaryFile()
         return None
 
     def unregisterProducer(self):
@@ -63,6 +62,17 @@ class WriteFile:
 class NoParentError(Exception):
     pass
 
+# filepath.Permissions was added in Twisted-11.1.0, which we require. Twisted
+# <15.0.0 expected an int, and only does '&' on it. Twisted >=15.0.0 expects
+# a filepath.Permissions. This satisfies both.
+
+class IntishPermissions(filepath.Permissions):
+    def __init__(self, statModeInt):
+        self._tahoe_statModeInt = statModeInt
+        filepath.Permissions.__init__(self, statModeInt)
+    def __and__(self, other):
+        return self._tahoe_statModeInt & other
+
 class Handler:
     implements(ftp.IFTPShell)
     def __init__(self, client, rootnode, username, convergence):
@@ -88,7 +98,7 @@ class Handler:
         d = node.get(path[0])
         def _maybe_create(f):
             f.trap(NoSuchChildError)
-            return node.create_empty_directory(path[0])
+            return node.create_subdirectory(path[0])
         d.addErrback(_maybe_create)
         d.addCallback(self._get_or_create_directories, path[1:])
         return d
@@ -198,15 +208,24 @@ class Handler:
                 if isdir:
                     value = 0
                 else:
-                    value = childnode.get_size()
+                    value = childnode.get_size() or 0
             elif key == "directory":
                 value = isdir
             elif key == "permissions":
-                value = 0600
+                # Twisted-14.0.2 (and earlier) expected an int, and used it
+                # in a rendering function that did (mode & NUMBER).
+                # Twisted-15.0.0 expects a
+                # twisted.python.filepath.Permissions , and calls its
+                # .shorthand() method. This provides both both.
+                value = IntishPermissions(0600)
             elif key == "hardlinks":
                 value = 1
             elif key == "modified":
-                value = metadata.get("mtime", 0)
+                # follow sftpd convention (i.e. linkmotime in preference to mtime)
+                if "linkmotime" in metadata.get("tahoe", {}):
+                    value = metadata["tahoe"]["linkmotime"]
+                else:
+                    value = metadata.get("mtime", 0)
             elif key == "owner":
                 value = self.username
             elif key == "group":
@@ -270,89 +289,7 @@ class Handler:
         d.addCallback(_got_parent)
         return d
 
-
-class FTPAvatarID:
-    def __init__(self, username, rootcap):
-        self.username = username
-        self.rootcap = rootcap
-
-class AccountFileChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,
-                            credentials.IUsernameHashedPassword)
-    def __init__(self, client, accountfile):
-        self.client = client
-        self.passwords = {}
-        self.rootcaps = {}
-        for line in open(os.path.expanduser(accountfile), "r"):
-            line = line.strip()
-            if line.startswith("#") or not line:
-                continue
-            name, passwd, rootcap = line.split()
-            self.passwords[name] = passwd
-            self.rootcaps[name] = rootcap
-
-    def _cbPasswordMatch(self, matched, username):
-        if matched:
-            return FTPAvatarID(username, self.rootcaps[username])
-        raise error.UnauthorizedLogin
-
-    def requestAvatarId(self, credentials):
-        if credentials.username in self.passwords:
-            d = defer.maybeDeferred(credentials.checkPassword,
-                                    self.passwords[credentials.username])
-            d.addCallback(self._cbPasswordMatch, str(credentials.username))
-            return d
-        return defer.fail(error.UnauthorizedLogin())
-
-class AccountURLChecker:
-    implements(checkers.ICredentialsChecker)
-    credentialInterfaces = (credentials.IUsernamePassword,)
-
-    def __init__(self, client, auth_url):
-        self.client = client
-        self.auth_url = auth_url
-
-    def _cbPasswordMatch(self, rootcap, username):
-        return FTPAvatarID(username, rootcap)
-
-    def post_form(self, username, password):
-        sepbase = base32.b2a(os.urandom(4))
-        sep = "--" + sepbase
-        form = []
-        form.append(sep)
-        fields = {"action": "authenticate",
-                  "email": username,
-                  "passwd": password,
-                  }
-        for name, value in fields.iteritems():
-            form.append('Content-Disposition: form-data; name="%s"' % name)
-            form.append('')
-            assert isinstance(value, str)
-            form.append(value)
-            form.append(sep)
-        form[-1] += "--"
-        body = "\r\n".join(form) + "\r\n"
-        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
-                   }
-        return getPage(self.auth_url, method="POST",
-                       postdata=body, headers=headers,
-                       followRedirect=True, timeout=30)
-
-    def _parse_response(self, res):
-        rootcap = res.strip()
-        if rootcap == "0":
-            raise error.UnauthorizedLogin
-        return rootcap
-
-    def requestAvatarId(self, credentials):
-        # construct a POST to the login form. While this could theoretically
-        # be done with something like the stdlib 'email' package, I can't
-        # figure out how, so we just slam together a form manually.
-        d = self.post_form(credentials.username, credentials.password)
-        d.addCallback(self._parse_response)
-        d.addCallback(self._cbPasswordMatch, str(credentials.username))
-        return d
+from allmydata.frontends.auth import AccountURLChecker, AccountFileChecker, NeedRootcapLookupScheme
 
 
 class Dispatcher:
@@ -371,24 +308,22 @@ class Dispatcher:
 
 class FTPServer(service.MultiService):
     def __init__(self, client, accountfile, accounturl, ftp_portstr):
+        precondition(isinstance(accountfile, (unicode, NoneType)), accountfile)
         service.MultiService.__init__(self)
 
+        r = Dispatcher(client)
+        p = portal.Portal(r)
+
         if accountfile:
             c = AccountFileChecker(self, accountfile)
-        elif accounturl:
+            p.registerChecker(c)
+        if accounturl:
             c = AccountURLChecker(self, accounturl)
-        else:
+            p.registerChecker(c)
+        if not accountfile and not accounturl:
             # we could leave this anonymous, with just the /uri/CAP form
-            raise RuntimeError("must provide some translation")
-
-        # make sure we're using a patched Twisted that uses IWriteFile.close:
-        # see docs/ftp.txt for details.
-        assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
+            raise NeedRootcapLookupScheme("must provide some translation")
 
-        r = Dispatcher(client)
-        p = portal.Portal(r)
-        p.registerChecker(c)
         f = ftp.FTPFactory(p)
-
         s = strports.service(ftp_portstr, f)
         s.setServiceParent(self)