]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/commitdiff
SFTP/FTP: merge user/account code, merge docs
authorBrian Warner <warner@allmydata.com>
Thu, 6 Nov 2008 02:25:58 +0000 (19:25 -0700)
committerBrian Warner <warner@allmydata.com>
Thu, 6 Nov 2008 02:25:58 +0000 (19:25 -0700)
docs/frontends/FTP-and-SFTP.txt [new file with mode: 0644]
docs/frontends/ftp.txt [deleted file]
docs/frontends/sftp.txt [deleted file]
src/allmydata/frontends/auth.py [new file with mode: 0644]
src/allmydata/frontends/ftpd.py
src/allmydata/frontends/sftpd.py

diff --git a/docs/frontends/FTP-and-SFTP.txt b/docs/frontends/FTP-and-SFTP.txt
new file mode 100644 (file)
index 0000000..0c44cde
--- /dev/null
@@ -0,0 +1,213 @@
+= Tahoe FTP/SFTP Frontend =
+
+== FTP/SFTP Background ==
+
+FTP is the venerable internet file-transfer protocol, first developed in
+1971. The FTP server usually listens on port 21. A separate connection is
+used for the actual data transfers, either in the same direction as the
+initial client-to-server connection (for PORT mode), or in the reverse
+direction (for PASV) mode. Connections are unencrypted, so passwords, file
+names, and file contents are visible to eavesdroppers.
+
+SFTP is the modern replacement, developed as part of the SSH "secure shell"
+protocol, and runs as a subchannel of the regular SSH connection. The SSH
+server usually listens on port 22. All connections are encrypted.
+
+Both FTP and SFTP were developed assuming a UNIX-like server, with accounts
+and passwords, octal file modes (user/group/other, read/write/execute), and
+ctime/mtime timestamps.
+
+== Tahoe Support ==
+
+All Tahoe client nodes can run a frontend FTP server, allowing regular FTP
+clients (like /usr/bin/ftp, ncftp, and countless others) to access the
+virtual filesystem. They can also run an SFTP server, so SFTP clients (like
+/usr/bin/sftp, the sshfs FUSE plugin, and others) can too. These frontends
+sit at the same level as the webapi interface.
+
+Since Tahoe does not use user accounts or passwords, the FTP/SFTP servers
+must be configured with a way to first authenticate a user (confirm that a
+prospective client has a legitimate claim to whatever authorities we might
+grant a particular user), and second to decide what root directory cap should
+be granted to the authenticated username. FTP uses a username and password
+for this purpose. SFTP can either use a username and password, or a username
+and an RSA or DSA public key (SSH servers are frequently configured to
+require public key logins and reject passwords, to remove the threat of
+password-guessing attacks, at the expense of requiring users to carry their
+private keys around with them).
+
+Tahoe provides two mechanisms to perform this user-to-rootcap mapping. The
+first is a simple flat file with one account per line. The second is an
+HTTP-based login mechanism, backed by simple PHP script and a database. The
+latter form is used by allmydata.com to provide secure access to customer
+rootcaps.
+
+== Creating an Account File ==
+
+To use the first form, create a file (probably in
+BASEDIR/private/ftp.accounts) in which each non-comment/non-blank line is a
+space-separated line of (USERNAME, PASSWORD/PUBKEY, ROOTCAP), like so:
+
+ % cat BASEDIR/private/ftp.accounts
+ # This is a password line, (username, password, rootcap)
+ alice password URI:DIR2:ioej8xmzrwilg772gzj4fhdg7a:wtiizszzz2rgmczv4wl6bqvbv33ag4kvbr6prz3u6w3geixa6m6a
+ bob sekrit URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
+
+ # and this is a public key line (username, pubkey, rootcap)
+ carol ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAv2xHRVBoXnwxHLzthRD1wOWtyZ08b8n9cMZfJ58CBdBwAYP2NVNXc0XjRvswm5hnnAO+jyWPVNpXJjm9XllzYhODSNtSN+TXuJlUjhzA/T+ZwdgsgSAeHuuMQBoWt4Qc9HV6rHCdAeMhcnyqm6Q0sRAsfA/wfwiIgbvE7+cWpFa2anB6WeAnvK8+dMN0nvnkPE7GNyf/WFR1Ffuh9ifKdRB6yDNp17bQAqA3OWSFjch6fGPhp94y4g2jmTHlEUTyVsilgGqvGOutOVYnmOMnFijugU1Vu33G39GGzXWla6+fXwTk/oiVPiCYD7A7WFKes3nqMg8iVN6a6sxujrhnHQ== warner@fluxx URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
+
+[TODO: the PUBKEY form is not yet supported]
+
+Note that if the second word of the line is "ssh-rsa" or "ssh-dss", the rest
+of the line is parsed differently, so users cannot have a password equal to
+either of these strings.
+
+Then add an 'accounts.file' directive to your tahoe.cfg file, as described
+in the next sections.
+
+== Configuring FTP Access ==
+
+To enable the FTP server with an accounts file, add the following lines to
+the BASEDIR/tahoe.cfg file:
+
+ [ftpd]
+ enabled = true
+ port = 8021
+ accounts.file = private/ftp.accounts
+
+The FTP server will listen on the given port number. The "accounts.file"
+pathname will be interpreted relative to the node's BASEDIR.
+
+To enable the FTP server with an account server instead, provide the URL of
+that server in an "accounts.url" directive:
+
+ [ftpd]
+ enabled = true
+ port = 8021
+ accounts.url = https://example.com/login
+
+You can provide both accounts.file and accounts.url, although it probably
+isn't very useful except for testing.
+
+== Configuring SFTP Access ==
+
+The Tahoe SFTP server requires a host keypair, just like the regular SSH
+server. It is important to give each server a distinct keypair, to prevent
+one server from masquerading as different one. The first time a client
+program talks to a given server, it will store the host key it receives, and
+will complain if a subsequent connection uses a different key. This reduces
+the opportunity for man-in-the-middle attacks to just the first connection.
+
+You will use directives in the tahoe.cfg file to tell the SFTP code where to
+find these keys. To create one, use the ssh-keygen tool (which comes with the
+normal openssl client distribution):
+
+% cd BASEDIR
+% ssh-keygen -f private/ssh_host_rsa_key
+
+Then, to enable the SFTP server with an accounts file, add the following
+lines to the BASEDIR/tahoe.cfg file:
+
+ [sftpd]
+ enabled = true
+ port = 8022
+ host_pubkey_file = private/ssh_host_rsa_key.pub
+ host_privkey_file = private/ssh_host_rsa_key
+ accounts.file = private/ftp.accounts
+
+The SFTP server will listen on the given port number. The "accounts.file"
+pathname will be interpreted relative to the node's BASEDIR.
+
+Or, to use an account server instead, do this:
+
+ [sftpd]
+ enabled = true
+ port = 8022
+ host_pubkey_file = private/ssh_host_rsa_key.pub
+ host_privkey_file = private/ssh_host_rsa_key
+ accounts.url = https://example.com/login
+
+You can provide both accounts.file and accounts.url, although it probably
+isn't very useful except for testing.
+
+
+== Dependencies ==
+
+The Tahoe SFTP server requires the Twisted "Conch" component (a "conch" is a
+twisted shell, get it?). Many Linux distributions package the Conch code
+separately: debian puts it in the "python-twisted-conch" package. Conch
+requires the "pycrypto" package, which is a Python+C implementation of many
+cryptographic functions (the debian package is named "python-crypto").
+
+Note that "pycrypto" is different than the "pycryptopp" package that Tahoe
+uses (which is a Python wrapper around the C++ -based Crypto++ library, a
+library that is frequently installed as /usr/lib/libcryptopp.a, to avoid
+problems with non-alphanumerics in filenames).
+
+The FTP server requires code in Twisted that enables asynchronous closing of
+file-upload operations. This code was not in the Twisted-8.1.0 release, and
+has not been committed to SVN trunk as of r24943. So it may be necessary to
+apply the following patch. The Tahoe node refuse to start the FTP server if
+it detects that this patch has not been applied.
+
+Index: twisted/protocols/ftp.py
+===================================================================
+--- twisted/protocols/ftp.py   (revision 24956)
++++ twisted/protocols/ftp.py   (working copy)
+@@ -1049,7 +1049,6 @@
+                 cons = ASCIIConsumerWrapper(cons)
+             d = self.dtpInstance.registerConsumer(cons)
+-            d.addCallbacks(cbSent, ebSent)
+             # Tell them what to doooo
+             if self.dtpInstance.isConnected:
+@@ -1062,6 +1061,8 @@
+         def cbOpened(file):
+             d = file.receive()
+             d.addCallback(cbConsumer)
++            d.addCallback(lambda ignored: file.close())
++            d.addCallbacks(cbSent, ebSent)
+             return d
+         def ebOpened(err):
+@@ -1434,7 +1435,14 @@
+         @rtype: C{Deferred} of C{IConsumer}
+         """
++    def close():
++        """
++        Perform any post-write work that needs to be done. This method may
++        only be invoked once on each provider, and will always be invoked
++        after receive().
++        @rtype: C{Deferred} of anything: the value is ignored
++        """
+ def _getgroups(uid):
+     """Return the primary and supplementary groups for the given UID.
+@@ -1795,6 +1803,8 @@
+         # FileConsumer will close the file object
+         return defer.succeed(FileConsumer(self.fObj))
++    def close(self):
++        return defer.succeed(None)
+ class FTPRealm:
+Index: twisted/vfs/adapters/ftp.py
+===================================================================
+--- twisted/vfs/adapters/ftp.py        (revision 24956)
++++ twisted/vfs/adapters/ftp.py        (working copy)
+@@ -295,6 +295,11 @@
+         """
+         return defer.succeed(IConsumer(self.node))
++    def close(self):
++        """
++        Perform post-write actions.
++        """
++        return defer.succeed(None)
+ class _FileToConsumerAdapter(object):
diff --git a/docs/frontends/ftp.txt b/docs/frontends/ftp.txt
deleted file mode 100644 (file)
index 50774c6..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-= Tahoe FTP Frontend =
-
-All Tahoe client nodes can run a frontend FTP server, allowing regular FTP
-clients to access the virtual filesystem.
-
-Since Tahoe does not use user accounts or passwords, the FTP server must be
-configured with a way to translate USER+PASS into a root directory cap. Two
-mechanisms are provided. The first is a simple flat file with one account per
-line. The second is an HTTP-based login mechanism, backed by simple PHP
-script and a database. The latter form is used by allmydata.com to provide
-secure access to customer rootcaps.
-
-== Configuring an Account File ==
-
-To configure the first form, create a file (probably in
-BASEDIR/private/ftp.accounts) in which each non-comment/non-blank line is a
-space-separated line of (USERNAME, PASSWORD, ROOTCAP), like so:
-
- % cat BASEDIR/private/ftp.accounts
- # This is a password file, (username, password, rootcap)
- alice password URI:DIR2:ioej8xmzrwilg772gzj4fhdg7a:wtiizszzz2rgmczv4wl6bqvbv33ag4kvbr6prz3u6w3geixa6m6a
- bob sekrit URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
-
-Then add the following lines to the BASEDIR/tahoe.cfg file:
-
- [ftpd]
- enabled = true
- ftp.port = 8021
- ftp.accounts.file = private/ftp.accounts
-
-The FTP server will listen on the given port number. The ftp.accounts.file
-pathname will be interpreted relative to the node's BASEDIR.
-
-== Configuring an Account Server ==
-
-Determine the URL of the account server, say https://example.com/login . Then
-add the following lines to BASEDIR/tahoe.cfg:
-
- [ftpd]
- enabled = true
- ftp.port = 8021
- ftp.accounts.url = https://example.com/login
-
-== Dependencies ==
-
-The FTP server requires code in Twisted that enables asynchronous closing of
-file-upload operations. This code was not in the Twisted-8.1.0 release, and
-has not been committed to SVN trunk as of r24943. So it may be necessary to
-apply the following patch. The Tahoe node refuse to start the FTP server if
-it detects that this patch has not been applied.
-
-Index: twisted/protocols/ftp.py
-===================================================================
---- twisted/protocols/ftp.py   (revision 24956)
-+++ twisted/protocols/ftp.py   (working copy)
-@@ -1049,7 +1049,6 @@
-                 cons = ASCIIConsumerWrapper(cons)
-             d = self.dtpInstance.registerConsumer(cons)
--            d.addCallbacks(cbSent, ebSent)
-             # Tell them what to doooo
-             if self.dtpInstance.isConnected:
-@@ -1062,6 +1061,8 @@
-         def cbOpened(file):
-             d = file.receive()
-             d.addCallback(cbConsumer)
-+            d.addCallback(lambda ignored: file.close())
-+            d.addCallbacks(cbSent, ebSent)
-             return d
-         def ebOpened(err):
-@@ -1434,7 +1435,14 @@
-         @rtype: C{Deferred} of C{IConsumer}
-         """
-+    def close():
-+        """
-+        Perform any post-write work that needs to be done. This method may
-+        only be invoked once on each provider, and will always be invoked
-+        after receive().
-+        @rtype: C{Deferred} of anything: the value is ignored
-+        """
- def _getgroups(uid):
-     """Return the primary and supplementary groups for the given UID.
-@@ -1795,6 +1803,8 @@
-         # FileConsumer will close the file object
-         return defer.succeed(FileConsumer(self.fObj))
-+    def close(self):
-+        return defer.succeed(None)
- class FTPRealm:
-Index: twisted/vfs/adapters/ftp.py
-===================================================================
---- twisted/vfs/adapters/ftp.py        (revision 24956)
-+++ twisted/vfs/adapters/ftp.py        (working copy)
-@@ -295,6 +295,11 @@
-         """
-         return defer.succeed(IConsumer(self.node))
-+    def close(self):
-+        """
-+        Perform post-write actions.
-+        """
-+        return defer.succeed(None)
- class _FileToConsumerAdapter(object):
diff --git a/docs/frontends/sftp.txt b/docs/frontends/sftp.txt
deleted file mode 100644 (file)
index cb08582..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-= Tahoe SFTP Frontend =
-
-All Tahoe client nodes can run a frontend SFTP server, allowing regular SFTP
-clients to access the virtual filesystem.
-
-Since Tahoe does not use user accounts or passwords, the FTP server must be
-configured with a way to translate a username (and either a password or
-public key) into a root directory cap. Two mechanisms are provided. The first
-is a simple flat file with one account per line. The second is an HTTP-based
-login mechanism, backed by simple PHP script and a database. The latter form
-is used by allmydata.com to provide secure access to customer rootcaps.
-
-The SFTP server must also be given a public/private host keypair.
-
-== Configuring a Keypair ==
-
-First, generate a keypair for your server:
-
-% cd BASEDIR
-% ssh-keygen -f private/ssh_host_rsa_key
-
-You will then use the following lines in the tahoe.cfg file:
-
- [sftpd]
- sftp.host_pubkey_file = private/ssh_host_rsa_key.pub
- sftp.host_privkey_file = private/ssh_host_rsa_key
-
-== Configuring an Account File ==
-
-To configure the first form, create a file (probably in
-BASEDIR/private/sftp.accounts) in which each non-comment/non-blank line is a
-space-separated line of (USERNAME, PASSWORD/PUBKEY, ROOTCAP), like so:
-
-[TODO: the PUBKEY form is not yet supported]
-
- % cat BASEDIR/private/sftp.accounts
- # This is a password file, (username, password/pubkey, rootcap)
- alice password URI:DIR2:ioej8xmzrwilg772gzj4fhdg7a:wtiizszzz2rgmczv4wl6bqvbv33ag4kvbr6prz3u6w3geixa6m6a
- bob sekrit URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
- carol ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAv2xHRVBoXnwxHLzthRD1wOWtyZ08b8n9cMZfJ58CBdBwAYP2NVNXc0XjRvswm5hnnAO+jyWPVNpXJjm9XllzYhODSNtSN+TXuJlUjhzA/T+ZwdgsgSAeHuuMQBoWt4Qc9HV6rHCdAeMhcnyqm6Q0sRAsfA/wfwiIgbvE7+cWpFa2anB6WeAnvK8+dMN0nvnkPE7GNyf/WFR1Ffuh9ifKdRB6yDNp17bQAqA3OWSFjch6fGPhp94y4g2jmTHlEUTyVsilgGqvGOutOVYnmOMnFijugU1Vu33G39GGzXWla6+fXwTk/oiVPiCYD7A7WFKes3nqMg8iVN6a6sxujrhnHQ== warner@fluxx URI:DIR2:6bdmeitystckbl9yqlw7g56f4e:serp5ioqxnh34mlbmzwvkp3odehsyrr7eytt5f64we3k9hhcrcja
-
-Note that if the second word of the line is "ssh-rsa" or "ssh-dss", the rest
-of the line is parsed differently, so users cannot have a password equal to
-either of these strings.
-
-Then add the following lines to the BASEDIR/tahoe.cfg file:
-
- [sftpd]
- enabled = true
- sftp.port = 8022
- sftp.host_pubkey_file = private/ssh_host_rsa_key.pub
- sftp.host_privkey_file = private/ssh_host_rsa_key
- sftp.accounts.file = private/sftp.accounts
-
-The SFTP server will listen on the given port number. The sftp.accounts.file
-pathname will be interpreted relative to the node's BASEDIR.
-
-== Configuring an Account Server ==
-
-Determine the URL of the account server, say https://example.com/login . Then
-add the following lines to BASEDIR/tahoe.cfg:
-
- [sftpd]
- enabled = true
- sftp.port = 8022
- sftp.host_pubkey_file = private/ssh_host_rsa_key.pub
- sftp.host_privkey_file = private/ssh_host_rsa_key
- sftp.accounts.url = https://example.com/login
-
-== Dependencies ==
-
-The Tahoe SFTP server requires the Twisted "Conch" component, which itself
-requires the pycrypto package (note that pycrypto is distinct from the
-pycryptopp that Tahoe uses).
diff --git a/src/allmydata/frontends/auth.py b/src/allmydata/frontends/auth.py
new file mode 100644 (file)
index 0000000..6adbebb
--- /dev/null
@@ -0,0 +1,98 @@
+import os
+from zope.interface import implements
+from twisted.web.client import getPage
+from twisted.internet import defer
+from twisted.cred import error, checkers, credentials
+from allmydata.util import base32
+
+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.pubkeys = {}
+        self.rootcaps = {}
+        for line in open(os.path.expanduser(accountfile), "r"):
+            line = line.strip()
+            if line.startswith("#") or not line:
+                continue
+            name, passwd, rest = line.split(None, 2)
+            if passwd in ("ssh-dss", "ssh-rsa"):
+                bits = rest.split()
+                keystring = " ".join(bits[-1])
+                rootcap = bits[-1]
+                self.pubkeys[name] = keystring
+            else:
+                self.passwords[name] = passwd
+                rootcap = rest
+            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
+
index 5ade0e2084febc9c989608667fe55559771eb22d..598c8db04798ffc3207f75e75c5b36d7c7f5bfab 100644 (file)
@@ -1,19 +1,16 @@
 
-import os
 import tempfile
 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.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
 
 class ReadFile:
     implements(ftp.IReadFile)
@@ -270,89 +267,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 auth import AccountURLChecker, AccountFileChecker
 
 
 class Dispatcher:
@@ -373,22 +288,23 @@ class FTPServer(service.MultiService):
     def __init__(self, client, accountfile, accounturl, ftp_portstr):
         service.MultiService.__init__(self)
 
+        # make sure we're using a patched Twisted that uses IWriteFile.close:
+        # see docs/frontends/ftp.txt for details.
+        assert "close" in ftp.IWriteFile.names(), "your twisted is lacking"
+
+        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"
-
-        r = Dispatcher(client)
-        p = portal.Portal(r)
-        p.registerChecker(c)
         f = ftp.FTPFactory(p)
-
         s = strports.service(ftp_portstr, f)
         s.setServiceParent(self)
index 790e5d7e01af2fd5f568281d12389d22e9a76cdc..d29aa8a6305f853232e3359591c672cfb128ebac 100644 (file)
@@ -1,5 +1,4 @@
 
-import os
 import tempfile
 from zope.interface import implements
 from twisted.python import components
@@ -11,13 +10,11 @@ from twisted.conch.interfaces import ISFTPServer, ISFTPFile, IConchUser
 from twisted.conch.avatar import ConchUser
 from twisted.conch.openssh_compat import primes
 from twisted.conch import ls
-from twisted.cred import error, portal, checkers, credentials
-from twisted.web.client import getPage
+from twisted.cred import portal
 
 from allmydata.interfaces import IDirectoryNode, ExistingChildError, \
      NoSuchChildError
 from allmydata.immutable.upload import FileHandle
-from allmydata.util import base32
 
 class MemoryConsumer:
     implements(IConsumer)
@@ -420,101 +417,12 @@ class SFTPHandler:
         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.pubkeys = {}
-        self.rootcaps = {}
-        for line in open(os.path.expanduser(accountfile), "r"):
-            line = line.strip()
-            if line.startswith("#") or not line:
-                continue
-            name, passwd, rest = line.split(None, 2)
-            if passwd in ("ssh-dss", "ssh-rsa"):
-                bits = rest.split()
-                keystring = " ".join(bits[-1])
-                rootcap = bits[-1]
-                self.pubkeys[name] = keystring
-            else:
-                self.passwords[name] = passwd
-                rootcap = rest
-            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
-
 # if you have an SFTPUser, and you want something that provides ISFTPServer,
 # then you get SFTPHandler(user)
 components.registerAdapter(SFTPHandler, SFTPUser, ISFTPServer)
 
+from auth import AccountURLChecker, AccountFileChecker
+
 class Dispatcher:
     implements(portal.IRealm)
     def __init__(self, client):
@@ -533,18 +441,19 @@ class SFTPServer(service.MultiService):
                  sftp_portstr, pubkey_file, privkey_file):
         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")
 
-        r = Dispatcher(client)
-        p = portal.Portal(r)
-        p.registerChecker(c)
-
         pubkey = keys.Key.fromFile(pubkey_file)
         privkey = keys.Key.fromFile(privkey_file)
         class SSHFactory(factory.SSHFactory):