From 7afc5c247d5c8cf99350fd6bc18e833f8a5ba2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Schneider?= Date: Wed, 7 Sep 2011 22:19:30 +0200 Subject: [PATCH] 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 --- TODO | 8 +++ mandaye/config.py | 40 +++++++----- mandaye/dispatchers/auth.py | 52 ++++++++++++++++ mandaye/dispatchers/default.py | 38 ++++++++++-- mandaye/dispatchers/vincennes.py | 101 +++++++++++++++---------------- mandaye/filters/vincennes.py | 12 ++++ mandaye/http.py | 4 +- mandaye/response.py | 53 +++++++++++++++- mandaye/server.py | 8 +-- mandaye/templates/response.html | 10 +++ mandaye.py => mandayectl | 0 setup.py | 5 +- 12 files changed, 246 insertions(+), 85 deletions(-) create mode 100644 TODO create mode 100644 mandaye/dispatchers/auth.py create mode 100644 mandaye/filters/vincennes.py create mode 100644 mandaye/templates/response.html rename mandaye.py => mandayectl (100%) diff --git a/TODO b/TODO new file mode 100644 index 0000000..1382258 --- /dev/null +++ b/TODO @@ -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 + diff --git a/mandaye/config.py b/mandaye/config.py index 87643a5..ee253dd 100644 --- a/mandaye/config.py +++ b/mandaye/config.py @@ -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 * diff --git a/mandaye/dispatchers/auth.py b/mandaye/dispatchers/auth.py new file mode 100644 index 0000000..e2e9df9 --- /dev/null +++ b/mandaye/dispatchers/auth.py @@ -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 diff --git a/mandaye/dispatchers/default.py b/mandaye/dispatchers/default.py index 1fb685a..13fc5a1 100644 --- a/mandaye/dispatchers/default.py +++ b/mandaye/dispatchers/default.py @@ -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 diff --git a/mandaye/dispatchers/vincennes.py b/mandaye/dispatchers/vincennes.py index 64e1b9a..51e44b6 100644 --- a/mandaye/dispatchers/vincennes.py +++ b/mandaye/dispatchers/vincennes.py @@ -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) diff --git a/mandaye/filters/vincennes.py b/mandaye/filters/vincennes.py new file mode 100644 index 0000000..ad56a52 --- /dev/null +++ b/mandaye/filters/vincennes.py @@ -0,0 +1,12 @@ + +import re + +def biblio_html_login_page(data, env): + """ data: response message body + env: Mandaye environment + """ + data = re.sub("", + "Connection via mon compte citoyen", + data) + return data + diff --git a/mandaye/http.py b/mandaye/http.py index 3b9bfb9..2de1313 100644 --- a/mandaye/http.py +++ b/mandaye/http.py @@ -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 diff --git a/mandaye/response.py b/mandaye/response.py index 6299033..9ee007e 100644 --- a/mandaye/response.py +++ b/mandaye/response.py @@ -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', {}, '') diff --git a/mandaye/server.py b/mandaye/server.py index 6bde271..58151ca 100644 --- a/mandaye/server.py +++ b/mandaye/server.py @@ -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 = '

%s

%s

%s
' % \ - (error_str, escape(path), escape(tb)) - _502.headers = HTTPHeader({'Content-Type': ['text/html']}) - return self.on_response(start_response, _502) + response = _502("

%s %s

%s
" % \ + (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: diff --git a/mandaye/templates/response.html b/mandaye/templates/response.html new file mode 100644 index 0000000..e1e749d --- /dev/null +++ b/mandaye/templates/response.html @@ -0,0 +1,10 @@ + + + + ${title} + + +

${title}

+

${body}

+ + diff --git a/mandaye.py b/mandayectl similarity index 100% rename from mandaye.py rename to mandayectl diff --git a/setup.py b/setup.py index b941695..4522a47 100644 --- a/setup.py +++ b/setup.py @@ -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' ], )