-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:
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):
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):
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
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":
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:
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)