diff --git a/mandaye/__init__.py b/mandaye/__init__.py index 2e94ead..38e5693 100644 --- a/mandaye/__init__.py +++ b/mandaye/__init__.py @@ -1 +1,15 @@ VERSION=0.1 + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from mandaye.config import db_url + +# an Engine, which the Session will use for connection +# resources +sql_engine = create_engine(db_url) +# create a configured "Session" class +Session = sessionmaker(bind=sql_engine) +# create a Session +sql_session = Session() + diff --git a/mandaye/auth/authform.py b/mandaye/auth/authform.py index bbd201e..57b6cf5 100644 --- a/mandaye/auth/authform.py +++ b/mandaye/auth/authform.py @@ -1,4 +1,3 @@ -import ipdb """ Dispatcher for basic auth form authentifications """ @@ -9,7 +8,10 @@ import urllib from urlparse import parse_qs -from mandaye.http import HTTPResponse +import mandaye + +from mandaye.models import Site, ExtUser, LocalUser +from mandaye.http import HTTPResponse, HTTPHeader from mandaye.response import _500, _302, _401 from mandaye.response import template_response @@ -47,42 +49,71 @@ class AuthForm(object): """ return template_response(values['template'], values) - def associate_submit(self, env, values, request, condition=None): + def associate_submit(self, env, values, condition, request, response): + """ Associate your login / password with Mandaye """ - """ - session = env['beaker.session'] + pseudo = env['beaker.session'].get('pseudo') if request.msg: post = parse_qs(request.msg.read()) - response = self._replay(env, post['username'][0], post['password'][0]) + replay_response = self._replay(env, post['username'][0], post['password'][0]) if eval(condition): - if not session.has_key('pseudo'): - # Find a way to put this in the configuration - return _302("/mandaye/connection") + if not pseudo: + return _302(values.get('connection_url')) else: - # TODO associate this login / password in the db - pass - return template_response(values['template'], values) + site = mandaye.sql_session.query(Site).\ + filter_by(name=values.get('site_name')).first() + if not site: + site = Site(values.get('site_name')) + mandaye.sql_session.add(site) + local_user = mandaye.sql_session.query(LocalUser).\ + filter_by(token=pseudo).first() + if not local_user: + local_user = LocalUser(token=pseudo) + mandaye.sql_session.add(local_user) + ext_user = mandaye.sql_session.query(ExtUser).join(LocalUser).\ + filter(LocalUser.token==pseudo).first() + if not ext_user: + ext_user = ExtUser() + mandaye.sql_session.add(ext_user) + ext_user.login = post['username'][0] + ext_user.password = post['password'][0] + ext_user.local_user = local_user + mandaye.sql_session.commit() + return _302(values.get('connection_url')) + + return response def check_auth(self, env, values, request=None): """ values: dict witth username, login """ pass - def login(self, env, values, request): + def login(self, env, values, condition, request): """ Automatic login on a site with a form """ login = self.local_auth.get_current_login(env) if not login: return _401('Access denied: invalid token') - #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'): - # return _500(env['PATH_INFO'], - # 'Invalid values for AuthFormDispatcher.login') - # TODO: Test get the username / password here - #response = self._replay(env, - #return _302(location, cookies) + ext_user = mandaye.sql_session.query(ExtUser).\ + join(LocalUser).\ + filter(LocalUser.token==login).\ + first() + if not ext_user: + return _302(values.get('associate_url')) + if not ext_user.login or not ext_user.password: + return _500(env['PATH_INFO'], + 'Invalid values for AuthFormDispatcher.login') + response = self._replay(env, ext_user.login, ext_user.password) + headers = HTTPHeader() + headers.load_from_list(response.getheaders()) + cookies = Cookie.BaseCookie(response.msg.getheader('set-cookie')) + response = HTTPResponse(response.status, response.reason, headers, + response.read(), cookies) + if condition and eval(condition): + return response + else: + # TODO; find a better solution + return _302(values.get('associate_url')) def connection(self, values, response): pass diff --git a/mandaye/auth/vincennes.py b/mandaye/auth/vincennes.py index e5f82df..1aeea06 100644 --- a/mandaye/auth/vincennes.py +++ b/mandaye/auth/vincennes.py @@ -6,7 +6,7 @@ import traceback from Crypto.Cipher import AES -from mandaye.dispatchers.auth import AuthFormDispatcher +#from mandaye.dispatchers.auth import AuthFormDispatcher from mandaye.response import _502, _302 class VincennesAuth(object): @@ -44,7 +44,6 @@ class VincennesAuth(object): session = env['beaker.session'] session['pseudo'] = info['pseudo'] session.save() - print info['pseudo'] return info['pseudo'] except Exception, e: if config.debug: @@ -55,8 +54,10 @@ class VincennesAuth(object): def connection(self, env, values, request): """ Connection to the compte citoyen """ + dest_url = "%s://%s%s" % (env['wsgi.url_scheme'], env['HTTP_HOST'], + values.get('next_url')) location = "%s?next_url=%s&service=%s" % \ - (self.url, values.get('next_url'), values.get('service_name')) + (self.url, dest_url, values.get('service_name')) return _302(location) diff --git a/mandaye/configs/__init__.py b/mandaye/configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mandaye/configs/biblio_vincennes.py b/mandaye/configs/biblio_vincennes.py new file mode 100644 index 0000000..c98c976 --- /dev/null +++ b/mandaye/configs/biblio_vincennes.py @@ -0,0 +1,78 @@ + +from mandaye.filters.vincennes import biblio_html, biblio_html_login_page +from mandaye.filters.vincennes import biblio_associate_login, biblio_associate_req +from mandaye.filters.default import MandayeFilter + +from mandaye.configs.vincennes import auth_vincennes, authform + + +biblio_mapping = [ + { + 'path': r'/', + 'on_response': [{'filter': biblio_html}] + }, + { + 'path': r'/mandaye/login$', + 'method': 'GET', + 'response': { + 'filter': authform.login, + 'values': { 'associate_url': '/mandaye/associate' }, + 'condition': 'response.code==302', + }, + }, + { + 'path': r'/mandaye/associate$', + 'method': 'GET', + 'target': 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne', + 'on_response': [{ + 'filter': biblio_associate_login, + 'values': { + 'action': '/mandaye/associate', + 'template': 'biblio_associate.html', + }, + }] + }, + { + 'path': r'/mandaye/associate$', + 'method': 'POST', + 'target': 'http://biblio.vincennes.fr/sezhame/page/connexion-abonne', + 'on_request': [{'filter': biblio_associate_req}], + 'on_response': [ + { + 'filter': authform.associate_submit, + 'values': { + 'site_name': 'biblio', + 'connection_url': '/mandaye/connection' + }, + 'condition': "replay_response.status==302" + }, + { + 'filter': biblio_associate_login, + 'values': { + 'action': '/mandaye/associate', + 'template': 'biblio_associate.html', + 'error_msg': "Mauvais numéro ou mot de passe", + }, + }, + ] + }, + { + 'path': r'/mandaye/connection$', + 'method': 'GET', + 'response': { + 'filter': auth_vincennes.connection, + 'values': { + 'destination': 'https://test.vincennes.fr/comptecitoyen/auth', + 'next_url': '/mandaye/login', + 'service_name': 'biblio' + } + } + }, + { + 'path': r'/sezhame/page/connexion-abonne$', + 'method': 'GET', + 'on_response': [{'filter': biblio_html_login_page}] + }, +] + + diff --git a/mandaye/configs/vincennes.py b/mandaye/configs/vincennes.py new file mode 100644 index 0000000..b9863fb --- /dev/null +++ b/mandaye/configs/vincennes.py @@ -0,0 +1,16 @@ +from mandaye.auth.authform import AuthForm +from mandaye.auth.vincennes import VincennesAuth + +form_values = { + 'form_action': '/sezhame/page/connexion-abonne', + 'from_headers': { 'Content-Type': 'application/x-www-form-urlencoded' }, + 'form_values': { 'user': '', + 'password': '', + 'op': 'Se connecter', + 'form_id': 'dk_opac15_login_form' }, + 'username_field': 'user', + 'password_field': 'password' +} + +auth_vincennes = VincennesAuth('https://test.vincennes.fr/comptecitoyen/auth') +authform = AuthForm(auth_vincennes, **form_values) diff --git a/mandaye/dispatcher.py b/mandaye/dispatcher.py index cc7178a..f19098e 100644 --- a/mandaye/dispatcher.py +++ b/mandaye/dispatcher.py @@ -27,11 +27,10 @@ class Dispatcher(object): """ parse the mapping on every request """ req_mapping = { - 'on_request_hook': None, - 'on_request_filters': [], - 'on_response_hook': None, - 'on_response_filters': [], + 'on_request': [], + 'on_response': [], 'response': None, + 'target': None } for conf in mapping: @@ -47,12 +46,17 @@ class Dispatcher(object): req_mapping[hookname] = conf[hookname] return req_mapping - def _call_dispatcher_hook(self, hook, *args): - if hook and hook.has_key('method'): + def _call_hook(self, hook, *args): + if hook and hook.has_key('filter'): values = hook.get('values') if not values: values = dict() - return hook['method'](self.env, values, *args) + if hook.has_key('condition'): + return hook['filter'](self.env, values, hook['condition'], *args) + else: + return hook['filter'](self.env, values, *args) + else: + logging.warning("%s hook failed (no filter option)" % self.env['PATH_INFO']) return None def get_target_url(self): @@ -61,8 +65,10 @@ class Dispatcher(object): # Disable reverse proxy if we have a static response if self.req_mapping['response']: return None - # TODO: manage redirect here - url = self.target.geturl() + self.env['PATH_INFO'] + if self.req_mapping['target']: + url = self.req_mapping['target'] + else: + url = self.target.geturl() + self.env['PATH_INFO'] if self.env['QUERY_STRING']: return url + "?" + self.env['QUERY_STRING'] else: @@ -71,18 +77,12 @@ class Dispatcher(object): def get_response(self, request): """ Called if you have a response hook for this request """ - if self.req_mapping['response'].has_key('condition'): - response = self._call_dispatcher_hook(self.req_mapping['response'], - request, self.req_mapping['response']['condition']) - else: - response = self._call_dispatcher_hook(self.req_mapping['response'], + response = self._call_hook(self.req_mapping['response'], request) if not response: return _500(self.env["PATH_INFO"], "The response hook failed") # 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) + response = self.filter.on_response(response) return response def mod_request(self, request): @@ -90,23 +90,16 @@ class Dispatcher(object): 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 - 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) + request = self.filter.on_request(request) # Calling hook function - if self.req_mapping['on_request_hook']: - return self._call_dispatcher_hook( - self.req_mapping['on_request_hook'], request) + for hook in self.req_mapping['on_request']: + new_request = self._call_hook(hook, request) + if new_request: + request = new_request + else: + logging.warning("%s On_request hook %s failed" % (self.env['PATH_INFO'], + self.req_mapping['on_request']['filter'])) return request def mod_response(self, request, response): @@ -116,23 +109,16 @@ class Dispatcher(object): 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 - 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) + response = self.filter.on_response(response) # Calling hook function - if self.req_mapping['on_response_hook']: - return self._call_dispatcher_hook( - self.req_mapping['on_response_hook'], request, response) + for hook in self.req_mapping['on_response']: + new_response = self._call_hook(hook, request, response) + if new_response: + response = new_response + else: + logging.warning("%s On_response hook %s failed" % (self.env['PATH_INFO'], + self.req_mapping['on_response']['filter'])) return response diff --git a/mandaye/filters/default.py b/mandaye/filters/default.py index 2d259ee..bb61be4 100644 --- a/mandaye/filters/default.py +++ b/mandaye/filters/default.py @@ -14,57 +14,28 @@ class MandayeFilter(object): self.env = env self.target = target - def request_cookies(self, cookies): - """ cookies: a Cookie.BaseCookie - http://docs.python.org/library/cookie.html - return a Cookie.BaseCookie - """ - return cookies - - def request_headers(self, headers): - """ headers: HTTPHeader instance - return HTTPHeader object - """ - referer = headers.getheader('referer') + def on_request(self, request): + referer = request.headers.getheader('referer') if referer: - referer = referer.replace(self.self.env["HTTP_HOST"], self.target.hostname) - headers.addheader("Referer", referer) - return headers + referer = referer.replace(self.env["HTTP_HOST"], self.target.hostname) + request.headers.addheader("Referer", referer) + return request - def request_msg(self, msg): - """ msg: request message body - return the modify message - """ - return msg - - def response_cookies(self, cookies): - """ cookies: a Cookie.BaseCookie object - return a Cookie.BaseCookie - """ + def on_response(self, response): + # Modify cookies src_hostname = re.sub(":\d*$", "", self.env["HTTP_HOST"]) - for cookie in cookies.values(): + for cookie in response.cookies.values(): if cookie.has_key('domain'): cookie['domain'] = cookie['domain'].replace(self.target.hostname, src_hostname) - return cookies - - def response_headers(self, headers): - """ headers: HTTPHeader instance - return a HTTPHeader object - """ + # Modify headers blacklist = ['transfer-encoding', 'content-length'] - for key, value in headers.iteritems(): + for key, value in response.headers.iteritems(): if key == 'location': location = value[0].replace(self.target.hostname, self.env["HTTP_HOST"]) - headers.addheader(key, location) + response.headers.addheader(key, location) for name in blacklist: - if headers.has_key(name): - del headers[name] - return headers - - def response_msg(self, msg): - """ msg: response message body - return the modify message """ - return msg - + if response.headers.has_key(name): + del response.headers[name] + return response diff --git a/mandaye/filters/vincennes.py b/mandaye/filters/vincennes.py index f8c8daf..a67a292 100644 --- a/mandaye/filters/vincennes.py +++ b/mandaye/filters/vincennes.py @@ -1,18 +1,39 @@ import re -def biblio_html_login_page(data, env): - """ data: response message body +from mandaye.response import serve_template + +def biblio_html_login_page(env, values, request, response): + """ msg: response message body env: Mandaye environment """ - data = re.sub("", + response.msg = re.sub("", "Connection via mon compte citoyen", - data) - return data + response.msg) + return response -def biblio_html(data, env): +def biblio_html(env, values, request, response): """ Global html filter the Vincenne library This fix the fucking absolute url of the biblio site """ - data = data.replace(env["target"].hostname, env["HTTP_HOST"]) - return data + response.msg = response.msg.replace(env["target"].hostname, env["HTTP_HOST"]) + return response + +def biblio_associate_req(env, values, request): + """ Modify the association request + """ + request.req_method = 'GET' + if request.headers.has_key('Content-Type'): + del request.headers['Content-Type'] + return request + +def biblio_associate_login(env, values, request, response): + """ Use default login page to associate an account on the SSO + """ + pattern = re.compile('
.*
\s+', + re.MULTILINE | re.DOTALL) + form = serve_template(values.get('template'), **values) + form = form.encode('utf-8') + response.msg = re.sub(pattern, form, response.msg) + return response + diff --git a/mandaye/http.py b/mandaye/http.py index 2de1313..4869495 100644 --- a/mandaye/http.py +++ b/mandaye/http.py @@ -14,6 +14,12 @@ class HTTPHeader(dict): for key in headers.keys(): self[key] = headers.getheaders(key) + def load_from_list(self, headers): + """ headers: list with the headers + """ + for header in headers: + self[header[0]] = [header[1]] + def addheader(self, name, value): """ name: field name value: string with the field value """ @@ -55,27 +61,28 @@ class HTTPHeader(dict): class HTTPRequest(object): - def __init__(self, cookies, headers, msg=None): - """ cookies: + def __init__(self, cookies, headers, req_method, msg=None): + """ """ self.cookies = cookies self.headers = headers self.msg = msg + self.req_method = req_method class HTTPResponse(object): """ Mandaye HTTP Response """ - def __init__(self, code=None, status=None, headers=None, msg=None, cookies=None): + def __init__(self, code=None, reason=None, headers=None, msg=None, cookies=None): """ code: integer return code (200, 302, 404, ..) - status: string with http status (ex: OK, Not Found, ...) + reason: string with http status (ex: OK, Not Found, ...) headers: dictionary with the headers msg: message body str """ self.code = code - self.status = status + self.reason = reason self.headers = headers self.msg = msg self.cookies = cookies @@ -88,11 +95,9 @@ class HTTPResponse(object): """ response: urrlib.addinfourl """ self.headers.load_httpmessage(response.headers) - if self.headers.has_key('set-cookie'): - del self.headers['set-cookie'] for setcookie in response.headers.getheaders('set-cookie'): self.cookies.load("Set-Cookie: %s" % setcookie) self.code = response.code - self.status = response.msg + self.reason = response.msg self.msg = response.read() diff --git a/mandaye/models.py b/mandaye/models.py index 8ea1cbd..88789de 100644 --- a/mandaye/models.py +++ b/mandaye/models.py @@ -13,22 +13,36 @@ class Site(Base): id = Column(Integer, primary_key=True) name = Column(String, unique=True, nullable=False) + def __init__(self, name): + self.name = name + + def __repr__(self): + return "" % (self.name) + class LocalUser(Base): """ Mandaye local user """ __tablename__ = 'local_users' id = Column(Integer, primary_key=True) - login = Column(String, nullable=True) - fullname = Column(String, nullable=True) + login = Column(String(80), nullable=True) + fullname = Column(String(150), nullable=True) password = Column(String, nullable=True) - token = Column(String, nullable=True) + token = Column(String(150), nullable=True) - def __init__(self, login, password): + def __init__(self, login=None, password=None, + fullname=None, token=None): + self.login = login self.password = password + self.fullname = fullname + self.token = token def __repr__(self): - return "" % (self.login) + if self.login: + return "" % (self.login) + elif self.token: + return "" % (self.token) + return "" class ExtUser(Base): """ User of externals applications @@ -36,22 +50,26 @@ class ExtUser(Base): __tablename__ = 'ext_users' id = Column(Integer, primary_key=True) - login = Column(String, nullable=True) + login = Column(String(80), nullable=True) password = Column(String, nullable=True) - token = Column(String, nullable=True) + token = Column(String(200), nullable=True) local_user_id = Column(Integer, ForeignKey('local_users.id')) site_id = Column(Integer, ForeignKey('sites.id')) local_user = relationship("LocalUser", backref=backref('ext_users', order_by=id)) site = relationship("LocalUser", backref=backref('users')) - def __init__(self, login, password, token): + def __init__(self, login=None, password=None, token=None): self.login = login self.password = password self.token = token def __repr__(self): - return "" % (self.login) + if self.login: + return "" % (self.login) + elif self.token: + return "" % (self.token) + return "" diff --git a/mandaye/response.py b/mandaye/response.py index 6795577..07eed7b 100644 --- a/mandaye/response.py +++ b/mandaye/response.py @@ -9,7 +9,8 @@ from mako.template import Template from mandaye.http import HTTPResponse, HTTPHeader from mandaye.config import template_directory, debug -mylookup = TemplateLookup(directories=[template_directory]) +mylookup = TemplateLookup(directories=[template_directory], + input_encoding='utf-8') def serve_template(templatename, **kwargs): mytemplate = mylookup.get_template(templatename) diff --git a/mandaye/server.py b/mandaye/server.py index e5955f3..f96ed6a 100644 --- a/mandaye/server.py +++ b/mandaye/server.py @@ -20,7 +20,7 @@ from gevent.pywsgi import WSGIServer from mandaye.dispatcher import Dispatcher from mandaye.handlers.default import MandayeRedirectHandler, MandayeErrorHandler from mandaye.http import HTTPHeader, HTTPRequest, HTTPResponse -from mandaye.response import _404, _502 +from mandaye.response import _404, _502, _500 # Init poster opener = poster.streaminghttp.register_openers() @@ -77,7 +77,7 @@ class MandayeApp(object): else: msg = None - return HTTPRequest(cookies, headers, msg) + return HTTPRequest(cookies, headers, self.env['REQUEST_METHOD'], msg) def on_request(self, start_response): mrequest = self._get_request() @@ -90,17 +90,18 @@ class MandayeApp(object): if not url: return self.on_response(start_response, self.dispatcher.get_response(mrequest)) - if self.env['REQUEST_METHOD'] == 'POST': - data = self.env['wsgi.input'].read() - req = urllib2.Request(url, data, mrequest.headers.getheaders()) + if mrequest.req_method == 'POST': + req = urllib2.Request(url, mrequest.msg, mrequest.headers.getheaders()) else: req = urllib2.Request(url, headers=mrequest.headers.getheaders()) # Load the cookies req.add_header('cookie', mrequest.cookies.output(sep=',', header='')) + print req.headers try: opener = urllib2.build_opener(MandayeErrorHandler, MandayeRedirectHandler) response = opener.open(req) except Exception, e: + response.close() response = _502(self.env['PATH_INFO'], req.get_host(), e) return self.on_response(start_response, response) else: @@ -113,7 +114,7 @@ class MandayeApp(object): """ start_response: wsgi start_response response: an instance of HTTPResponse """ response.headers.addsetcookies(response.cookies) - start_response('%d %s' % (response.code, response.status), + start_response('%d %s' % (response.code, response.reason), response.headers.items()) return [response.msg] diff --git a/mandaye/templates/biblio_associate.html b/mandaye/templates/biblio_associate.html new file mode 100644 index 0000000..4cc2c06 --- /dev/null +++ b/mandaye/templates/biblio_associate.html @@ -0,0 +1,37 @@ + +
+

Associer votre compte

+ + % if error_msg: +
+ ${error_msg} +
+ % endif + +
+
+ N° de carte = les 6 derniers numéros de  la carte
+ Mot de passe = lors de la première connexion votre nom de famille +
+
+ +
+ +

Associer votre compte au compte citoyen

+ +
+
+
+ + +
+
+ + +
+ + + +
+
+