From bf3d09d430a58198e1c32e8ddd6f38703d978d9e Mon Sep 17 00:00:00 2001
From: Brian Warner <warner@allmydata.com>
Date: Mon, 6 Oct 2008 16:15:11 -0700
Subject: [PATCH] ftpd: add native_client.php -based HTTP authentication scheme

---
 src/allmydata/ftpd.py | 54 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 53 insertions(+), 1 deletion(-)

diff --git a/src/allmydata/ftpd.py b/src/allmydata/ftpd.py
index 0182126e..06289619 100644
--- a/src/allmydata/ftpd.py
+++ b/src/allmydata/ftpd.py
@@ -8,10 +8,12 @@ from twisted.internet.interfaces import IConsumer
 from twisted.protocols import ftp
 from twisted.cred import error, portal, checkers, credentials
 from twisted.python import log
+from twisted.web.client import getPage
 
 from allmydata.interfaces import IDirectoryNode, ExistingChildError
 from allmydata.immutable.download import ConsumerAdapter
 from allmydata.immutable.upload import FileHandle
+from allmydata.util import base32
 
 class ReadFile:
     implements(ftp.IReadFile)
@@ -354,6 +356,56 @@ class AccountFileChecker:
             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
+
+
 class Dispatcher:
     implements(portal.IRealm)
     def __init__(self, client):
@@ -375,7 +427,7 @@ class FTPServer(service.MultiService):
         if accountfile:
             c = AccountFileChecker(self, accountfile)
         elif accounturl:
-            raise NotImplementedError("account URL not yet implemented")
+            c = AccountURLChecker(self, accounturl)
         else:
             # we could leave this anonymous, with just the /uri/CAP form
             raise RuntimeError("must provide some translation")
-- 
2.45.2