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    
019    package net.sourceforge.spnego;
020    
021    import java.io.ByteArrayOutputStream;
022    import java.io.IOException;
023    import java.net.MalformedURLException;
024    import java.net.URL;
025    import java.security.PrivilegedActionException;
026    
027    import javax.security.auth.login.LoginException;
028    import javax.xml.soap.MessageFactory;
029    import javax.xml.soap.MimeHeaders;
030    import javax.xml.soap.SOAPConnection;
031    import javax.xml.soap.SOAPConstants;
032    import javax.xml.soap.SOAPException;
033    import javax.xml.soap.SOAPMessage;
034    
035    import org.ietf.jgss.GSSCredential;
036    import org.ietf.jgss.GSSException;
037    
038    /**
039     * This class can be used to make SOAP calls to a protected SOAP Web Service.
040     * 
041     * <p>
042     * The idea for this class is to replace code that looks like this...
043     * <pre>
044     *  final SOAPConnectionFactory soapConnectionFactory =
045     *      SOAPConnectionFactory.newInstance();
046     *  conn = soapConnectionFactory.createConnection();
047     * </pre>
048     * </p>
049     * 
050     * <p>
051     * with code that looks like this...
052     * <pre>
053     *  conn = new SpnegoSOAPConnection("spnego-client", "dfelix", "myp@s5");
054     * </pre>
055     * </p>
056     * 
057     * <p><b>Example:</b></p>
058     * <pre>
059     * SOAPMessage response = null;
060     * 
061     * <b>final SpnegoSOAPConnection conn =
062     *     new SpnegoSOAPConnection(this.module, this.kuser, this.kpass);</b>
063     * 
064     * try {
065     *     final MessageFactory msgFactory = MessageFactory.newInstance();
066     *     final SOAPMessage message = msgFactory.createMessage();
067     * 
068     *     final SOAPBody body = message.getSOAPBody();
069     *     
070     *     final SOAPBodyElement bodyElement = body.addBodyElement(
071     *             new QName(this.namespace, this.methodName, this.nsprefix));
072     *     
073     *     for (int i=0; i&lt;args.length; i++) {
074     *         final SOAPElement element = bodyElement.addChildElement(
075     *                 new QName("arg" + i));
076     * 
077     *         element.addTextNode(args[i]);
078     *     }
079     *     
080     *     response = conn.call(message, this.serviceLocation);
081     * 
082     * } finally {
083     *     conn.close();
084     * }
085     * </pre>
086     * 
087     * <p>
088     * To see a full working example, take a look at the 
089     * <a href="http://spnego.sourceforge.net/ExampleSpnegoSOAPClient.java" 
090     * target="_blank">ExampleSpnegoSOAPClient.java</a> 
091     * example.
092     * </p>
093     * 
094     * <p>
095     * Also, take a look at the  
096     * <a href="http://spnego.sourceforge.net/protected_soap_service.html" 
097     * target="_blank">how to connect to a protected SOAP Web Service</a> 
098     *  example.
099     * </p>
100     * 
101     * @see SpnegoHttpURLConnection
102     * 
103     * @author Darwin V. Felix
104     *
105     */
106    public class SpnegoSOAPConnection extends SOAPConnection {
107    
108        private final transient SpnegoHttpURLConnection conn;
109        
110        /**
111         * Creates an instance where the LoginContext relies on a keytab 
112         * file being specified by "java.security.auth.login.config" or 
113         * where LoginContext relies on tgtsessionkey.
114         * 
115         * @param loginModuleName 
116         * @throws LoginException 
117         */
118        public SpnegoSOAPConnection(final String loginModuleName) throws LoginException {
119    
120            super();
121            this.conn = new SpnegoHttpURLConnection(loginModuleName);
122        }
123    
124        /**
125         * Create an instance where the GSSCredential is specified by the parameter 
126         * and where the GSSCredential is automatically disposed after use.
127         *  
128         * @param creds credentials to use
129         */
130        public SpnegoSOAPConnection(final GSSCredential creds) {
131            this(creds, true);
132        }
133    
134        /**
135         * Create an instance where the GSSCredential is specified by the parameter 
136         * and whether the GSSCredential should be disposed after use.
137         * 
138         * @param creds credentials to use
139         * @param dispose true if GSSCredential should be diposed after use
140         */
141        public SpnegoSOAPConnection(final GSSCredential creds, final boolean dispose) {
142            super();
143            this.conn = new SpnegoHttpURLConnection(creds, dispose);
144        }
145        
146        /**
147         * Creates an instance where the LoginContext does not require a keytab
148         * file. However, the "java.security.auth.login.config" property must still
149         * be set prior to instantiating this object.
150         * 
151         * @param loginModuleName 
152         * @param username 
153         * @param password 
154         * @throws LoginException 
155         */
156        public SpnegoSOAPConnection(final String loginModuleName,
157            final String username, final String password) throws LoginException {
158    
159            super();
160            this.conn = new SpnegoHttpURLConnection(loginModuleName, username, password);
161        }
162    
163        @Override
164        public final SOAPMessage call(final SOAPMessage request, final Object endpoint)
165            throws SOAPException {
166            
167            SOAPMessage message = null;
168            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
169            
170            try {
171                final MimeHeaders headers = request.getMimeHeaders();
172                final String[] contentType = headers.getHeader("Content-Type");
173                final String[] soapAction = headers.getHeader("SOAPAction");
174                
175                // build the Content-Type HTTP header parameter if not defined
176                if (null == contentType) {
177                    final StringBuilder header = new StringBuilder();
178        
179                    if (null == soapAction) {
180                        header.append("application/soap+xml; charset=UTF-8;");
181                    } else {
182                        header.append("text/xml; charset=UTF-8;");
183                    }
184        
185                    // not defined as a MIME header but we need it as an HTTP header parameter
186                    this.conn.addRequestProperty("Content-Type", header.toString());
187                } else {
188                    if (contentType.length > 1) {
189                        throw new IllegalArgumentException("Content-Type defined more than once.");
190                    }
191                    
192                    // user specified as a MIME header so add it as an HTTP header parameter
193                    this.conn.addRequestProperty("Content-Type", contentType[0]);
194                }
195                
196                // specify SOAPAction as an HTTP header parameter
197                if (null != soapAction) {
198                    if (soapAction.length > 1) {
199                        throw new IllegalArgumentException("SOAPAction defined more than once.");
200                    }
201                    this.conn.addRequestProperty("SOAPAction", soapAction[0]);
202                }
203        
204                request.writeTo(bos);
205                
206                this.conn.connect(new URL(endpoint.toString()), bos);
207                
208                final MessageFactory factory = MessageFactory.newInstance(
209                        SOAPConstants.SOAP_1_2_PROTOCOL);
210            
211                try {
212                    message = factory.createMessage(null, this.conn.getInputStream());
213                } catch (IOException e) {
214                    message = factory.createMessage(null, this.conn.getErrorStream());
215                }
216                
217            } catch (MalformedURLException e) {
218                throw new SOAPException(e);
219            } catch (IOException e) {
220                throw new SOAPException(e);
221            } catch (GSSException e) {
222                throw new SOAPException(e);
223            } catch (PrivilegedActionException e) {
224                throw new SOAPException(e);
225            } finally {
226                try {
227                    bos.close();
228                } catch (IOException ioe) {
229                    assert true;
230                }
231                this.close();
232            }
233    
234            return message;
235        }
236    
237        @Override
238        public final void close() {
239            if (null != this.conn) {
240                this.conn.disconnect();
241            }
242        }
243    }