paulb@155 | 1 | #!/usr/bin/env python |
paulb@155 | 2 | |
paulb@403 | 3 | """ |
paulb@403 | 4 | Login resources which redirect clients back to an application after a successful |
paulb@403 | 5 | login. |
paulb@403 | 6 | |
paulb@610 | 7 | Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk> |
paulb@403 | 8 | |
paulb@403 | 9 | This library is free software; you can redistribute it and/or |
paulb@403 | 10 | modify it under the terms of the GNU Lesser General Public |
paulb@403 | 11 | License as published by the Free Software Foundation; either |
paulb@403 | 12 | version 2.1 of the License, or (at your option) any later version. |
paulb@403 | 13 | |
paulb@403 | 14 | This library is distributed in the hope that it will be useful, |
paulb@403 | 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
paulb@403 | 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
paulb@403 | 17 | Lesser General Public License for more details. |
paulb@403 | 18 | |
paulb@403 | 19 | You should have received a copy of the GNU Lesser General Public |
paulb@403 | 20 | License along with this library; if not, write to the Free Software |
paulb@489 | 21 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA |
paulb@403 | 22 | """ |
paulb@155 | 23 | |
paulb@155 | 24 | import WebStack.Generic |
paulb@155 | 25 | from WebStack.Helpers.Auth import get_token |
paulb@155 | 26 | |
paulb@155 | 27 | class LoginResource: |
paulb@155 | 28 | |
paulb@155 | 29 | "A resource providing a login screen." |
paulb@155 | 30 | |
paulb@451 | 31 | encoding = "utf-8" |
paulb@451 | 32 | |
paulb@624 | 33 | def __init__(self, authenticator, use_redirect=1, urlencoding=None, encoding=None): |
paulb@155 | 34 | |
paulb@155 | 35 | """ |
paulb@155 | 36 | Initialise the resource with an 'authenticator'. |
paulb@155 | 37 | |
paulb@451 | 38 | If the optional 'use_redirect' flag is set to 0, a confirmation screen |
paulb@451 | 39 | is given instead of redirecting the user back to the original |
paulb@451 | 40 | application. |
paulb@451 | 41 | |
paulb@451 | 42 | The optional 'urlencoding' parameter allows a special encoding to be |
paulb@451 | 43 | used in producing the redirection path. |
paulb@612 | 44 | |
paulb@624 | 45 | The optional 'encoding' parameter allows a special encoding to be used |
paulb@624 | 46 | in producing the login pages. |
paulb@624 | 47 | |
paulb@612 | 48 | To change the pages employed by this resource, either redefine the |
paulb@612 | 49 | 'login_page' and 'success_page' attributes in instances of this class or |
paulb@612 | 50 | a subclass, or override the 'show_login' and 'show_success' methods. |
paulb@155 | 51 | """ |
paulb@155 | 52 | |
paulb@155 | 53 | self.authenticator = authenticator |
paulb@155 | 54 | self.use_redirect = use_redirect |
paulb@624 | 55 | self.urlencoding = urlencoding |
paulb@624 | 56 | self.encoding = encoding or self.encoding |
paulb@155 | 57 | |
paulb@155 | 58 | def respond(self, trans): |
paulb@155 | 59 | |
paulb@155 | 60 | "Respond using the transaction 'trans'." |
paulb@155 | 61 | |
paulb@589 | 62 | app, path, qs = get_target(trans, self.urlencoding, self.encoding) |
paulb@155 | 63 | |
paulb@155 | 64 | # Check for a submitted login form. |
paulb@155 | 65 | |
paulb@610 | 66 | fields_body = trans.get_fields_from_body(self.encoding) |
paulb@610 | 67 | |
paulb@155 | 68 | if fields_body.has_key("login"): |
paulb@610 | 69 | if self.authenticator.authenticate(trans, fields_body.get("username", [None])[0], fields_body.get("password", [None])[0]): |
paulb@451 | 70 | self._redirect(trans, app, path, qs) |
paulb@506 | 71 | # The above method does not return. |
paulb@155 | 72 | |
paulb@155 | 73 | # Otherwise, show the login form. |
paulb@155 | 74 | |
paulb@589 | 75 | self.show_login(trans, app, path, qs) |
paulb@451 | 76 | |
paulb@451 | 77 | def _redirect(self, trans, app, path, qs): |
paulb@155 | 78 | |
paulb@451 | 79 | """ |
paulb@451 | 80 | Redirect the client using 'trans' and the given 'app', 'path' and 'qs' |
paulb@451 | 81 | details. |
paulb@451 | 82 | """ |
paulb@155 | 83 | |
paulb@155 | 84 | # Show the success page anyway. |
paulb@155 | 85 | |
paulb@589 | 86 | self.show_success(trans, app, path, qs) |
paulb@506 | 87 | if self.use_redirect: |
paulb@506 | 88 | trans.redirect(app + trans.encode_path(path, self.urlencoding) + qs) |
paulb@506 | 89 | else: |
paulb@506 | 90 | raise WebStack.Generic.EndOfResponse |
paulb@155 | 91 | |
paulb@589 | 92 | def show_login(self, trans, app, path, qs): |
paulb@155 | 93 | |
paulb@155 | 94 | """ |
paulb@612 | 95 | Writes a login screen using the transaction 'trans', including details |
paulb@612 | 96 | of the 'app', 'path' and 'qs' which the client was attempting to access. |
paulb@155 | 97 | """ |
paulb@155 | 98 | |
paulb@451 | 99 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) |
paulb@155 | 100 | out = trans.get_response_stream() |
paulb@612 | 101 | out.write(self.login_page % (app, path, qs)) |
paulb@612 | 102 | |
paulb@612 | 103 | def show_success(self, trans, app, path, qs): |
paulb@612 | 104 | |
paulb@612 | 105 | """ |
paulb@612 | 106 | Writes a success screen using the transaction 'trans', including details |
paulb@612 | 107 | of the 'app', 'path' and 'qs' which the client was attempting to access. |
paulb@612 | 108 | """ |
paulb@612 | 109 | |
paulb@612 | 110 | # When authentication fails or is yet to take place, show the login |
paulb@612 | 111 | # screen. |
paulb@612 | 112 | |
paulb@612 | 113 | trans.set_content_type(WebStack.Generic.ContentType("text/html", self.encoding)) |
paulb@612 | 114 | out = trans.get_response_stream() |
paulb@612 | 115 | out.write(self.success_page % (app, trans.encode_path(path, self.urlencoding), qs)) |
paulb@612 | 116 | |
paulb@612 | 117 | login_page = """ |
paulb@155 | 118 | <html> |
paulb@155 | 119 | <head> |
paulb@612 | 120 | <title>Login</title> |
paulb@155 | 121 | </head> |
paulb@155 | 122 | <body> |
paulb@155 | 123 | <h1>Login</h1> |
paulb@155 | 124 | <form method="POST"> |
paulb@155 | 125 | <p>Username: <input name="username" type="text" size="12"/></p> |
paulb@610 | 126 | <p>Password: <input name="password" type="password" size="12"/></p> |
paulb@155 | 127 | <p><input name="login" type="submit" value="Login"/></p> |
paulb@451 | 128 | <input name="app" type="hidden" value="%s"/> |
paulb@451 | 129 | <input name="path" type="hidden" value="%s"/> |
paulb@451 | 130 | <input name="qs" type="hidden" value="%s"/> |
paulb@155 | 131 | </form> |
paulb@155 | 132 | </body> |
paulb@155 | 133 | </html> |
paulb@612 | 134 | """ |
paulb@155 | 135 | |
paulb@612 | 136 | success_page = """ |
paulb@155 | 137 | <html> |
paulb@155 | 138 | <head> |
paulb@155 | 139 | <title>Login Example</title> |
paulb@155 | 140 | </head> |
paulb@155 | 141 | <body> |
paulb@155 | 142 | <h1>Login Successful</h1> |
paulb@451 | 143 | <p>Please proceed <a href="%s%s%s">to the application</a>.</p> |
paulb@155 | 144 | </body> |
paulb@155 | 145 | </html> |
paulb@612 | 146 | """ |
paulb@155 | 147 | |
paulb@155 | 148 | class LoginAuthenticator: |
paulb@155 | 149 | |
paulb@155 | 150 | def __init__(self, secret_key, credentials, cookie_name=None): |
paulb@155 | 151 | |
paulb@155 | 152 | """ |
paulb@155 | 153 | Initialise the authenticator with a 'secret_key', the authenticator's registry of |
paulb@155 | 154 | 'credentials' and an optional 'cookie_name'. |
paulb@155 | 155 | |
paulb@155 | 156 | The 'credentials' must be an object which supports tests of the form |
paulb@155 | 157 | '(username, password) in credentials'. |
paulb@155 | 158 | """ |
paulb@155 | 159 | |
paulb@155 | 160 | self.secret_key = secret_key |
paulb@155 | 161 | self.credentials = credentials |
paulb@155 | 162 | self.cookie_name = cookie_name or "LoginAuthenticator" |
paulb@155 | 163 | |
paulb@589 | 164 | def authenticate(self, trans, username, password): |
paulb@155 | 165 | |
paulb@155 | 166 | """ |
paulb@155 | 167 | Authenticate the sender of the transaction 'trans', returning 1 (true) if they are |
paulb@589 | 168 | recognised, 0 (false) otherwise. Use the 'username' and 'password' supplied as |
paulb@589 | 169 | credentials. |
paulb@155 | 170 | """ |
paulb@155 | 171 | |
paulb@155 | 172 | # Process any supplied parameters. |
paulb@155 | 173 | |
paulb@155 | 174 | fields = trans.get_fields_from_body() |
paulb@155 | 175 | |
paulb@589 | 176 | # Check against the class's credentials. |
paulb@155 | 177 | |
paulb@589 | 178 | if (username, password) in self.credentials: |
paulb@155 | 179 | |
paulb@589 | 180 | # Make a special cookie token. |
paulb@155 | 181 | |
paulb@589 | 182 | self.set_token(trans, username) |
paulb@589 | 183 | return 1 |
paulb@155 | 184 | |
paulb@155 | 185 | return 0 |
paulb@155 | 186 | |
paulb@155 | 187 | def set_token(self, trans, username): |
paulb@155 | 188 | |
paulb@155 | 189 | "Set an authentication token in 'trans' with the given 'username'." |
paulb@155 | 190 | |
paulb@155 | 191 | trans.set_cookie_value( |
paulb@155 | 192 | self.cookie_name, |
paulb@268 | 193 | get_token(username, self.secret_key), |
paulb@268 | 194 | path="/" |
paulb@155 | 195 | ) |
paulb@155 | 196 | |
paulb@589 | 197 | # General functions. |
paulb@589 | 198 | |
paulb@624 | 199 | def get_target(trans, urlencoding=None, encoding=None): |
paulb@589 | 200 | |
paulb@589 | 201 | """ |
paulb@624 | 202 | Return the application, path and query string for 'trans' using the optional |
paulb@589 | 203 | 'urlencoding' (or path encoding) and request body 'encoding'. |
paulb@589 | 204 | """ |
paulb@589 | 205 | |
paulb@589 | 206 | fields_path = trans.get_fields_from_path(urlencoding) |
paulb@589 | 207 | fields_body = trans.get_fields_from_body(encoding) |
paulb@589 | 208 | |
paulb@589 | 209 | # NOTE: Handle missing redirects better. |
paulb@589 | 210 | |
paulb@589 | 211 | if fields_body.has_key("app"): |
paulb@589 | 212 | apps = fields_body["app"] |
paulb@589 | 213 | app = apps[0] |
paulb@589 | 214 | elif fields_path.has_key("app"): |
paulb@589 | 215 | apps = fields_path["app"] |
paulb@589 | 216 | app = apps[0] |
paulb@589 | 217 | else: |
paulb@589 | 218 | app = u"" |
paulb@589 | 219 | |
paulb@589 | 220 | if fields_body.has_key("path"): |
paulb@589 | 221 | paths = fields_body["path"] |
paulb@589 | 222 | path = paths[0] |
paulb@589 | 223 | elif fields_path.has_key("path"): |
paulb@589 | 224 | paths = fields_path["path"] |
paulb@589 | 225 | path = paths[0] |
paulb@589 | 226 | else: |
paulb@589 | 227 | path = u"" |
paulb@589 | 228 | |
paulb@589 | 229 | if fields_body.has_key("qs"): |
paulb@589 | 230 | qss = fields_body["qs"] |
paulb@589 | 231 | qs = qss[0] |
paulb@589 | 232 | elif fields_path.has_key("qs"): |
paulb@589 | 233 | qss = fields_path["qs"] |
paulb@589 | 234 | qs = qss[0] |
paulb@589 | 235 | else: |
paulb@589 | 236 | qs = u"" |
paulb@589 | 237 | |
paulb@589 | 238 | return app, path, qs |
paulb@589 | 239 | |
paulb@155 | 240 | # vim: tabstop=4 expandtab shiftwidth=4 |