import libxml2
import urllib
import urlparse
import httplib
import re
import os

from quixote import get_field, get_request, get_response, get_session, get_session_manager, redirect
from quixote.directory import Directory
from quixote.http_request import parse_header

import lasso

import logger
import misc
import storage
from form import *
from template import *
from users import User
from hosts import Host
from federations import Federation

class Liberty(Directory):
    _q_exports = ["", "login", "assertionConsumer", "soapEndpoint",
            "singleLogout", "singleLogoutReturn",
            "federationTermination", "federationTerminationReturn",
            ('metadata.xml', 'metadata'), 'public_key',
            'local_auth', 'local_auth_check']

    def perform_login(self, idp = None):
        server = misc.get_lasso_server()
        login = lasso.Login(server)
        login.initAuthnRequest(idp, lasso.HTTP_METHOD_REDIRECT)
        login.request.nameIdPolicy = "federated"
        login.request.forceAuthn = False
        login.request.isPassive = False
        login.request.consent = "urn:liberty:consent:obtained"
        login.buildAuthnRequestMsg()
        return redirect(login.msgUrl)

    def assertionConsumer(self):
        server = misc.get_lasso_server()
        if not server:
            return error_page(_('Liberty support is not yet configured'))
        login = lasso.Login(server)
        request = get_request()
        if request.get_method() == 'GET' or get_field('LAREQ'):
            if request.get_method() == 'GET':
                login.initRequest(request.get_query(), lasso.HTTP_METHOD_REDIRECT)
            else:
                login.initRequest(get_field('LAREQ'), lasso.HTTP_METHOD_POST)

            login.buildRequestMsg()
            try:
                soap_answer = soap_call(login.msgUrl, login.msgBody)
            except SOAPException:
                return error_page(_("Failure to communicate with identity provider"))
            try:
                login.processResponseMsg(soap_answer)
            except lasso.Error, error:
                if error[0] == lasso.LOGIN_ERROR_STATUS_NOT_SUCCESS:
                    return error_page(_('Unknown authentication failure'))
                if hasattr(lasso, 'LOGIN_ERROR_UNKNOWN_PRINCIPAL'):
                    if error[0] == lasso.LOGIN_ERROR_UNKNOWN_PRINCIPAL:
                        return error_page(_('Authentication failure; unknown principal'))
                return error_page(_("Identity Provider didn't accept artifact transaction."))
        else:
            login.processAuthnResponseMsg(get_field('LARES'))
        login.acceptSso()
        session = get_session()
        if login.isSessionDirty:
            if login.session:
                session.lasso_session_dump = login.session.dump()
            else:
                session.lasso_session_dump = None
        user = self.lookup_user(session, login)
        if user:
            session.set_user(user.id)
        else:
            session.set_user('anonymous-%s' % login.nameIdentifier.content)
            session.lasso_anonymous_identity_dump = login.identity.dump()

        user = session.get_user()

        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')
#        if host.name == 'larpe':
#            user.is_admin = True
#            user.store()
#            return redirect('/admin/')
        federations = Federation.select(lambda x: host.id == x.host_id \
                                        and user.name_identifiers[0] in x.name_identifiers)

        if federations:        
            return self.sso_local_login(federations[0])
        else:
            if hasattr(session, 'token'):
                self.federate_token(session.token)
            else:
                response = get_response()
                if session.after_url:
                    after_url = session.after_url
                    session.after_url = None
                    return redirect(after_url)
                response.set_status(303)
                response.headers['location'] = urlparse.urljoin(request.get_url(), str('local_auth'))
                response.content_type = 'text/plain'
                return "Your browser should redirect you"

    def lookup_user(self, session, login):
        ni = login.nameIdentifier.content
        session.name_identifier = ni
        nis = list(User.select(lambda x: ni in x.name_identifiers))
        if nis:
            user = nis[0]
        else:
            if lasso.WSF_SUPPORT and misc.cfg.get('misc', {}).get('grab-user-with-wsf', False):
                disco = lasso.Discovery(login.server)
                disco.setSessionFromDump(session.lasso_session_dump)
                try:
                    disco.initQuery()
                except lasso.Error, error:
                    pass # XXX: there is no defined error code on lasso side
                    service = None
                else:
                    disco.addRequestedServiceType(lasso.PP_HREF)
                    disco.buildRequestMsg()
                    soap_answer = soap_call(disco.msgUrl, disco.msgBody)
                    disco.processQueryResponseMsg(soap_answer)

                    service = disco.getService()

                if not service:
                    return None

                service.initQuery('/pp:PP/pp:InformalName', 'name')
                service.addQueryItem('/pp:PP/pp:MsgContact', 'email')
                service.buildRequestMsg()
                try:
                    soap_answer = soap_call(service.msgUrl, service.msgBody)
                except SOAPException:
                    # it was advertised, it didn't work, too bad.
                    return None
                service.processQueryResponseMsg(soap_answer)

                email, name = None, None

                emailNode = service.getAnswer('/pp:PP/pp:MsgContact')
                if emailNode:
                    # horrible <MsgContact>; rebuild email
                    doc = libxml2.parseDoc(emailNode)
                    node = doc.children.children
                    account, provider = None, None
                    while node:
                        if node.name == 'MsgAccount':
                            account = node.getContent()
                        if node.name == 'MsgProvider':
                            provider = node.getContent()
                        node = node.next
                    if account and provider:
                        email = '%s@%s' % (account, provider)
                    else:
                        email = ''

                nameNode = service.getAnswer('/pp:PP/pp:InformalName')
                if nameNode:
                    doc = libxml2.parseDoc(nameNode)
                    name = unicode(doc.getContent(), 'utf-8').encode('iso-8859-1')
        
                if email and name:
                    user = User()
                    user.email = email
                    user.name = name
                    user.name_identifiers.append(login.nameIdentifier.content)
                    user.lasso_dump = login.identity.dump()
                    user.store()
                    return user

            return None

        user.lasso_dump = login.identity.dump()
        user.store()
        return user

    def singleLogout(self):
#        host = Host.get_host_from_url()
#        if host is not None and host.name != 'larpe':
        self.local_logout()
        request = get_request()
        logout = lasso.Logout(misc.get_lasso_server())
        if lasso.isLibertyQuery(request.get_query()):
            request = get_request()
            try:
                logout.processRequestMsg(request.get_query())
            except lasso.Error, error:
                if error[0] == lasso.DS_ERROR_INVALID_SIGNATURE:
                    return error_page(_('Failed to check single logout request signature.'))
                raise
            session = get_session()
            if not session.id:
  	            # session has not been found, this may be because the user has
  	            # its browser configured so that cookies are not sent for
  	            # remote queries and IdP is using image-based SLO.
  	            # so we look up a session with the appropriate name identifier
                name_identifier = logout.nameIdentifier.content
                for session in get_session_manager().values():
                    if name_identifier == session.name_identifier:
                        break
                else:
                    session = get_session()
            return self.slo_idp(logout, get_session())
        else:
            return self.slo_sp(logout, get_session())

    def singleLogoutReturn(self):
        logout = lasso.Logout(misc.get_lasso_server())
        try:
            logout.processResponseMsg(get_request().get_query())
        except lasso.Error, error:
            if error[0] == lasso.PROFILE_ERROR_INVALID_QUERY:
                raise AccessError()
            if error[0] == lasso.DS_ERROR_INVALID_SIGNATURE:
                return error_page(_('Failed to check single logout request signature.'))
            if hasattr(lasso, 'LOGOUT_ERROR_REQUEST_DENIED') and \
                    error[0] == lasso.LOGOUT_ERROR_REQUEST_DENIED:
                return redirect(misc.get_site_root_url()) # ignore silently
            elif error[0] == lasso.ERROR_UNDEFINED:
                # XXX: unknown status; ignoring for now.
                return redirect(misc.get_site_root_url()) # ignore silently
            raise
        return redirect(misc.get_site_root_url())

    def slo_idp(self, logout, session):
        # Single Logout initiated by IdP
        if session.lasso_session_dump:
            logout.setSessionFromDump(session.lasso_session_dump)
        user = get_request().user
#        user = get_session().get_user()
        if user and user.lasso_dump:
            logout.setIdentityFromDump(user.lasso_dump)
        if logout.nameIdentifier.content != session.name_identifier:
            raise 'No appropriate name identifier in session (%s and %s)' % (
                    logout.nameIdentifier.content, session.name_identifier)

        try:
            logout.validateRequest()
        except lasso.Error, error:
            if error[0] != lasso.PROFILE_ERROR_SESSION_NOT_FOUND:
                raise
        else:
            get_session_manager().expire_session() 

        logout.buildResponseMsg()
        if logout.msgBody: # soap answer
            return logout.msgBody
        else:
            return redirect(logout.msgUrl)

    def slo_sp(self, logout, session):
        if not session.user:
            get_session_manager().expire_session()
            return redirect(misc.get_site_root_url())
        if session.lasso_session_dump:
            logout.setSessionFromDump(session.lasso_session_dump)
        user = get_request().user
        #user = get_session().get_user()
        if user and user.lasso_dump:
            logout.setIdentityFromDump(user.lasso_dump)
        return self.slo_sp_redirect(logout)

    def slo_sp_redirect(self, logout):
        try:
            logout.initRequest(None, lasso.HTTP_METHOD_REDIRECT)
        except lasso.Error, error:
            if error[0] == lasso.PROFILE_ERROR_NAME_IDENTIFIER_NOT_FOUND:
                get_session_manager().expire_session() 
                return redirect(misc.get_site_root_url())
            raise
        logout.buildRequestMsg()
        get_session_manager().expire_session() 
        return redirect(logout.msgUrl)

    def soapEndpoint(self):
        request = get_request()
        ctype = request.environ.get("CONTENT_TYPE")
        if not ctype:
            return

        ctype, ctype_params = parse_header(ctype)
        if ctype != 'text/xml':
            return

        response = get_response()
        response.set_content_type('text/xml')

        length = int(request.environ.get('CONTENT_LENGTH'))
        soap_message = request.stdin.read(length)

        request_type = lasso.getRequestTypeFromSoapMsg(soap_message) 

        if request_type == lasso.REQUEST_TYPE_LOGOUT:
            logout = lasso.Logout(misc.get_lasso_server())
            logout.processRequestMsg(soap_message)
            name_identifier = logout.nameIdentifier.content
            for session in get_session_manager().values():
                if name_identifier == session.name_identifier:
                    break
            else:
                session = None
            return self.slo_idp(logout, session)

        if request_type == lasso.REQUEST_TYPE_DEFEDERATION:
            defederation = lasso.Defederation(misc.get_lasso_server())
            defederation.processNotificationMsg(soap_message)
            name_identifier = defederation.nameIdentifier.content
            for session in get_session_manager().values():
                if name_identifier == session.name_identifier:
                    break
            else:
                session = None
            return self.fedterm(defederation, session)

    def defederate(self, idp = None):
        session = get_session()
        user = session.get_user()

        self.local_defederate(session)
        
        defederation = lasso.Defederation(misc.get_lasso_server())
        defederation.setSessionFromDump(session.lasso_session_dump)
        if user and user.lasso_dump:
            defederation.setIdentityFromDump(user.lasso_dump)

        defederation.initNotification(idp, lasso.HTTP_METHOD_SOAP);
        defederation.buildNotificationMsg();

        try:
            soap_call(defederation.msgUrl, defederation.msgBody);
        except SOAPException:
            return error_page(_("Failure to communicate with identity provider"))

        rootUrl = '/' + misc.get_proxied_site_name() + '/'
        return redirect(rootUrl)

    def local_defederate(self, session):
        user = session.get_user()
        self.local_logout()
        if user is not None:
            federations = Federation.select(lambda x: user.name_identifiers[0] in x.name_identifiers)
            for federation in federations:
                federation.remove_name_identifier(user.name_identifiers[0])            

    def federationTermination(self):
        request = get_request()
        if not lasso.isLibertyQuery(request.get_query()):
            return redirect('/')
        
        defederation = lasso.Defederation(misc.get_lasso_server())
        defederation.processNotificationMsg(request.get_query())
        session = get_session()
        return self.fedterm(defederation, session)

    def fedterm(self, defederation, session):
#        host = Host.get_host_from_url()
#        if host is not None and host.name != 'larpe':
        self.local_defederate(session)
        if session.lasso_session_dump:
            defederation.setSessionFromDump(session.lasso_session_dump)

        user = get_request().user
        #user = get_session().get_user()
        if user and user.lasso_dump:
            defederation.setIdentityFromDump(user.lasso_dump)

        try:
            defederation.validateNotification()
        except lasso.Error, error:
            pass # ignore failure (?)
        else:
            if not defederation.identity:
                # if it was the last federation the whole identity dump collapsed
                user.lasso_dump = None
            else:
                user.lasso_dump = defederation.identity.dump()
            user.store()

        if defederation.isSessionDirty:
            if defederation.session:
                session.lasso_session_dump = defederation.session.dump()
            else:
                session.lasso_session_dump = None

        if defederation.msgUrl:
            return redirect(defederation.msgUrl)
        else:
            get_session_manager().commit_changes(session)
            response = get_response()
            response.set_status(204)
            return ''

    def federationTerminationReturn(self):
        return redirect(misc.get_site_root_url())

    def metadata(self):
        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')
        response = get_response()
        response.set_content_type('text/xml', 'utf-8')
        metadata = unicode(open(host.metadata).read(), 'utf-8')
        return metadata

    def public_key(self):
        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')
        response = get_response()
        response.set_content_type('application/octet-stream')
        public_key = open(host.public_key).read()
        return public_key

    def federate(self, user_id, user_password):
        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')
        liberty_user = get_session().get_user()
        if liberty_user is not None:
            Federation(user_id, user_password, host.id, liberty_user.name_identifiers[0]).store()

    def federate_token(self, token):
        token_dir = os.path.join(misc.get_proxied_site_path(), 'tokens')
        token_file_name = os.path.join(token_dir, token)
        token_file = open(token_file_name, 'r')
        user_id, user_passwd = token_file.read().split()        
        token_file.close()

    def sso_local_login(self, federation):
        status, data = self.local_auth_check_post(federation.username, federation.password)
        success, return_content = self.check_auth(status, data)
        if success:
            return return_content
        else:
            return self.local_auth(False)

    def local_auth [html] (self, first_time=True):
        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')

        response = get_response()
        response.set_content_type('text/html')
        self.local_auth_default(first_time)
        # We can use it to make a more specific/integrated authentication page
        #function_name = 'local_auth_' + host.name
        #getattr(self, str(function_name))(host)

# Example of such a specific method
#    def local_auth_xxx [html] (self, host):
#        get_response().filter.update({'no_template': True})
#        page = misc.get_html_page(host.auth_form)
#        old_url = re.compile(str('(<form action=")[^"]*"'))
#        page = old_url.sub(str(r"""<p>Please type your login and password for Dotclear.
#            Your account will be federated with your Liberty Alliance account</p>
#            \1local_auth_check"
#            """), page)
#        remember_checkbox = re.compile(str('<p><input type="checkbox" id="user_remember".*?</p>'), re.DOTALL)
#        page = remember_checkbox.sub(str(r''), page)
#        htmltext(page)


    def local_auth_default [html] (self, first_time=True):
        if hasattr(get_response(), str('breadcrumb')):
            del get_response().breadcrumb
        get_response().filter['body_class'] = 'login'
        get_response().filter['title'] = _('Local authentication')
        
        form = form = Form(enctype='multipart/form-data')
        form.add(StringWidget, 'username', title = _('Username'), required = True,
                size = 30)
        form.add(PasswordWidget, 'password', title = _('Password'), required = True,
                size = 30)
        form.add_submit('submit', _('Submit'))
        #form.add_submit('cancel', _('Cancel'))
        
#        if form.get_widget('cancel').parse():
#            return redirect('.')

        if not form.is_submitted() or form.has_errors() or form.get_submit() is True:
            '<p>'
            _("""Please type your login and password for this Service Provider.
            Your local account will be federated with your Liberty Alliance account.""")
            '</p>'
            form.render()
        else:
            username = form.get_widget('username').parse()
            password = form.get_widget('password').parse()
            return self.local_auth_check_default(username, password)
        
#        """
#    <div style="text-align: center;">
#        <form action="local_auth_check" method="post">
#"""

#        if not first_time:
#            """
#                <p>
#                    <div style="color: red; font-weight: bold;">Authentication failed</div>
#                </p>
#"""

#        """
#                <p>
#                    Please type your login and password for this Service Provider.<br />
#                    Your local account will be federated with your Liberty Alliance account.
#                </p>
#                <p>
#                    <span style="font-weight: bold;">%(identifier_label)s :</span>
#                    <input name="user_id" id="user_id" type="text" maxlength="32" value="" tabindex="1"/>
#                </p>
#                <p>
#                    <span style="font-weight: bold;">%(password_label)s :</span>
#                    <input name="user_pwd" id="user_pwd" type="password" tabindex="2" />
#                </p>
#                <p>
#                    <input class="submit" type="submit" value="%(submit_label)s :" />
#                </p>
#        </form>
#    </div>
#    <script type="text/javascript">
#        document.forms[0]['user_id'].focus();
#    </script>
#""" % { 'identifier_label': _('Identifier'), 'password_label': _('Password'), 'submit_label': _('Submit') }

    def local_auth_check (self, user_id = None, user_password = None):
        return self.local_auth_check_default (user_id, user_password)

#        host = Host.get_host_from_url()
#        if host is None:
#            return redirect('/')
        #function_name = 'local_auth_check_' + host.name
        #return getattr(self, str(function_name))(host, user_id, user_password)

    def local_auth_check_default (self, user_id, user_password):
#        return_url = host.get_return_url()
        request = get_request()
        if user_id is None:
            user_id = request.get_field('user_id')
        if user_password is None:
            user_password = request.get_field('user_pwd')
        status, data = self.local_auth_check_post(user_id, user_password)
        success, return_content = self.check_auth(status, data)
        if success:
            self.federate(user_id, user_password)
            return return_content
        else:
            return self.local_auth(False)

    def local_auth_check_post (self, user_id, user_password):
        host = Host.get_host_from_url()
        assert host is not None
        if not hasattr(host, 'auth_check_url'):
            print 'This service provider is not properly configured : No authentication form was found'
            return (0, '')
        return self.local_auth_check_post_default (host, user_id, user_password)
        #function_name = 'local_auth_check_post_' + host.name
        #return getattr(self, str(function_name))(user_id, user_password, host)

    def local_auth_check_post_default (self, host, user_id, user_password):
        url = host.auth_check_url
        if url.startswith('http://'):
            host_addr, query = urllib.splithost(url[5:])
            conn = httplib.HTTPConnection(host_addr)
        else:
            host_addr, query = urllib.splithost(url[6:])
            conn = httplib.HTTPSConnection(host_addr)
        query = query.replace('&amp;', '&')
        body = '%s=%s&%s=%s' % (host.login_field_name, user_id, host.password_field_name, user_password)
        for key, value in host.other_fields.iteritems():
            body += '&%s=%s' % (key, value)
        conn.request('POST', query, body, {'Content-Type': 'application/x-www-form-urlencoded'})
        response = conn.getresponse()
        data = response.read()
        conn.close()
        cookies = response.getheader('Set-Cookie', None)
        host.cookies = []
        misc.reload_cfg()
        if cookies is not None:
            get_session().cookies = cookies
            cookies_list = []
            for cookie in cookies.split(', '):
                # Drop the path and other attributes
                cookie_only = cookie.split('; ')[0]
                regexp = re.compile('=')
                if regexp.search(cookie_only) is None:
                    continue
                # Split name and value
                cookie_split = cookie_only.split('=')
                cookie_name = cookie_split[0]
                cookie_value = cookie_split[1]
                set_cookie = '%s=%s; path=/' % (cookie_name, cookie_value)
                cookies_list.append(set_cookie)
                host.cookies.append(cookie_name)
            cookies_headers = '\r\nSet-Cookie: '.join(cookies_list)
            get_response().set_header('Set-Cookie', cookies_headers)
            host.store()
        return (response.status, data)

    def local_logout(self):
        host = Host.get_host_from_url()
        if host is None:
            return
#        misc.get_html_page(host.logout_url)
        if hasattr(host, 'cookies'):
            for cookie in host.cookies:
                get_response().expire_cookie(cookie, path='/')
#        function_name = 'local_logout_' + host.name
#        getattr(self, str(function_name))()

    def check_auth(self, status, data):
        host = Host.get_host_from_url()
        if host is None:
            return redirect('/')
        success = False
        return_content = ''
        # If there is a password field, authentication probably failed
        regexp = re.compile("""<input[^>]*?type=["']?password["']?[^>]*?>""", re.DOTALL | re.IGNORECASE)
        if not regexp.findall(data):
            success = True
            if host.orig_site.startswith('http://listes.entrouvert.com'):
                get_response().filter.update({'no_template': True})
                return_content = data
            else:
                return_content = redirect(host.get_return_url())

#        if status == 302:
#            success = True
#            return_content = redirect(host.return_url)
#        else:
#            if host.orig_site.startswith('http://listes.entrouvert.com') \
#                    and re.search('You have logged in', data) is not None:
#                success = True
#                return_content = data
        return (success, return_content)


class SOAPException(Exception):
    pass


def soap_call(url, msg):
    if url.startswith('http://'):
        host, query = urllib.splithost(url[5:])
        conn = httplib.HTTPConnection(host)
    else:
        host, query = urllib.splithost(url[6:])
        conn = httplib.HTTPSConnection(host)
    conn.request("POST", query, msg, {'Content-Type': 'text/xml'})
    response = conn.getresponse()
    data = response.read()
    conn.close()
    if response.status not in (200, 204): # 204 ok for federation termination
        logger.warn('SOAP error (%s) (on %s)' % (response.status, url))
        raise SOAPException()
    return data
