VIEW_LOGIN = 0 VIEW_LOGIN_SUCCESS = 1 VIEW_LOGOUT = 2 VIEW_INVALID_SERVICE = 3 VIEW_ERROR_5XX = 4 VIEW_NOT_FOUND = 5 class IRealmFactory(Interface):

    tag = Attribute('String used to identify the plugin factory.')
    opt_help = Attribute('String description of the plugin.')
    opt_usage = Attribute('String describes how to provide arguments for factory.')

    def generateRealm(argstring=""):
        """
        Create an object that implements IRealm.
        """ class IServiceManagerFactory(Interface):

    tag = Attribute('String used to identify the plugin factory.')
    opt_help = Attribute('String description of the plugin.')
    opt_usage = Attribute('String describes how to provide arguments for factory.')

    def generateServiceManager(argstring=""):
        """
        Create an object that implements txcas.IServiceManager
        """ class ITicketStoreFactory(Interface):

    tag = Attribute('String used to identify the plugin factory.')
    opt_help = Attribute('String description of the plugin.')
    opt_usage = Attribute('String describes how to provide arguments for factory.')

    def generateTicketStore(argstring=""):
        """
        Create an object that implements ITicketStore.
        """ class IViewProviderFactory(Interface):

    tag = Attribute('String used to identify the plugin factory.')
    opt_help = Attribute('String description of the plugin.')
    opt_usage = Attribute('String describes how to provide arguments for factory.')

    def generateViewProvider(argstring=""):
        """
        Create an object that provides one or more views.
        """ class IServiceManagerAcceptor(Interface):
    
    service_manager = Attribute('None or a reference to the current service manager plugin.') class ServerApp(object):
    app = Klein()
    cookie_name = 'tgc'

    def __init__(self, ticket_store, realm, checkers, validService=None,
                 requireSSL=True, page_views=None, validate_pgturl=True,
                 static=None, reactor=None):
        """
        Initialize an instance of the CAS server.

        @param ticket_store: The ticket store to use.
        @param realm: The t.c.p.Portal asks the realm for an avatar.
        @param checkers: A list of credential checkers to try (in order).
        @param validService: A callable that takes a service as an argument 
            and returns True if the server will authenticate for that service.
        @param requireSSL: Require SSL for Ticket Granting Cookie
        @param page_views: A mapping of functions that are used to render
            custom pages.
            - All views may either be synchronous or async (deferreds).
            - List of views:
                - login: rendered when credentials are requested.
                    - Should accept args (loginTicket, service, failed, request).
                    - Rendered page should POST to /login according to CAS protocol.
                - login_success: Rendered when no service is specified and a
                    valid SSO session already exists.
                    - Should accept args (avatar, request)
                - logout: Rendered on logout.
                    - Should accept args (request,).
                - invalid_service: Rendered when an invalid service is provided.
                    - Should accept args (service, request).
                - error5xx: Rendered on an internal error.
                    - Should accept args (err, request).
                    - `err` is a twisted.python.failure.Failure
                - not_found: Rendered when the requested resource is not found.
                    - Should accept `request`.
        @param validate_pgturl: If True, follow the protocol and validate the pgtUrl
            peer.  Useful to set to False for development purposes.
        @param static_dir: None or path to a static folder to serve content
            from at /static.
        """
        assert ticket_store is not None, "No Ticket Store was configured."
        assert realm is not None, "No Realm was configured."
        assert len(checkers) > 0, "No Credential Checkers were configured."
        if reactor is None:
            from twisted.internet import reactor
        self.reactor = reactor
        for n, checker in enumerate(checkers):
            assert checker is not None, "Credential Checker #%d was not configured." % n
        self.cookies = {}
        self.ticket_store = ticket_store
        self.cred_requestor_portal = Portal(realm)
        self.cred_acceptor_portal = Portal(realm)
        self.realm = realm
        self.checkers = checkers
        map(
            self.cred_requestor_portal.registerChecker, 
            [c for c in checkers 
                if (ICASAuthWhen.providedBy(c) and c.auth_when == 'cred_requestor')])
        map(
            self.cred_acceptor_portal.registerChecker, 
            [c for c in checkers
                if (ICASAuthWhen.providedBy(c) and c.auth_when == 'cred_acceptor')
                    or (not ICASAuthWhen.providedBy(c)) ])
        self.requireSSL = requireSSL
        self.validService = validService or (lambda x: True)
        self.validate_pgturl = validate_pgturl
        self._static = static
        default_page_views = {
                VIEW_LOGIN: self._renderLogin,
                VIEW_LOGIN_SUCCESS: self._renderLoginSuccess,
                VIEW_LOGOUT: self._renderLogout,
                VIEW_INVALID_SERVICE: self._renderInvalidService,
                VIEW_ERROR_5XX: self._renderError5xx,
                VIEW_NOT_FOUND: self._renderNotFound,
            }
        self._default_page_views = default_page_views
        if page_views is None:
            page_views = default_page_views
        else:
            temp = dict(default_page_views)
            temp.update(page_views)
            page_views = temp
            del temp
        self.page_views = page_views
        self.ticket_store.register_ticket_expiration_callback(log_ticket_expiration)
        self.show_login_page = self._isLoginPageRequired()

    def _isLoginPageRequired(self):
        # If no username/password cred checkers are present, showing the login
        # page does not make sense.
        checkers = self.checkers
        found = False
        for checker in checkers:
            supported_creds = checker.credentialInterfaces
            for supported_cred in supported_creds:
                if supported_cred.isOrExtends(IUsernamePassword): 
                    found = True
                    break
        return found

    def _set_response_code_filter(self, result, code, request, msg=None):
        """
        Set the response code during deferred chain processing.
        """
        request.setResponseCode(code, message=msg)
        return result
        
    def _log_failure_filter(self, err, request):
        """
        Log a failure.
        """
        log.msg('[ERROR] type="error" client_ip="%s" uri="%s"' % (request.getClientIP(), request.uri))
        log.err(err)
        return err

    def _get_page_view(self, symbol, *args):

        def eb(err, symbol, *args):
            err.trap(ViewNotImplementedError) 
            log.err(err)            
            return defer.maybeDeferred(self._default_page_views[symbol], *args)

        d = defer.maybeDeferred(self.page_views[symbol], *args)
        d.addErrback(eb, symbol, *args) 
        return d

    def _page_view_callback(self, _, symbol, *args):
        d = self._get_page_view(symbol, *args)
        return d

    def _page_view_result_callback(self, result, symbol, *args):
        d = self._get_page_view(symbol, result, *args)
        return d

    def _page_view_errback(self, err, symbol, *args):
        d = self._get_page_view(symbol, err, *args) 
        return d

    @app.route('/login', methods=['GET'])
    def login_GET(self, request):
        """
        Present a username/password login page to the browser.
        OR
        authenticate using an existing TGC.
        """
        log_http_event(request)
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        portal = self.cred_requestor_portal

        def handle_trust_auth_failed(err, request):
            err.trap(UnhandledCredentials, UnauthorizedLogin)
            if err.check(UnauthorizedLogin):
                log_cas_event("Trust authentication failed", [('auth_fail_reason', err.getErrorMessage()),])
            if self.show_login_page:
                d = self._presentLogin(request)
                return d
            else:
                #TODO: How to respond if trust auth fails and there is no login auth?
                return err

        if renew != "":
            d = self._authenticateByTrust(portal, service, request)
            d.addCallbacks(
                self._authenticated, 
                handle_trust_auth_failed,
                (True, service, request), None,
                (request,), None)
            return d
            
        def service_err(err, service, request):
            err.trap(InvalidService)
            log.err(err)
            request.setResponseCode(403)
            return self._get_page_view(VIEW_INVALID_SERVICE, service, request)

        d = self._authenticateByCookie(request)
        d.addErrback(lambda _: self._authenticateByTrust(portal, service, request).addCallbacks(
            self._authenticated, 
            handle_trust_auth_failed,
            (True, service, request), None,
            (request,), None))
        d.addErrback(service_err, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX,  request)
        return d

    def _authenticateByTrust(self, portal, service, request):
        if hasattr(request, 'transport'):
            transport = request.transport
        elif hasattr(request, 'channel'):
            transport = request.channel.transport
        else:
            raise Unauthorized("Could not get transport from request!")
        mind = {'service': service}

        def log_auth(avatar_id, request):
            client_ip = request.getClientIP()
            log_cas_event("Authenticated via Trust", [
                        ('client_ip', client_ip), ('username', avatar_id)])
            return avatar_id

        d = portal.login(transport, mind, ICASUser)
        d.addCallback(lambda x: x[1].username)
        d.addCallback(log_auth, request)
        return d

    def _authenticateByCookie(self, request):
        tgc = request.getCookie(self.cookie_name)
        if not tgc:
            return defer.fail(CookieAuthFailed("No cookie"))
        service = get_single_param_or_default(request, 'service', "")

        def log_tgc_auth(result, request):
            client_ip = request.getClientIP()
            avatar_id = result['avatar_id']
            log_cas_event("Authenticated via TGC", [
                        ('client_ip', client_ip), ('username', avatar_id)])
            return result

        def extract_avatar_id(result):
            return result['avatar_id']

        def eb(err, request):
            err.trap(InvalidTicket, NotSSOService)
            # delete the cookie
            request.addCookie(self.cookie_name, '',
                              expires='Thu, 01 Jan 1970 00:00:00 GMT')
            return err

        d = self.ticket_store.useTicketGrantingCookie(tgc, service)
        d.addCallback(log_tgc_auth, request)
        d.addCallback(extract_avatar_id)
        d.addErrback(eb, request)
        return d.addCallback(self._authenticated, False, service, request)

    def _presentLogin(self, request, failed=False):
        # If the login is presented, the TGC should be removed and the TGT
        # should be expired.

        def expireTGT():
            tgc = request.getCookie(self.cookie_name)
            if tgc:
                #Delete the cookie.
                request.addCookie(
                    self.cookie_name, '', expires='Thu, 01 Jan 1970 00:00:00 GMT')
                #Expire the ticket.
                d = self.ticket_store.expireTGT(tgc)
                return d
            return None

        service = get_single_param_or_default(request, 'service', "")
        gateway = get_single_param_or_default(request, 'gateway', "")
        if gateway != "" and service != "":
            #Redirect to `service` with no ticket.
            request.redirect(service)            
            request.finish()
            return
       
        def service_err(err, service, request):
            err.trap(InvalidService)
            log.err(err)
            request.setResponseCode(403)
            return self._get_page_view(VIEW_INVALID_SERVICE, service, request)
 
        d = defer.maybeDeferred(expireTGT)
        d.addCallback(lambda x: service)
        d.addCallback(self.ticket_store.mkLoginTicket)
        if failed:
            d.addCallback(self._set_response_code_filter, 403, request)
        d.addCallback(self._page_view_result_callback, VIEW_LOGIN, service, failed, request)
        d.addErrback(service_err, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d
            
    def _authenticated(self, avatar_id, primaryCredentials, service, request):
        """
        Call this after authentication has succeeded to finish the request.
        """
        tgc = request.getCookie(self.cookie_name)
        
        @defer.inlineCallbacks
        def maybeAddCookie(avatar_id, service, request):
            ticket = request.getCookie(self.cookie_name)
            if not ticket:
                path = request.URLPath().sibling('').path
                ticket = yield self.ticket_store.mkTicketGrantingCookie(avatar_id)
                request.addCookie(self.cookie_name, ticket, path=path,
                                  secure=self.requireSSL)
                request.cookies[-1] += '; HttpOnly'
                attribs = [
                    ('client_ip', request.getClientIP()),
                    ('username', avatar_id),
                    ('TGC', ticket),]
                if service != "":
                    attribs.append(('service', service))
                log_cas_event("Created TGC", attribs)
            defer.returnValue(ticket)

        def mkServiceTicket(tgc, service, request):
            
            def log_service_ticket_created(ticket, service, tgc, request):
                client_ip = request.getClientIP()
                log_cas_event("Created service ticket", [
                            ('client_ip', client_ip), 
                            ('ticket', ticket),
                            ('service', service),
                            ('TGC', tgc),])
                return ticket
            
            def log_failed_to_create_ticket(err, service, tgc, request):
                client_ip = request.getClientIP()
                log_cas_event("Failed to create service ticket", [
                    ('client_ip', client_ip), 
                    ('service', service),
                    ('TGC', tgc)])
                return err
                
            return self.ticket_store.mkServiceTicket(service, tgc, primaryCredentials).addCallback(
                log_service_ticket_created, service, tgc, request).addErrback(
                log_failed_to_create_ticket, service, tgc, request)
            
        def redirect(ticket, service, avatar_id, request):
            p = urlparse.urlparse(service)
            query = urlparse.parse_qs(p.query)
            if 'ticket' in query:
                del qs_map['ticket']
                log.msg('''[WARN] warning="Removed 'ticket' parameter from service URL '%s'."  client_ip="%s" username="%s"''' % (
                    service, request.getClientIP(), avatar_id))
            query['ticket'] = ticket
            param_str = urlencode(query, doseq=True)
            p = urlparse.ParseResult(*tuple(p[:4] + (param_str,) + p[5:]))
            service_url = urlparse.urlunparse(p)
            log.msg("[DEBUG] Redirecting to: '{0}'.".format(service_url))
            request.redirect(service_url)

        d = maybeAddCookie(avatar_id, service, request)
        if service != "":
            d.addCallback(mkServiceTicket, service, request)
            d.addCallback(redirect, service, avatar_id, request)
        else:
            d.addCallback(replace_result, avatar_id)
            mind = {'service': ""}
            d.addCallback(self.realm.requestAvatar, mind, ICASUser)
            d.addCallback(extract_avatar)
            d.addCallback(self._page_view_result_callback, VIEW_LOGIN_SUCCESS, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d

    def _renderLogin(self, ticket, service, failed, request):
        html_parts = []
        html_parts.append(dedent('''\
        <html>
            <body>
                <form method="post" action="">
                    Username: <input type="text" name="username" />
                    <br />Password: <input type="password" name="password" />
                    <input type="hidden" name="lt" value="%(lt)s" />
        ''') % {
            'lt': cgi.escape(ticket),
        })
        if service != "":
            html_parts.append(
                '            '
                '<input type="hidden" name="service" value="%(service)s" />' % {
                    'service': service
                })
        html_parts.append(dedent('''\
                    <input type="submit" value="Sign in" />
                </form>
            </body>
        </html>
        '''))
        return '\n'.join(html_parts)

    def _renderLoginSuccess(self, avatar, request):
        html = dedent("""\
            <html>
                <body>
                    <h1>A CAS Session Exists</h1>
                    <p>
                        A CAS session exists for account '%s'.
                    </p>
                </body>
            </html>
            """) % escape_html(avatar.username)
        return html
        
    def _renderLogout(self, request):
        return "You have been logged out."
        
    def _renderInvalidService(self, service, request):
        html = dedent("""\
            <html>
                <head>
                    <title>Invalid Service</title>
                </head>
                <body>
                    <h1>Invalid Service</h1>
                    <p>
                        The service provided is not authorized to use this CAS
                        implementation.
                    </p>
                </body>
            </html>
            """)
        request.setResponseCode(403)
        return html
        
    def _renderError5xx(self, err, request):
        html = dedent("""\
            <html>
                <head>
                    <title>Internal Error - 500</title>
                </head>
                <body>
                    <h1>HTTP 500 - Internal Error</h1>
                    <p>
                        Please contact your system administrator.
                    </p>
                </body>
            </html>
            """)
        request.setResponseCode(500)
        return html
        
    def _renderNotFound(self, request):
        return dedent("""\
            <html>
            <head>
                <title>Not Found</title>
            </head>
            <body>
                <h1>Not Found</h1>
                <p>
                The resource you were looking for was not found.
                </p>
            </body>
            </html>
            """)

    @app.route('/login', methods=['POST'])
    def login_POST(self, request):
        """
        Accept a username/password, verify the credentials and redirect them
        appropriately.
        """
        log_http_event(request, redact_args=['password'])
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        username = get_single_param_or_default(request, 'username', None)
        password = get_single_param_or_default(request, 'password', None)
        ticket = get_single_param_or_default(request, 'lt', None)

        def log_trust_auth_failed(err, request):
            err.trap(UnhandledCredentials, UnauthorizedLogin)
            if err.check(UnauthorizedLogin):
                client_ip = request.getClientIP()
                log_cas_event("Trust authentication failed", [
                    ('auth_fail_reason', err.getErrorMessage()),
                    ('client_ip', client_ip)])
            elif err.check(UnhandledCredentials):
                return None
            return err
            
        def does_trust_avatar_match_username(trust_avatar_id, username, request):
            if trust_avatar_id.lower() != username.lower():
                raise UnauthorizedLogin(
                    "Trust-based avatar ID, '%s', does not match submitted username '%s'." % (
                        trust_avatar_id, username))
            return True

        def checkPassword(_, username, password, service):
            credentials = UsernamePassword(username, password)
            mind = {'service': service}
            return self.cred_acceptor_portal.login(credentials, mind, ICASUser)

        def log_auth_failed(err, username, request):
            err.trap(Unauthorized, InvalidTicket, InvalidService)
            client_ip = request.getClientIP()
            log_cas_event("Failed to authenticate using primary credentials: %s" %
                    err.getErrorMessage(), [
                    ('client_ip', client_ip), ('username', username)])
            return err

        def log_authentication(result, username, request):
            client_ip = request.getClientIP()
            log_cas_event("Authenticated using primary credentials", [
                        ('client_ip', client_ip), ('username', username)])
            return result

        def inject_avatar_id(_, avatar_id):
            return avatar_id

        def eb(err, service, request):
            if not err.check(Unauthorized):
                log.err(err)

            if self.show_login_page:
                d = self._presentLogin(request, failed=True)
                return d
            else:
                return err

        # check credentials
        portal = self.cred_acceptor_portal
        d = self.ticket_store.useLoginTicket(ticket, service)
        d.addCallback(lambda x: self._authenticateByTrust(portal, service, request))
        d.addCallback(does_trust_avatar_match_username, username, request)
        d.addErrback(log_trust_auth_failed, request)
        d.addCallback(checkPassword, username, password, service)
        d.addErrback(log_auth_failed, username, request)
        d.addCallback(log_authentication, username, request)
        d.addCallback(inject_avatar_id, username)
        d.addCallback(self._authenticated, True, service, request)
        d.addErrback(eb, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX,  request)
        return d


    @app.route('/logout', methods=['GET'])
    def logout_GET(self, request):
        log_http_event(request)
        service = get_single_param_or_default(request, 'service', "")

        def _validService(_, service):
            def eb(err):
                err.trap(InvalidService)
                return self._get_page_view(VIEW_INVALID_SERVICE, service, request)
            return defer.maybeDeferred(
                self.validService, service).addErrback(eb)
                
        tgc = request.getCookie(self.cookie_name)
        if tgc:
            #Delete the cookie.
            request.addCookie(
                self.cookie_name, '', expires='Thu, 01 Jan 1970 00:00:00 GMT')
            #Expire the ticket.

            def log_ticket_expired(result, tgc, request):
                log_cas_event("Explicitly logged out of SSO", [
                    ('client_ip', request.getClientIP()),
                    ('TGC', tgc)])
                return result

            d = self.ticket_store.expireTGT(tgc)
            d.addCallback(log_ticket_expired, tgc, request)
        else:
            d = defer.maybeDeferred(lambda : None)

        if service != "":
            def redirect(_):
                request.redirect(service)
            d.addCallback(_validService, service)
            d.addCallback(redirect)
        else:
            d.addCallback(self._page_view_callback, VIEW_LOGOUT, request)

        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d

    @app.route('/validate', methods=['GET'])
    def validate_GET(self, request):
        """
        Validate a service ticket, consuming the ticket in the process.
        """
        log_http_event(request)
        ticket = get_single_param_or_default(request, 'ticket', "")
        service = get_single_param_or_default(request, 'service', "")
        renew = get_single_param_or_default(request, 'renew', "")
        if service == "" or ticket == "":
            request.setResponseCode(403)
            return 'no\n\n'
        if renew != "":
            require_pc = True
        else:
            require_pc = False        
        d = self.ticket_store.useServiceTicket(ticket, service, require_pc)

        def renderUsername(data, ticket, service, request):
            avatar_id = data['avatar_id']
            
            def successResult(result, ticket_info, ticket, service, request):
                iface, avatarAspect, logout = result
                attribs = [
                    ('client_ip', request.getClientIP()),
                    ('user', avatarAspect.username),
                    ('ticket', ticket),
                    ('service', service),
                    ('TGT', ticket_info['tgt']),
                    ('primary_credentials', ticket_info['primary_credentials']),]
                if 'pgt' in ticket_info:
                    attribs.append(("PGT", ticket_info['pgt']))
                if 'proxy_chain' in ticket_info:
                    attribs.append(("proxy_chain", ', '.join(ticket_info['proxy_chain'])))
                log_cas_event("Validated service ticket (/validate)", attribs)
                return 'yes\n' + avatarAspect.username + '\n'
            
            mind = {'service': service}
            return self.realm.requestAvatar(avatar_id, mind, ICASUser).addCallback(
                successResult, data, ticket, service, request)
            
        def renderFailure(err, ticket, service, request):
            log_cas_event("Failed to validate service ticket (/validate).", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket),
                ('service', service),])
            err.trap(InvalidTicket, InvalidService, Unauthorized)
            request.setResponseCode(403)
            return 'no\n\n'

        d.addCallback(renderUsername, ticket, service, request)
        d.addErrback(renderFailure, ticket, service, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d        

    @app.route('/serviceValidate', methods=['GET'])
    def serviceValidate_GET(self, request):
        log_http_event(request)
        return self._serviceOrProxyValidate(request, False)
        
    @app.route('/proxyValidate', methods=['GET'])
    def proxyValidate_GET(self, request):
        log_http_event(request)
        return self._serviceOrProxyValidate(request, True)
    
    def _serviceOrProxyValidate(self, request, proxyValidate=True):
        """
        Validate a service ticket or proxy ticket, consuming the ticket in the process.
        """
        ticket = get_single_param_or_default(request, 'ticket', None)
        service = get_single_param_or_default(request, 'service', None)
        pgturl = get_single_param_or_default(request, 'pgtUrl', "")
        renew = get_single_param_or_default(request, 'renew', "")
        if service is None or ticket is None:
            request.setResponseCode(400)
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                   <cas:authenticationFailure code="INVALID_REQUEST">
                      Missing parameters.
                   </cas:authenticationFailure>
                </cas:serviceResponse>
                """)
        if renew != "":
            require_pc = True
        else:
            require_pc = False
        if proxyValidate: 
            d = self.ticket_store.useServiceOrProxyTicket(ticket, service, require_pc)
        else:
            d = self.ticket_store.useServiceTicket(ticket, service, require_pc)

        def getAvatar(ticket_data, service):
            def avatarResult(result, ticket_data):
                """
                Append the avatarAspect to the ticket data.
                """
                iface, avatarAspect, logout = result
                ticket_data['avatar'] = avatarAspect
                return ticket_data
            mind = {'service': service}
            return self.realm.requestAvatar(ticket_data['avatar_id'], mind, ICASUser).addCallback(
                avatarResult, ticket_data) 

        def renderSuccess(results, ticket, request):
            avatar = results['avatar']
            attribs = [
                ('client_ip', request.getClientIP()),
                ('user', avatar.username),
                ('ticket', ticket),
                ('service', service),
                ('TGT', results['tgt']),
                ('primary_credentials', results['primary_credentials']),]
            if 'pgt' in results:
                attribs.append(("PGT", results['pgt']))
            if 'proxy_chain' in results:
                attribs.append(("proxy_chain", ', '.join(results['proxy_chain'])))
            log_cas_event("Validated ticket.", attribs)
            iou = results.get('iou', None)
            proxy_chain = results.get('proxy_chain', None)
            doc_begin = dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:authenticationSuccess>
                        <cas:user>%s</cas:user>
                """) % xml_escape(avatar.username)
            doc_attributes = make_cas_attributes(avatar.attribs)
            doc_proxy = ""
            if iou is not None:
                doc_proxy = "    <cas:proxyGrantingTicket>%s</cas:proxyGrantingTicket>" % (
                    xml_escape(iou))
            doc_proxy_chain = ""
            if proxy_chain is not None:
                parts = ['''        <cas:proxies>''']
                for pgturl in proxy_chain:
                    parts.append("""            <cas:proxy>%s</cas:proxy>""" % xml_escape(pgturl))
                parts.append('''        </cas:proxies>''')
                doc_proxy_chain = '\n'.join(parts)
                del parts
            doc_end = dedent("""\
                    </cas:authenticationSuccess>
                </cas:serviceResponse>
                """)
            doc_parts = [doc_begin]
            for part in (doc_attributes, doc_proxy, doc_proxy_chain):
                if len(part) > 0:
                    doc_parts.append(part)
            doc_parts.append(doc_end)
            return '\n'.join(doc_parts)

        def renderFailure(err, ticket, request):
            log_cas_event("Failed to validate ticket.", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket)])
            err.trap(InvalidTicket, InvalidProxyCallback, InvalidService)
            request.setResponseCode(403)
            code = "INVALID_TICKET"
            msg = "Validation failed for ticket '%s'." % ticket
            if err.check(InvalidTicketSpec):
                code = "INVALID_TICKET_SPEC"
            elif err.check(InvalidProxyCallback):
                code = "INVALID_PROXY_CALLBACK"
                msg = "Invalid proxy callback."
            elif err.check(InvalidService):
                code = "INVALID_SERVICE"
                msg = "Invalid service."
            doc_fail = dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                   <cas:authenticationFailure code="%(code)s">
                      %(msg)s.    
                   </cas:authenticationFailure>
                </cas:serviceResponse>
                """) % {
                    'code': xml_escape(code),
                    'msg': xml_escape(msg),}
            return doc_fail

        d.addCallback(self._validateProxyUrl, pgturl, service, ticket, request)
        d.addCallback(getAvatar, service)
        d.addCallback(renderSuccess, ticket, request)
        d.addErrback(renderFailure, ticket, request)
        d.addErrback(self._log_failure_filter, request)
        d.addErrback(self._set_response_code_filter, 500, request)
        d.addErrback(self._page_view_errback, VIEW_ERROR_5XX, request)
        return d        

    def _validateProxyUrl(self, data, pgturl, service, ticket, request):
        """
        Validate service callback.
        Generate PGT + IOU.
        POST both to pgturl.
        Return avatar.
        Optionally return IOU, proxy_chain.
        """
        # NOTE: `data` is the validated ST or PT data.
        # `ticket` is the ST or PT *identifier*, but the ticket has already
        # been consumed at this point.  The ID is needed just to in case a 
        # PGT is created and we want to record its origin ST/PT.
        avatar_id = data['avatar_id']
        tgt = data['tgt']
        # If the validated ticket was a PT, extract the proxy_chain that was used
        # to create *its* parent PGT so it can be added to the proxy chain for
        # the requested PGT.
        #
        # E.g. Service A obtains a PGT.  It uses PGT-A to get PT-A and request
        # a service from B.  Service B uses PT-A to get PGT-B.  It requests
        # PT-B from CAS, and uses PT-B to request service from C.  C validates
        # PT-B.  The response to C would include the pgturls for A (first) and
        # B (second).
        if 'proxy_chain' in data:
            proxy_chain = data['proxy_chain']
        else:
            proxy_chain = None
        if pgturl == "":
            return data
        
        def _mkPGT(_):
            return self.ticket_store.mkProxyGrantingTicket(
                service, ticket, tgt, pgturl, proxy_chain=proxy_chain)
        
        def _sendTicketAndIou(pgt_info, pgturl, httpClientFactory):
            pgt = pgt_info['pgt']
            iou = pgt_info['iou']

            def iou_cb(resp_text, pgtiou):
                """
                Return the iou parameter.
                """
                return pgtiou
                
            q = {'pgtId': pgt, 'pgtIou': iou}
            log_cas_event("Sending pgtId and pgtIou to client.", [
                ('pgturl', pgturl),
                ('pgtIou', iou),
                ('pgtId', pgt),])
            httpClient = httpClientFactory(self.reactor)
            d = httpClient.get(pgturl, params=q, timeout=30)
            d.addCallback(http_status_filter, [(200, 200)], InvalidProxyCallback)
            d.addCallback(treq.content)
            d.addCallback(iou_cb, iou)
            return d
            
        def _package_result(iou, data):
            data['iou'] = iou
            return data
        
        if self.validate_pgturl:
            httpClientFactory = createVerifyingHTTPClient
            p = urlparse.urlparse(pgturl)
            if p.scheme.lower() != "https":
                raise NotHTTPSError("The pgtUrl '%s' is not HTTPS.")
        else:
            httpClientFactory = createNonVerifyingHTTPClient
        httpClient = httpClientFactory(self.reactor)
        d = httpClient.get(pgturl)
        d.addCallback(http_status_filter, [(200, 200)], InvalidProxyCallback)
        d.addCallback(treq.content)
        d.addCallback(_mkPGT)
        d.addCallback(_sendTicketAndIou, pgturl, httpClientFactory)
        d.addCallback(_package_result, data)
        return d

    @app.route('/proxy', methods=['GET'])
    def proxy_GET(self, request):
        log_http_event(request)
        try:
            pgt = get_single_param(request, 'pgt')
            targetService = get_single_param(request, 'targetService')
        except BadRequestError as ex:
            log.err(ex)
            request.setResponseCode(400)
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxyFailure code="INVALID_REQUEST">
                        Error requesting proxy ticket.       
                        &apos;pgt&apos; and &apos;targetService&apos; parameters are both required exactly once.
                    </cas:proxyFailure>
                </cas:serviceResponse>
                """)
        # Validate the PGT and get the PT

        def successResult(ticket, targetService, pgt, request):
            log_cas_event("Issued proxy ticket", [
                ('client_ip', request.getClientIP()),
                ('ticket', ticket),
                ('targetService', targetService),
                ('PGT', pgt),])
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxySuccess>
                        <cas:proxyTicket>%(ticket)s</cas:proxyTicket>
                    </cas:proxySuccess>
                </cas:serviceResponse>
                """) % {'ticket': xml_escape(ticket)}

        def failureResult(err, targetService, pgt, request):
            log_cas_event("Failed to issue proxy ticket", [
                ('client_ip', request.getClientIP()),
                ('targetService', targetService),
                ('PGT', pgt),])
            if not err.check(InvalidTicket, InvalidService):
                log.err(err)
            code = "INTERNAL_ERROR"
            msg = "An internal error occured."
            if err.check(InvalidTicket):
                code = "BAD_PGT"
                msg = "PGT '%s' is invalid." % pgt
            elif err.check(InvalidService):
                code = "INVALID_SERVICE"
                msg = "Target service is not authorized."
            return dedent("""\
                <cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">
                    <cas:proxyFailure code="%(code)s">
                        %(msg)s
                    </cas:proxyFailure>
                </cas:serviceResponse>
                """) % {
                    'code': xml_escape(code),
                    'msg': xml_escape(msg),
                }

        d = defer.maybeDeferred(self.ticket_store.mkProxyTicket, targetService, pgt)
        d.addCallback(successResult, targetService, pgt, request)
        d.addErrback(failureResult, targetService, pgt, request)
        return d

    @app.route('/static/', methods=['GET'], branch=True)
    def static_GET(self, request):
        static = self._static
        if static is None:
            log.msg('[ERROR] type="not_found" client_ip="%s" uri="%s"' % (
                        request.getClientIP(), request.uri))
            return self._get_page_view(VIEW_NOT_FOUND, request)
        else:
            return File(static)

    @app.handle_errors(werkzeug.exceptions.NotFound)
    def error_handler(self, request, failure):
        log.msg('[ERROR] type="not_found" client_ip="%s" uri="%s"' % (
                    request.getClientIP(), request.uri))
        return self._get_page_view(VIEW_NOT_FOUND, request)

    @app.handle_errors(BadRequestError)
    def handle_bad_request(self, request, failure):
        log.msg('[ERROR] type="bad_request" client_ip="%s" uri="%s"' % (
                    request.getClientIP(), request.uri))
        return self._get_page_view(VIEW_ERROR_5XX, failure, request)
import sys
import txcas.settings
from txcas.constants import (
    VIEW_LOGIN, VIEW_LOGIN_SUCCESS, VIEW_LOGOUT, 
    VIEW_INVALID_SERVICE, VIEW_ERROR_5XX, VIEW_NOT_FOUND)
from txcas.interface import (
    IRealmFactory, IServiceManagerFactory,
    ITicketStoreFactory, IViewProviderFactory,
    IServiceManagerAcceptor)
from txcas.server import ServerApp
from twisted.application.service import Service
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
from twisted.cred.strcred import ICheckerFactory
from twisted.cred.portal import IRealm
from twisted.internet.endpoints import serverFromString 
from twisted.python import log
from twisted.web.server import Site
            from twisted.internet import reactor
                static_dir=None,
                validate_pgturl=None):
        if self.reactor is None:
            self.reactor = reactor
        # Provide reasonable defaults for `endpoint_options`.
        self.endpoint_s = endpoint_s
        # Load the config.
        scp = txcas.settings.load_settings('cas', syspath='/etc/cas', defaults={
                'CAS': {
                    'lt_lifespan': 300,
                    'st_lifespan': 10,
                    'pt_lifespan': 10,
                    'pgt_lifespan': 600,
                    'tgt_lifespan': 86400,
                    'validate_pgturl': 1,
                    'ticket_size': 128,
                },
                'PLUGINS': {
                    'cred_checker': 'file:./cas_users.passwd',
                    'realm': 'basic_realm',
                    'ticket_store': 'memory_ticket_store'}})
        # Choose plugin that implements IServiceManager.
        if service_manager is None and scp.has_option('PLUGINS', 'service_manager'):
            tag_args = scp.get('PLUGINS', 'service_manager')
            parts = tag_args.split(':')
            tag = parts[0]
            args = ':'.join(parts[1:])
            factory = txcas.settings.get_plugin_factory(tag, IServiceManagerFactory)
            if factory is None:
                sys.stderr.write("[ERROR] Service manager type '%s' is not available.\n" % tag)
                sys.exit(1)
            service_manager = factory.generateServiceManager(args)
        if service_manager is not None:
            sys.stderr.write("[CONFIG] Service manager: %s\n" % service_manager.__class__.__name__)
        # Choose plugin that implements IViewProvider.
        if view_provider is None and scp.has_option('PLUGINS', 'view_provider'):
            tag_args = scp.get('PLUGINS', 'view_provider')
            parts = tag_args.split(':')
            tag = parts[0]
            args = ':'.join(parts[1:])
            factory = txcas.settings.get_plugin_factory(tag, IViewProviderFactory)
            if factory is None:
                sys.stderr.write("[ERROR] View provider type '%s' is not available.\n" % tag)
                sys.exit(1)
            view_provider = factory.generateViewProvider(args)
        if view_provider is not None:
            sys.stderr.write("[CONFIG] View provider: %s\n" % view_provider.__class__.__name__)
        # Connect service manager, if available.
        if IServiceManagerAcceptor.providedBy(view_provider):
            view_provider.service_manager = service_manager
            sys.stderr.write("[CONFIG] View provider received a reference to the service manager.\n")
        # Choose plugin that implements ITicketStore.
        if ticket_store is None:
            tag_args = scp.get('PLUGINS', 'ticket_store')
            parts = tag_args.split(':')
            tag = parts[0]
            args = ':'.join(parts[1:])
            factory = txcas.settings.get_plugin_factory(tag, ITicketStoreFactory)
            if factory is None:
                sys.stderr.write("[ERROR] Ticket store type '%s' is not available.\n" % tag)
                sys.exit(1)
            ticket_store = factory.generateTicketStore(args)

        assert ticket_store is not None, "Ticket store has not been configured!"
        sys.stderr.write("[CONFIG] Ticket store: %s\n" % ticket_store.__class__.__name__)
        # Connect service manager, if available.
        if IServiceManagerAcceptor.providedBy(ticket_store):
            ticket_store.service_manager = service_manager
            sys.stderr.write("[CONFIG] Ticket store received a reference to the service manager.\n")
        # Choose plugin(s) that implement ICredentialChecker 
        if checkers is None or len(checkers) == 0:        
            try:
                tag_args =  scp.get('PLUGINS', 'cred_checker')
            except Exception:
                sys.stderr.write("[ERROR] No valid credential checker was configured.\n")
                sys.exit(1)
            factories = []
            for tag_arg in tag_args.split(','):
                parts = tag_arg.split(':')
                tag = parts[0]
                args = ':'.join(parts[1:])
                factories.extend(txcas.settings.get_plugins_by_predicate(
                                ICheckerFactory, 
                                lambda x: x.authType == tag))
            if len(factories) == 0:
                checkers= [InMemoryUsernamePasswordDatabaseDontUse(foo='password')]
            else:
                checkers=[f.generateChecker(args) for f in factories]
        for checker in checkers:
            sys.stderr.write("[CONFIG] Credential Checker: %s\n" % checker.__class__.__name__)
            # Connect service manager, if available.
            if IServiceManagerAcceptor.providedBy(checker):
                checker.service_manager = service_manager
                sys.stderr.write("[CONFIG] Credential checker received a reference to the service manager.\n")
        # Choose the plugin that implements IRealm.
        if realm is None:
            tag_args = scp.get('PLUGINS', 'realm')
            parts = tag_args.split(':')
            tag = parts[0]
            args = ':'.join(parts[1:])
            factory = txcas.settings.get_plugin_factory(tag, IRealmFactory)
            if factory is None:
                sys.stderr.write("[ERROR] Realm type '%s' is not available.\n" % tag)
                sys.exit(1)
            realm = factory.generateRealm(args)
        assert realm is not None, "User Realm has not been configured!"
        sys.stderr.write("[CONFIG] User Realm: %s\n" % realm.__class__.__name__)
        # Connect service manager, if available.
        if IServiceManagerAcceptor.providedBy(realm):
            realm.service_manager = service_manager
            sys.stderr.write("[CONFIG] User realm received a reference to the service manager.\n")
        # Page views
        page_views = None
        if view_provider is not None:
            page_views = {}
            symbol_table = [
                VIEW_LOGIN,
                VIEW_LOGIN_SUCCESS,
                VIEW_LOGOUT,
                VIEW_INVALID_SERVICE,

                VIEW_ERROR_5XX,