3 from zope.interface import implements
4 from twisted.web.client import getPage
5 from twisted.internet import defer
6 from twisted.cred import error, checkers, credentials
7 from twisted.conch import error as conch_error
8 from twisted.conch.ssh import keys
10 from allmydata.util import base32
11 from allmydata.util.fileutil import abspath_expanduser_unicode
14 class NeedRootcapLookupScheme(Exception):
15 """Accountname+Password-based access schemes require some kind of
16 mechanism to translate name+passwd pairs into a rootcap, either a file of
17 name/passwd/rootcap tuples, or a server to do the translation."""
20 def __init__(self, username, rootcap):
21 self.username = username
22 self.rootcap = rootcap
24 class AccountFileChecker:
25 implements(checkers.ICredentialsChecker)
26 credentialInterfaces = (credentials.IUsernamePassword,
27 credentials.IUsernameHashedPassword,
28 credentials.ISSHPrivateKey)
29 def __init__(self, client, accountfile):
34 for line in open(abspath_expanduser_unicode(accountfile), "r"):
36 if line.startswith("#") or not line:
38 name, passwd, rest = line.split(None, 2)
39 if passwd.startswith("ssh-"):
41 keystring = " ".join([passwd] + bits[:-1])
43 self.pubkeys[name] = keystring
45 self.passwords[name] = passwd
47 self.rootcaps[name] = rootcap
49 def _avatarId(self, username):
50 return FTPAvatarID(username, self.rootcaps[username])
52 def _cbPasswordMatch(self, matched, username):
54 return self._avatarId(username)
55 raise error.UnauthorizedLogin
57 def requestAvatarId(self, creds):
58 if credentials.ISSHPrivateKey.providedBy(creds):
59 # Re-using twisted.conch.checkers.SSHPublicKeyChecker here, rather
60 # than re-implementing all of the ISSHPrivateKey checking logic,
61 # would be better. That would require Twisted 14.1.0 or newer,
63 return self._checkKey(creds)
64 elif credentials.IUsernameHashedPassword.providedBy(creds):
65 return self._checkPassword(creds)
66 elif credentials.IUsernamePassword.providedBy(creds):
67 return self._checkPassword(creds)
69 raise NotImplementedError()
71 def _checkPassword(self, creds):
73 Determine whether the password in the given credentials matches the
74 password in the account file.
76 Returns a Deferred that fires with the username if the password matches
77 or with an UnauthorizedLogin failure otherwise.
80 correct = self.passwords[creds.username]
82 return defer.fail(error.UnauthorizedLogin())
84 d = defer.maybeDeferred(creds.checkPassword, correct)
85 d.addCallback(self._cbPasswordMatch, str(creds.username))
88 def _checkKey(self, creds):
90 Determine whether some key-based credentials correctly authenticates a
93 Returns a Deferred that fires with the username if so or with an
94 UnauthorizedLogin failure otherwise.
97 # Is the public key indicated by the given credentials allowed to
98 # authenticate the username in those credentials?
99 if creds.blob == self.pubkeys.get(creds.username):
100 if creds.signature is None:
101 return defer.fail(conch_error.ValidPublicKey())
103 # Is the signature in the given credentials the correct
104 # signature for the data in those credentials?
105 key = keys.Key.fromString(creds.blob)
106 if key.verify(creds.signature, creds.sigData):
107 return defer.succeed(self._avatarId(creds.username))
109 return defer.fail(error.UnauthorizedLogin())
111 class AccountURLChecker:
112 implements(checkers.ICredentialsChecker)
113 credentialInterfaces = (credentials.IUsernamePassword,)
115 def __init__(self, client, auth_url):
117 self.auth_url = auth_url
119 def _cbPasswordMatch(self, rootcap, username):
120 return FTPAvatarID(username, rootcap)
122 def post_form(self, username, password):
123 sepbase = base32.b2a(os.urandom(4))
127 fields = {"action": "authenticate",
131 for name, value in fields.iteritems():
132 form.append('Content-Disposition: form-data; name="%s"' % name)
134 assert isinstance(value, str)
138 body = "\r\n".join(form) + "\r\n"
139 headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
141 return getPage(self.auth_url, method="POST",
142 postdata=body, headers=headers,
143 followRedirect=True, timeout=30)
145 def _parse_response(self, res):
146 rootcap = res.strip()
148 raise error.UnauthorizedLogin
151 def requestAvatarId(self, credentials):
152 # construct a POST to the login form. While this could theoretically
153 # be done with something like the stdlib 'email' package, I can't
154 # figure out how, so we just slam together a form manually.
155 d = self.post_form(credentials.username, credentials.password)
156 d.addCallback(self._parse_response)
157 d.addCallback(self._cbPasswordMatch, str(credentials.username))