View Javadoc

1   /*
2    * Copyright 2008 University Corporation for Advanced Internet Development, Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.ws.soap.client.http;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStreamWriter;
23  import java.nio.charset.Charset;
24  import java.util.List;
25  
26  import net.jcip.annotations.ThreadSafe;
27  
28  import org.apache.commons.httpclient.HttpClient;
29  import org.apache.commons.httpclient.HttpStatus;
30  import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
31  import org.apache.commons.httpclient.methods.PostMethod;
32  import org.apache.commons.httpclient.methods.RequestEntity;
33  import org.opensaml.ws.security.SecurityPolicy;
34  import org.opensaml.ws.security.SecurityPolicyResolver;
35  import org.opensaml.ws.soap.client.SOAPClient;
36  import org.opensaml.ws.soap.client.SOAPClientException;
37  import org.opensaml.ws.soap.client.SOAPFaultException;
38  import org.opensaml.ws.soap.client.SOAPMessageContext;
39  import org.opensaml.ws.soap.common.SOAPException;
40  import org.opensaml.ws.soap.soap11.Envelope;
41  import org.opensaml.ws.soap.soap11.Fault;
42  import org.opensaml.xml.Configuration;
43  import org.opensaml.xml.XMLObject;
44  import org.opensaml.xml.io.Marshaller;
45  import org.opensaml.xml.io.MarshallingException;
46  import org.opensaml.xml.io.Unmarshaller;
47  import org.opensaml.xml.io.UnmarshallingException;
48  import org.opensaml.xml.parse.ParserPool;
49  import org.opensaml.xml.parse.XMLParserException;
50  import org.opensaml.xml.security.SecurityException;
51  import org.opensaml.xml.util.XMLHelper;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  import org.w3c.dom.Element;
55  
56  /**
57   * SOAP client that uses HTTP as the underlying transport and POST as the binding.
58   * 
59   * <strong>NOTE</strong> this client does not provide access to a {@link org.opensaml.ws.transport.InTransport} or
60   * {@link org.opensaml.ws.transport.OutTransport}. Therefore any {@link SecurityPolicy} which operates on these object
61   * can not be used with this client.
62   */
63  @ThreadSafe
64  public class HttpSOAPClient implements SOAPClient {
65  
66      /** Class logger. */
67      private final Logger log = LoggerFactory.getLogger(HttpSOAPClient.class);
68  
69      /** HTTP client used to send requests and receive responses. */
70      private HttpClient httpClient;
71  
72      /** Pool of XML parsers used to parser incoming responses. */
73      private ParserPool parserPool;
74  
75      /**
76       * Constructor.
77       * 
78       * @param client Client used to make outbound HTTP requests. This client SHOULD employ a
79       *            {@link org.apache.commons.httpclient.MultiThreadedHttpConnectionManager} and may be shared with other
80       *            objects.
81       * @param parser pool of XML parsers used to parse incoming responses
82       */
83      public HttpSOAPClient(HttpClient client, ParserPool parser) {
84          if (client == null) {
85              throw new IllegalArgumentException("HtppClient may not be null");
86          }
87          httpClient = client;
88  
89          if (parser == null) {
90              throw new IllegalArgumentException("ParserPool may not be null");
91          }
92          parserPool = parser;
93      }
94  
95      /** {@inheritDoc} */
96      public void send(String endpoint, SOAPMessageContext messageContext) throws SOAPException, SecurityException {
97          PostMethod post = null;
98          try {
99              post = createPostMethod(endpoint, (HttpSOAPRequestParameters) messageContext.getSOAPRequestParameters(),
100                     (Envelope) messageContext.getOutboundMessage());
101 
102             int result = httpClient.executeMethod(post);
103             log.debug("Received HTTP status code of {} when POSTing SOAP message to {}", result, endpoint);
104 
105             if (result == HttpStatus.SC_OK) {
106                 processSuccessfulResponse(post, messageContext);
107             } else if (result == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
108                 processFaultResponse(post, messageContext);
109             } else {
110                 throw new SOAPClientException("Received " + result + " HTTP response status code from HTTP request to "
111                         + endpoint);
112             }
113         } catch (IOException e) {
114             throw new SOAPClientException("Unable to send request to " + endpoint, e);
115         } finally {
116             if (post != null) {
117                 post.releaseConnection();
118             }
119         }
120     }
121 
122     /**
123      * Creates the post method used to send the SOAP request.
124      * 
125      * @param endpoint endpoint to which the message is sent
126      * @param requestParams HTTP request parameters
127      * @param message message to be sent
128      * 
129      * @return the post method to be used to send this message
130      * 
131      * @throws SOAPClientException thrown if the message could not be marshalled
132      */
133     protected PostMethod createPostMethod(String endpoint, HttpSOAPRequestParameters requestParams, Envelope message)
134             throws SOAPClientException {
135         log.debug("POSTing SOAP message to {}", endpoint);
136 
137         PostMethod post = new PostMethod(endpoint);
138         post.setRequestEntity(createRequestEntity(message, Charset.forName("UTF-8")));
139         if (requestParams != null && requestParams.getSoapAction() != null) {
140             post.setRequestHeader(HttpSOAPRequestParameters.SOAP_ACTION_HEADER, requestParams.getSoapAction());
141         }
142 
143         return post;
144     }
145 
146     /**
147      * Creates the request entity that makes up the POST message body.
148      * 
149      * @param message message to be sent
150      * @param charset character set used for the message
151      * 
152      * @return request entity that makes up the POST message body
153      * 
154      * @throws SOAPClientException thrown if the message could not be marshalled
155      */
156     protected RequestEntity createRequestEntity(Envelope message, Charset charset) throws SOAPClientException {
157         try {
158             Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(message);
159             ByteArrayOutputStream arrayOut = new ByteArrayOutputStream();
160             OutputStreamWriter writer = new OutputStreamWriter(arrayOut, charset);
161 
162             if (log.isDebugEnabled()) {
163                 log.debug("Outbound SOAP message is:\n" + XMLHelper.prettyPrintXML(marshaller.marshall(message)));
164             }
165             XMLHelper.writeNode(marshaller.marshall(message), writer);
166             return new ByteArrayRequestEntity(arrayOut.toByteArray(), "text/xml");
167         } catch (MarshallingException e) {
168             throw new SOAPClientException("Unable to marshall SOAP envelope", e);
169         }
170     }
171 
172     /**
173      * Processes a successful, as determined by an HTTP 200 status code, response.
174      * 
175      * @param httpMethod the HTTP method used to send the request and receive the response
176      * @param messageContext current messages context
177      * 
178      * @throws SOAPClientException thrown if there is a problem reading the response from the {@link PostMethod}
179      */
180     protected void processSuccessfulResponse(PostMethod httpMethod, SOAPMessageContext messageContext)
181             throws SOAPClientException {
182         try {
183             Envelope response = unmarshallResponse(httpMethod.getResponseBodyAsStream());
184             messageContext.setInboundMessage(response);
185             evaluateSecurityPolicy(messageContext);
186         } catch (IOException e) {
187             throw new SOAPClientException("Unable to read response", e);
188         }
189     }
190 
191     /**
192      * Processes a SOAP fault, as determined by an HTTP 500 status code, response.
193      * 
194      * @param httpMethod the HTTP method used to send the request and receive the response
195      * @param messageContext current messages context
196      * 
197      * @throws SOAPClientException thrown if the response can not be read from the {@link PostMethod}
198      * @throws SOAPFaultException an exception containing the SOAP fault
199      */
200     protected void processFaultResponse(PostMethod httpMethod, SOAPMessageContext messageContext)
201             throws SOAPClientException, SOAPFaultException {
202         try {
203             Envelope response = unmarshallResponse(httpMethod.getResponseBodyAsStream());
204             messageContext.setInboundMessage(response);
205 
206             List<XMLObject> faults = response.getBody().getUnknownXMLObjects(Fault.DEFAULT_ELEMENT_NAME);
207             if (faults.size() < 1) {
208                 throw new SOAPClientException("HTTP status code was 500 but SOAP response did not contain a Fault");
209             }
210             Fault fault = (Fault) faults.get(0);
211 
212             log.debug("SOAP fault code {} with message {}", fault.getCode().getValue(), fault.getMessage().getValue());
213             SOAPFaultException faultException = new SOAPFaultException("SOAP Fault: " + fault.getCode().getValue()
214                     + " Fault Message: " + fault.getMessage().getValue());
215             faultException.setFault(fault);
216             throw faultException;
217         } catch (IOException e) {
218             throw new SOAPClientException("Unable to read response", e);
219         }
220     }
221 
222     /**
223      * Unmarshalls the incoming response from a POST request.
224      * 
225      * @param responseStream input stream bearing the response
226      * 
227      * @return the response
228      * 
229      * @throws SOAPClientException thrown if the incoming response can not be unmarshalled into an {@link Envelope}
230      */
231     protected Envelope unmarshallResponse(InputStream responseStream) throws SOAPClientException {
232         try {
233             Element responseElem = parserPool.parse(responseStream).getDocumentElement();
234             if (log.isDebugEnabled()) {
235                 log.debug("Inbound SOAP message was:\n" + XMLHelper.prettyPrintXML(responseElem));
236             }
237             Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(responseElem);
238             return (Envelope) unmarshaller.unmarshall(responseElem);
239         } catch (XMLParserException e) {
240             throw new SOAPClientException("Unable to parse the XML within the response", e);
241         } catch (UnmarshallingException e) {
242             throw new SOAPClientException("unable to unmarshall the response DOM", e);
243         }
244     }
245 
246     /**
247      * Evaluates the security policy associated with the given message context. If no policy resolver is registered or
248      * no policy is located during the resolution process then no policy is evaluated. Note that neither the inbound or
249      * outbound message transport is available.
250      * 
251      * @param messageContext current message context
252      * 
253      * @throws SOAPClientException thrown if there is a problem resolving or evaluating a security policy
254      */
255     protected void evaluateSecurityPolicy(SOAPMessageContext messageContext) throws SOAPClientException {
256         SecurityPolicyResolver policyResolver = messageContext.getSecurityPolicyResolver();
257         if (policyResolver == null) {
258             return;
259         }
260 
261         SecurityPolicy policy = null;
262         try {
263             policy = policyResolver.resolveSingle(messageContext);
264             if (policy == null) {
265                 return;
266             }
267         } catch (SecurityException e) {
268             throw new SOAPClientException("Unable to resolve security policy for inbound SOAP response", e);
269         }
270 
271         try {
272             log.debug("Evaluating security policy for inbound SOAP response");
273             policy.evaluate(messageContext);
274         } catch (SecurityException e) {
275             throw new SOAPClientException("Inbound SOAP response does not meet security policy", e);
276         }
277     }
278 }