PageMaker » History » Version 6
Elmer de Looff, 2012-05-09 17:34
Static file handling explained and beginning of Login mixins.
1 | 3 | Elmer de Looff | PageMaker is the _Controller_ of the MVC approach in µWeb. After a request is received by the web server (either [[Standalone]] or [[Apache]]) and wrapped inside a [[Request]] object, it is [[Request Router|routed]] here to be answered. |
---|---|---|---|
2 | 1 | Elmer de Looff | |
3 | 3 | Elmer de Looff | In the PageMaker, there might be database lookups done through the [[Model|data abstraction layer (model)]] and likely output is sent back making use of the [[TemplateParser]]. |
4 | 3 | Elmer de Looff | |
5 | 3 | Elmer de Looff | {{toc}} |
6 | 3 | Elmer de Looff | |
7 | 3 | Elmer de Looff | h1. A very minimal PageMaker |
8 | 3 | Elmer de Looff | |
9 | 3 | Elmer de Looff | In the simplest form, a PageMaker for a project subclasses from µWeb's default @PageMaker@ class and provides its own methods to handle requests. The full source for this would look something like this: |
10 | 3 | Elmer de Looff | |
11 | 3 | Elmer de Looff | <pre><code class="python"> |
12 | 3 | Elmer de Looff | #!/usr/bin/python |
13 | 3 | Elmer de Looff | """PageMaker demonstration module""" |
14 | 3 | Elmer de Looff | |
15 | 3 | Elmer de Looff | # uWeb framework |
16 | 3 | Elmer de Looff | import uweb |
17 | 3 | Elmer de Looff | |
18 | 5 | Elmer de Looff | class Minimalist(uweb.PageMaker): |
19 | 3 | Elmer de Looff | def Index(self): |
20 | 3 | Elmer de Looff | return 'Welcome to our website, it is still very much under construction.' |
21 | 3 | Elmer de Looff | |
22 | 3 | Elmer de Looff | def Catchall(self, path): |
23 | 3 | Elmer de Looff | return 'The requested page %r does not exist yet' % path |
24 | 3 | Elmer de Looff | </code></pre> |
25 | 3 | Elmer de Looff | |
26 | 1 | Elmer de Looff | h1. DebuggingPageMaker |
27 | 3 | Elmer de Looff | |
28 | 3 | Elmer de Looff | Before we do anything else, during development you are _strongly advised_ to use µWeb's @DebuggingPageMaker@. This has a lot of additional features for when something goes wrong on the server side. When the regular PageMaker runs into a server side error, it returns a very plain HTTP 500 response: |
29 | 3 | Elmer de Looff | |
30 | 3 | Elmer de Looff | <pre> |
31 | 3 | Elmer de Looff | INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF '/' |
32 | 3 | Elmer de Looff | </pre> |
33 | 3 | Elmer de Looff | |
34 | 4 | Elmer de Looff | Where @'/'@ is the path requested by the client. When running @DebuggingPageMaker@ there is a significantly more helpful (for the developer at least) page whenever an internal server error is encountered. It will show a full stack trace, the local variables on each stack level (typically at the point of calling another function), which helps to arrive to the point of failure more quickly. |
35 | 4 | Elmer de Looff | |
36 | 4 | Elmer de Looff | Example Internal Server Error response "as image":http://bugs.underdark.nl/attachments/download/185/http500_full.png or in the "µWeb demo project":http://info.underdark.nl/haltandcatchfire?debug |
37 | 3 | Elmer de Looff | |
38 | 3 | Elmer de Looff | In all cases, an internal server error will cause a full stacktrace to be logged in the log file database. |
39 | 1 | Elmer de Looff | |
40 | 2 | Elmer de Looff | h1. Templateparser |
41 | 1 | Elmer de Looff | |
42 | 2 | Elmer de Looff | The µWeb *[[TemplateParser]]* is available on the standard PageMaker instance. When using PageMaker, an instantiated TemplateParser instance is available through the @parser@ member of PageMaker. Basic usage looks like this: |
43 | 2 | Elmer de Looff | |
44 | 2 | Elmer de Looff | <pre><code class="python"> |
45 | 2 | Elmer de Looff | import uweb |
46 | 2 | Elmer de Looff | import time |
47 | 2 | Elmer de Looff | |
48 | 5 | Elmer de Looff | class TemplateDemo(uweb.PageMaker): |
49 | 2 | Elmer de Looff | def VersionPage(self): |
50 | 2 | Elmer de Looff | return self.parser.Parse( |
51 | 2 | Elmer de Looff | 'version.utp', year=time.strftime('%Y'), version=uweb.__version__) |
52 | 2 | Elmer de Looff | </code></pre> |
53 | 2 | Elmer de Looff | |
54 | 2 | Elmer de Looff | The example template for the above file could look something like this: |
55 | 2 | Elmer de Looff | |
56 | 2 | Elmer de Looff | <pre><code class="html"> |
57 | 2 | Elmer de Looff | <!DOCTYPE html> |
58 | 2 | Elmer de Looff | <html> |
59 | 2 | Elmer de Looff | <head> |
60 | 2 | Elmer de Looff | <title>µWeb version info</title> |
61 | 2 | Elmer de Looff | </head> |
62 | 2 | Elmer de Looff | <body> |
63 | 2 | Elmer de Looff | <p>µWeb version [version] - Copyright 2010-[year] Underdark</p> |
64 | 2 | Elmer de Looff | </body> |
65 | 2 | Elmer de Looff | </html> |
66 | 2 | Elmer de Looff | </code></pre> |
67 | 2 | Elmer de Looff | |
68 | 2 | Elmer de Looff | And would result in the following output: |
69 | 2 | Elmer de Looff | |
70 | 2 | Elmer de Looff | <pre><code class="html"> |
71 | 2 | Elmer de Looff | <!DOCTYPE html> |
72 | 2 | Elmer de Looff | <html> |
73 | 2 | Elmer de Looff | <head> |
74 | 2 | Elmer de Looff | <title>µWeb version info</title> |
75 | 2 | Elmer de Looff | </head> |
76 | 2 | Elmer de Looff | <body> |
77 | 2 | Elmer de Looff | <p>µWeb version 0.12 - Copyright 2010-2012 Underdark</p> |
78 | 2 | Elmer de Looff | </body> |
79 | 2 | Elmer de Looff | </html> |
80 | 2 | Elmer de Looff | </code></pre> |
81 | 2 | Elmer de Looff | |
82 | 2 | Elmer de Looff | Full documentation, with plenty of example template uses can be found on the [[TemplateParser|TemplateParser wiki-entry]]. |
83 | 2 | Elmer de Looff | |
84 | 2 | Elmer de Looff | h2. Template directory configuration |
85 | 2 | Elmer de Looff | |
86 | 2 | Elmer de Looff | By default, template are loaded from the 'templates' directory that is expected to be on the same path as the pagemaker module. If your pagemaker is located on @/var/www/uweb_project/project.py@, then templates will be automatically loaded from @/var/www/uweb_project/templates/@. |
87 | 2 | Elmer de Looff | |
88 | 2 | Elmer de Looff | To change the default template loading path, define a new path in the class variable @TEMPLATE_DIR@. This should be a relative path (and defaults to @'templates'@). |
89 | 2 | Elmer de Looff | |
90 | 6 | Elmer de Looff | h1. Serving static content |
91 | 1 | Elmer de Looff | |
92 | 6 | Elmer de Looff | Your website most likely has a few static files that need to be served up. If you have a large website you would run many of these from a separate domain (to reduce the amount of overhead from a heavy web server and complex processes), but often there are at least some files that need to be served up from the local disk. |
93 | 1 | Elmer de Looff | |
94 | 6 | Elmer de Looff | µWeb has built-in facilities to serve static files, which prevent filesystem traversal by those lesser-behaved browsers. A browser requesting http://example.com/../secret_configuration.txt should *not* get the keys to your database server. The static handler has a base (configurable) directory from which all static content is served. For a client it is impossible to 'browse' up from that directory, preventing these leaks of information. |
95 | 6 | Elmer de Looff | |
96 | 6 | Elmer de Looff | The MIME-Type for the content will be determined using the @mimetypes@ module (available by default in Python), based on the file's extension. |
97 | 6 | Elmer de Looff | |
98 | 6 | Elmer de Looff | h2. Static handler demonstration |
99 | 6 | Elmer de Looff | |
100 | 6 | Elmer de Looff | In your router configuration, add a route the directs to the static handler: |
101 | 6 | Elmer de Looff | |
102 | 6 | Elmer de Looff | <pre><source class="python"> |
103 | 6 | Elmer de Looff | ROUTES = ( |
104 | 6 | Elmer de Looff | ... |
105 | 6 | Elmer de Looff | ('/images/(.*)', 'Static'), |
106 | 6 | Elmer de Looff | ... |
107 | 6 | Elmer de Looff | ) |
108 | 6 | Elmer de Looff | </code></pre> |
109 | 6 | Elmer de Looff | |
110 | 6 | Elmer de Looff | This will cause the following behaviour: |
111 | 6 | Elmer de Looff | * A browser requesting http://example.com/images/fish.jpg will be presented with the content from @'static/fish.jpg'@ |
112 | 6 | Elmer de Looff | * A browser requesting http://example.com/images/../../secret.txt will be presented with the content from @'static/secret.txt'@ (if it exists) |
113 | 6 | Elmer de Looff | |
114 | 1 | Elmer de Looff | h2. Static directory configuration |
115 | 1 | Elmer de Looff | |
116 | 6 | Elmer de Looff | As can be seen in the previous example, the content is served from the @'static'@ directory (this is not dependent on the selected route. This path is relative to the absolute path of the PageMaker itself. If the PageMaker module exists on @'/var/www/project/controller.py'@ then the default static directory is @'/var/www/project/static/'@. |
117 | 1 | Elmer de Looff | |
118 | 6 | Elmer de Looff | This default path can be changed by setting the @PUBLIC_DIR@ class variable of PageMaker. The path can be made absolute simply by providing one: |
119 | 6 | Elmer de Looff | |
120 | 6 | Elmer de Looff | <pre><code class="python"> |
121 | 6 | Elmer de Looff | import uweb |
122 | 6 | Elmer de Looff | |
123 | 6 | Elmer de Looff | class DifferentPublic(uweb.PageMaker): |
124 | 6 | Elmer de Looff | PUBLIC_DIR = '/var/www/project/public_http' |
125 | 6 | Elmer de Looff | </code></pre> |
126 | 6 | Elmer de Looff | |
127 | 6 | Elmer de Looff | h2. 404 on static content |
128 | 6 | Elmer de Looff | |
129 | 6 | Elmer de Looff | Whenever a request for static content (through @Static@) cannot be fulfilled, the method @_StaticNotFound@ is called, with the requested relative path as the sole argument. The default response for which is a simple plain text: |
130 | 6 | Elmer de Looff | |
131 | 6 | Elmer de Looff | <pre><code class="python"> |
132 | 6 | Elmer de Looff | def _StaticNotFound(self, _path): |
133 | 6 | Elmer de Looff | message = 'This is not the path you\'re looking for. No such file %r' % ( |
134 | 6 | Elmer de Looff | self.req.env['PATH_INFO']) |
135 | 6 | Elmer de Looff | return response.Response(message, content_type='text/plain', httpcode=404) |
136 | 6 | Elmer de Looff | </code></pre> |
137 | 6 | Elmer de Looff | |
138 | 6 | Elmer de Looff | Override this page if you want to provide your user with a more informative or styled response. |
139 | 6 | Elmer de Looff | |
140 | 6 | Elmer de Looff | h1. Login and sessions |
141 | 6 | Elmer de Looff | |
142 | 6 | Elmer de Looff | h2. OpenID |
143 | 6 | Elmer de Looff | |
144 | 6 | Elmer de Looff | To enable users of your website to log in using OpenID, there are only a few steps that need to be taken: |
145 | 6 | Elmer de Looff | |
146 | 6 | Elmer de Looff | h3. Add the OpenID mixin class to PageMaker |
147 | 6 | Elmer de Looff | |
148 | 6 | Elmer de Looff | This is as simple as adding the OpenID mixin class as one of the ancestors of your PageMaker. It is generally advisable to place mixin classes before the base class): |
149 | 6 | Elmer de Looff | |
150 | 6 | Elmer de Looff | <pre><code class="python"> |
151 | 6 | Elmer de Looff | import uweb |
152 | 6 | Elmer de Looff | from uweb.pagemaker import login |
153 | 6 | Elmer de Looff | |
154 | 6 | Elmer de Looff | class Pages(login.OpenIdMixin, uweb.PageMaker): |
155 | 6 | Elmer de Looff | </code></pre> |
156 | 6 | Elmer de Looff | |
157 | 6 | Elmer de Looff | h3. Set up routes to the OpenID validator |
158 | 6 | Elmer de Looff | |
159 | 6 | Elmer de Looff | The following routes (or similar ones) should be added to the [[router]]: |
160 | 6 | Elmer de Looff | |
161 | 6 | Elmer de Looff | <pre><code class="python"> |
162 | 6 | Elmer de Looff | ROUTES = ( |
163 | 6 | Elmer de Looff | ... |
164 | 6 | Elmer de Looff | ('/OpenIDLogin/?(\w+)?', '_OpenIdInitiate') |
165 | 6 | Elmer de Looff | ('/OpenIDValidate', '_OpenIdValidate') |
166 | 6 | Elmer de Looff | ... |
167 | 6 | Elmer de Looff | ) |
168 | 6 | Elmer de Looff | </code></pre> |
169 | 6 | Elmer de Looff | |
170 | 6 | Elmer de Looff | The optional capture after @'OpenIDLogin'@ here is to provide the optional provider URL (instead of through the POST field @'openid_provider'@). |
171 | 6 | Elmer de Looff | |
172 | 6 | Elmer de Looff | h3. Add handlers to PageMaker for sucess, failure, etc: |
173 | 6 | Elmer de Looff | |
174 | 6 | Elmer de Looff | <pre><code class="python"> |
175 | 6 | Elmer de Looff | def OpenIdAuthCancel(self, message): |
176 | 6 | Elmer de Looff | return 'OpenID Authentication canceled by user: %s' % message |
177 | 6 | Elmer de Looff | |
178 | 6 | Elmer de Looff | def OpenIdAuthFailure(self, message): |
179 | 6 | Elmer de Looff | return 'Authentication failed: %s' % message |
180 | 6 | Elmer de Looff | |
181 | 6 | Elmer de Looff | def OpenIdAuthSuccess(self, auth_dict): |
182 | 6 | Elmer de Looff | # Authentication succeeded, the auth_dict contains the information we received from the provider |
183 | 6 | Elmer de Looff | # |
184 | 6 | Elmer de Looff | # Next: Retrieve user information from database, or create a new user |
185 | 6 | Elmer de Looff | # Store the user's session (in the database, cookie, or both) |
186 | 6 | Elmer de Looff | session_id = base64.urlsafe_b64encode(os.urandom(30)) |
187 | 6 | Elmer de Looff | self.req.AddCookie('OpenIDSession', session_id, max_age=86400) |
188 | 6 | Elmer de Looff | return 'OpenID Authentication successful!' |
189 | 6 | Elmer de Looff | |
190 | 6 | Elmer de Looff | def OpenIdProviderBadLink(self, message): |
191 | 6 | Elmer de Looff | return 'Bad OpenID Provider URL: %s' % message |
192 | 6 | Elmer de Looff | |
193 | 6 | Elmer de Looff | def OpenIdProviderError(self, message): |
194 | 6 | Elmer de Looff | return 'The OpenID provider did not respond as expected: %r' % message |
195 | 6 | Elmer de Looff | </code></pre> |
196 | 6 | Elmer de Looff | |
197 | 6 | Elmer de Looff | h2. Underdark Login Framework |
198 | 6 | Elmer de Looff | |
199 | 6 | Elmer de Looff | Using the Underdark Login Framework requires steps comparable to using OpenID, but comes with a little more default setup. The system has two modes for logging in, one that is a straightforward plaintext form submit. This is slightly easier to implement, but when used without SSL it is highly vulnerable to man-in-the-middle (MITM) attacks. The second mode is a Javascript enabled mode where the password is hashed (using the SHA-1 algorithm), and to prevent replay attacks (from a MITM), a random 'challenge' is provided for each login attempt, which is also hashed with the result. This prevents a MITM from learning the password value, or using it to log in later (though the plaintext communication remains visible). |
200 | 6 | Elmer de Looff | |
201 | 6 | Elmer de Looff | h3. Add the ULF mixin class to PageMaker |
202 | 6 | Elmer de Looff | |
203 | 6 | Elmer de Looff | <pre><code class="python"> |
204 | 6 | Elmer de Looff | import uweb |
205 | 6 | Elmer de Looff | from uweb.pagemaker import login |
206 | 6 | Elmer de Looff | |
207 | 6 | Elmer de Looff | class Pages(login.LoginMixin, uweb.PageMaker): |
208 | 6 | Elmer de Looff | </code></pre> |
209 | 6 | Elmer de Looff | |
210 | 6 | Elmer de Looff | h3. Set up routes to the OpenID validator |
211 | 6 | Elmer de Looff | |
212 | 6 | Elmer de Looff | The following routes (or similar ones) should be added to the [[router]]: |
213 | 6 | Elmer de Looff | |
214 | 6 | Elmer de Looff | <pre><code class="python"> |
215 | 6 | Elmer de Looff | ROUTES = ( |
216 | 6 | Elmer de Looff | ... |
217 | 6 | Elmer de Looff | ('/ULF-Challenge', '_ULF_Challenge'), |
218 | 6 | Elmer de Looff | ('/ULF-Login', '_ULF_Verify'), |
219 | 6 | Elmer de Looff | ... |
220 | 6 | Elmer de Looff | ) |
221 | 6 | Elmer de Looff | </code></pre> |
222 | 6 | Elmer de Looff | |
223 | 6 | Elmer de Looff | h3. Add handlers to PageMaker for sucess and failure: |
224 | 6 | Elmer de Looff | |
225 | 6 | Elmer de Looff | <pre><code class="python"> |
226 | 6 | Elmer de Looff | def _ULF_Failure(self, secure): |
227 | 6 | Elmer de Looff | reutrn 'ULF authentication failed (secure mode: %d)' % secure |
228 | 6 | Elmer de Looff | |
229 | 6 | Elmer de Looff | def _ULF_Success(self, secure): |
230 | 6 | Elmer de Looff | return 'ULF authentication successful! (secure mode: %d)' % secure |
231 | 6 | Elmer de Looff | </code></pre> |
232 | 2 | Elmer de Looff | |
233 | 2 | Elmer de Looff | h1. Persistent storage between requests |
234 | 1 | Elmer de Looff | |
235 | 1 | Elmer de Looff | µWeb allows you to store objects in a process-persistent storage. This means that the storage will be properly persistent and available when µWeb is in [[standalone]] mode. When running on top of [[apache]], this persistence is only as good as the apache process, which is typically a couple hundred to a few thousand requests. |
236 | 2 | Elmer de Looff | |
237 | 1 | Elmer de Looff | h2. Default users of the persistent storage |
238 | 1 | Elmer de Looff | |
239 | 1 | Elmer de Looff | By default, the [[TemplateParser]] and the various database connectors are stored in the persistent storage. This has the benefit that pre-parsed templates will not need to be read from disk on subsequent requests. For databases the benefit is that connections need not be made on-the-fly, but can mostly be retrieved from the storage. |
240 | 2 | Elmer de Looff | |
241 | 1 | Elmer de Looff | h2. Storing persistent values |
242 | 1 | Elmer de Looff | |
243 | 1 | Elmer de Looff | Storing persistent values is done with the @Set@ method, as follows: |
244 | 1 | Elmer de Looff | |
245 | 1 | Elmer de Looff | <pre><code class="python"> |
246 | 1 | Elmer de Looff | def _PostInit(self): |
247 | 1 | Elmer de Looff | if 'connection' not in self.persistent: |
248 | 1 | Elmer de Looff | self.persistent.Set('connection', self._MakeConnection()) |
249 | 1 | Elmer de Looff | </code></pre> |
250 | 1 | Elmer de Looff | |
251 | 1 | Elmer de Looff | In the example above, the database connection is only created, and added to the persistent storage, if it's not already present. This way expensive but reusable actions can be optimized by performing them only once (or once every few so many requests, if running on Apache). |
252 | 2 | Elmer de Looff | |
253 | 1 | Elmer de Looff | h2. Retrieving persistent values |
254 | 1 | Elmer de Looff | |
255 | 1 | Elmer de Looff | Retrieving stored values works much like this, but uses the @Get@ method: |
256 | 1 | Elmer de Looff | |
257 | 1 | Elmer de Looff | <pre><code class="python"> |
258 | 1 | Elmer de Looff | def DatabaseAccess(self): |
259 | 1 | Elmer de Looff | with self.persistent.Get('connection') as cursor: |
260 | 1 | Elmer de Looff | cursor.Execute('INSERT INTO `message` SET `text` = "success!"') |
261 | 1 | Elmer de Looff | </code></pre> |
262 | 1 | Elmer de Looff | |
263 | 1 | Elmer de Looff | This uses the connection we created (or still had) during @_PostInit@, and uses it to update the database. |
264 | 1 | Elmer de Looff | |
265 | 1 | Elmer de Looff | In case a key has is not present in the persistent storage (because it wasn't set in the process' lifetime or because it was exlicitly dropped), the @Get@ method has an optional second argument, that is returned when the key is not present: |
266 | 1 | Elmer de Looff | |
267 | 1 | Elmer de Looff | <pre><code class="python"> |
268 | 1 | Elmer de Looff | def FirstVisit(self): |
269 | 1 | Elmer de Looff | when = self.persistent.Get('first_visit_time', 'just now') |
270 | 1 | Elmer de Looff | return 'Your first visit was %s.' % when |
271 | 1 | Elmer de Looff | </code></pre> |
272 | 1 | Elmer de Looff | |
273 | 1 | Elmer de Looff | This will return the stored date and time when there was a previously recorded visit, or the text _just now_ if there was no previous time logged. |
274 | 1 | Elmer de Looff | |
275 | 1 | Elmer de Looff | Finally, the persistent storage has a @SetDefault@ method, that acts much like the similarly named dictionary method. It returns the value for the given key, but if it's not present, it will set the key to the provided value, and return it as well. With this, we can improve on our first-visit tracker, and in one call retrieve or store the first time someone visited: |
276 | 1 | Elmer de Looff | |
277 | 1 | Elmer de Looff | <pre><code class="python"> |
278 | 1 | Elmer de Looff | def FirstVisit(self): |
279 | 1 | Elmer de Looff | when = self.persistent.SetDefault('first_visit_time', datetime.datetime.now()) |
280 | 1 | Elmer de Looff | return 'Your first visit was %s.' % when |
281 | 1 | Elmer de Looff | </code></pre> |
282 | 2 | Elmer de Looff | |
283 | 1 | Elmer de Looff | h2. Deleting persistent values |
284 | 1 | Elmer de Looff | |
285 | 1 | Elmer de Looff | If for any reason you need to delete a value from the persistent storage, this can be done using the @Del@ method. The given key name is removed from the storage. *N.B.:* If the key was already removed from the storage (this can happen if the delete code runs more than once, or the key was not defined in the process' lifetime), no error is raised. It is assumed that removing the key is the only desired action. |
286 | 1 | Elmer de Looff | |
287 | 1 | Elmer de Looff | <pre><code class="python"> |
288 | 1 | Elmer de Looff | def DeletePersistentKey(self, key): |
289 | 1 | Elmer de Looff | self.persistent.Del(key) |
290 | 1 | Elmer de Looff | </code></pre> |