diff options
author | Emmanuel Raviart <eraviart@entrouvert.com> | 2004-08-11 09:59:58 +0000 |
---|---|---|
committer | Emmanuel Raviart <eraviart@entrouvert.com> | 2004-08-11 09:59:58 +0000 |
commit | 80a5b0009a69fd1e4d0451278dbbc5808563dc42 (patch) | |
tree | 80aa70e18baf8e9b3e537d690061ee137b316319 /python | |
parent | 2d1f06f55b37d677f4c17e1fa1c60a0bf65778b3 (diff) | |
download | lasso-80a5b0009a69fd1e4d0451278dbbc5808563dc42.tar.gz lasso-80a5b0009a69fd1e4d0451278dbbc5808563dc42.tar.xz lasso-80a5b0009a69fd1e4d0451278dbbc5808563dc42.zip |
In python/tests, there are now a sample IDP (sample-idp.py) and a sample SP
(sample-sp.py). The two applications are real servers.
Diffstat (limited to 'python')
-rw-r--r-- | python/tests/IdentityProvider.py | 11 | ||||
-rw-r--r-- | python/tests/LibertyEnabledClientProxy.py | 9 | ||||
-rw-r--r-- | python/tests/Provider.py | 10 | ||||
-rw-r--r-- | python/tests/ServiceProvider.py | 80 | ||||
-rw-r--r-- | python/tests/abstractweb.py | 22 | ||||
-rw-r--r-- | python/tests/http.py | 73 | ||||
-rw-r--r-- | python/tests/liberty.py | 53 | ||||
-rw-r--r-- | python/tests/libertysimulator.py | 53 | ||||
-rw-r--r-- | python/tests/login_tests.py | 4 | ||||
-rwxr-xr-x | python/tests/sample-idp.py | 146 | ||||
-rwxr-xr-x | python/tests/sample-sp.py | 147 | ||||
-rw-r--r-- | python/tests/submissions.py | 292 | ||||
-rw-r--r-- | python/tests/web.py | 98 | ||||
-rw-r--r-- | python/tests/websimulator.py | 10 |
14 files changed, 939 insertions, 69 deletions
diff --git a/python/tests/IdentityProvider.py b/python/tests/IdentityProvider.py index c48ddb5d..4e3dd158 100644 --- a/python/tests/IdentityProvider.py +++ b/python/tests/IdentityProvider.py @@ -24,15 +24,14 @@ import lasso -from Provider import Provider -from websimulator import * +import Provider -class IdentityProvider(Provider): +class IdentityProviderMixin(Provider.ProviderMixin): soapResponseMsgs = None - def __init__(self, internet, url): - Provider.__init__(self, internet, url) + def __init__(self): + Provider.ProviderMixin.__init__(self) self.soapResponseMsgs = {} def singleSignOn(self, handler): @@ -103,6 +102,7 @@ class IdentityProvider(Provider): session = handler.session if session is None: session = handler.createSession() + session.publishToken = True session.lassoLoginDump = login.dump() login = None return self.authenticate(handler, self.singleSignOn_authenticate_part2, login) @@ -173,6 +173,7 @@ class IdentityProvider(Provider): soapResponseMsg = self.soapResponseMsgs.get(artifact, None) if soapResponseMsg is None: raise Exception('FIXME: Handle the case when artifact is wrong') + del self.soapResponseMsgs[artifact] return handler.respond( headers = {'Content-Type': 'text/xml'}, body = soapResponseMsg) elif requestType == lasso.requestTypeLogout: diff --git a/python/tests/LibertyEnabledClientProxy.py b/python/tests/LibertyEnabledClientProxy.py index 12871ca9..46a2f9dc 100644 --- a/python/tests/LibertyEnabledClientProxy.py +++ b/python/tests/LibertyEnabledClientProxy.py @@ -24,10 +24,10 @@ import lasso -from websimulator import * +import abstractweb -class LibertyEnabledClientProxy(WebClient): +class LibertyEnabledClientProxyMixin(abstractweb.WebClientMixin): # A service provider MAY provide a list of identity providers it recognizes by including the # <lib:IDPList> element in the <lib:AuthnRequestEnvelope>. The format and processing rules for # the identity provider list MUST be as defined in [LibertyProtSchema]. @@ -58,7 +58,7 @@ class LibertyEnabledClientProxy(WebClient): # <lib:AuthnResponse> MUST be encoded by applying a base64 transformation (refer to # [RFC2045]) to the <lib:AuthnResponse> and all its elements. - httpRequestHeaders = WebClient.httpRequestHeaders.copy() + httpRequestHeaders = abstractweb.WebClientMixin.httpRequestHeaders.copy() httpRequestHeaders.update({ # FIXME: Is this the correct syntax for several URLs in LIBV? 'Liberty-Enabled': 'LIBV=urn:liberty:iff:2003-08,http://projectliberty.org/specs/v1', @@ -73,9 +73,6 @@ class LibertyEnabledClientProxy(WebClient): lassoServerDump = None principal = None - def __init__(self, internet): - WebClient.__init__(self, internet) - def getLassoServer(self): return lasso.Server.new_from_dump(self.lassoServerDump) diff --git a/python/tests/Provider.py b/python/tests/Provider.py index a0e1a08f..07a62ed0 100644 --- a/python/tests/Provider.py +++ b/python/tests/Provider.py @@ -24,11 +24,11 @@ import lasso -from websimulator import * +import abstractweb -class Provider(WebSite): - httpResponseHeaders = WebSite.httpResponseHeaders.copy() +class ProviderMixin(abstractweb.WebSiteMixin): + httpResponseHeaders = abstractweb.WebSiteMixin.httpResponseHeaders.copy() httpResponseHeaders.update({ 'Liberty-Enabled': 'LIBV=urn:liberty:iff:2003-08,http://projectliberty.org/specs/v1', }) @@ -37,8 +37,8 @@ class Provider(WebSite): sessionTokensByNameIdentifier = None userIdsByNameIdentifier = None - def __init__(self, internet, url): - WebSite.__init__(self, internet, url) + def __init__(self): + abstractweb.WebSiteMixin.__init__(self) self.userIdsByNameIdentifier = {} self.sessionTokensByNameIdentifier = {} diff --git a/python/tests/ServiceProvider.py b/python/tests/ServiceProvider.py index e7b2eeb9..b616a87f 100644 --- a/python/tests/ServiceProvider.py +++ b/python/tests/ServiceProvider.py @@ -24,11 +24,11 @@ import lasso -from Provider import Provider -from websimulator import * +import Provider -class ServiceProvider(Provider): +class ServiceProviderMixin(Provider.ProviderMixin): + createNewAccountWhenNewFederationForUnknownUser = False idpSite = None # The identity provider, this service provider will use to authenticate users. def assertionConsumer(self, handler): @@ -111,26 +111,81 @@ class ServiceProvider(Provider): # If there was no web session yet, create it. Idem for the web user account. if session is None: session = handler.createSession() + session.publishToken = True if user is None: - # A real service provider would ask user to login locally to create federation. Or it - # would ask user informations to create a local account. - userId = handler.httpRequest.client.keyring.get(self.url, None) - userAuthenticated = userId in self.users - if not userAuthenticated: - return handler.respond(401, 'Access Unauthorized: User has no account.') - user = self.users[userId] + # A real service provider would ask user to login locally to create a federation. Or it + # would ask user informations to create a local account. Or it would automatically + # create a new account... + if self.createNewAccountWhenNewFederationForUnknownUser: + user = handler.createUser() + else: + return self.assertionConsumer_newFederationForUnknownUser( + handler, nameIdentifier, lassoSessionDump, lassoIdentityDump) session.userId = user.uniqueId + user.sessionToken = session.token # Store the updated identity dump and session dump. + session.lassoSessionDump = lassoSessionDump if login.is_identity_dirty(): user.lassoIdentityDump = lassoIdentityDump + + self.userIdsByNameIdentifier[nameIdentifier] = user.uniqueId + self.sessionTokensByNameIdentifier[nameIdentifier] = session.token + + # We do a redirect now because we don't want the user to be able to reload + # assertionConsumer page (because the artifact has been removed from identity-provider). + # FIXME: Add the session token to redirect URL. + return handler.respondRedirectTemporarily('/assertionConsumer_success') + + def assertionConsumer_newFederationForUnknownUser( + self, handler, nameIdentifier, lassoSessionDump, lassoIdentityDump): + # Called whe the user has been successfully authenticated on identity provider, but he has + # no account on this service provider or is account is not federated yet and he is not + # logged. + # Depending of the policy of the service provider, the user account can be created + # immediately, or the user can be asked to provide informations to create a new account. + # He also can be asked to authenticate locally (for the last time :-) in order for the + # service-provider to create the federation. + + # Save Lasso login as a dump in session. + session = handler.session + session.nameIdentifier = nameIdentifier session.lassoSessionDump = lassoSessionDump + session.lassoIdentityDump = lassoIdentityDump + nameIdentifier = lassoSessionDump = lassoIdentityDump = None + + # We do a redirect now for two reasons: + # - We don't want the user to be able to reload assertionConsumer page (because the + # artifact has been removed from identity-provider). + # - For HTTP authentication, we don't want to emit a 401 Unauthorized that would force the + # Principal to reload assertionConsumer page. + # FIXME: Add the session token to redirect URL. + return handler.respondRedirectTemporarily( + '/assertionConsumer_newFederationForUnknownUser_part2') + + def assertionConsumer_newFederationForUnknownUser_part2(self, handler): + return self.authenticate(handler, self.assertionConsumer_newFederationForUnknownUser_part3) + def assertionConsumer_newFederationForUnknownUser_part3( + self, handler, userAuthenticated, authenticationMethod): + if not userAuthenticated: + return handler.respond(401, 'Access Unauthorized: User has no account.') + + # User has been authenticated => Create federation. + session = handler.session + nameIdentifier = session.nameIdentifier + del session.nameIdentifier + user = handler.user + user.lassoIdentityDump = session.lassoIdentityDump + del session.lassoIdentityDump self.userIdsByNameIdentifier[nameIdentifier] = user.uniqueId self.sessionTokensByNameIdentifier[nameIdentifier] = session.token + return self.assertionConsumer_success(handler) - return handler.respond() + def assertionConsumer_success(self, handler): + return handler.respond(200, headers = {'Content-Type': 'text/plain'}, + body = 'Liberty authentication succeeded') def login(self, handler): libertyEnabled = handler.httpRequest.headers.get('Liberty-Enabled', None) @@ -252,4 +307,5 @@ class ServiceProvider(Provider): failUnless(nameIdentifier) del self.sessionTokensByNameIdentifier[nameIdentifier] - return handler.respond() + return handler.respond(200, headers = {'Content-Type': 'text/plain'}, + body = 'Liberty logout succeeded') diff --git a/python/tests/abstractweb.py b/python/tests/abstractweb.py index 671c0a73..2f189c94 100644 --- a/python/tests/abstractweb.py +++ b/python/tests/abstractweb.py @@ -27,6 +27,7 @@ class HttpRequestMixin: + body = None headers = None method = None # 'GET' or 'POST' or 'PUT' or... url = None @@ -170,13 +171,23 @@ class HttpRequestHandlerMixin: raise NotImplementedError -class WebSessionMixin: +class WebClientMixin: + httpRequestHeaders = { + 'User-Agent': 'LassoSimulator/0.0.0', + 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html', + } + + def __init__(self): + pass + + +class WebSessionMixin(WebClientMixin): isDirty = True - publishToken = False token = None userId = None # ID of logged user def __init__(self, token): + WebClientMixin.__init__(self) self.token = token def getSimpleLabel(self): @@ -229,14 +240,15 @@ class WebSiteMixin: def newSession(self): self.lastSessionToken += 1 - session = self.WebSession(self.lastSessionToken) - self.sessions[self.lastSessionToken] = session + sessionToken = str(self.lastSessionToken) + session = self.WebSession(sessionToken) + self.sessions[sessionToken] = session return session def newUser(self, name = None): if name is None: self.lastUserId += 1 - userId = self.lastUserId + userId = str(self.lastUserId) else: userId = name user = self.WebUser(userId, name = name) diff --git a/python/tests/http.py b/python/tests/http.py index 750c4e93..30561e77 100644 --- a/python/tests/http.py +++ b/python/tests/http.py @@ -33,6 +33,7 @@ Features: """ +import base64 import BaseHTTPServer import Cookie import cStringIO @@ -50,6 +51,7 @@ except ImportError: SSL = None import abstractweb +import submissions try: @@ -116,9 +118,14 @@ class BaseHTTPSServer(SocketServer.TCPServer): class HttpRequest(abstractweb.HttpRequestMixin, object): handler = None + submission = None def __init__(self, handler): self.handler = handler + self.submission = submissions.readSubmission(self.handler) + + def getBody(self): + return self.submission.readFile() def getHeaders(self): return self.handler.headers @@ -145,6 +152,7 @@ class HttpRequest(abstractweb.HttpRequestMixin, object): def getUrl(self): return "%s://%s%s" % (self.scheme, self.headers.get('Host'), self.pathAndQuery) + body = property(getBody) headers = property(getHeaders) method = property(getMethod) path = property(getPath) @@ -276,6 +284,7 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): server_version = 'HttpRequestHandlerMixin/1.0' site = None # Class variable testCookieSupport = False + useHttpAuthentication = True def createSession(self): session = abstractweb.HttpRequestHandlerMixin.createSession(self) @@ -285,7 +294,6 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): def handle(self): """Handle multiple requests if necessary.""" - self.httpRequest = HttpRequest(self) self.socketCreationTime = time.time() try: try: @@ -300,7 +308,8 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): except SSL.ZeroReturnError: pass except SSL.Error, exception: - raise str((exception, exception[0])) + logger.debug('SSL error in handle. Error = %s, %s' % (exception, exception[0])) + raise # FIXME if exception[0] == ('PEM routines', 'PEM_read_bio', 'no start line'): pass else: @@ -314,13 +323,22 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): """Handle a single HTTP request.""" self.raw_requestline = self.rfile.readline() if not self.raw_requestline: - self.close_connection = 1 + self.close_connection = True return if not self.parse_request(): # An error code has been sent, just exit return logger.info(self.raw_requestline.strip()) logger.debug(str(self.headers)) + # The server isn't forked nor threaded, so we don't want to keep connections open, to avoid + # dead-locks which occur for example when the connection with the navigator to the identity + # provider is kept open, while a service provider sends a SOAP request to the identity + # provider. + # Remove this line for forked or threaded servers. + self.close_connection = True + + self.httpRequest = HttpRequest(self) + # Retrieve the session and user, if possible. session = None @@ -353,7 +371,7 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): # Handle HTTP authentication. authorization = self.httpRequest.headers.get('authorization') if self.httpRequest.hasQueryField('login') and not authorization \ - and rootDataHolder.getConfigBoolean('yep:useHttpAuthentication', default = False): + and self.useHttpAuthentication: # Ask for HTTP authentication. return self.outputErrorUnauthorized(httpPath) if self.httpRequest.hasQueryField('logout') and authorization: @@ -462,7 +480,6 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): # token (it is better not to store it in a cookie or in URLs). if session.publishToken: del session.publishToken - self.canUseCookie = canUseCookie if session is None and user is not None: # The user has been authenticated (using HTTP or X.509 authentication), but the session # doesn't exist yet (or was too old, or...). Create a new session. @@ -480,6 +497,7 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): # use cookie. canUseCookie = False logger.debug('Session: %s' % session.simpleLabel) + self.canUseCookie = canUseCookie self.user = user if user is not None: logger.debug('User: %s' % user.simpleLabel) @@ -674,15 +692,17 @@ class HttpRequestHandlerMixin(abstractweb.HttpRequestHandlerMixin): ## data = '<html><body>%s</body></html>' % message ## return self.send_error(404, message, data, setCookie = True) -## def outputErrorUnauthorized(self, filePath): -## if filePath is None: -## message = 'Access Unauthorized' -## else: -## message = 'Access to "%s" Unauthorized.' % filePath -## logger.info(message) -## data = '<html><body>%s</body></html>' % message -## headers = {} -## return self.send_error(401, message, data, headers, setCookie = True) + def outputErrorUnauthorized(self, filePath): + if filePath is None: + message = 'Access Unauthorized' + else: + message = 'Access to "%s" Unauthorized.' % filePath + logger.info(message) + data = '<html><body>%s</body></html>' % message + headers = {} + if self.useHttpAuthentication: + headers["WWW-Authenticate"] = 'Basic realm="%s"' % self.realm + return self.send_error(401, message, data, headers, setCookie = True) ## def outputInformationContinue(self): ## message = 'Continue' @@ -878,13 +898,26 @@ class HttpsRequestHandler(HttpRequestHandlerMixin, BaseHTTPSRequestHandler): scheme = 'https' -# We use ForkingMixIn instead of ThreadingMixIn because the Python binding for -# libxml2 limits the number of registered xpath functions to 10. Even if we use -# only one xpathContext, this would limit the number of threads to 10, wich is -# not enough for a web server. +## # We use ForkingMixIn instead of ThreadingMixIn because the Python binding for +## # libxml2 limits the number of registered xpath functions to 10. Even if we use +## # only one xpathContext, this would limit the number of threads to 10, wich is +## # not enough for a web server. + + +## class HttpServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): +## pass + -class HttpServer(SocketServer.ForkingMixIn, BaseHTTPServer.HTTPServer): +## class HttpsServer(SocketServer.ForkingMixIn, BaseHTTPSServer): +## pass + + +# No fork nor thread. + +class HttpServer(BaseHTTPServer.HTTPServer): pass -class HttpsServer(SocketServer.ForkingMixIn, BaseHTTPSServer): + +class HttpsServer(BaseHTTPSServer): pass + diff --git a/python/tests/liberty.py b/python/tests/liberty.py new file mode 100644 index 00000000..46c21783 --- /dev/null +++ b/python/tests/liberty.py @@ -0,0 +1,53 @@ +# -*- coding: UTF-8 -*- + + +# Lasso Simulator +# By: Emmanuel Raviart <eraviart@entrouvert.com> +# +# Copyright (C) 2004 Entr'ouvert +# http://lasso.entrouvert.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from LibertyEnabledClientProxy import LibertyEnabledClientProxyMixin +from IdentityProvider import IdentityProviderMixin +from ServiceProvider import ServiceProviderMixin +from Provider import ProviderMixin +import web + + +class LibertyEnabledClientProxy(LibertyEnabledClientProxyMixin, web.WebClient): + def __init__(self): + web.WebClient.__init__(self) + LibertyEnabledClientProxyMixin.__init__(self) + + +class Provider(ProviderMixin, web.WebSite): + def __init__(self, url): + web.WebSite.__init__(self, url) + ProviderMixin.__init__(self) + + +class IdentityProvider(IdentityProviderMixin, Provider): + def __init__(self, url): + Provider.__init__(self, url) + IdentityProviderMixin.__init__(self) + + +class ServiceProvider(ServiceProviderMixin, Provider): + def __init__(self, url): + Provider.__init__(self, url) + ServiceProviderMixin.__init__(self) diff --git a/python/tests/libertysimulator.py b/python/tests/libertysimulator.py new file mode 100644 index 00000000..3cc5cb7a --- /dev/null +++ b/python/tests/libertysimulator.py @@ -0,0 +1,53 @@ +# -*- coding: UTF-8 -*- + + +# Lasso Simulator +# By: Emmanuel Raviart <eraviart@entrouvert.com> +# +# Copyright (C) 2004 Entr'ouvert +# http://lasso.entrouvert.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from LibertyEnabledClientProxy import LibertyEnabledClientProxyMixin +from IdentityProvider import IdentityProviderMixin +from ServiceProvider import ServiceProviderMixin +from Provider import ProviderMixin +import websimulator + + +class LibertyEnabledClientProxy(LibertyEnabledClientProxyMixin, websimulator.WebClient): + def __init__(self, internet): + websimulator.WebClient.__init__(self, internet) + LibertyEnabledClientProxyMixin.__init__(self) + + +class Provider(ProviderMixin, websimulator.WebSite): + def __init__(self, internet, url): + websimulator.WebSite.__init__(self, internet, url) + ProviderMixin.__init__(self) + + +class IdentityProvider(IdentityProviderMixin, Provider): + def __init__(self, internet, url): + Provider.__init__(self, internet, url) + IdentityProviderMixin.__init__(self) + + +class ServiceProvider(ServiceProviderMixin, Provider): + def __init__(self, internet, url): + Provider.__init__(self, internet, url) + ServiceProviderMixin.__init__(self) diff --git a/python/tests/login_tests.py b/python/tests/login_tests.py index 5dd09b76..68c511f7 100644 --- a/python/tests/login_tests.py +++ b/python/tests/login_tests.py @@ -35,9 +35,7 @@ if not '../.libs' in sys.path: import lasso import builtins -from IdentityProvider import IdentityProvider -from LibertyEnabledClientProxy import LibertyEnabledClientProxy -from ServiceProvider import ServiceProvider +from libertysimulator import * from websimulator import * diff --git a/python/tests/sample-idp.py b/python/tests/sample-idp.py new file mode 100755 index 00000000..6db7c18f --- /dev/null +++ b/python/tests/sample-idp.py @@ -0,0 +1,146 @@ +#! /usr/bin/env python +# -*- coding: UTF-8 -*- + + +# Lasso Simulator +# By: Emmanuel Raviart <eraviart@entrouvert.com> +# +# Copyright (C) 2004 Entr'ouvert +# http://lasso.entrouvert.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import logging +from optparse import OptionParser +import sys + +if not '..' in sys.path: + sys.path.insert(0, '..') +if not '../.libs' in sys.path: + sys.path.insert(0, '../.libs') + +import lasso + +import assertions +import builtins +import http +import liberty + + +applicationCamelCaseName = 'LassoSimulator' +applicationPublicName = 'Lasso Simulator' +applicationVersion = '(Unreleased CVS Version)' +logger = None + + +class HttpRequestHandlerMixin: + realm = '%s Web Site' % applicationPublicName + server_version = '%s/%s' % (applicationCamelCaseName, applicationVersion) + + def version_string(self): + return '%s %s' % (applicationPublicName, applicationVersion) + + +class HttpRequestHandler(HttpRequestHandlerMixin, http.HttpRequestHandler): + pass + + +class HttpsRequestHandler(HttpRequestHandlerMixin, http.HttpsRequestHandler): + pass + + +def main(): + # Parse command line options. + parser = OptionParser(version = '%%prog %s' % applicationVersion) + parser.add_option( + '-c', '--config', metavar = 'FILE', dest = 'configurationFilePath', + help = 'specify an alternate configuration file', + default = '/etc/lasso-simulator/config.xml') + parser.add_option( + '-d', '--daemon', dest = 'daemonMode', help = 'run main process in background', + action = 'store_true', default = False) + parser.add_option( + '-D', '--debug', dest = 'debugMode', help = 'enable program debugging', + action = 'store_true', default = False) + parser.add_option( + '-l', '--log', metavar = 'FILE', dest = 'logFilePath', help = 'specify log file', + default = '/dev/null') + parser.add_option( + '-L', '--log-level', metavar = 'LEVEL', dest = 'logLevel', + help = 'specify log level (debug, info, warning, error, critical)', default = 'info') + (options, args) = parser.parse_args() + if options.logLevel.upper() not in logging._levelNames: + raise Exception('Unknown log level: "%s"' % options.logLevel) + + # Configure logger. + logger = logging.getLogger() + if options.debugMode and not options.daemonMode: + handler = logging.StreamHandler(sys.stderr) + else: + handler = logging.FileHandler(options.logFilePath) + formatter = logging.Formatter('%(asctime)s %(levelname)-9s %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging._levelNames[options.logLevel.upper()]) + builtins.set('logger', logger) + + site = liberty.IdentityProvider('https://identity-provider/') + site.providerId = 'https://identity-provider/metadata' + + lassoServer = lasso.Server.new( + '../../examples/data/idp-metadata.xml', + '../../examples/data/idp-public-key.pem', + '../../examples/data/idp-private-key.pem', + '../../examples/data/idp-crt.pem', + lasso.signatureMethodRsaSha1) + lassoServer.add_provider( + '../../examples/data/sp-metadata.xml', + '../../examples/data/sp-public-key.pem', + '../../examples/data/ca-crt.pem') + site.lassoServerDump = lassoServer.dump() + failUnless(site.lassoServerDump) + lassoServer.destroy() + + site.certificateAbsolutePath = '../../examples/data/idp-ssl-crt.pem' + site.privateKeyAbsolutePath = '../../examples/data/idp-ssl-private-key.pem' + site.peerCaCertificateAbsolutePath = '../../examples/data/ca-ssl-crt.pem' + + site.newUser('Chantereau') + site.newUser('Clapies') + site.newUser('Febvre') + site.newUser('Nowicki') + # Frederic Peters has no account on identity provider. + + HttpRequestHandlerMixin.site = site # Directly a site, not a server => no virtual host. +## httpServer = http.HttpServer(('127.0.0.2', 80), HttpRequestHandler) +## logger.info('Serving HTTP on %s port %s...' % httpServer.socket.getsockname()) + httpServer = http.HttpsServer( + ('127.0.0.2', 443), + HttpsRequestHandler, + '../../examples/data/idp-ssl-private-key.pem', # Server private key + '../../examples/data/idp-ssl-crt.pem', # Server certificate + '../../examples/data/ca-ssl-crt.pem', # Clients certification authority certificate + None, # sslCertificateChainFile see mod_ssl, ssl_engine_init.c, line 852 + None, # sslVerifyClient http://www.modssl.org/docs/2.1/ssl_reference.html#ToC13 + ) + logger.info('Serving HTTPS on %s port %s...' % httpServer.socket.getsockname()) + try: + httpServer.serve_forever() + except KeyboardInterrupt: + pass + +if __name__ == '__main__': + main() diff --git a/python/tests/sample-sp.py b/python/tests/sample-sp.py new file mode 100755 index 00000000..321b114e --- /dev/null +++ b/python/tests/sample-sp.py @@ -0,0 +1,147 @@ +#! /usr/bin/env python +# -*- coding: UTF-8 -*- + + +# Lasso Simulator +# By: Emmanuel Raviart <eraviart@entrouvert.com> +# +# Copyright (C) 2004 Entr'ouvert +# http://lasso.entrouvert.org +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +import logging +from optparse import OptionParser +import sys + +if not '..' in sys.path: + sys.path.insert(0, '..') +if not '../.libs' in sys.path: + sys.path.insert(0, '../.libs') + +import lasso + +import assertions +import builtins +import http +import liberty + +applicationCamelCaseName = 'LassoSimulator' +applicationPublicName = 'Lasso Simulator' +applicationVersion = '(Unreleased CVS Version)' +logger = None + + +class HttpRequestHandlerMixin: + realm = '%s Web Site' % applicationPublicName + server_version = '%s/%s' % (applicationCamelCaseName, applicationVersion) + + def version_string(self): + return '%s %s' % (applicationPublicName, applicationVersion) + + +class HttpRequestHandler(HttpRequestHandlerMixin, http.HttpRequestHandler): + pass + + +class HttpsRequestHandler(HttpRequestHandlerMixin, http.HttpsRequestHandler): + pass + + +def main(): + # Parse command line options. + parser = OptionParser(version = '%%prog %s' % applicationVersion) + parser.add_option( + '-c', '--config', metavar = 'FILE', dest = 'configurationFilePath', + help = 'specify an alternate configuration file', + default = '/etc/lasso-simulator/config.xml') + parser.add_option( + '-d', '--daemon', dest = 'daemonMode', help = 'run main process in background', + action = 'store_true', default = False) + parser.add_option( + '-D', '--debug', dest = 'debugMode', help = 'enable program debugging', + action = 'store_true', default = False) + parser.add_option( + '-l', '--log', metavar = 'FILE', dest = 'logFilePath', help = 'specify log file', + default = '/dev/null') + parser.add_option( + '-L', '--log-level', metavar = 'LEVEL', dest = 'logLevel', + help = 'specify log level (debug, info, warning, error, critical)', default = 'info') + (options, args) = parser.parse_args() + if options.logLevel.upper() not in logging._levelNames: + raise Exception('Unknown log level: "%s"' % options.logLevel) + + # Configure logger. + logger = logging.getLogger() + if options.debugMode and not options.daemonMode: + handler = logging.StreamHandler(sys.stderr) + else: + handler = logging.FileHandler(options.logFilePath) + formatter = logging.Formatter('%(asctime)s %(levelname)-9s %(message)s') + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging._levelNames[options.logLevel.upper()]) + builtins.set('logger', logger) + + site = liberty.ServiceProvider('https://service-provider/') + site.providerId = 'https://service-provider/metadata' + site.idpSite = liberty.IdentityProvider('https://identity-provider/') + site.idpSite.providerId = 'https://identity-provider/metadata' + + lassoServer = lasso.Server.new( + '../../examples/data/sp-metadata.xml', + '../../examples/data/sp-public-key.pem', + '../../examples/data/sp-private-key.pem', + '../../examples/data/sp-crt.pem', + lasso.signatureMethodRsaSha1) + lassoServer.add_provider( + '../../examples/data/idp-metadata.xml', + '../../examples/data/idp-public-key.pem', + '../../examples/data/ca-crt.pem') + site.lassoServerDump = lassoServer.dump() + failUnless(site.lassoServerDump) + lassoServer.destroy() + + site.certificateAbsolutePath = '../../examples/data/sp-ssl-crt.pem' + site.privateKeyAbsolutePath = '../../examples/data/sp-ssl-private-key.pem' + site.peerCaCertificateAbsolutePath = '../../examples/data/ca-ssl-crt.pem' + + site.newUser('Nicolas') + site.newUser('Romain') + site.newUser('Valery') + # Christophe Nowicki has no account on service provider. + site.newUser('Frederic') + + HttpRequestHandlerMixin.site = site # Directly a site, not a server => no virtual host. +## httpServer = http.HttpServer(('127.0.0.3', 80), HttpRequestHandler) +## logger.info('Serving HTTP on %s port %s...' % httpServer.socket.getsockname()) + httpServer = http.HttpsServer( + ('127.0.0.3', 443), + HttpsRequestHandler, + '../../examples/data/sp-ssl-private-key.pem', # Server private key + '../../examples/data/sp-ssl-crt.pem', # Server certificate + '../../examples/data/ca-ssl-crt.pem', # Clients certification authority certificate + None, # sslCertificateChainFile see mod_ssl, ssl_engine_init.c, line 852 + None, # sslVerifyClient http://www.modssl.org/docs/2.1/ssl_reference.html#ToC13 + ) + logger.info('Serving HTTPS on %s port %s...' % httpServer.socket.getsockname()) + try: + httpServer.serve_forever() + except KeyboardInterrupt: + pass + +if __name__ == '__main__': + main() diff --git a/python/tests/submissions.py b/python/tests/submissions.py new file mode 100644 index 00000000..a9f9504c --- /dev/null +++ b/python/tests/submissions.py @@ -0,0 +1,292 @@ +# -*- coding: UTF-8 -*- + + +# HTTP Client and Server Enhanced Classes +# By: Frederic Peters <fpeters@entrouvert.com> +# Emmanuel Raviart <eraviart@entrouvert.com> +# +# Copyright (C) 2004 Entr'ouvert +# http://www.entrouvert.org +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + + +"""Wrapper for HTML form submissions, simulating Web Forms 2 behaviour + +See http://whatwg.org/specs/web-forms/2004-06-27-call-for-comments/#x-www-form-xml +""" + + +import cgi + + +class AbstractSubmission(object): + httpRequestHandler = None + length = None + mimeType = None + + def __init__(self, httpRequestHandler, contentLength): + assert httpRequestHandler + self.httpRequestHandler = httpRequestHandler + assert isinstance(contentLength, int) + self.length = contentLength + + def getField(self, name, index = 0, default = None): + # Return either a string or a sequence of strings. + fieldList = self.getFieldList(name, index) + if not fieldList: + return default + elif len(fieldList) == 1: + return fieldList[0] + else: + return fieldList + + def getFieldList(self, name, index = 0): + # Return a sequence of strings. + raise NotImplementedError + + def getFile(self, name, index = 0, default = None): + # Return either an instance of FileUpload or a sequence of FileUpload instances. + fileList = self.getFileList(name, index) + if not fileList: + return default + elif len(fileList) == 1: + return fileList[0] + else: + return fileList + + def getFileList(self, name, index = 0): + # Return a sequence of FileUpload instances. + raise NotImplementedError + +## def getRepeat(self, template): +## raise NotImplementedError + + def hasField(self, name, index = 0): + raise NotImplementedError + + def hasFile(self, name, index = 0): + raise NotImplementedError + + def readFile(self): + raise NotImplementedError + + +class FakeSubmission(AbstractSubmission): + _fields = None + + def __init__(self, fields = None, query = None): + self._fields = {} + if fields: + for name, value in fields.items(): + self._fields[name] = [value] + if query: + for name, value in cgi.parse_qsl(query, keep_blank_values = True): + if name in self._fields: + self._fields[name].append(value) + else: + self._fields[name] = [value] + + def getFieldList(self, name, index = 0): + if index == 0 and name in self._fields: + return self._fields[name] + return [] + + def getFileList(self, name, index = 0): + return [] + + def hasField(self, name, index = 0): + return index == 0 and name in self._fields + + def hasFile(self, name, index = 0): + return False + + def readFile(self): + return None + + +class FieldStorageSubmission(AbstractSubmission): + """Submission wrapper for all encoding types handled by module 'cgi': + 'application/x-www-form-urlencoded', 'multipart/form-data'... + + This submission method discards the control index and repetition block parts of the form data + set. So, for these encoding types, control index is always 0. + """ + + fieldStorage = None + + def __init__(self, httpRequestHandler, contentType, contentLength, contentTypeHeader): + super(FieldStorageSubmission, self).__init__(httpRequestHandler, contentLength) + assert contentType + self.mimeType = contentType + # The use of environ seems to be required by cgi.FieldStorage. + # It also needs to add "content-type" in headers. + fakeHeaders = {} + for key, value in httpRequestHandler.headers.items(): + fakeHeaders[key] = value + environ = { + "CONTENT_TYPE": contentTypeHeader, + "REQUEST_METHOD": httpRequestHandler.command, + } + if not "content-type" in fakeHeaders: + fakeHeaders["content-type"] = environ["CONTENT_TYPE"] + if contentLength: + environ["CONTENT_LENGTH"] = str(contentLength) + splitedPath = httpRequestHandler.path.split("?") + if len(splitedPath) >= 2: + httpQuery = splitedPath[1] + if httpQuery: + environ["QUERY_STRING"] = httpQuery + self.fieldStorage = cgi.FieldStorage( + environ = environ, + fp = httpRequestHandler.rfile, + headers = fakeHeaders, + keep_blank_values = True) + + def getFieldList(self, name, index = 0): + if index > 0: + return [] + return [item.value + for item in self.fieldStorage.list + if item.name == name and item.filename is None] + + def getFileList(self, name, index = 0): + if index > 0: + return [] + return [FileUpload(item.filename, item.type, item.file) + for item in self.fieldStorage.list + if item.name == name and item.filename is not None] + + def hasField(self, name, index = 0): + if index == 0: + for item in self.fieldStorage.list: + if item.name == name and item.filename is None: + return True + return False + + def hasFile(self, name, index = 0): + if index == 0: + for item in self.fieldStorage.list: + if item.name == name and item.filename is not None: + return True + return False + + def readFile(self): + return None + + +class FileUpload(object): + file = None + filename = None # Optional + mimeType = None # Optional: MIME type with optional parameters. + + def __init__(self, filename, mimeType, file): + if filename is not None: + self.filename = filename + if mimeType is not None: + self.mimeType = mimeType + assert file is not None + self.file = file + + +class FileUploadSubmission(AbstractSubmission): + """Submission for exactly one file + + If the enctype attribute is not specified in the form (or is set to the empty string), and the + form consists of exactly one file upload control with exactly one file selected, then the user + agent use this submission method. + Also used for HTTP PUT... + + Note: FileUploadSubmission contains all the FileUpload interface, so that it can be used as a + FileUpload. + """ + + file = None + filename = None # Always None + + def __init__(self, httpRequestHandler, contentType, contentLength): + super(FileUploadSubmission, self).__init__(httpRequestHandler, contentLength) + assert contentType + self.mimeType = contentType + self.file = httpRequestHandler.rfile + + def getFieldList(self, name, index = 0): + return [] + + def getFileList(self, name, index = 0): + return [] + + def hasField(self, name, index = 0): + return False + + def hasFile(self, name, index = 0): + return False + + def readFile(self): + if self.length == 0: + return None + return self.file.read(self.length) + + +class XmlFormSubmission(AbstractSubmission): + """Submission for encoding type 'application/x-www-form+xml'""" + + file = None + mimeType = "application/x-www-form+xml" + + def __init__(self, httpRequestHandler, contentType, contentLength): + super(XmlFormSubmission, self).__init__(httpRequestHandler, contentLength) + assert contentType == self.mimeType + self.file = httpRequestHandler.rfile + + def getFieldList(self, name, index = 0): + raise NotImplementedError + + def getFileList(self, name, index = 0): + return NotImplementedError + + def hasField(self, name, index = 0): + raise NotImplementedError + + def hasFile(self, name, index = 0): + raise NotImplementedError + + def readFile(self): + return None + + +def readSubmission(httpRequestHandler): + # Get query, headers and form variables. + if httpRequestHandler.headers.typeheader is None: + if httpRequestHandler.command in ("GET", "HEAD", "POST"): + contentTypeHeader = "application/x-www-form-urlencoded" + else: + contentTypeHeader = httpRequestHandler.headers.type + else: + contentTypeHeader = httpRequestHandler.headers.typeheader + contentType, contentTypeOptions = cgi.parse_header(contentTypeHeader) + contentLength = httpRequestHandler.headers.get("content-length") + try: + contentLength = int(contentLength) + except (TypeError, ValueError): + contentLength = 0 + if contentType == "application/x-www-form+xml": + submission = XmlFormSubmission(httpRequestHandler, contentType, contentLength) + elif contentType in ("application/x-www-form-urlencoded", "multipart/form-data"): + submission = FieldStorageSubmission( + httpRequestHandler, contentType, contentLength, contentTypeHeader) + else: + submission = FileUploadSubmission(httpRequestHandler, contentType, contentLength) + return submission diff --git a/python/tests/web.py b/python/tests/web.py index 90d74a73..d2db8ed4 100644 --- a/python/tests/web.py +++ b/python/tests/web.py @@ -33,7 +33,84 @@ Features: """ +import urlparse + +from OpenSSL import SSL + import abstractweb +import http + + +class ReceivedHttpResponse(object): + body = None + headers = None + statusCode = None # 200 or... + statusMessage = None + + def __init__(self, statusCode = 200, statusMessage = None, headers = None, body = None): + if statusCode: + self.statusCode = statusCode + if statusMessage: + self.statusMessage = statusMessage + if headers: + self.headers = headers + if body: + self.body = body + + +class WebClient(abstractweb.WebClientMixin, object): + certificateAbsolutePath = None + privateKeyAbsolutePath = None + peerCaCertificateAbsolutePath = None + + def sendHttpRequest(self, method, url, headers = None, body = None): + parsedUrl = urlparse.urlparse(url) + addressingScheme, hostName, path = parsedUrl[:3] + if addressingScheme == 'https': + connection = http.HttpsConnection( + hostName, None, self.privateKeyAbsolutePath, self.certificateAbsolutePath, + self.peerCaCertificateAbsolutePath) + else: + connection = httplib.HTTPConnection(hostName) + if headers: + httpRequestHeaders = self.httpRequestHeaders.copy() + for name, value in headers.iteritems(): + httpRequestHeaders[name] = value + else: + httpRequestHeaders = self.httpRequestHeaders + failUnless('Content-Type' in httpRequestHeaders) + try: + connection.request('POST', path, body, httpRequestHeaders) + except SSL.Error, error: + if error.args and error.args[0] and error.args[0][0] \ + and error.args[0][0][0] == 'SSL routines': + logger.debug('SSL Error in sendHttpRequest. Error = %s' % repr(error)) + raise + response = connection.getresponse() + try: + body = response.read() + except SSL.SysCallError, error: + logger.debug('No SOAP answer in sendHttpRequest. Error = %s' % repr(error)) + raise + httpResponse = ReceivedHttpResponse(response.status, response.reason, response.msg, body) + return httpResponse + + +class WebSession(abstractweb.WebSessionMixin, object): + """Simulation of session of a web site""" + + expirationTime = None # A sample session variable + lassoLoginDump = None # Used only by some identity providers + lassoSessionDump = None + publishToken = False + + +class WebUser(abstractweb.WebUserMixin, object): + """Simulation of user of a web site""" + + lassoIdentityDump = None + language = 'fr' # A sample user variable + password = None class WebSite(abstractweb.WebSiteMixin, WebClient): @@ -42,14 +119,19 @@ class WebSite(abstractweb.WebSiteMixin, WebClient): WebSession = WebSession WebUser = WebUser - def __init__(self, internet, url): - WebClient.__init__(self, internet) + def __init__(self, url): + WebClient.__init__(self) abstractweb.WebSiteMixin.__init__(self) self.url = url - self.internet.addWebSite(self) def authenticate(self, handler, callback, *arguments, **keywordArguments): - FIXME: TODO. + user = handler.user + if user is None: + failUnless(handler.useHttpAuthentication) + return handler.outputErrorUnauthorized(handler.httpRequest.path) + else: + # The user is already authenticated using HTTP authentication. + userAuthenticated = True import lasso authenticationMethod = lasso.samlAuthenticationMethodPassword # FIXME @@ -57,6 +139,9 @@ class WebSite(abstractweb.WebSiteMixin, WebClient): session = handler.session if session is None: session = handler.createSession() + # No need to publish token, because we are using HTTP authentication. + if session.publishToken: + del session.publishToken user = handler.user if user is None: user = handler.createUser() @@ -64,3 +149,8 @@ class WebSite(abstractweb.WebSiteMixin, WebClient): user.sessionToken = session.token return callback(handler, userAuthenticated, authenticationMethod, *arguments, **keywordArguments) + + def authenticateLoginPasswordUser(self, login, password): + # We should check login & password and return the user if one matches or None otherwise. + # FIXME: Check password also. + return self.users.get(login) diff --git a/python/tests/websimulator.py b/python/tests/websimulator.py index f5fb843f..cd75195d 100644 --- a/python/tests/websimulator.py +++ b/python/tests/websimulator.py @@ -22,10 +22,6 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# FIXME: Replace principal with client in most methods. -# FIXME: Rename user to userAccount. - - import abstractweb @@ -126,13 +122,9 @@ class Internet(object): raise Exception('Unknown web site: %s' % url) -class WebClient(object): +class WebClient(abstractweb.WebClientMixin, object): internet = None keyring = None - httpRequestHeaders = { - 'User-Agent': 'LassoSimulator/0.0.0', - 'Accept': 'text/xml,application/xml,application/xhtml+xml,text/html', - } sessionTokens = None # Simulate the cookies, stored in user's navigator, and containing the # IDs of sessions already opened by the user. |