/*
 * Asimba Server
 * 
 * Copyright (C) 2012 Asimba
 * Copyright (C) 2007-2010 Alfa & Ariss B.V.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see www.gnu.org/licenses
 * 
 * Asimba - Serious Open Source SSO - More information on www.asimba.org
 * 
 */
package com.alfaariss.oa.profile.saml2.profile.artifactresolution;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opensaml.common.SAMLObject;
import org.opensaml.common.SignableSAMLObject;
import org.opensaml.common.binding.SAMLMessageContext;
import org.opensaml.common.binding.artifact.SAMLArtifactMap;
import org.opensaml.common.binding.decoding.SAMLMessageDecoder;
import org.opensaml.common.binding.encoding.SAMLMessageEncoder;
import org.opensaml.common.xml.SAMLConstants;
import org.opensaml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml2.metadata.IDPSSODescriptor;
import org.opensaml.saml2.metadata.SPSSODescriptor;
import org.opensaml.ws.message.decoder.MessageDecodingException;
import org.opensaml.ws.message.encoder.MessageEncodingException;
import org.opensaml.xml.Configuration;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilderFactory;
import org.opensaml.xml.security.SecurityException;
import org.opensaml.xml.security.credential.Credential;
import org.w3c.dom.Element;

import com.alfaariss.oa.OAException;
import com.alfaariss.oa.RequestorEvent;
import com.alfaariss.oa.SystemErrors;
import com.alfaariss.oa.api.configuration.IConfigurationManager;
import com.alfaariss.oa.engine.core.Engine;
import com.alfaariss.oa.profile.saml2.profile.artifactresolution.protocol.ArtifactResolutionProtocol;
import com.alfaariss.oa.util.logging.RequestorEventLogItem;
import com.alfaariss.oa.util.saml2.SAML2IssueInstantWindow;
import com.alfaariss.oa.util.saml2.ISAML2Requestors;
import com.alfaariss.oa.util.saml2.SAML2SecurityException;
import com.alfaariss.oa.util.saml2.StatusException;
import com.alfaariss.oa.util.saml2.binding.AbstractDecodingFactory;
import com.alfaariss.oa.util.saml2.binding.AbstractEncodingFactory;
import com.alfaariss.oa.util.saml2.binding.soap11.SOAP11Utils;
import com.alfaariss.oa.util.saml2.crypto.SAML2CryptoUtils;
import com.alfaariss.oa.util.saml2.metadata.role.sso.IDPSSODescriptorBuilder;
import com.alfaariss.oa.util.saml2.profile.AbstractSAML2Profile;
import com.alfaariss.oa.util.saml2.profile.ISAML2Profile;
import com.alfaariss.oa.util.saml2.storage.artifact.ArtifactStoreFactory;

/**
 * Artifact Resolution Profile implementation.
 *
 * Implements the Artifact Resolution protocol for dereferencing a SAML 
 * artifact into a corresponding protocol message.
 * 
 * @author EVB
 * @author Alfa & Ariss
 * @see <a 
 *  href="http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf" 
 *  target="_new">
 *  Profiles for the OASIS Security Assertion Markup Language (SAML) V2.0
 *  </a>
 */
public class ArtifactResolutionService extends AbstractSAML2Profile
{   
    /** XMLObjectBuilderFactory */
    protected XMLObjectBuilderFactory _builderFactory;
    
    private static Log _logger = LogFactory.getLog(
        ArtifactResolutionService.class);
    private Log _eventLogger;
    /** The protocol handler */
    private ArtifactResolutionProtocol _protocol;
       
    /**
     * Constructor. 
     */
    public ArtifactResolutionService ()
    {
        _eventLogger = LogFactory.getLog(Engine.EVENT_LOGGER); 
        _builderFactory = Configuration.getBuilderFactory();
    }

    /**
     * @see AbstractSAML2Profile#init(IConfigurationManager, Element, 
     * EntityDescriptor, String, String, SAML2Requestors, 
     * SAML2IssueInstantWindow, String)
     */
    public void init(IConfigurationManager configurationManager,
        Element config, EntityDescriptor entityDescriptor, 
        String sBaseUrl, String sWebSSOPath, ISAML2Requestors requestors, 
        SAML2IssueInstantWindow issueInstantWindow, String sProfileID) 
        throws OAException
    {
        super.init(configurationManager, config, entityDescriptor, sBaseUrl, 
            sWebSSOPath, requestors, issueInstantWindow, sProfileID);
        
        //Start Artifact storage 
        ArtifactStoreFactory factory = ArtifactStoreFactory.getInstance();
        factory.init(configurationManager, config, _cryptoManager);
        SAMLArtifactMap artifactStorage = factory.getStoreInstance();
        
        //Create protocol
        _protocol = new ArtifactResolutionProtocol(
            _cryptoManager.getSecureRandom(),
            artifactStorage, _sProfileURL, _issueInstantWindow);
        
        //Append role descriptor
        IDPSSODescriptor idpSSODescriptor = 
            entityDescriptor.getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
       
        IDPSSODescriptorBuilder builder = new IDPSSODescriptorBuilder(
            configurationManager, config, idpSSODescriptor);
               
        //Build ArtifactResolutionService
        builder.buildArtifactResolutionService(_sProfileURL);
        
        _logger.info("ArtifactResolutionService Started at endpoint: " 
            + _sProfileURL);
    }

    /**
     * @see ISAML2Profile#process(javax.servlet.http.HttpServletRequest, 
     *  javax.servlet.http.HttpServletResponse)
     */
    public void process(HttpServletRequest servletRequest,
        HttpServletResponse servletResponse) throws OAException
    {
        SAMLMessageContext<SignableSAMLObject, 
            SignableSAMLObject,SAMLObject>  context = null;
        try
        {
            //Decode SOAP message
            AbstractDecodingFactory decFactory = 
                AbstractDecodingFactory.createInstance(
                    servletRequest, servletResponse, 
                    SAMLConstants.SAML2_SOAP11_BINDING_URI, null);

            SAMLMessageDecoder decoder = decFactory.getDecoder();                       
            context = decFactory.getContext();
            context.setLocalEntityId(_sEntityID);
            context.setLocalEntityMetadata(_entityDescriptor);
            
            //Decode request
            try
            {
                decoder.decode(context);
            }
            catch (SecurityException e)
            {
                _logger.debug(
                    "Could not decode inbound message due to security exception", e);
               throw new SAML2SecurityException(RequestorEvent.REQUEST_INVALID);
            }
            
            //verify saml message in request
            SignableSAMLObject requestMessage = context.getInboundSAMLMessage();
            
            if (_logger.isDebugEnabled())
            {
                if (requestMessage != null)
                    logXML(requestMessage);
            }
            
            //Validate requestor and signature
            validateRequest(context,SPSSODescriptor.DEFAULT_ELEMENT_NAME);
             
            _protocol.processProtocol(context);
            //DD TOKEN_DEREFERENCE events are used for artifact resolution
            _eventLogger.info(new RequestorEventLogItem(null, null, null, 
                RequestorEvent.TOKEN_DEREFERENCE_SUCCESSFUL, null,
                servletRequest.getRemoteAddr(), 
                context.getInboundMessageIssuer(), this, 
                context.getOutboundSAMLMessageId()));
            
            sendResponse(context, servletRequest, servletResponse);           
        }
        catch(StatusException e) //SAML processing error
        {
            _eventLogger.info(new RequestorEventLogItem(null, null, null, 
                e.getEvent(), null, servletRequest.getRemoteAddr(), 
                e.getRequestorID(), this, e.getMessage()));
            
            sendResponse(context, servletRequest, servletResponse);
        }
        catch (MessageDecodingException e) //SOAP binding processing error  
        {    
           _logger.debug(
               "SOAP decoding error", e);
           _eventLogger.info(new RequestorEventLogItem(null, null, null, 
               RequestorEvent.REQUEST_INVALID, null, 
               servletRequest.getRemoteAddr(), null, this, "SOAP Fault"));
           SOAP11Utils.sendSOAPFault(context, RequestorEvent.REQUEST_INVALID);
        }
        catch (SAML2SecurityException e)
            //The message does not meet the required security constraints
        {            
            //DD Security error -> Return a "403 Forbidden" response [saml-bindings-2.0-os r370]
            _logger.debug(
                "Security error", e);
            _eventLogger.info(new RequestorEventLogItem(null, null, null, 
                e.getEvent(), null, servletRequest.getRemoteAddr(), 
                null, this, "Security Fault"));
            try
            {
                if (!servletResponse.isCommitted())
                    servletResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            }
            catch (IOException e1)
            {
                _logger.warn("Could not send response", e1);
            }
        }
        catch (OAException e) //Internal error
        {
            throw e;
        }         
        catch (Exception e)
        {
            _logger.fatal("Could not process SAML request message", e);
            throw new OAException(SystemErrors.ERROR_INTERNAL);
        }        
    }  

    /**
     * Close artifact storage service.
     * @see ISAML2Profile#destroy()
     */
    public void destroy()
    {        
        //Stop Artifact storage
        ArtifactStoreFactory.getInstance().stop();
        super.destroy();
    }

    //Send SAML response message using SOAP binding
    private void sendResponse(SAMLMessageContext<SignableSAMLObject, 
        SignableSAMLObject,SAMLObject> context, 
        HttpServletRequest servletRequest,  
        HttpServletResponse servletResponse) throws OAException
    {
        try
        {            
            //Prepare the response signing
            if (_signingEnabled)
            {
                Credential credentials = SAML2CryptoUtils.retrieveMySigningCredentials(
                    _cryptoManager, _sEntityID);  
                context.setOutboundSAMLMessageSigningCredential(credentials);
            }
            
            AbstractEncodingFactory encodingFactory = 
                AbstractEncodingFactory.createInstance(servletRequest, 
                    servletResponse, SAMLConstants.SAML2_SOAP11_BINDING_URI, 
                    null);       
            SAMLMessageEncoder encoder = encodingFactory.getEncoder();      
            encoder.encode(context);
            
            if (_logger.isDebugEnabled())
            {
                XMLObject xmlObject = context.getOutboundSAMLMessage();
                if (xmlObject != null)
                    logXML(xmlObject);
            }
        }
        catch (MessageEncodingException e)
        {
            _logger.error("Could not send reponse with binding " 
                + SAMLConstants.SAML2_SOAP11_BINDING_URI, e);
            throw new OAException(SystemErrors.ERROR_INTERNAL);
        }
    }
}
