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
17package net.oauth.client;
18
19import java.io.ByteArrayInputStream;
20import java.io.IOException;
21import java.io.InputStream;
22import java.net.URISyntaxException;
23import java.net.URL;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.Iterator;
27import java.util.List;
28import java.util.Map;
29import net.oauth.OAuth;
30import net.oauth.OAuthAccessor;
31import net.oauth.OAuthConsumer;
32import net.oauth.OAuthException;
33import net.oauth.OAuthMessage;
34import net.oauth.OAuthProblemException;
35import net.oauth.http.HttpClient;
36import net.oauth.http.HttpMessage;
37import net.oauth.http.HttpMessageDecoder;
38import 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 */
66public 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