Home | History | Annotate | Download | only in client
      1 /*
      2  * Copyright 2007, 2008 Netflix, 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 net.oauth.client;
     18 
     19 import java.io.ByteArrayInputStream;
     20 import java.io.IOException;
     21 import java.io.InputStream;
     22 import java.net.URISyntaxException;
     23 import java.net.URL;
     24 import java.util.ArrayList;
     25 import java.util.Collection;
     26 import java.util.Iterator;
     27 import java.util.List;
     28 import java.util.Map;
     29 import net.oauth.OAuth;
     30 import net.oauth.OAuthAccessor;
     31 import net.oauth.OAuthConsumer;
     32 import net.oauth.OAuthException;
     33 import net.oauth.OAuthMessage;
     34 import net.oauth.OAuthProblemException;
     35 import net.oauth.http.HttpClient;
     36 import net.oauth.http.HttpMessage;
     37 import net.oauth.http.HttpMessageDecoder;
     38 import net.oauth.http.HttpResponseMessage;
     39 
     40 /**
     41  * Methods for an OAuth consumer to request tokens from a service provider.
     42  * <p>
     43  * This class can also be used to request access to protected resources, in some
     44  * cases. But not in all cases. For example, this class can't handle arbitrary
     45  * HTTP headers.
     46  * <p>
     47  * Methods of this class return a response as an OAuthMessage, from which you
     48  * can get a body or parameters but not both. Calling a getParameter method will
     49  * read and close the body (like readBodyAsString), so you can't read it later.
     50  * If you read or close the body first, then getParameter can't read it. The
     51  * response headers should tell you whether the response contains encoded
     52  * parameters, that is whether you should call getParameter or not.
     53  * <p>
     54  * Methods of this class don't follow redirects. When they receive a redirect
     55  * response, they throw an OAuthProblemException, with properties
     56  * HttpResponseMessage.STATUS_CODE = the redirect code
     57  * HttpResponseMessage.LOCATION = the redirect URL. Such a redirect can't be
     58  * handled at the HTTP level, if the second request must carry another OAuth
     59  * signature (with different parameters). For example, Google's Service Provider
     60  * routinely redirects requests for access to protected resources, and requires
     61  * the redirected request to be signed.
     62  *
     63  * @author John Kristian
     64  * @hide
     65  */
     66 public class OAuthClient {
     67 
     68     public OAuthClient(HttpClient http)
     69     {
     70         this.http = http;
     71     }
     72 
     73     private HttpClient http;
     74 
     75     public void setHttpClient(HttpClient http) {
     76         this.http = http;
     77     }
     78 
     79     public HttpClient getHttpClient() {
     80         return http;
     81     }
     82 
     83     /**
     84      * Get a fresh request token from the service provider.
     85      *
     86      * @param accessor
     87      *            should contain a consumer that contains a non-null consumerKey
     88      *            and consumerSecret. Also,
     89      *            accessor.consumer.serviceProvider.requestTokenURL should be
     90      *            the URL (determined by the service provider) for getting a
     91      *            request token.
     92      * @throws OAuthProblemException
     93      *             the HTTP response status code was not 200 (OK)
     94      */
     95     public void getRequestToken(OAuthAccessor accessor) throws IOException,
     96             OAuthException, URISyntaxException {
     97         getRequestToken(accessor, null);
     98     }
     99 
    100     /**
    101      * Get a fresh request token from the service provider.
    102      *
    103      * @param accessor
    104      *            should contain a consumer that contains a non-null consumerKey
    105      *            and consumerSecret. Also,
    106      *            accessor.consumer.serviceProvider.requestTokenURL should be
    107      *            the URL (determined by the service provider) for getting a
    108      *            request token.
    109      * @param httpMethod
    110      *            typically OAuthMessage.POST or OAuthMessage.GET, or null to
    111      *            use the default method.
    112      * @throws OAuthProblemException
    113      *             the HTTP response status code was not 200 (OK)
    114      */
    115     public void getRequestToken(OAuthAccessor accessor, String httpMethod)
    116             throws IOException, OAuthException, URISyntaxException {
    117         getRequestToken(accessor, httpMethod, null);
    118     }
    119 
    120     /** Get a fresh request token from the service provider.
    121      *
    122      * @param accessor
    123      *            should contain a consumer that contains a non-null consumerKey
    124      *            and consumerSecret. Also,
    125      *            accessor.consumer.serviceProvider.requestTokenURL should be
    126      *            the URL (determined by the service provider) for getting a
    127      *            request token.
    128      * @param httpMethod
    129      *            typically OAuthMessage.POST or OAuthMessage.GET, or null to
    130      *            use the default method.
    131      * @param parameters
    132      *            additional parameters for this request, or null to indicate
    133      *            that there are no additional parameters.
    134      * @throws OAuthProblemException
    135      *             the HTTP response status code was not 200 (OK)
    136      */
    137     public void getRequestToken(OAuthAccessor accessor, String httpMethod,
    138             Collection<? extends Map.Entry> parameters) throws IOException,
    139             OAuthException, URISyntaxException {
    140         accessor.accessToken = null;
    141         accessor.tokenSecret = null;
    142         {
    143             // This code supports the 'Variable Accessor Secret' extension
    144             // described in http://oauth.pbwiki.com/AccessorSecret
    145             Object accessorSecret = accessor
    146                     .getProperty(OAuthConsumer.ACCESSOR_SECRET);
    147             if (accessorSecret != null) {
    148                 List<Map.Entry> p = (parameters == null) ? new ArrayList<Map.Entry>(
    149                         1)
    150                         : new ArrayList<Map.Entry>(parameters);
    151                 p.add(new OAuth.Parameter("oauth_accessor_secret",
    152                         accessorSecret.toString()));
    153                 parameters = p;
    154                 // But don't modify the caller's parameters.
    155             }
    156         }
    157         OAuthMessage response = invoke(accessor, httpMethod,
    158                 accessor.consumer.serviceProvider.requestTokenURL, parameters);
    159         accessor.requestToken = response.getParameter(OAuth.OAUTH_TOKEN);
    160         accessor.tokenSecret = response.getParameter(OAuth.OAUTH_TOKEN_SECRET);
    161         response.requireParameters(OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET);
    162     }
    163 
    164     /**
    165      * Get an access token from the service provider, in exchange for an
    166      * authorized request token.
    167      *
    168      * @param accessor
    169      *            should contain a non-null requestToken and tokenSecret, and a
    170      *            consumer that contains a consumerKey and consumerSecret. Also,
    171      *            accessor.consumer.serviceProvider.accessTokenURL should be the
    172      *            URL (determined by the service provider) for getting an access
    173      *            token.
    174      * @param httpMethod
    175      *            typically OAuthMessage.POST or OAuthMessage.GET, or null to
    176      *            use the default method.
    177      * @param parameters
    178      *            additional parameters for this request, or null to indicate
    179      *            that there are no additional parameters.
    180      * @throws OAuthProblemException
    181      *             the HTTP response status code was not 200 (OK)
    182      */
    183     public OAuthMessage getAccessToken(OAuthAccessor accessor, String httpMethod,
    184             Collection<? extends Map.Entry> parameters) throws IOException, OAuthException, URISyntaxException {
    185         if (accessor.requestToken != null) {
    186             if (parameters == null) {
    187                 parameters = OAuth.newList(OAuth.OAUTH_TOKEN, accessor.requestToken);
    188             } else if (!OAuth.newMap(parameters).containsKey(OAuth.OAUTH_TOKEN)) {
    189                 List<Map.Entry> p = new ArrayList<Map.Entry>(parameters);
    190                 p.add(new OAuth.Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
    191                 parameters = p;
    192             }
    193         }
    194         OAuthMessage response = invoke(accessor, httpMethod,
    195                 accessor.consumer.serviceProvider.accessTokenURL, parameters);
    196         response.requireParameters(OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET);
    197         accessor.accessToken = response.getParameter(OAuth.OAUTH_TOKEN);
    198         accessor.tokenSecret = response.getParameter(OAuth.OAUTH_TOKEN_SECRET);
    199         return response;
    200     }
    201 
    202     /**
    203      * Construct a request message, send it to the service provider and get the
    204      * response.
    205      *
    206      * @param httpMethod
    207      *            the HTTP request method, or null to use the default method
    208      * @return the response
    209      * @throws URISyntaxException
    210      *             the given url isn't valid syntactically
    211      * @throws OAuthProblemException
    212      *             the HTTP response status code was not 200 (OK)
    213      */
    214     public OAuthMessage invoke(OAuthAccessor accessor, String httpMethod,
    215             String url, Collection<? extends Map.Entry> parameters)
    216     throws IOException, OAuthException, URISyntaxException {
    217         String ps = (String) accessor.consumer.getProperty(PARAMETER_STYLE);
    218         ParameterStyle style = (ps == null) ? ParameterStyle.BODY : Enum
    219                 .valueOf(ParameterStyle.class, ps);
    220         OAuthMessage request = accessor.newRequestMessage(httpMethod, url,
    221                 parameters);
    222         return invoke(request, style);
    223     }
    224 
    225     /**
    226      * The name of the OAuthConsumer property whose value is the ParameterStyle
    227      * to be used by invoke.
    228      */
    229     public static final String PARAMETER_STYLE = "parameterStyle";
    230 
    231     /**
    232      * The name of the OAuthConsumer property whose value is the Accept-Encoding
    233      * header in HTTP requests.
    234      * @deprecated use {@link OAuthConsumer#ACCEPT_ENCODING} instead
    235      */
    236     @Deprecated
    237     public static final String ACCEPT_ENCODING = OAuthConsumer.ACCEPT_ENCODING;
    238 
    239     /**
    240      * Construct a request message, send it to the service provider and get the
    241      * response.
    242      *
    243      * @return the response
    244      * @throws URISyntaxException
    245      *                 the given url isn't valid syntactically
    246      * @throws OAuthProblemException
    247      *                 the HTTP response status code was not 200 (OK)
    248      */
    249     public OAuthMessage invoke(OAuthAccessor accessor, String url,
    250             Collection<? extends Map.Entry> parameters) throws IOException,
    251             OAuthException, URISyntaxException {
    252         return invoke(accessor, null, url, parameters);
    253     }
    254 
    255     /**
    256      * Send a request message to the service provider and get the response.
    257      *
    258      * @return the response
    259      * @throws IOException
    260      *                 failed to communicate with the service provider
    261      * @throws OAuthProblemException
    262      *             the HTTP response status code was not 200 (OK)
    263      */
    264     public OAuthMessage invoke(OAuthMessage request, ParameterStyle style)
    265             throws IOException, OAuthException {
    266         final boolean isPost = POST.equalsIgnoreCase(request.method);
    267         InputStream body = request.getBodyAsStream();
    268         if (style == ParameterStyle.BODY && !(isPost && body == null)) {
    269             style = ParameterStyle.QUERY_STRING;
    270         }
    271         String url = request.URL;
    272         final List<Map.Entry<String, String>> headers =
    273             new ArrayList<Map.Entry<String, String>>(request.getHeaders());
    274         switch (style) {
    275         case QUERY_STRING:
    276             url = OAuth.addParameters(url, request.getParameters());
    277             break;
    278         case BODY: {
    279             byte[] form = OAuth.formEncode(request.getParameters()).getBytes(
    280                     request.getBodyEncoding());
    281             headers.add(new OAuth.Parameter(HttpMessage.CONTENT_TYPE,
    282                     OAuth.FORM_ENCODED));
    283             headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length + ""));
    284             body = new ByteArrayInputStream(form);
    285             break;
    286         }
    287         case AUTHORIZATION_HEADER:
    288             headers.add(new OAuth.Parameter("Authorization", request.getAuthorizationHeader(null)));
    289             // Find the non-OAuth parameters:
    290             List<Map.Entry<String, String>> others = request.getParameters();
    291             if (others != null && !others.isEmpty()) {
    292                 others = new ArrayList<Map.Entry<String, String>>(others);
    293                 for (Iterator<Map.Entry<String, String>> p = others.iterator(); p
    294                         .hasNext();) {
    295                     if (p.next().getKey().startsWith("oauth_")) {
    296                         p.remove();
    297                     }
    298                 }
    299                 // Place the non-OAuth parameters elsewhere in the request:
    300                 if (isPost && body == null) {
    301                     byte[] form = OAuth.formEncode(others).getBytes(
    302                             request.getBodyEncoding());
    303                     headers.add(new OAuth.Parameter(HttpMessage.CONTENT_TYPE,
    304                             OAuth.FORM_ENCODED));
    305                     headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length
    306                             + ""));
    307                     body = new ByteArrayInputStream(form);
    308                 } else {
    309                     url = OAuth.addParameters(url, others);
    310                 }
    311             }
    312             break;
    313         }
    314         final HttpMessage httpRequest = new HttpMessage(request.method, new URL(url), body);
    315         httpRequest.headers.addAll(headers);
    316         HttpResponseMessage httpResponse = http.execute(httpRequest);
    317         httpResponse = HttpMessageDecoder.decode(httpResponse);
    318         OAuthMessage response = new OAuthResponseMessage(httpResponse);
    319         if (httpResponse.getStatusCode() != HttpResponseMessage.STATUS_OK) {
    320             OAuthProblemException problem = new OAuthProblemException();
    321             try {
    322                 response.getParameters(); // decode the response body
    323             } catch (IOException ignored) {
    324             }
    325             problem.getParameters().putAll(response.getDump());
    326             try {
    327                 InputStream b = response.getBodyAsStream();
    328                 if (b != null) {
    329                     b.close(); // release resources
    330                 }
    331             } catch (IOException ignored) {
    332             }
    333             throw problem;
    334         }
    335         return response;
    336     }
    337 
    338     /** Where to place parameters in an HTTP message. */
    339     public enum ParameterStyle {
    340         AUTHORIZATION_HEADER, BODY, QUERY_STRING;
    341     };
    342 
    343     protected static final String PUT = OAuthMessage.PUT;
    344     protected static final String POST = OAuthMessage.POST;
    345     protected static final String DELETE = OAuthMessage.DELETE;
    346     protected static final String CONTENT_LENGTH = HttpMessage.CONTENT_LENGTH;
    347 
    348 }
    349