| /* |
| * Copyright 2003-2004 The Apache Software Foundation. |
| * |
| * Licensed 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 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) { |
| if ((action = (String) getOption(WSHandlerConstants.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.setProperty(WSHandlerConstants.USER, null); |
| } |
| |
| /* |
| * 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) { |
| /* |
| * We need a username - if none throw an JAXRPCException. For encryption |
| * there is a specific parameter to get a username. |
| */ |
| if (reqData.getUsername() == null || reqData.getUsername().equals("")) { |
| 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.setProperty(WSHandlerConstants.SND_SECURITY, null); |
| } |
| 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) { |
| if ((action = (String) getOption(WSHandlerConstants.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) { |
| if (!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()) { |
| if (!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 = DocumentBuilderFactory.newInstance(); |
| dbf.setNamespaceAware(true); |
| 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); |
| } |
| } |