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}