New dispatcher for form authentification, improve mapping support,

add templates support and big cleaning

 * TODO: new file
 * mandaye/config.py: change next_url for biblio mapping and add local
 configuration support
 * mandaye/dispatchers/auth.py: rename authentification dispatcher
 * mandaye/dispatchers/default.py: improve the default dispatcher
 * mandaye/dispatchers/vincennes.py: login through the compte citoyen
 * mandaye/filters/vincennes.py: add a login biblio html filter
 * mandaye/http.py: fix HTTPResponse
 * mandaye/response.py: improve the responses
 * mandaye/server.py: cleaning
 * mandaye/templates/response.html: html template for the responses
 * setup.py: add breaker, sqlalchemy and mako
This commit is contained in:
Jérôme Schneider 2011-09-07 22:19:30 +02:00
parent 2b114abe74
commit 7afc5c247d
12 changed files with 246 additions and 85 deletions

8
TODO Normal file
View File

@ -0,0 +1,8 @@
* Improve README.rst
* Move the default filters into the mapping
* Add a debug mode
* Mapping should match the url
* Manage a folder with the configurations
* Finish authentification management

View File

@ -1,20 +1,23 @@
from dispatchers.default import Dispatcher
from dispatchers.vincennes import AuthFormDispatcher
#from filters.vincennes import biblio_html_login_page
# Mandaye configuration
host = "127.0.0.1"
port = 8088
# Needed if ssl is activate
ssl = False
keyfile = "/home/jschneider/wildcard.entrouvert.org.key"
certfile = "/home/jschneider/wildcard.entrouvert.org.crt"
keyfile = ""
certfile = ""
# Template directory
template_directory = "mandaye/templates"
# Database configuration
# Follow the rfc 1738 http://rfc.net/rfc1738.html
db_url = 'sqlite:///test.db'
from filters.vincennes import biblio_html_login_page
from filters.default import MandayeFilter
biblio_mapping = {
'/mandaye/login': {
'method': 'GET',
@ -38,19 +41,23 @@ biblio_mapping = {
'dispatcher_method': 'connection',
'values': {
'destination': 'https://test.vincennes.fr/comptecitoyen/auth',
'next_url': 'http://biblio.local:8088/'
'next_url': 'http://biblio.local:8088/mandaye/login',
'service_name': 'biblio'
}
}
},
#'/sezhame/page/connexion-abonne': {
# 'method': 'GET',
# 'on_response_filter': {
# 'type': 'html',
# 'filter': biblio_html_login_page
# }
# },
'/sezhame/page/connexion-abonne': {
'method': 'GET',
'on_response_filters': [{
'type': 'data',
'filter': biblio_html_login_page
},]
},
}
from dispatchers.default import Dispatcher
from dispatchers.vincennes import VincennesDispatcher
hosts = {
'sfd.local:8088': [
(r'/', Dispatcher('http://www.sfdiabete.org')),
@ -59,7 +66,7 @@ hosts = {
(r'/', Dispatcher('http://linuxfr.org')),
],
'biblio.local:8088': [
(r'/', AuthFormDispatcher('http://biblio.vincennes.fr', biblio_mapping)),
(r'/', VincennesDispatcher('http://biblio.vincennes.fr', biblio_mapping)),
],
'localhost:8088': [
(r'/', Dispatcher('http://localhost')),
@ -69,6 +76,7 @@ hosts = {
session_opts = {
'session.type': 'file',
'session.cookie_expires': True,
}
}
from mandaye.local_config import *

View File

@ -0,0 +1,52 @@
"""
Dispatcher for basic auth form authentifications
"""
import Cookie
import httplib
import urllib
from mandaye.dispatchers.default import Dispatcher
from mandaye.response import _502, _302, _401
class AuthFormDispatcher(Dispatcher):
def __init__(self, target_url, mapping=None):
super(AuthFormDispatcher, self).__init__(target_url, mapping)
self.local_auth = None
self.dest_auth = None
def login(self, values, request):
""" Automatic login on a site with a form
"""
login = self.local_auth.get_current_login()
if not login:
return _401('Invalid login')
if not values.has_key('form_action') \
or not values.has_key('form_values') \
or not values.has_key('username_field') \
or not values.has_key('password_field'):
logging.warning('Invalid values for AuthFormDispatcher.login')
return _502
headers = values.get('form_headers')
if not headers:
headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
params = values['form_values']
# TODO: find the login / password in the destaut or redirect
params[values['username_field']] = '754443'
params[values['password_field']] = 'lecteur'
params = urllib.urlencode(params)
conn = httplib.HTTPConnection(self.target.hostname)
conn.request("POST", values.get('form_action'), params, headers)
res = conn.getresponse()
cookies = Cookie.BaseCookie(res.msg.getheader('set-cookie'))
cookies = self.filter.response_cookies(cookies)
location = res.msg.getheader('location')
if location:
location = location.replace(self.target.hostname, self.env["HTTP_HOST"])
conn.close()
# TODO: Test if he is login or not
return _302(location, cookies)
def connection(self, values, response):
pass

View File

@ -1,9 +1,9 @@
import logging
from urlparse import urlparse
from mandaye.filters.default import MandayeFilter
from mandaye.response import _502
# TODO: add an external url mapping
@ -39,13 +39,14 @@ class Dispatcher(object):
if hook and hook.has_key('dispatcher_method'):
try:
method = getattr(self, hook['dispatcher_method'])
except AttributeError:
logging.warning('%s: Invalid dispatcher method for hook %s' % \
(self.env['PATH_INFO'], hookname))
else:
values = hook.get('values')
if not values:
values = dict()
return method(values, *args)
except AttributeError:
logging.warning('%s: Invalid dispatcher method for hook %s' % \
(self.env['PATH_INFO'], hookname))
return None
def get_target_url(self):
@ -62,17 +63,32 @@ class Dispatcher(object):
return url
def get_response(self, request):
response = self._call_dispatcher_hook('response')
# TODO: test if response is not empty
""" Called if you have a response hook for this request
"""
response = self._call_dispatcher_hook('response', request)
if not response:
return _500("The response dispatcher hook failed for URL %s" %\
self.env["PATH_INFO"])
return response
def mod_request(self, request):
""" Modify the request
request: MandayeRequest object with cookies and headers
Return the request object """
# Loading defaults filters
request.headers = self.filter.request_headers(request.headers)
request.cookies = self.filter.request_cookies(request.cookies)
request.msg = self.filter.request_msg(request.msg)
# Loading specific filters
if self.req_mapping and self.req_mapping.has_key('on_request_filters'):
for filter in self.req_mapping['on_request_filters']:
if filter['type'] == "data":
request.msg = filter['filter'](request.msg, self.env)
elif filter['type'] == "cookies":
request.cookies = filter['filter'](request.cookies, self.env)
elif filter['type'] == "headers":
request.headers = filter['filter'](request.headers, self.env)
# Calling hook function
new_request = self._call_dispatcher_hook('on_request_hook', request)
if new_request:
return new_request
@ -83,9 +99,19 @@ class Dispatcher(object):
request: the Mandaye request
response: MandayeResponse object with cookies, headers and HTML
you can modify the cookies and the HTTP headers """
# Loading defaults filters
response.headers = self.filter.response_headers(response.headers)
response.cookies = self.filter.response_cookies(response.cookies)
response.msg = self.filter.response_msg(response.msg)
# Loading specific filters
if self.req_mapping and self.req_mapping.has_key('on_response_filters'):
for filter in self.req_mapping['on_response_filters']:
if filter['type'] == "data":
response.msg = filter['filter'](response.msg, self.env)
elif filter['type'] == "cookies":
response.cookies = filter['filter'](response.cookies, self.env)
elif filter['type'] == "headers":
response.headers = filter['filter'](response.headers, self.env)
new_response = self._call_dispatcher_hook('on_response_hook', request, response)
if new_response:
return new_response

View File

@ -1,65 +1,60 @@
"""
Dispatcher for basic auth form authentification
Dispatcher for the Vincennes authentifications
"""
import base64
from mandaye.dispatchers.default import Dispatcher
from Crypto.Cipher import AES
from mandaye.dispatchers.auth import AuthFormDispatcher
from mandaye.response import _502, _302
class BiblioAuthForm(object):
class VincennesAuth(object):
def __init__(self, target):
self.target = target
def __init__(self, env):
self.env = env
def is_auth(self, response):
if "/sezhame/logout" in response.msg:
return True
return False
def _parse_qs(self, query):
""" Parse query string
Return a dict
"""
res = {}
values = query.split('&')
for value in values:
keyvalue = value.split('=', 1)
res[keyvalue[0]] = keyvalue[1]
return res
def auth(self, login, request):
import urllib, httplib
import cookielib
values = { 'user': "754443",
'password': "XXXXX",
'op': "Se connecter",
'form_id': 'dk_opac15_login_form' }
action = "/sezhame/page/connexion-abonne"
#url = 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne'
params = urllib.urlencode(values)
params = 'user=754443&password=lecteur&op=Se+connecter&form_id=dk_opac15_login_form'
headers = { 'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Cookie': request.cookies.output(header="", sep=",")
}
conn = httplib.HTTPConnection(self.target.hostname)
conn.request("POST", action, params, headers)
res = conn.getresponse()
return res.msg.getheaders('set-cookie')
def get_current_login(self):
""" Return the current Vincennes pseudo
"""
from mandaye import config
# TODO: test time validity
if not self.env['QUERY_STRING']:
return None
query = self._parse_qs(self.env['QUERY_STRING'])
if query.has_key('token'):
# TODO: catch exceptions
token = query['token']
token = base64.b64decode(token)
cipher = AES.new(config.secret, AES.MODE_CFB)
decode = cipher.decrypt(token)
info = eval(decode[16:])
return info['pseudo']
return None
class AuthFormDispatcher(Dispatcher):
class VincennesDispatcher(AuthFormDispatcher):
def __init__(self, target_url, mapping=None):
super(AuthFormDispatcher, self).__init__(target_url, mapping)
#self.local_auth = LocalAuthForm(self.target)
#self.dest_auth = DestAuthForm(self.target)
def login(self, values):
from mandaye.response import _502
return _502
#id = self.local_auth.get_current_user():
#action = "/sezhame/page/connexion-abonne"
#url = 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne'
#params = urllib.urlencode(values)
#params = 'user=754443&password=lecteur&op=Se+connecter&form_id=dk_opac15_login_form'
#headers = { 'Content-Type': 'application/x-www-form-urlencoded',
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Cookie': request.cookies.output(header="", sep=",")
# }
#conn = httplib.HTTPConnection(self.target.hostname)
#conn.request("POST", action, params, headers)
#res = conn.getresponse()
#conn.close()
def connection(self, request, response):
pass
def init(self, env):
""" overload the init method to change the backends
"""
super(VincennesDispatcher, self).init(env)
self.local_auth = VincennesAuth(env)
def connection(self, values, request):
""" Connection to the compte citoyen
"""
location = values.get('destination')
location += "?next_url=%s&service=%s" % \
(values.get('next_url'), values.get('service_name'))
return _302(location)

View File

@ -0,0 +1,12 @@
import re
def biblio_html_login_page(data, env):
""" data: response message body
env: Mandaye environment
"""
data = re.sub("</center></div>",
"<a href=\"/mandaye/connection\">Connection via mon compte citoyen</center></div>",
data)
return data

View File

@ -76,11 +76,13 @@ class HTTPResponse(object):
"""
self.code = code
self.status = status
self.headers = headers
self.msg = msg
self.cookies = cookies
if not headers:
self.headers = HTTPHeader()
if not cookies:
self.cookies = Cookie.BaseCookie()
self.msg = msg
def load_from_urllib(self, response):
""" response: urrlib.addinfourl

View File

@ -1,5 +1,52 @@
from mandaye.http import HTTPResponse
from mako.lookup import TemplateLookup
from mako.template import Template
from mandaye.config import template_directory
from mandaye.http import HTTPResponse, HTTPHeader
mylookup = TemplateLookup(directories=[template_directory])
def serve_template(templatename, **kwargs):
mytemplate = mylookup.get_template(templatename)
return mytemplate.render(**kwargs)
def _302(location, cookies=None):
headers = HTTPHeader({'Location': [location],
'Content-Type': ['text/html']})
msg = "Reload the page to get source for: %s" % location
return HTTPResponse(302, 'Found', headers, msg, cookies)
def _401(msg):
title='Unauthorized'
headers = HTTPHeader({'Content-Type': ['text/html']})
return HTTPResponse(401, 'Unauthorized', headers,
serve_template("response.html", title=title, body=msg))
def _404(url):
title = "Not Found"
body = "The requested URL %s was not found on this server." % url
headers = HTTPHeader({'Content-Type': ['text/html']})
return HTTPResponse(404, 'Not found', headers,
serve_template("response.html", title=title, body=body))
def _500(msg=None):
if not msg:
msg = "The server encountered an internal error or misconfiguration \
and was unable to completeyour request.Please contact the server \
administrator and inform them of the time the error occurred, and \
anything you might have done that may have caused the error. \
More information about this error may be availablein the server error log."
title = 'Internal Server Error'
headers = HTTPHeader({'Content-Type': ['text/html']})
return HTTPResponse(500, 'Internal Server Error', headers,
serve_template("response.html", title=title, body=msg))
def _502(msg):
title = 'Bad Gateway'
headers = HTTPHeader({'Content-Type': ['text/html']})
return HTTPResponse(502, 'Bad Gateway', headers,
serve_template("response.html", title=title, body=msg))
_404 = HTTPResponse(404, 'Not found', {}, '')
_502 = HTTPResponse(502, 'Bad Gateway', {}, '')

View File

@ -103,10 +103,9 @@ class MandayeApp(object):
traceback.print_exc()
tb = traceback.format_exc()
error_str = escape(str(ex) or ex.__class__.__name__ or 'Error')
_502.content = '<h1>%s</h1><h2>%s</h2><pre>%s</pre>' % \
(error_str, escape(path), escape(tb))
_502.headers = HTTPHeader({'Content-Type': ['text/html']})
return self.on_response(start_response, _502)
response = _502("<h2>%s %s</h2><pre>%s</pre>" % \
(escape(path), error_str, escape(tb)))
return self.on_response(start_response, response)
else:
mresponse = HTTPResponse()
mresponse.load_from_urllib(response)
@ -129,7 +128,6 @@ def serve():
keyfile=config.keyfile, certfile=config.certfile)
else:
s = WSGIServer((config.host, config.port), wsgi_app)
s.serve_forever()
try:
s.serve_forever()
except KeyboardInterrupt:

View File

@ -0,0 +1,10 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html>
<head>
<title>${title}</title>
</head>
<body>
<h1>${title}</h1>
<p>${body}</p>
</body>
</html>

View File

@ -17,11 +17,14 @@ setup(name="mandaye",
author_email="info@entrouvert.org",
maintainer="Jerome Schneider",
maintainer_email="jschneider@entrouvert.com",
scripts=['mandaye.py'],
scripts=['mandayectl'],
packages=find_pacakges(),
package_data={},
install_requires=[
'beaker>=1.5',
'gevent>=0.13',
'mako>=0.3',
'poster>=0.8',
'sqlachemy>=0.6'
],
)