001/** 002 * Copyright (C) 2009 "Darwin V. Felix" <darwinfelix@users.sourceforge.net> 003 * 004 * This library is free software; you can redistribute it and/or 005 * modify it under the terms of the GNU Lesser General Public 006 * License as published by the Free Software Foundation; either 007 * version 2.1 of the License, or (at your option) any later version. 008 * 009 * This library is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 012 * Lesser General Public License for more details. 013 * 014 * You should have received a copy of the GNU Lesser General Public 015 * License along with this library; if not, write to the Free Software 016 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 017 */ 018 019package net.sourceforge.spnego; 020 021import java.io.ByteArrayInputStream; 022import java.io.ByteArrayOutputStream; 023import java.io.IOException; 024import java.io.InputStream; 025import java.net.MalformedURLException; 026import java.net.URL; 027import java.security.PrivilegedActionException; 028import java.util.logging.Logger; 029 030import javax.security.auth.login.LoginException; 031import javax.xml.parsers.DocumentBuilderFactory; 032import javax.xml.parsers.ParserConfigurationException; 033import javax.xml.soap.MessageFactory; 034import javax.xml.soap.MimeHeaders; 035import javax.xml.soap.SOAPConnection; 036import javax.xml.soap.SOAPException; 037import javax.xml.soap.SOAPMessage; 038import javax.xml.transform.Transformer; 039import javax.xml.transform.TransformerException; 040import javax.xml.transform.TransformerFactory; 041import javax.xml.transform.TransformerFactoryConfigurationError; 042import javax.xml.transform.dom.DOMSource; 043import javax.xml.transform.stream.StreamResult; 044 045import net.sourceforge.spnego.SpnegoHttpFilter.Constants; 046 047import org.ietf.jgss.GSSCredential; 048import org.ietf.jgss.GSSException; 049import org.w3c.dom.Document; 050import org.w3c.dom.Element; 051import org.w3c.dom.Node; 052import org.w3c.dom.NodeList; 053import org.xml.sax.SAXException; 054 055/** 056 * This class can be used to make SOAP calls to a protected SOAP Web Service. 057 * 058 * <p> 059 * The idea for this class is to replace code that looks like this... 060 * <pre> 061 * final SOAPConnectionFactory soapConnectionFactory = 062 * SOAPConnectionFactory.newInstance(); 063 * conn = soapConnectionFactory.createConnection(); 064 * </pre> 065 * </p> 066 * 067 * <p> 068 * with code that looks like this... 069 * <pre> 070 * conn = new SpnegoSOAPConnection("spnego-client", "dfelix", "myp@s5"); 071 * </pre> 072 * </p> 073 * 074 * <p><b>Example:</b></p> 075 * <pre> 076 * System.setProperty("java.security.krb5.conf", "C:/Users/dfelix/krb5.conf"); 077 * System.setProperty("java.security.auth.login.config", "C:/Users/dfelix/login.conf"); 078 * 079 * <b>final SpnegoSOAPConnection conn = 080 * new SpnegoSOAPConnection(this.module, this.kuser, this.kpass);</b> 081 * 082 * try { 083 * MessageFactory factory = MessageFactory.newInstance(); 084 * SOAPMessage message = factory.createMessage(); 085 * 086 * SOAPHeader header = message.getSOAPHeader(); 087 * SOAPBody body = message.getSOAPBody(); 088 * header.detachNode(); 089 * 090 * QName bodyName = new QName("http://wombat.ztrade.com", 091 * "GetLastTradePrice", "m"); 092 * SOAPBodyElement bodyElement = body.addBodyElement(bodyName); 093 * 094 * QName name = new QName("symbol"); 095 * SOAPElement symbol = bodyElement.addChildElement(name); 096 * symbol.addTextNode("SUNW"); 097 * 098 * URL endpoint = new URL("http://spnego.sourceforge.net/soap.html"); 099 * SOAPMessage response = conn.call(message, endpoint); 100 * 101 * SOAPBody soapBody = response.getSOAPBody(); 102 * 103 * Iterator iterator = soapBody.getChildElements(bodyName); 104 * bodyElement = (SOAPBodyElement)iterator.next(); 105 * String lastPrice = bodyElement.getValue(); 106 * 107 * System.out.print("The last price for SUNW is "); 108 * System.out.println(lastPrice); 109 * 110 * } finally { 111 * conn.close(); 112 * } 113 * </pre> 114 * 115 * <p> 116 * To see a full working example, take a look at the 117 * <a href="http://spnego.sourceforge.net/ExampleSpnegoSOAPClient.java" 118 * target="_blank">ExampleSpnegoSOAPClient.java</a> 119 * example. 120 * </p> 121 * 122 * <p> 123 * Also, take a look at the 124 * <a href="http://spnego.sourceforge.net/protected_soap_service.html" 125 * target="_blank">how to connect to a protected SOAP Web Service</a> 126 * example. 127 * </p> 128 * 129 * @see SpnegoHttpURLConnection 130 * 131 * @author Darwin V. Felix 132 * 133 */ 134public class SpnegoSOAPConnection extends SOAPConnection { 135 136 /** Default LOGGER. */ 137 private static final Logger LOGGER = 138 Logger.getLogger(SpnegoSOAPConnection.class.getName()); 139 140 private final transient SpnegoHttpURLConnection conn; 141 142 private final transient DocumentBuilderFactory documentFactory = 143 DocumentBuilderFactory.newInstance(); 144 145 private final transient MessageFactory messageFactory; 146 147 /** 148 * Creates an instance where the LoginContext relies on a keytab 149 * file being specified by "java.security.auth.login.config" or 150 * where LoginContext relies on tgtsessionkey. 151 * 152 * @param loginModuleName 153 * @throws LoginException 154 */ 155 public SpnegoSOAPConnection(final String loginModuleName) throws LoginException { 156 super(); 157 this.conn = new SpnegoHttpURLConnection(loginModuleName); 158 159 try { 160 this.messageFactory = MessageFactory.newInstance(); 161 } catch (SOAPException e) { 162 throw new IllegalStateException(e); 163 } 164 } 165 166 /** 167 * Create an instance where the GSSCredential is specified by the parameter 168 * and where the GSSCredential is automatically disposed after use. 169 * 170 * @param creds credentials to use 171 */ 172 public SpnegoSOAPConnection(final GSSCredential creds) { 173 this(creds, true); 174 } 175 176 /** 177 * Create an instance where the GSSCredential is specified by the parameter 178 * and whether the GSSCredential should be disposed after use. 179 * 180 * @param creds credentials to use 181 * @param dispose true if GSSCredential should be diposed after use 182 */ 183 public SpnegoSOAPConnection(final GSSCredential creds, final boolean dispose) { 184 super(); 185 this.conn = new SpnegoHttpURLConnection(creds, dispose); 186 187 try { 188 this.messageFactory = MessageFactory.newInstance(); 189 } catch (SOAPException e) { 190 throw new IllegalStateException(e); 191 } 192 } 193 194 /** 195 * Create an instance where the GSSCredential is specified by the parameter 196 * and whether the GSSCredential should be disposed after use. 197 * 198 * Set confidentiality and mutual integrity to both be false or both be true. 199 * 200 * @param creds credentials to use 201 * @param dispose true if GSSCredential should be diposed after use 202 * @param confidential 203 * @param integrity 204 */ 205 public SpnegoSOAPConnection(final GSSCredential creds, final boolean dispose 206 , final boolean confidential, final boolean integrity) { 207 super(); 208 this.conn = new SpnegoHttpURLConnection(creds, dispose); 209 this.conn.setConfidentiality(confidential); 210 this.conn.setMessageIntegrity(integrity); 211 212 try { 213 this.messageFactory = MessageFactory.newInstance(); 214 } catch (SOAPException e) { 215 throw new IllegalStateException(e); 216 } 217 } 218 219 /** 220 * Creates an instance where the LoginContext does not require a keytab 221 * file. However, the "java.security.auth.login.config" property must still 222 * be set prior to instantiating this object. 223 * 224 * @param loginModuleName 225 * @param username 226 * @param password 227 * @throws LoginException 228 */ 229 public SpnegoSOAPConnection(final String loginModuleName, 230 final String username, final String password) throws LoginException { 231 232 super(); 233 this.conn = new SpnegoHttpURLConnection(loginModuleName, username, password); 234 235 try { 236 this.messageFactory = MessageFactory.newInstance(); 237 } catch (SOAPException e) { 238 throw new IllegalStateException(e); 239 } 240 } 241 242 @Override 243 public final SOAPMessage call(final SOAPMessage request, final Object endpoint) 244 throws SOAPException { 245 246 LOGGER.finer("endpoint=" + endpoint); 247 248 SOAPMessage message = null; 249 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 250 251 this.conn.setRequestMethod("POST"); 252 253 try { 254 final MimeHeaders headers = request.getMimeHeaders(); 255 final String[] contentType = headers.getHeader(Constants.CONTENT_TYPE); 256 final String[] soapAction = headers.getHeader(Constants.SOAP_ACTION); 257 258 // build the Content-Type HTTP header parameter if not defined 259 if (null == contentType) { 260 final StringBuilder header = new StringBuilder(); 261 262 if (null == soapAction) { 263 header.append("application/soap+xml; charset=UTF-8;"); 264 } else { 265 header.append("text/xml; charset=UTF-8;"); 266 } 267 268 // not defined as a MIME header but we need it as an HTTP header parameter 269 this.conn.addRequestProperty(Constants.CONTENT_TYPE, header.toString()); 270 } else { 271 if (contentType.length > 1) { 272 throw new IllegalArgumentException("Content-Type defined more than once."); 273 } 274 275 // user specified as a MIME header so add it as an HTTP header parameter 276 this.conn.addRequestProperty(Constants.CONTENT_TYPE, contentType[0]); 277 } 278 279 // specify SOAPAction as an HTTP header parameter 280 if (null != soapAction) { 281 if (soapAction.length > 1) { 282 throw new IllegalArgumentException("SOAPAction defined more than once."); 283 } 284 this.conn.addRequestProperty(Constants.SOAP_ACTION, soapAction[0]); 285 } 286 287 request.writeTo(bos); 288 289 // make the call 290 this.conn.connect(new URL(endpoint.toString()), bos); 291 292 // parse the response 293 message = this.createMessage(this.conn.getInputStream()); 294 295 } catch (MalformedURLException e) { 296 throw new SOAPException(e); 297 } catch (IOException e) { 298 throw new SOAPException(e); 299 } catch (GSSException e) { 300 throw new SOAPException(e); 301 } catch (PrivilegedActionException e) { 302 throw new SOAPException(e); 303 } finally { 304 try { 305 bos.close(); 306 } catch (IOException ioe) { 307 assert true; 308 } 309 this.close(); 310 } 311 312 return message; 313 } 314 315 @Override 316 public final void close() { 317 if (null != this.conn) { 318 this.conn.disconnect(); 319 } 320 } 321 322 private SOAPMessage createMessage(final InputStream stream) throws SOAPException { 323 final Document document; 324 325 try { 326 document = this.parse(stream); 327 } catch (IOException e) { 328 throw new SOAPException(e); 329 } catch (SAXException e) { 330 throw new SOAPException(e); 331 } catch (ParserConfigurationException e) { 332 throw new SOAPException(e); 333 } 334 335 Node soapBody = null; 336 337 // confirm that we have a soap envelope 338 final Element parent = document.getDocumentElement(); 339 340 if ("Envelope".equalsIgnoreCase(parent.getLocalName())) { 341 // confirm that we have a body element 342 343 final NodeList children = parent.getChildNodes(); 344 345 for (int i = 0; i < children.getLength(); i++) { 346 final Node node = children.item(i); 347 if ("Body".equalsIgnoreCase(node.getLocalName())) { 348 soapBody = parent.removeChild(node); 349 break; 350 } 351 } 352 353 if (null == soapBody) { 354 throw new IllegalArgumentException( 355 "Response did not contain a SOAP 'Body'."); 356 } 357 } else { 358 throw new IllegalArgumentException( 359 "Response did not contain a SOAP 'Envelope'."); 360 } 361 362 try { 363 return this.transform(soapBody); 364 } catch (TransformerFactoryConfigurationError e) { 365 throw new SOAPException(e); 366 } catch (TransformerException e) { 367 throw new SOAPException(e); 368 } catch (IOException e) { 369 throw new SOAPException(e); 370 } catch (SAXException e) { 371 throw new SOAPException(e); 372 } catch (ParserConfigurationException e) { 373 throw new SOAPException(e); 374 } 375 } 376 377 private Document parse(final InputStream stream) 378 throws SAXException, IOException, ParserConfigurationException { 379 380 this.documentFactory.setNamespaceAware(true); 381 382 final Document document = 383 this.documentFactory.newDocumentBuilder().parse(stream); 384 385 return document; 386 } 387 388 private SOAPMessage transform(final Node soapBody) throws SOAPException, IOException 389 , TransformerException, SAXException, ParserConfigurationException { 390 391 final SOAPMessage message = messageFactory.createMessage(); 392 393 final Transformer transformer = 394 TransformerFactory.newInstance().newTransformer(); 395 396 final NodeList children = soapBody.getChildNodes(); 397 398 LOGGER.finer("number of children=" + children.getLength()); 399 400 for (int i = 0; i < children.getLength(); i++) { 401 402 LOGGER.finest("child[" + i + "]=" + children.item(i).getLocalName()); 403 404 final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 405 406 transformer.transform( 407 new DOMSource(children.item(i)), new StreamResult(bos)); 408 bos.flush(); 409 410 final ByteArrayInputStream bis = 411 new ByteArrayInputStream(bos.toByteArray()); 412 413 final Document document = this.parse(bis); 414 bis.close(); 415 bos.close(); 416 417 message.getSOAPBody().addDocument(document); 418 } 419 420 return message; 421 } 422}