-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrestlite.py
484 lines (435 loc) · 27.1 KB
/
restlite.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
'''
restlite: REST + Python + JSON + XML + SQLite + authentication.
http://code.google.com/p/restlite
Copyright (c) 2009, Kundan Singh, kundan10@gmail.com. All rights reserved.
License: released under LGPL (Lesser GNU Public License).
This light-weight module allows quick prototyping of web services using the RESTful architecture and allows easy
integration with sqlite3 database, and JSON and XML representation format. The approach is to provide all the
appropriate tools which you can use to build your own application, instead of providing a intrusive framework.
Features:
1. Very lightweight module in pure Python and no other dependencies hence ideal for quick prototyping.
2. Two levels of API: one is not intrusive (for low level WSGI) and other is intrusive (for high level @resource).
3. High level API can conveniently use sqlite3 database for resource storage.
4. Common list and tuple-based representation that is converted to JSON and/or XML.
5. Supports pure REST as well as allows browser and Flash Player access (with GET, POST only).
6. Integrates unit testing using doctest module.
7. Handles HTTP cookies and authentication.
Dependencies: Python 2.6.
'''
from wsgiref.util import setup_testing_defaults
from xml.dom import minidom
import os, re, sys, sqlite3, json, Cookie, base64, md5, time, traceback
_debug = True
defaultType = 'application/json' # default content type if ACCEPT is */*. Used in represent and router.
#------------------------------------------------------------------------------
# REST router
#------------------------------------------------------------------------------
def router(routes):
'''This is the main low level REST router function that takes a list of routes and sequentially tries to match the
request method and URL pattern. If a valid route is matched, request transformation is applied. If an application
is specified for a route, then the (wsgiref) application is invoked and the response is returned. This is used
together with wsgiref.make_server to launch a RESTful service.
Your can use the routes to do several things: identify the response type (JSON, XML) from the URL, identify
some parts in the URL as variables available to your application handler, modify some HTTP header or message body
based on the URL, convert a GET or POST URL from the browser with URL suffix of /put or /delete to PUT or DELETE
URL to handle these commands from the browser, etc. For more details see the project web page.
>>> def files_handler(env, start_response):
... return '<files><type>' + env['ACCEPT'] + '</type><file>somefile.txt</file></files>'
>>> routes = [
... (r'GET,PUT,POST /xml/(?P<path>.*)$', 'GET,PUT,POST /%(path)s', 'ACCEPT=text/xml'),
... (r'GET /files$', files_handler) ]
>>> r = router(routes) # create the router using these routes
>>> # and test using the following code
>>> env, start_response = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/xml/files', 'SCRIPT_NAME': '', 'QUERY_STRING': ''}, lambda x,y: (x, y)
>>> print r(env, start_response)
<files><type>text/xml</type><file>somefile.txt</file></files>
'''
if isinstance(routes, dict) or hasattr(routes, 'items'): routes = routes.iteritems()
def handler(env, start_response):
setup_testing_defaults(env)
if 'wsgiorg.routing_args' not in env: env['wsgiorg.routing_args'] = dict()
env['COOKIE'] = Cookie.SimpleCookie()
if 'HTTP_COOKIE' in env: env['COOKIE'].load(env['HTTP_COOKIE'])
for route in routes:
method, pattern = route[0].split(' ', 1)
methods = method.split(',')
if env['REQUEST_METHOD'] not in methods: continue
path = env['PATH_INFO'] + ('?' + env['QUERY_STRING'] if env['QUERY_STRING'] else '')
match = re.match(pattern, path)
if match:
app = None
if callable(route[-1]):
route, app = route[:-1], route[-1] # found the app
if len(route) > 1:
new_methods, path = route[1].split(' ', 1)
env['REQUEST_METHOD'] = new_methods.split(',')[methods.index(env['REQUEST_METHOD'])]
env['PATH_INFO'], ignore, env['QUERY_STRING'] = (path % match.groupdict()).partition('?')
for name, value in [x.split('=', 1) for x in route[2:]]:
env[name] = value % match.groupdict()
env['wsgiorg.routing_args'].update(match.groupdict())
if app is not None:
matching = match.group(0)
env['PATH_INFO'], env['SCRIPT_NAME'] = env['PATH_INFO'][len(matching):], env['SCRIPT_NAME'] + env['PATH_INFO'][:len(matching)]
def my_response(status, headers):
if 'RESPONSE_HEADERS' not in env: env['RESPONSE_STATUS'], env['RESPONSE_HEADERS'] = status, headers
try: response = app(env, my_response)
except Status: response, env['RESPONSE_STATUS'] = None, str(sys.exc_info()[1])
except:
if _debug: print traceback.format_exc()
response, env['RESPONSE_STATUS'] = [traceback.format_exc()], '500 Internal Server Error'
if response is None: response = []
headers = env.get('RESPONSE_HEADERS', [('Content-Type', 'text/plain')])
orig = Cookie.SimpleCookie(); cookie = env['COOKIE']
if 'HTTP_COOKIE' in env: orig.load(env['HTTP_COOKIE'])
map(lambda x: cookie.__delitem__(x), [x for x in orig if x in cookie and str(orig[x]) == str(cookie[x])])
if len(cookie): headers.extend([(x[0], x[1].strip()) for x in [str(y).split(':', 1) for y in cookie.itervalues()]])
start_response(env.get('RESPONSE_STATUS', '200 OK'), headers)
if _debug:
if response: print headers, '\n'+str(response)[:256]
return response
start_response('404 Not Found', [('Content-Type', 'text/plain')])
return ['Use one of these URL forms\n ' + '\n '.join(str(x[0]) for x in routes)]
return handler
#------------------------------------------------------------------------------
# Representations: JSON, XML
#------------------------------------------------------------------------------
def tojson(value):
'''The function converts the supplied value to JSON representation. It assumes the unified list format of value.
Typically you just call represent(value, type=request['ACCEPT']) instead of manually invoking this method.
To be consistent with str(obj) function which uses obj.__str__() method if available, tojson() uses obj._json_()
method if available on value. Otherwise it checks obj._list_() method if available to get the unified list format.
Otherwise it assumes that the value is in unified list format. The _json_ and _list_ semantics allow you to
customize the JSON representation of your object, if needed.
>>> value = ('file', (('name', 'myfile.txt'), ('acl', [('allow', 'kundan'), ('allow', 'admin')])))
>>> tojson(value)
'{"file": {"name": "myfile.txt", "acl": [{"allow": "kundan"}, {"allow": "admin"}]}}'
'''
def list2dict(value):
if hasattr(value, '_json_') and callable(value._json_): return value._json_()
if hasattr(value, '_list_') and callable(value._list_): value = value._list_()
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], basestring):
if isinstance(value[1], list):
return {value[0]: [list2dict(x) for x in value[1]]}
elif isinstance(value[1], tuple) and not [x for x in value[1] if not isinstance(x, tuple) or len(x) != 2 or not isinstance(x[0], basestring)]:
return {value[0]: dict([(x[0], list2dict(x[1])) for x in value[1]])}
else:
return {value[0]: list2dict(value[1])}
elif isinstance(value, tuple) and not [x for x in value if not isinstance(x, tuple) or len(x) != 2 or not isinstance(x[0], basestring)]:
return dict([(x[0], list2dict(x[1])) for x in value])
elif isinstance(value, list):
return [list2dict(x) for x in value]
else:
return value
return json.dumps(list2dict(value))
def xml(value):
'''The function converts the supplied value to XML representation. It assumes the unified list format of value.
Typically you just call represent(value, type=request['ACCEPT']) instead of manually invoking this method.
To be consistent with str(obj) function which uses obj.__str__() method if available, xml() uses obj._xml_()
method if available on value. Otherwise it checks obj._list_() method if available to get the unified list format.
Otherwise it assumes that the value is in unified list format. The _xml_ and _list_ semantics allow you to
customize the XML representation of your object, if needed.
>>> value = ('file', (('name', 'myfile.txt'), ('acl', [('allow', 'kundan'), ('allow', 'admin')])))
>>> xml(value)
'<file><name>myfile.txt</name><acl><allow>kundan</allow><allow>admin</allow></acl></file>'
'''
if hasattr(value, '_xml_') and callable(value._xml_): return value._xml_()
if hasattr(value, '_list_') and callable(value._list_): value = value._list_()
if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], basestring):
if value[1] is None: return '<%s />'%(value[0])
else: return '<%s>%s</%s>'%(value[0], xml(value[1]), value[0])
elif isinstance(value, list) or isinstance(value, tuple):
return ''.join(xml(x) for x in value)
else:
return str(value) if value is not None else None
def prettyxml(value):
'''This function is similar to xml except that it invokes minidom's toprettyxml() function. Note that due to the
addition of spaces even in text nodes of prettyxml result, you cannot use this reliably for structured data
representation, and should use only for debug trace of XML.
'''
return minidom.parseString(xml(value)).toprettyxml().encode('utf-8')
def represent(value, type='*/*'):
'''You can use this method to convert a unified value to JSON, XML or text based on the type. The JSON representation
is preferred if type is default, otherwise the type values of "application/json", "text/xml" and
"text/plain" map to tojson, xml and str functions, respectively. If you would like to customize the representation of
your object, you can define _json_(), _xml_() and/or __str__() methods on your object. Note that _json_ and _xml_
fall back to _list_ if available for getting the unified list representation, and __str__ falls back to __repr__ if
available. The return value is a tuple containing type and value.
>>> class user:
... def __init__(self, name): self.name = name
... def _list_(self): return ('allow', self.name)
... def __str(self): return 'allow=' + self.name
>>> u1, u2 = user('kundan'), user('admin')
>>> value = ('file', (('name', 'myfile.txt'), ('acl', [u1, u2])))
>>> represent(value, type='application/json')[1]
'{"file": {"name": "myfile.txt", "acl": [{"allow": "kundan"}, {"allow": "admin"}]}}'
>>> represent(value, type='text/xml')[1]
'<file><name>myfile.txt</name><acl><allow>kundan</allow><allow>admin</allow></acl></file>'
'''
types = map(lambda x: x.lower(), re.split(r'[, \t]+', type))
if '*/*' in types: types.append(defaultType)
for type, func in (('application/json', tojson), ('text/xml', xml), ('text/plain', str)):
if type in types: return (type, func(value))
return ('application/octet-stream', str(value))
#------------------------------------------------------------------------------
# High Level API: @resources
#------------------------------------------------------------------------------
class Request(dict):
'''A request object is supplied to the resource definition in various methods: GET, PUT, POST, DELETE.
It is a dictionary containing env information. Additionally, all the matching attributes from the router are
stored as properties of this object, extracted from env['wsgiorg.routing_args'].'''
def __init__(self, env, start_response):
self.update(env.iteritems())
self.__dict__.update(env.get('wsgiorg.routing_args', {}))
self.start_response = start_response
def response(self, value, type=None):
type, result = represent(value, type if type is not None else self.get('ACCEPT', defaultType))
self.start_response('200 OK', [('Content-Type', type)])
return result
class Status(Exception):
'''The exception object that is used to throw HTTP response exception, e.g., raise Status, '404 Not Found'.
The resource definition can throw this exception.
'''
def resource(func):
'''A decorator to convert a function with nested function GET, PUT, POST and/or DELETE to a resource. The resource
object allows you to write applications in high-level semantics and translate it to wsgiref compatible handler that
is handled the router. The GET and DELETE methods take one argument (request) of type Request, whereas PUT and POST
take additional argument (first is request of type Request, and second is) entity extracted from message body.
Note that the function definition that is made as a resource, must have a "return locals()" at the end so that all
the methods GET, PUT, POST and/or DELETE are returned when function is called with no arguments.
>>> @resource
... def files():
... def GET(request):
... return represent(('files', [('file', 'myfile.txt')]), type='text/xml')[1]
... def PUT(request, entity):
... pass
... return locals()
>>> # test using the following code
>>> env, start_response = {'REQUEST_METHOD': 'GET', 'PATH_INFO': '/xml/files', 'SCRIPT_NAME': '', 'QUERY_STRING': ''}, lambda x,y: (x, y)
>>> print files(env, start_response)
['<files><file>myfile.txt</file></files>']
'''
method_funcs = func()
if method_funcs is None:
raise Status, '500 No "return locals()" in the definition of resource "%r"'%(func.__name__)
def handler(env, start_response):
if env['REQUEST_METHOD'] not in method_funcs:
raise Status, '405 Method Not Allowed'
req = Request(env, start_response)
if env['REQUEST_METHOD'] in ('GET', 'HEAD', 'DELETE'):
result = method_funcs[env['REQUEST_METHOD']](req)
elif env['REQUEST_METHOD'] in ('POST', 'PUT'):
if 'BODY' not in env:
try: env['BODY'] = env['wsgi.input'].read(int(env['CONTENT_LENGTH']))
except (TypeError, ValueError): raise Status, '400 Invalid Content-Length'
if env['CONTENT_TYPE'].lower() == 'application/json' and env['BODY']:
try: env['BODY'] = json.loads(env['BODY'])
except: raise Status, '400 Invalid JSON content'
result = method_funcs[env['REQUEST_METHOD']](req, env['BODY'])
return [result] if result is not None else []
return handler
def bind(obj):
'''Bind the given object to a resource. It returns a wsgiref compliant application for that resource.
Suppose an object obj={'kundan': user1, 'singh': user2} is bound to a resource '/users'
then GET, PUT, POST and DELETE are implemented on that obj as
'GET /users' returns the obj description with its properties and methods.
'GET /users/kundan' returns the user1 object description.
'PUT /users/kundan' replaces user1 with the supplied value.
'POST /users' adds a new property, attribute or list element.
'''
def handler(env, start_response):
current, result = obj, None
if env['REQUEST_METHOD'] == 'GET':
while env['PATH_INFO']:
print 'path=', env['PATH_INFO']
part, index = None, env['PATH_INFO'].find('/', 1)
if index < 0: index = len(env['PATH_INFO'])
part, env['SCRIPT_NAME'], env['PATH_INFO'] = env['PATH_INFO'][1:index], env['SCRIPT_NAME'] + env['PATH_INFO'][:index], env['PATH_INFO'][index:]
if not part: break
if current is None: raise Status, '404 Object Not Found'
try: current = current[int(part)] if isinstance(current, list) else current[part] if isinstance(current, dict) else current.__dict__[part] if hasattr(current, part) else None
except: print sys.exc_info(); raise Status, '400 Invalid Scope %r'%(part,)
if current is None: result = None
elif isinstance(current, list): result = [('url', '%s/%d'%(env['SCRIPT_NAME'], i,)) for i in xrange(len(current))]
elif isinstance(current, dict): result = tuple([(k, v if isinstance(v, basestring) else '%s/%s'%(env['SCRIPT_NAME'], k)) for k, v in current.iteritems()])
else:result = current
type, value = represent(('result', result), type=env.get('ACCEPT', 'application/json'))
start_response('200 OK', [('Content-Type', type)])
return [value]
else: raise Status, '405 Method Not Allowed'
return handler
#------------------------------------------------------------------------------
# Data Model with sqlite3
#------------------------------------------------------------------------------
class Model(dict):
'''A data model that abstracts the SQL table creation and uses sqlite3. Instead of defining a ORM (object-relation
mapping), this just lets the application handle the SQL commands. The only convenience of this class is to allow
creating the SQL tables using text description of the data model, define python class for each table that can
constructed using all the values of a row of that table, and define sql and sql1 convenience methods.
>>> desc = """
... user
... id integer primary key
... name text
...
... files
... id integer primary key
... name text not null
... owner int
... created datetime
... size int default 0
... foreign key (owner) references user(id)
... """
>>> m1 = Model()
>>> m1.create(desc)
>>> c1 = m1.sql('INSERT INTO user VALUES (NULL, ?)', ('Kundan Singh',))
>>> c2 = m1.sql('INSERT INTO user VALUES (NULL, ?)', ('Alok Singh',))
>>> row = m1.sql1('SELECT * FROM user WHERE id=?', (1,))
>>> u1 = m1['user'](*row)
>>> print u1
'id'=1, 'name'=u'Kundan Singh'
>>> print u1._list_()
('user', (('id', 1), ('name', u'Kundan Singh')))
>>> print 'table=%r attrs=%r properties=%r'%(u1.__class__._table_, u1.__class__._attrs_, u1.__dict__)
table='user' attrs=['id', 'name'] properties={'id': 1, 'name': u'Kundan Singh'}
'''
def __init__(self, conn=None):
'''Construct the model using optional sqlite3 connection. If missing, use a in-memory database.'''
if conn is None:
self.conn = sqlite3.connect(':memory:')
self.conn.isolation_level = None
else:
self.conn = conn
def close(self):
'''Close the connection with the database.'''
self.conn.close()
self.conn = None
def sql(self, *args):
'''Execute a single SQL command and return the cursor. For select commands application should use the
cursor as an iterator, or invoke fetchone or fetchall as applicable.'''
if _debug: print 'SQL: ' + ': '.join(map(str, args))
return self.conn.execute(*args)
def sql1(self, *args):
'''Execute a single SELECT SQL command and return a single row of the result.'''
return self.sql(*args).fetchone()
def create(self, data_model, createTable=True, createType=True):
'''Create the SQL tables using the data_model text description. An example text description is shown below. It
defines two tables, user and files. Note that the primary key of id must be defined as "integer" instead of
"int" or other variation for auto-increment of the id to work.
'''
# list of tuples (table-name, [list of attributes])
tables = [(x[0], [y.strip() for y in x[1:]]) for x in (z.split('\n') for z in re.split(r'\r?\n\r?\n', re.sub(r'[ \t]{2,}', ' ', '\n'.join(map(str.rstrip, data_model.strip().split('\n'))))))]
if createTable:
map(lambda t: self.sql("CREATE TABLE %s (%s)"%(t[0], ', '.join(t[1]))), tables)
if createType:
for name, attrs in tables:
class klass(object):
_defn_ = [(y, z) for y, z in (x.split(' ', 1) for x in attrs) if y.lower() not in ('foreign', 'primary', 'key')]
__doc__ = name + '\n ' + '\n '.join(['%s\t%s'%(x, y) for x, y in _defn_])
_table_, _attrs_, _defn_ = name, [x for x, y in _defn_], [y for x, y in _defn_]
def __init__(self, *args, **kwargs):
keys = self.__class__._attrs_
for x in keys: self.__dict__[x] = None
for x, y in zip(keys[:len(args)], args): self.__dict__[x] = y
for k, v in kwargs.iteritems(): self.__dict__[k] = v
def __str__(self):
return ', '.join(['%r=%r'%(x, self.__dict__[x]) for x in self.__class__._attrs_ if x in self.__dict__])
def _list_(self):
return (self.__class__._table_, tuple((k, self.__dict__[k]) for k in self.__class__._attrs_ if k in self.__dict__))
self[name] = klass
#------------------------------------------------------------------------------
# Authentication
#------------------------------------------------------------------------------
_loginTable = '''
user_login
id integer primary key
email text not null
realm text not null
hash tinyblob(32) not null
token tinyblob(32)
'''
class AuthModel(Model):
'''Authenticated Model class, which creates a database table of type user_login and uses that to provide various
authentication methods.'''
def __init__(self, conn=None):
Model.__init__(self, conn)
self.mypass = md5.new(str(id(self)) + str(time.time())).hexdigest()
self.create(_loginTable)
def hash(self, email, realm, password):
return md5.new('%s:%s:%s'%(email, realm, password)).hexdigest()
def token(self, user_id):
tm = '%010x'%(int(time.time()),)
return md5.new(self.mypass + str(user_id) + tm).hexdigest() + tm
def valid(self, user_id, token):
hash, tm = token[:-10], token[-10:]
return md5.new(self.mypass + str(user_id) + tm).hexdigest() == hash
def register(self, email, realm, password):
self.sql('INSERT INTO user_login VALUES (NULL, ?, ?, ?, NULL)', (email, realm, self.hash(email, realm, password)))
def login(self, request):
hdr = request.get('HTTP_AUTHORIZATION', None)
if hdr:
method, value = map(str.strip, hdr.split(' ', 1))
if method == 'Basic':
email, password = base64.b64decode(value).split(':', 1)
found = self.sql1('SELECT id, hash FROM user_login WHERE email=?', (email,))
if not found:
request.start_response('401 Unauthorized', [('WWW-Authenticate', 'Basic realm="%s"'%('localhost',))])
raise Status, '401 Not Found'
user_id, hash = found;
realm = "localhost" # TODO: implement this
hash_recv = self.hash(email, realm, password)
if hash != hash_recv:
request.start_response('401 Unauthorized', [('WWW-Authenticate', 'Basic realm="%s"'%(realm,))])
raise Status, '401 Unauthorized'
token = self.token(user_id)
self.sql('UPDATE user_login SET token=? WHERE id=?', (token, user_id))
request['COOKIE']['token'] = token; request['COOKIE']['token']['path'] = '/'
request['COOKIE']['user_id'] = user_id; request['COOKIE']['user_id']['path'] = '/'
return (user_id, email, token)
elif (hasattr(request, 'user_id') or hasattr(request, 'email')) and hasattr(request, 'token'):
if request.email == 'admin':
adminhash = md5.new('%s::%s'%(request.email, self.mypass)).hexdigest()
print request.token, adminhash
if adminhash != request.token: raise Status, '401 Not Authorized'
user_id, email, token = 0, request.email, adminhash
else:
found = self.sql1('SELECT id, email, token FROM user_login WHERE (id=? OR email=?) AND (token=? OR hash=?)', (request.user_id, request.email, request.token, request.token))
if not found:
if not self.sql1('SELECT id FROM user_login WHERE id=? OR email=?', (request.user_id, request.email)):
raise Status, '404 Not Found'
else:
raise Status, '401 Unauthorized'
user_id, email, token = int(found[0]), found[1], found[2]
if token != request.token:
token = self.token(user_id)
self.sql('UPDATE user_login SET token=? WHERE id=?', (token, user_id))
request['COOKIE']['token'] = token; request['COOKIE']['token']['path'] = '/'
request['COOKIE']['user_id'] = user_id; request['COOKIE']['user_id']['path'] = '/'
return (user_id, email, token)
elif 'COOKIE' in request and 'user_id' in request['COOKIE'] and 'token' in request['COOKIE']:
user_id, token = int(request['COOKIE'].get('user_id').value), request['COOKIE'].get('token').value
if user_id == 0:
email = 'admin'; hash = md5.new('%s::%s'%(email, self.mypass)).hexdigest()
if hash != token:
raise Status, '401 Not Authorized as Admin'
else:
found = self.sql1('SELECT email FROM user_login WHERE id=? AND token=?', (user_id, token))
if not found:
request['COOKIE']['token']['expires'] = 0
raise Status, '404 Not Found'
email = found[0]
return (user_id, email, token)
else:
realm = "localhost"
request.start_response('401 Unauthorized', [('WWW-Authenticate', 'Basic realm="%s"'%(realm,))])
raise Status, '401 Unauthorized'
def logout(self, request):
if 'COOKIE' in request and 'user_id' in request['COOKIE'] and 'token' in request['COOKIE']:
user_id, token, request['COOKIE']['token']['expires'] = request['COOKIE']['user_id'].value, request['COOKIE']['token'].value, 0
if user_id != 0:
self.sql('UPDATE user_login SET token=NULL WHERE id=? AND token=?', (user_id, token))
#------------------------------------------------------------------------------
# Test and Examples
#------------------------------------------------------------------------------
if __name__ == '__main__':
import doctest
_debug = False
doctest.testmod()