/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.ws.security.handler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ws.security.SOAPConstants;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityEngineResult;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.message.token.Timestamp;
import org.apache.ws.security.util.WSSecurityUtil;
import org.apache.xml.security.utils.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

import javax.security.auth.callback.CallbackHandler;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.rpc.Call;
import javax.xml.rpc.JAXRPCException;
import javax.xml.rpc.handler.Handler;
import javax.xml.rpc.handler.HandlerInfo;
import javax.xml.rpc.handler.MessageContext;
import javax.xml.rpc.handler.soap.SOAPMessageContext;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.soap.SOAPHeaderElement;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.cert.X509Certificate;
import java.util.Iterator;
import java.util.Vector;

/**
 * Merged and converted the the axis handlers WSDoAllReceiver and WSDoAllSender
 * into a single JAX-RPC Handler. All the axis dependencies are removed.
 *
 * @author Venkat Reddy (vreddyp@gmail.com).
 */
public class WSS4JHandler extends WSHandler implements Handler {
    private HandlerInfo handlerInfo;
    
    private static final Log log = LogFactory.getLog(WSS4JHandler.class.getName());

    private static boolean doDebug = log.isDebugEnabled();

    static final String DEPLOYMENT = "deployment";
    static final String CLIENT_DEPLOYMENT = "client";
    static final String SERVER_DEPLOYMENT = "server";
    static final String FLOW = "flow";
    static final String REQUEST_ONLY = "request-only";
    static final String RESPONSE_ONLY = "response-only";
    static final String ALLOW_FORM_OPTIMIZATION = "axis.form.optimization";

    /**
     * Initializes the instance of the handler.
     */
    public void init(HandlerInfo hi) {
        handlerInfo = hi;
    }

    /**
     * Destroys the Handler instance.
     */
    public void destroy() {
    }

    public QName[] getHeaders() {
        return handlerInfo.getHeaders();
    }

    public boolean handleRequest(MessageContext mc) {
        mc.setProperty(ALLOW_FORM_OPTIMIZATION,
            Boolean.TRUE);
        try {
            return processMessage(mc, true);
        } catch (WSSecurityException e) {
            if (doDebug) {
                log.debug(e.getMessage(), e);
            }
            throw new JAXRPCException(e);
        }
    }

    public boolean handleResponse(MessageContext mc) {
        mc.setProperty(ALLOW_FORM_OPTIMIZATION,
            Boolean.TRUE);
        try {
            return processMessage(mc, false);
        } catch (WSSecurityException e) {
            if (doDebug) {
                log.debug(e.getMessage(), e);
            }
            throw new JAXRPCException(e);
        }
    }

    /**
     * Handles SOAP Faults that may occur during message processing
     */
    public boolean handleFault(MessageContext mc) {
        if (doDebug) {
            log.debug("Entered handleFault");
        }
        return true;
    }

    /**
     * Switch for transferring control to doReceiver and doSender
     */
    public boolean processMessage(MessageContext mc, boolean isRequestMessage) throws WSSecurityException {

        RequestData reqData = new RequestData();
        reqData.setMsgContext(mc);

        doDebug = log.isDebugEnabled();
        String deployment = null;
        String handleFlow = null;

        if ((deployment = (String) getOption(DEPLOYMENT)) == null) {
            deployment = (String) mc.getProperty(DEPLOYMENT);
        }
        if (deployment == null) {
            throw new JAXRPCException("WSS4JHandler.processMessage: No deployment defined");
        }
        if ((handleFlow = (String) getOption(FLOW)) == null) {
            handleFlow = (String) mc.getProperty(FLOW);
        }
        if (handleFlow == null) {
            handleFlow = "";
        }

        // call doSender if we are -
        // (handling request and client-side deployment) or (handling response and server-side deployment).
        // call doReceiver if we are -
        // (handling request and server-side deployment) or (handling response and client-side deployment).

        boolean needsHandling = ( isRequestMessage && !handleFlow.equals(RESPONSE_ONLY)) ||
                                (!isRequestMessage && !handleFlow.equals(REQUEST_ONLY));
        try {
            if (deployment.equals(CLIENT_DEPLOYMENT) ^ isRequestMessage) {
                if (needsHandling) {
                    return doReceiver(mc, reqData, isRequestMessage);
                }
            } else {
                if (needsHandling) {
                    return doSender(mc, reqData, isRequestMessage);
                }
            }
        } finally {
            reqData.clear();
            reqData = null;
        }
        return true;
    }

    /**
     * Handles incoming web service requests and outgoing responses
     */
    public boolean doSender(MessageContext mc, RequestData reqData, boolean isRequest) throws WSSecurityException {

        reqData.getSignatureParts().removeAllElements();
        reqData.getEncryptParts().removeAllElements();
        reqData.setNoSerialization(false);
        /*
        * Get the action first.
        */
        Vector actions = new Vector();
        String action = (String) getOption(WSHandlerConstants.SEND + '.' + WSHandlerConstants.ACTION);
        if (action == null) {
            action = (String) getOption(WSHandlerConstants.ACTION);
            if (action == null) {
                action = (String) mc.getProperty(WSHandlerConstants.ACTION);
            }
        }
        if (action == null) {
            throw new JAXRPCException("WSS4JHandler: No action defined");
        }
        int doAction = WSSecurityUtil.decodeAction(action, actions);
        if (doAction == WSConstants.NO_SECURITY) {
            return true;
        }
        
        /*
        * For every action we need a username, so get this now. The username
        * defined in the deployment descriptor takes precedence.
        */
        reqData.setUsername((String) getOption(WSHandlerConstants.USER));
        if (reqData.getUsername() == null || reqData.getUsername().equals("")) {
            reqData.setUsername((String) mc.getProperty(WSHandlerConstants.USER));
            mc.removeProperty(WSHandlerConstants.USER);
        }

        /*
        * Now we perform some set-up for UsernameToken and Signature
        * functions. No need to do it for encryption only. Check if username
        * is available and then get a password.
        */
        if (((doAction & (WSConstants.SIGN | WSConstants.UT | WSConstants.UT_SIGN)) != 0)
            && (reqData.getUsername() == null || reqData.getUsername().equals(""))) {
            /*
            * We need a username - if none throw an JAXRPCException. For encryption
            * there is a specific parameter to get a username.
            */
            throw new JAXRPCException("WSS4JHandler: Empty username for specified action");
        }
        if (doDebug) {
            log.debug("Action: " + doAction);
            log.debug("Actor: " + reqData.getActor());
        }
        /*
        * Now get the SOAP part from the request message and convert it into a
        * Document.
        *
        * This forces Axis to serialize the SOAP request into FORM_STRING.
        * This string is converted into a document.
        *
        * During the FORM_STRING serialization Axis performs multi-ref of
        * complex data types (if requested), generates and inserts references
        * for attachments and so on. The resulting Document MUST be the
        * complete and final SOAP request as Axis would send it over the wire.
        * Therefore this must shall be the last (or only) handler in a chain.
        *
        * Now we can perform our security operations on this request.
        */
        Document doc = null;
        SOAPMessage message = ((SOAPMessageContext)mc).getMessage();
        Boolean propFormOptimization = (Boolean)mc.getProperty("axis.form.optimization");
        log.debug("Form optimization: " + propFormOptimization);
        /*
        * If the message context property contains a document then this is a
        * chained handler.
        */
        SOAPPart sPart = message.getSOAPPart();
        if ((doc = (Document) mc.getProperty(WSHandlerConstants.SND_SECURITY))
                == null) {
            try {
                doc = messageToDocument(message);
            } catch (Exception e) {
                if (doDebug) {
                    log.debug(e.getMessage(), e);
                }
                throw new JAXRPCException("WSS4JHandler: cannot get SOAP envlope from message", e);
            }
        }
        if (doDebug) {
            log.debug("WSS4JHandler: orginal SOAP request: ");
            log.debug(org.apache.ws.security.util.XMLUtils.PrettyDocumentToString(doc));
        }
        doSenderAction(doAction, doc, reqData, actions, isRequest);
 
        /*
        * If required convert the resulting document into a message first. The
        * outputDOM() method performs the necessary c14n call. After that we
        * extract it as a string for further processing.
        *
        * Set the resulting byte array as the new SOAP message.
        *
        * If noSerialization is false, this handler shall be the last (or only)
        * one in a handler chain. If noSerialization is true, just set the
        * processed Document in the transfer property. The next Axis WSS4J
        * handler takes it and performs additional security processing steps.
        *
        */
        if (reqData.isNoSerialization()) {
            mc.setProperty(WSHandlerConstants.SND_SECURITY, doc);
        } else {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            XMLUtils.outputDOM(doc, os, true);
            if (doDebug) {
                String osStr = null;
                try {
                    osStr = os.toString("UTF-8");
                } catch (UnsupportedEncodingException e) {
                    if (doDebug) {
                        log.debug(e.getMessage(), e);
                    }
                    osStr = os.toString();
                }
                log.debug("Send request:");
                log.debug(osStr);
            }

            try {
                sPart.setContent(new StreamSource(new ByteArrayInputStream(os.toByteArray())));
            } catch (SOAPException se) {
                if (doDebug) {
                    log.debug(se.getMessage(), se);
                }
                throw new JAXRPCException("Couldn't set content on SOAPPart" + se.getMessage(), se);
            }
            mc.removeProperty(WSHandlerConstants.SND_SECURITY);
        }
        if (doDebug) {
            log.debug("WSS4JHandler: exit invoke()");
        }
        return true;
    }

    /**
     * handle responses
     *
     * @param mc
     * @param reqData
     * @return true on successful processing
     * @throws WSSecurityException
     */
    public boolean doReceiver(MessageContext mc, RequestData reqData, boolean isRequest) throws WSSecurityException {

        Vector actions = new Vector();
        String action = (String) getOption(WSHandlerConstants.RECEIVE + '.' + WSHandlerConstants.ACTION);
        if (action == null) {
            action = (String) getOption(WSHandlerConstants.ACTION);
            if (action == null) {
                action = (String) mc.getProperty(WSHandlerConstants.ACTION);
            }
        }
        if (action == null) {
            throw new JAXRPCException("WSS4JHandler: No action defined");
        }
        int doAction = WSSecurityUtil.decodeAction(action, actions);

        String actor = (String) getOption(WSHandlerConstants.ACTOR);

        SOAPMessage message = ((SOAPMessageContext)mc).getMessage();
        SOAPPart sPart = message.getSOAPPart();
        Document doc = null;
        try {
            doc = messageToDocument(message);
        } catch (Exception ex) {
            if (doDebug) {
                log.debug(ex.getMessage(), ex);
            }
            throw new JAXRPCException("WSS4JHandler: cannot convert into document",
                    ex);
        }
        /*
        * Check if it's a fault. Don't process faults.
        *
        */
        SOAPConstants soapConstants =
                WSSecurityUtil.getSOAPConstants(doc.getDocumentElement());
        if (WSSecurityUtil
                .findElement(doc.getDocumentElement(),
                        "Fault",
                        soapConstants.getEnvelopeURI())
                != null) {
            return false;
        }

        /*
        * To check a UsernameToken or to decrypt an encrypted message we need
        * a password.
        */
        CallbackHandler cbHandler = null;
        if ((doAction & (WSConstants.ENCR | WSConstants.UT)) != 0) {
            cbHandler = getPasswordCB(reqData);
        }

        /*
        * Get and check the Signature specific parameters first because they
        * may be used for encryption too.
        */
        doReceiverAction(doAction, reqData);

        Vector wsResult = null;
        try {
            wsResult =
                    secEngine.processSecurityHeader(doc,
                            actor,
                            cbHandler,
                            reqData.getSigCrypto(),
                            reqData.getDecCrypto());
        } catch (WSSecurityException ex) {
            if (doDebug) {
                log.debug(ex.getMessage(), ex);
            }
            throw new JAXRPCException("WSS4JHandler: security processing failed",
                    ex);
        }
        if (wsResult == null) {         // no security header found
            if (doAction == WSConstants.NO_SECURITY) {
                return true;
            } else {
                throw new JAXRPCException("WSS4JHandler: Request does not contain required Security header");
            }
        }
        if (reqData.getWssConfig().isEnableSignatureConfirmation() && !isRequest) {
            checkSignatureConfirmation(reqData, wsResult);
        }

        /*
        * If we had some security processing, get the original
        * SOAP part of Axis' message and replace it with new SOAP
        * part. This new part may contain decrypted elements.
        */

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        XMLUtils.outputDOM(doc, os, true);
        try {
            sPart.setContent(new StreamSource(new ByteArrayInputStream(os.toByteArray())));
        } catch (SOAPException se) {
            if (doDebug) {
                log.debug(se.getMessage(), se);
            }
            throw new JAXRPCException(
                "Couldn't set content on SOAPPart" + se.getMessage(), se
            );
        }

        if (doDebug) {
            log.debug("Processed received SOAP request");
        }

        /*
        * After setting the new current message, probably modified because
        * of decryption, we need to locate the security header. That is,
        * we force Axis (with getSOAPEnvelope()) to parse the string, build
        * the new header. Then we examine, look up the security header
        * and set the header as processed.
        *
        * Please note: find all header elements that contain the same
        * actor that was given to processSecurityHeader(). Then
        * check if there is a security header with this actor.
        */

        SOAPHeader sHeader = null;
        try {
            sHeader = message.getSOAPPart().getEnvelope().getHeader();
        } catch (Exception ex) {
            if (doDebug) {
                log.debug(ex.getMessage(), ex);
            }
            throw new JAXRPCException("WSS4JHandler: cannot get SOAP header after security processing", ex);
        }

        Iterator headers = sHeader.examineHeaderElements(actor);

        SOAPHeaderElement headerElement = null;
        while (headers.hasNext()) {
            SOAPHeaderElement hE = (SOAPHeaderElement) headers.next();
            if (hE.getElementName().getLocalName().equals(WSConstants.WSSE_LN)
                    && ((Node) hE).getNamespaceURI().equals(WSConstants.WSSE_NS)) {
                headerElement = hE;
                break;
            }
        }

        /* JAXRPC conversion changes */
        headerElement.setMustUnderstand(false); // is this sufficient?

        /*
        * Now we can check the certificate used to sign the message.
        * In the following implementation the certificate is only trusted
        * if either it itself or the certificate of the issuer is installed
        * in the keystore.
        *
        * Note: the method verifyTrust(X509Certificate) allows custom
        * implementations with other validation algorithms for subclasses.
        */

        // Extract the signature action result from the action vector

        WSSecurityEngineResult actionResult = WSSecurityUtil.fetchActionResult(wsResult, WSConstants.SIGN);

        if (actionResult != null) {
            X509Certificate returnCert = 
                (X509Certificate)actionResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATE);

            if (returnCert != null && !verifyTrust(returnCert, reqData)) {
                throw new JAXRPCException("WSS4JHandler: The certificate used for the signature is not trusted");
            }
        }

        /*
        * Perform further checks on the timestamp that was transmitted in the header.
        * In the following implementation the timestamp is valid if it was
        * created after (now-ttl), where ttl is set on server side, not by the client.
        *
        * Note: the method verifyTimestamp(Timestamp) allows custom
        * implementations with other validation algorithms for subclasses.
        */

        // Extract the timestamp action result from the action vector
        actionResult = WSSecurityUtil.fetchActionResult(wsResult, WSConstants.TS);

        if (actionResult != null) {
            Timestamp timestamp = 
                (Timestamp)actionResult.get(WSSecurityEngineResult.TAG_TIMESTAMP);

            if (timestamp != null && reqData.getWssConfig().isTimeStampStrict()
                && !verifyTimestamp(timestamp, decodeTimeToLive(reqData))) {
                throw new JAXRPCException("WSS4JHandler: The timestamp could not be validated");
            }
        }

        /*
        * now check the security actions: do they match, in right order?
        */
        if (!checkReceiverResults(wsResult, actions)) {
            throw new JAXRPCException("WSS4JHandler: security processing failed (actions mismatch)");
        }

        /*
        * All ok up to this point. Now construct and setup the
        * security result structure. The service may fetch this
        * and check it.
        */
        Vector results = null;
        if ((results = (Vector) mc.getProperty(WSHandlerConstants.RECV_RESULTS))
                == null) {
            results = new Vector();
            mc.setProperty(WSHandlerConstants.RECV_RESULTS, results);
        }
        WSHandlerResult rResult =
                new WSHandlerResult(actor,
                        wsResult);
        results.add(0, rResult);
        if (doDebug) {
            log.debug("WSS4JHandler: exit invoke()");
        }

        return true;
    }

    /**
     * Utility method to convert SOAPMessage to org.w3c.dom.Document
     */
    public static Document messageToDocument(SOAPMessage message) {
        try {
            Source content = message.getSOAPPart().getContent();
            DocumentBuilderFactory dbf = org.apache.ws.security.util.XMLUtils.getSecuredDocumentBuilder();
            DocumentBuilder builder = dbf.newDocumentBuilder();
            return builder.parse(org.apache.ws.security.util.XMLUtils.sourceToInputSource(content));
        } catch (Exception ex) {
            if (doDebug) {
                log.debug(ex.getMessage(), ex);
            }
            throw new JAXRPCException("messageToDocument: cannot convert SOAPMessage into Document", ex);
        }
    }

    public Object getOption(String key) {
        return handlerInfo.getHandlerConfig().get(key);
    }

    public Object getProperty(Object msgContext, String key) {
        return ((MessageContext)msgContext).getProperty(key);
    }

    public void setProperty(Object msgContext, String key, Object value) {
        ((MessageContext)msgContext).setProperty(key, value);
    }

    public String getPassword(Object msgContext) {
        return (String) ((MessageContext)msgContext).getProperty(Call.PASSWORD_PROPERTY);
    }

    public void setPassword(Object msgContext, String password) {
        ((MessageContext)msgContext).setProperty(Call.PASSWORD_PROPERTY, password);
    }
}
