001/*
002 * Copyright 2008-2011 Thomas Nichols.  http://blog.thomnichols.org
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * You are receiving this code free of charge, which represents many hours of
017 * effort from other individuals and corporations.  As a responsible member
018 * of the community, you are encouraged (but not required) to donate any
019 * enhancements or improvements back to the community under a similar open
020 * source license.  Thank you. -TMN
021 */
022package groovyx.net.http;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URL;
029import java.security.GeneralSecurityException;
030import java.security.KeyStore;
031import java.util.HashMap;
032import java.util.Map;
033
034import oauth.signpost.OAuthConsumer;
035import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer;
036import oauth.signpost.exception.OAuthException;
037
038import org.apache.http.Header;
039import org.apache.http.HttpEntityEnclosingRequest;
040import org.apache.http.HttpException;
041import org.apache.http.HttpHost;
042import org.apache.http.HttpRequest;
043import org.apache.http.HttpRequestInterceptor;
044import org.apache.http.auth.AuthScope;
045import org.apache.http.auth.NTCredentials;
046import org.apache.http.auth.UsernamePasswordCredentials;
047import org.apache.http.client.HttpClient;
048import org.apache.http.conn.scheme.Scheme;
049import org.apache.http.conn.ssl.SSLSocketFactory;
050import org.apache.http.impl.client.AbstractHttpClient;
051import org.apache.http.protocol.ExecutionContext;
052import org.apache.http.protocol.HttpContext;
053
054/**
055 * Encapsulates all configuration related to HTTP authentication methods.
056 * @see HTTPBuilder#getAuth()
057 *
058 * @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a>
059 */
060public class AuthConfig {
061    protected HTTPBuilder builder;
062    public AuthConfig( HTTPBuilder builder ) {
063        this.builder = builder;
064    }
065
066    /**
067     * Set authentication credentials to be used for the current
068     * {@link HTTPBuilder#getUri() default host}.  This method name is a bit of
069     * a misnomer, since these credentials will actually work for "digest"
070     * authentication as well.
071     * @param user
072     * @param pass
073     */
074    public void basic( String user, String pass ) {
075        URI uri = ((URIBuilder)builder.getUri()).toURI();
076        if ( uri == null ) throw new IllegalStateException( "a default URI must be set" );
077        this.basic( uri.getHost(), uri.getPort(), user, pass );
078    }
079
080    /**
081     * Set authentication credentials to be used for the given host and port.
082     * @param host
083     * @param port
084     * @param user
085     * @param pass
086     */
087    public void basic( String host, int port, String user, String pass ) {
088          final HttpClient client = builder.getClient();
089          if ( !(client instanceof AbstractHttpClient )) {
090                throw new IllegalStateException("client is not an AbstractHttpClient");
091          }
092      ((AbstractHttpClient)client).getCredentialsProvider().setCredentials(
093            new AuthScope( host, port ),
094            new UsernamePasswordCredentials( user, pass )
095        );
096    }
097
098    /**
099     * Set NTLM authentication credentials to be used for the current
100     * {@link HTTPBuilder#getUri() default host}.
101     * @param user
102     * @param pass
103     * @param workstation
104     * @param domain
105     */
106    public void ntlm( String user, String pass, String workstation, String domain ) {
107        URI uri = ((URIBuilder)builder.getUri()).toURI();
108        if ( uri == null ) throw new IllegalStateException( "a default URI must be set" );
109        this.ntlm( uri.getHost(), uri.getPort(), user, pass, workstation, domain );
110    }
111
112    /**
113     * Set NTLM authentication credentials to be used for the given host and port.
114     * @param host
115     * @param port
116     * @param user
117     * @param pass
118     * @param workstation
119     * @param domain
120     */
121    public void ntlm( String host, int port, String user, String pass, String workstation, String domain ) {
122      final HttpClient client = builder.getClient();
123      if ( !(client instanceof AbstractHttpClient )) {
124        throw new IllegalStateException("client is not an AbstractHttpClient");
125      }
126      ((AbstractHttpClient)client).getCredentialsProvider().setCredentials(
127            new AuthScope( host, port ),
128            new NTCredentials( user, pass, workstation, domain )
129        );
130    }
131
132    /**
133     * Sets a certificate to be used for SSL authentication.  See
134     * {@link Class#getResource(String)} for how to get a URL from a resource
135     * on the classpath.
136     * @param certURL URL to a JKS keystore where the certificate is stored.
137     * @param password password to decrypt the keystore
138     */
139    public void certificate( String certURL, String password )
140            throws GeneralSecurityException, IOException {
141
142        KeyStore keyStore = KeyStore.getInstance( KeyStore.getDefaultType() );
143        InputStream jksStream = new URL(certURL).openStream();
144        try {
145            keyStore.load( jksStream, password.toCharArray() );
146        } finally { jksStream.close(); }
147
148        SSLSocketFactory ssl = new SSLSocketFactory(keyStore, password);
149        ssl.setHostnameVerifier( SSLSocketFactory.STRICT_HOSTNAME_VERIFIER );
150
151        builder.getClient().getConnectionManager().getSchemeRegistry()
152            .register( new Scheme("https", ssl, 443) );
153    }
154
155    /**
156     * </p>OAuth sign all requests.  Note that this currently does <strong>not</strong>
157     * wait for a <code>WWW-Authenticate</code> challenge before sending the
158     * the OAuth header.  All requests to all domains will be signed for this
159     * instance.</p>
160     *
161     * <p>This assumes you've already generated an <code>accessToken</code> and
162     * <code>secretToken</code> for the site you're targeting.  For More information
163     * on how to achieve this, see the
164     * <a href='http://code.google.com/p/oauth-signpost/wiki/GettingStarted#Using_Signpost'>Signpost documentation</a>.</p>
165     * @since 0.5.1
166     * @param consumerKey <code>null</code> if you want to <strong>unset</strong>
167     *  OAuth handling and stop signing requests.
168     * @param consumerSecret
169     * @param accessToken
170     * @param secretToken
171     */
172    public void oauth( String consumerKey, String consumerSecret,
173            String accessToken, String secretToken ) {
174                  final HttpClient client = builder.getClient();
175            if ( !(client instanceof AbstractHttpClient )) {
176                  throw new IllegalStateException("client is not an AbstractHttpClient");
177            }
178        ((AbstractHttpClient)client).removeRequestInterceptorByClass( OAuthSigner.class );
179        if ( consumerKey != null )
180            ((AbstractHttpClient)client).addRequestInterceptor( new OAuthSigner(
181                consumerKey, consumerSecret, accessToken, secretToken ) );
182    }
183
184    /**
185     * This class is used to sign all requests via an {@link HttpRequestInterceptor}
186     * until the context-aware AuthScheme is released in HttpClient 4.1.
187     * @since 0.5.1
188     */
189    static class OAuthSigner implements HttpRequestInterceptor {
190        protected OAuthConsumer oauth;
191        public OAuthSigner( String consumerKey, String consumerSecret,
192            String accessToken, String secretToken ) {
193            this.oauth = new CommonsHttpOAuthConsumer( consumerKey, consumerSecret );
194            oauth.setTokenWithSecret( accessToken, secretToken );
195        }
196
197        public void process(HttpRequest request, HttpContext ctx) throws HttpException, IOException {
198            /* The full request URI must be reconstructed between the context and the request URI.
199             * Best we can do until AuthScheme supports HttpContext.  See:
200             * https://issues.apache.org/jira/browse/HTTPCLIENT-901 */
201            try {
202                HttpHost host = (HttpHost) ctx.getAttribute( ExecutionContext.HTTP_TARGET_HOST );
203                final URI requestURI = new URI( host.toURI() ).resolve( request.getRequestLine().getUri() );
204
205                oauth.signpost.http.HttpRequest oAuthRequest =
206                    new OAuthRequestAdapter(request, requestURI);
207                this.oauth.sign( oAuthRequest );
208            }
209            catch ( URISyntaxException ex ) {
210                throw new HttpException( "Error rebuilding request URI", ex );
211            }
212            catch (OAuthException e) {
213                throw new HttpException( "OAuth signing error", e);
214            }
215        }
216
217        static class OAuthRequestAdapter implements oauth.signpost.http.HttpRequest {
218
219            final HttpRequest request;
220            final URI requestURI;
221            OAuthRequestAdapter( HttpRequest request, URI requestURI ) {
222                this.request = request;
223                this.requestURI = requestURI;
224            }
225
226            public String getRequestUrl() { return requestURI.toString(); }
227            public void setRequestUrl(String url) {/*ignore*/}
228            public Map<String, String> getAllHeaders() {
229                Map<String,String> headers = new HashMap<String,String>();
230                // FIXME this doesn't account for repeated headers,
231                // which are allowed by the HTTP spec!!
232                for ( Header h : request.getAllHeaders() )
233                    headers.put(h.getName(), h.getValue());
234                return headers;
235            }
236            public String getContentType() {
237                try {
238                    return request.getFirstHeader("content-type").getValue();
239                }
240                catch ( Exception ex ) { // NPE or ArrayOOBEx
241                    return null;
242                }
243            }
244            public String getHeader(String name) {
245                Header h = request.getFirstHeader(name);
246                return h != null ? h.getValue() : null;
247            }
248            public InputStream getMessagePayload() throws IOException {
249                if ( request instanceof HttpEntityEnclosingRequest )
250                    return ((HttpEntityEnclosingRequest)request).getEntity().getContent();
251                return null;
252            }
253            public String getMethod() {
254                return request.getRequestLine().getMethod();
255            }
256            public void setHeader(String key, String val) {
257                request.setHeader(key, val);
258            }
259            public Object unwrap() {
260                return request;
261            }
262        };
263    }
264}