/*
* Copyright 2001-2005 Internet2
*
* 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 gov.nih.nci.cagrid.opensaml.provider;
import gov.nih.nci.cagrid.opensaml.*;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.ProviderException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.log4j.Logger;
import org.apache.log4j.NDC;
import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.Canonicalizer;
import org.apache.xml.security.c14n.InvalidCanonicalizerException;
import org.w3c.dom.*;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* SOAP over HTTP binding implementation.
*
* The following properties can be placed in the OpenSAML configuration
* file to enable SSL client-certificate authentication with the server.
* <ul>
* <li>gov.nih.nci.cagrid.opensaml.ssl.keystore - path to the store that contains the client's key and certificate</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.keystore-pwd - the password to the keystore</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.key-pwd - the passphrase to the private key in the keystore</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.keystore-type - the key store type, defaults to JKS if not set</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.truststore - path to the store that contains the server/CA certs needed to validate the cert chain</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.truststore-pwd - the password to the trust store</li>
* <li>gov.nih.nci.cagrid.opensaml.ssl.truststore-type - the trust store type, defaults to JKS if not set</li>
* </ul>
*
* Notes:
* <ul>
* <li>If the <tt>gov.nih.nci.cagrid.opensaml.ssl.keystore</tt> property is set the remaing *key* properties must also be set.</li>
* <li>If the <tt>gov.nih.nci.cagrid.opensaml.ssl.truststore</tt> property is set the remaing *trust* properties must also be set.</li>
* <li>The private key <strong>MUST</strong> be passphrase protected.</li>
* <li>The properties described above apply to <strong>ALL</strong> instances of this binding within a given VM.</li>
* </ul>
*
* @author Scott Cantor
* @created November 25, 2001
*/
public class SOAPHTTPBindingProvider extends SOAPBinding implements SAMLSOAPHTTPBinding
{
private static SAMLConfig config = SAMLConfig.instance();
private static SSLContext sslctx = null;
private Logger log = Logger.getLogger(SOAPHTTPBindingProvider.class.getName());
private Map /* <HTTPHook,Object> */ httpHooks = Collections.synchronizedMap(new HashMap(4));
/** Defeault constructor for a SAMLSOAPBinding object */
public SOAPHTTPBindingProvider(String binding, Element e) throws SAMLException {
if (!binding.equals(SOAP))
throw new SAMLException("SOAPHTTPBindingProvider does not support requested binding (" + binding + ")");
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLSOAPHTTPBinding#addHook(gov.nih.nci.cagrid.opensaml.SAMLSOAPHTTPBinding.HTTPHook)
*/
public void addHook(HTTPHook h) {
addHook(h, null);
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLSOAPHTTPBinding#addHook(gov.nih.nci.cagrid.opensaml.SAMLSOAPHTTPBinding.HTTPHook, java.lang.Object)
*/
public void addHook(HTTPHook h, Object globalCtx) {
httpHooks.put(h, globalCtx);
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#send(java.lang.String, gov.nih.nci.cagrid.opensaml.SAMLRequest, java.lang.Object)
*/
public SAMLResponse send(String endpoint, SAMLRequest request, Object callCtx)
throws SAMLException
{
try {
NDC.push("send");
if(log.isDebugEnabled()) {
log.debug("Preparing to send the following SAML request to " + endpoint + "\n" + request.toString());
}
// Use SOAP layer to package message.
if(log.isDebugEnabled()) {
log.debug("Wrapping request to " + endpoint + " in a SOAP envelope");
}
Element envelope = sendRequest(request, callCtx);
// Connect to authority and setup basic headers.
log.debug("Setting connection properties for connecting to " + endpoint);
URLConnection conn=new URL(endpoint).openConnection();
conn.setAllowUserInteraction(false);
conn.setDoOutput(true);
((HttpURLConnection)conn).setInstanceFollowRedirects(false);
((HttpURLConnection)conn).setRequestMethod("POST");
((HttpURLConnection)conn).setRequestProperty("Content-Type","text/xml; charset=UTF-8");
((HttpURLConnection)conn).setRequestProperty("SOAPAction","http://www.oasis-open.org/committees/security");
// For an SSL connection, we check for a custom configuration.
if (conn instanceof javax.net.ssl.HttpsURLConnection && sslctx != null) {
if (log.isDebugEnabled()) {
log.debug("Connection to " + endpoint + " is an HTTPS connection, setting default SSL socket factory.");
}
((javax.net.ssl.HttpsURLConnection)conn).setSSLSocketFactory(sslctx.getSocketFactory());
}
// Run the outgoing client-side HTTP hooks.
if(log.isDebugEnabled()) {
log.debug("Connection to " + endpoint + " set up, running " + httpHooks.size() + " outgoing client-side HTTP hooks.");
}
for (Iterator hooks=httpHooks.entrySet().iterator(); hooks.hasNext();) {
Entry h = (Entry)hooks.next();
if (!((HTTPHook)h.getKey()).outgoing((HttpURLConnection)conn, h.getValue(), callCtx)) {
log.warn("HTTP processing hook returned false, aborting outgoing request");
throw new BindingException(SAMLException.REQUESTER,"SOAPHTTPBindingProvider.send() HTTP processing hook returned false, aborted outgoing request");
}
}
// Send the message.
if(log.isDebugEnabled()) {
log.debug("Connecting to " + endpoint);
}
conn.connect();
if(log.isDebugEnabled()) {
log.debug("Canonicalizing SOAP envelope-wrapped request and sending it to " + endpoint);
}
Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
conn.getOutputStream().write(c.canonicalizeSubtree(envelope));
// Run the incoming client-side HTTP hooks.
if(log.isDebugEnabled()) {
log.debug("Message sent to " + endpoint + ", running " + httpHooks.size() + " incoming client-side HTTP hooks.");
}
for (Iterator hooks=httpHooks.entrySet().iterator(); hooks.hasNext();) {
Entry h = (Entry)hooks.next();
if (!((HTTPHook)h.getKey()).incoming((HttpURLConnection)conn, h.getValue(), callCtx)) {
log.warn("HTTP processing hook returned false, aborting incoming response");
throw new BindingException("SOAPHTTPBindingProvider.send() HTTP processing hook returned false, aborted incoming response");
}
}
if(log.isDebugEnabled()) {
log.debug("Starting to process response from " + endpoint);
}
String content_type=((HttpURLConnection)conn).getContentType();
if(log.isDebugEnabled()) {
log.debug("Response content type is " + content_type);
}
if (content_type == null || !content_type.startsWith("text/xml")) {
log.error(
"received an invalid content type in the response ("
+ (content_type!=null ? content_type : "none")
+ "), with the following content:"
);
BufferedReader reader=new BufferedReader(new InputStreamReader(conn.getInputStream()));
log.error(reader.readLine());
throw new BindingException(
"SOAPHTTPBindingProvider.send() detected an invalid content type ("
+ (content_type!=null ? content_type : "none")
+ ") in the response.");
}
// Parse the envelope using the specified SAML schema set.
if(log.isDebugEnabled()) {
log.debug("Unmarshalling response from " + endpoint + " into a DOM document.");
}
envelope=XML.parserPool.parse(
new InputSource(conn.getInputStream()),
(request.getMinorVersion()>0) ? XML.parserPool.getSchemaSAML11() : XML.parserPool.getSchemaSAML10()
).getDocumentElement();
// Process the SOAP envelope and check message correlation.
if(log.isDebugEnabled()) {
log.debug("Parsing and verifying SOAP response and retrieving SAML response from it.");
}
SAMLResponse ret = recvResponse(envelope, callCtx);
if(log.isDebugEnabled()) {
log.debug("Received the following SAML response as the response to the request to " + endpoint + "\n" + ret.toString());
}
if (!ret.getInResponseTo().equals(request.getId())) {
log.error("Unable to match SAML InResponseTo value to request made to " + endpoint);
throw new BindingException("SOAPHTTPBindingProvider.send() unable to match SAML InResponseTo value to request");
}
return ret;
}
catch (MalformedURLException ex) {
throw new SAMLException("SAMLSOAPBinding.send() detected a malformed URL in the binding provided", ex);
}
catch (SAXException ex) {
throw new SAMLException("SAMLSOAPBinding.send() caught an XML exception while parsing the response", ex);
}
catch (InvalidCanonicalizerException ex) {
throw new SAMLException("SAMLSOAPBinding.send() caught a C14N exception while serializing the request", ex);
}
catch (CanonicalizationException ex) {
throw new SAMLException("SAMLSOAPBinding.send() caught a C14N exception while serializing the request", ex);
}
catch (java.io.IOException ex) {
throw new SAMLException("SAMLSOAPBinding.send() caught an I/O exception", ex);
}
finally {
NDC.pop();
}
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#receive(java.lang.Object, java.lang.Object, int)
*/
public SAMLRequest receive(Object reqContext, Object callCtx, int minor)
throws SAMLException
{
// The SAML SOAP binding requires that we receieve a SOAP envelope via
// the POST method as text/xml.
HttpServletRequest req = (HttpServletRequest)reqContext;
if (!req.getMethod().equals("POST") || !req.getContentType().startsWith("text/xml"))
throw new BindingException(SAMLException.REQUESTER, "SOAPHTTPBindingProvider.receive() found bad HTTP method or content type");
// Run the incoming server-side HTTP hooks.
for (Iterator hooks=httpHooks.entrySet().iterator(); hooks.hasNext();) {
Entry h = (Entry)hooks.next();
if (!((HTTPHook)h.getKey()).incoming(req, h.getValue(), callCtx)) {
log.warn("HTTP processing hook returned false, aborting incoming request");
throw new BindingException(SAMLException.REQUESTER,"SOAPHTTPBindingProvider.recvRequest() HTTP processing hook returned false, aborted incoming request");
}
}
try {
// The body of the POST contains the XML document to parse as a SOAP envelope.
/* This is less than ideal because it assumes the envelope can be validated
using the 2001/Schema namespace against the unofficial SOAP 1.1 schema. This isn't
so terrible, except that if a SOAP toolkit used by a requester produces an envelope
that explicitly sets the xsd or xsi namespaces to something older, we're screwed.
(Apache SOAP parses without validating, so they can handle multiple schema levels.)
*/
return recvRequest(
XML.parserPool.parse(
new InputSource(req.getInputStream()),
(minor>0) ? XML.parserPool.getSchemaSAML11() : XML.parserPool.getSchemaSAML10()
).getDocumentElement(),
callCtx
);
}
catch (SAXException e) {
throw new SOAPException(SOAPException.CLIENT, "SOAPHTTPBindingProvider.receive() detected an XML parsing error: " + e.getMessage());
}
catch (java.io.IOException e) {
throw new SOAPException(SOAPException.SERVER, "SOAPHTTPBindingProvider.receive() detected an I/O error: " + e.getMessage());
}
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#respond(java.lang.Object, gov.nih.nci.cagrid.opensaml.SAMLResponse, gov.nih.nci.cagrid.opensaml.SAMLException, java.lang.Object)
*/
public void respond(Object respContext, SAMLResponse response, SAMLException e, Object callCtx)
throws SAMLException
{
HttpServletResponse resp=(HttpServletResponse)respContext;
try {
// Package response or error in SOAP envelope.
Element env = sendResponse(response, e, callCtx);
// Run the outgoing server-side HTTP hooks.
for (Iterator hooks=httpHooks.entrySet().iterator(); hooks.hasNext();) {
Entry h = (Entry)hooks.next();
if (!((HTTPHook)h.getKey()).outgoing(resp, h.getValue(), callCtx)) {
log.warn("HTTP processing hook returned false, aborting outgoing response");
throw new BindingException("SOAPHTTPBindingProvider.respond() HTTP processing hook returned false, aborted outgoing response");
}
}
// If all went well, send the envelope back.
Canonicalizer c = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
if (e != null)
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setContentType("text/xml; charset=UTF-8");
resp.getOutputStream().write(c.canonicalizeSubtree(env));
}
catch (InvalidCanonicalizerException ex) {
ex.printStackTrace();
try {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "SAMLSOAPBinding.respond() unable to serialize XML document instance");
}
catch (IOException e1) {
throw new SAMLException("SAMLSOAPBinding.respond() caught I/O exception while sending error response", e1);
}
}
catch (CanonicalizationException ex) {
ex.printStackTrace();
try {
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "SAMLSOAPBinding.respond() unable to serialize XML document instance");
}
catch (IOException e1) {
throw new SAMLException("SAMLSOAPBinding.respond() caught I/O exception while sending error response", e1);
}
}
catch (IOException ex) {
ex.printStackTrace();
throw new SAMLException("SAMLSOAPBinding.respond() caught I/O exception while sending error response", ex);
}
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#send(java.lang.String, gov.nih.nci.cagrid.opensaml.SAMLRequest)
*/
public SAMLResponse send(String endpoint, SAMLRequest request) throws SAMLException {
return send(endpoint, request, null);
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#receive(java.lang.Object, int)
*/
public SAMLRequest receive(Object reqContext, int minor) throws SAMLException {
return receive(reqContext, null, minor);
}
/**
* @see gov.nih.nci.cagrid.opensaml.SAMLBinding#respond(java.lang.Object, gov.nih.nci.cagrid.opensaml.SAMLResponse, gov.nih.nci.cagrid.opensaml.SAMLException)
*/
public void respond(Object respContext, SAMLResponse response, SAMLException e) throws SAMLException {
respond(respContext, response, e, null);
}
static {
try {
// See if we need to setup a custom SSL context.
String ks_path=config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.keystore");
String ts_path = config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.truststore");
if (ks_path != null || ts_path != null) {
KeyManagerFactory kmf = null;
TrustManagerFactory tmf = null;
if (ks_path != null) {
String ks_pwd=config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.keystore-pwd");
String key_pwd=config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.key-pwd");
String ks_type = config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.keystore-type");
KeyStore ks = KeyStore.getInstance(ks_type != null ? ks_type : "JKS");
ks.load(new FileInputStream(ks_path),(ks_pwd!=null) ? ks_pwd.toCharArray() : null);
kmf=KeyManagerFactory.getInstance("SunX509");
kmf.init(ks,(key_pwd!=null) ? key_pwd.toCharArray() : null);
}
if (ts_path != null) {
String ts_pwd = config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.truststore-pwd");
String ts_type = config.getProperty("gov.nih.nci.cagrid.opensaml.ssl.truststore-type");
KeyStore ts = KeyStore.getInstance(ts_type != null ? ts_type : "JKS");
ts.load(new FileInputStream(ts_path),(ts_pwd!=null) ? ts_pwd.toCharArray() : null);
tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ts);
}
sslctx=SSLContext.getInstance("SSL");
sslctx.init(kmf != null ? kmf.getKeyManagers() : null, tmf != null ? tmf.getTrustManagers() : null, null);
}
}
catch (IOException e) {
throw new ProviderException("SOAPHTTPBindingProvider caught I/O error initializing SSL context: " + e.getMessage());
}
catch (GeneralSecurityException e) {
throw new ProviderException("SOAPHTTPBindingProvider caught security exception initializing SSL context: " + e.getMessage());
}
}
}