blob: 4be9e7fc03d131ac343b7687714c098b67176ed3 [file] [log] [blame]
/*
* Copyright 2007, 2008 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.oauth.client;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
import net.oauth.http.HttpClient;
import net.oauth.http.HttpMessage;
import net.oauth.http.HttpMessageDecoder;
import net.oauth.http.HttpResponseMessage;
/**
* Methods for an OAuth consumer to request tokens from a service provider.
* <p>
* This class can also be used to request access to protected resources, in some
* cases. But not in all cases. For example, this class can't handle arbitrary
* HTTP headers.
* <p>
* Methods of this class return a response as an OAuthMessage, from which you
* can get a body or parameters but not both. Calling a getParameter method will
* read and close the body (like readBodyAsString), so you can't read it later.
* If you read or close the body first, then getParameter can't read it. The
* response headers should tell you whether the response contains encoded
* parameters, that is whether you should call getParameter or not.
* <p>
* Methods of this class don't follow redirects. When they receive a redirect
* response, they throw an OAuthProblemException, with properties
* HttpResponseMessage.STATUS_CODE = the redirect code
* HttpResponseMessage.LOCATION = the redirect URL. Such a redirect can't be
* handled at the HTTP level, if the second request must carry another OAuth
* signature (with different parameters). For example, Google's Service Provider
* routinely redirects requests for access to protected resources, and requires
* the redirected request to be signed.
*
* @author John Kristian
* @hide
*/
public class OAuthClient {
public OAuthClient(HttpClient http)
{
this.http = http;
}
private HttpClient http;
public void setHttpClient(HttpClient http) {
this.http = http;
}
public HttpClient getHttpClient() {
return http;
}
/**
* Get a fresh request token from the service provider.
*
* @param accessor
* should contain a consumer that contains a non-null consumerKey
* and consumerSecret. Also,
* accessor.consumer.serviceProvider.requestTokenURL should be
* the URL (determined by the service provider) for getting a
* request token.
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public void getRequestToken(OAuthAccessor accessor) throws IOException,
OAuthException, URISyntaxException {
getRequestToken(accessor, null);
}
/**
* Get a fresh request token from the service provider.
*
* @param accessor
* should contain a consumer that contains a non-null consumerKey
* and consumerSecret. Also,
* accessor.consumer.serviceProvider.requestTokenURL should be
* the URL (determined by the service provider) for getting a
* request token.
* @param httpMethod
* typically OAuthMessage.POST or OAuthMessage.GET, or null to
* use the default method.
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public void getRequestToken(OAuthAccessor accessor, String httpMethod)
throws IOException, OAuthException, URISyntaxException {
getRequestToken(accessor, httpMethod, null);
}
/** Get a fresh request token from the service provider.
*
* @param accessor
* should contain a consumer that contains a non-null consumerKey
* and consumerSecret. Also,
* accessor.consumer.serviceProvider.requestTokenURL should be
* the URL (determined by the service provider) for getting a
* request token.
* @param httpMethod
* typically OAuthMessage.POST or OAuthMessage.GET, or null to
* use the default method.
* @param parameters
* additional parameters for this request, or null to indicate
* that there are no additional parameters.
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public void getRequestToken(OAuthAccessor accessor, String httpMethod,
Collection<? extends Map.Entry> parameters) throws IOException,
OAuthException, URISyntaxException {
accessor.accessToken = null;
accessor.tokenSecret = null;
{
// This code supports the 'Variable Accessor Secret' extension
// described in http://oauth.pbwiki.com/AccessorSecret
Object accessorSecret = accessor
.getProperty(OAuthConsumer.ACCESSOR_SECRET);
if (accessorSecret != null) {
List<Map.Entry> p = (parameters == null) ? new ArrayList<Map.Entry>(
1)
: new ArrayList<Map.Entry>(parameters);
p.add(new OAuth.Parameter("oauth_accessor_secret",
accessorSecret.toString()));
parameters = p;
// But don't modify the caller's parameters.
}
}
OAuthMessage response = invoke(accessor, httpMethod,
accessor.consumer.serviceProvider.requestTokenURL, parameters);
accessor.requestToken = response.getParameter(OAuth.OAUTH_TOKEN);
accessor.tokenSecret = response.getParameter(OAuth.OAUTH_TOKEN_SECRET);
response.requireParameters(OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET);
}
/**
* Get an access token from the service provider, in exchange for an
* authorized request token.
*
* @param accessor
* should contain a non-null requestToken and tokenSecret, and a
* consumer that contains a consumerKey and consumerSecret. Also,
* accessor.consumer.serviceProvider.accessTokenURL should be the
* URL (determined by the service provider) for getting an access
* token.
* @param httpMethod
* typically OAuthMessage.POST or OAuthMessage.GET, or null to
* use the default method.
* @param parameters
* additional parameters for this request, or null to indicate
* that there are no additional parameters.
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public OAuthMessage getAccessToken(OAuthAccessor accessor, String httpMethod,
Collection<? extends Map.Entry> parameters) throws IOException, OAuthException, URISyntaxException {
if (accessor.requestToken != null) {
if (parameters == null) {
parameters = OAuth.newList(OAuth.OAUTH_TOKEN, accessor.requestToken);
} else if (!OAuth.newMap(parameters).containsKey(OAuth.OAUTH_TOKEN)) {
List<Map.Entry> p = new ArrayList<Map.Entry>(parameters);
p.add(new OAuth.Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
parameters = p;
}
}
OAuthMessage response = invoke(accessor, httpMethod,
accessor.consumer.serviceProvider.accessTokenURL, parameters);
response.requireParameters(OAuth.OAUTH_TOKEN, OAuth.OAUTH_TOKEN_SECRET);
accessor.accessToken = response.getParameter(OAuth.OAUTH_TOKEN);
accessor.tokenSecret = response.getParameter(OAuth.OAUTH_TOKEN_SECRET);
return response;
}
/**
* Construct a request message, send it to the service provider and get the
* response.
*
* @param httpMethod
* the HTTP request method, or null to use the default method
* @return the response
* @throws URISyntaxException
* the given url isn't valid syntactically
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public OAuthMessage invoke(OAuthAccessor accessor, String httpMethod,
String url, Collection<? extends Map.Entry> parameters)
throws IOException, OAuthException, URISyntaxException {
String ps = (String) accessor.consumer.getProperty(PARAMETER_STYLE);
ParameterStyle style = (ps == null) ? ParameterStyle.BODY : Enum
.valueOf(ParameterStyle.class, ps);
OAuthMessage request = accessor.newRequestMessage(httpMethod, url,
parameters);
return invoke(request, style);
}
/**
* The name of the OAuthConsumer property whose value is the ParameterStyle
* to be used by invoke.
*/
public static final String PARAMETER_STYLE = "parameterStyle";
/**
* The name of the OAuthConsumer property whose value is the Accept-Encoding
* header in HTTP requests.
* @deprecated use {@link OAuthConsumer#ACCEPT_ENCODING} instead
*/
@Deprecated
public static final String ACCEPT_ENCODING = OAuthConsumer.ACCEPT_ENCODING;
/**
* Construct a request message, send it to the service provider and get the
* response.
*
* @return the response
* @throws URISyntaxException
* the given url isn't valid syntactically
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public OAuthMessage invoke(OAuthAccessor accessor, String url,
Collection<? extends Map.Entry> parameters) throws IOException,
OAuthException, URISyntaxException {
return invoke(accessor, null, url, parameters);
}
/**
* Send a request message to the service provider and get the response.
*
* @return the response
* @throws IOException
* failed to communicate with the service provider
* @throws OAuthProblemException
* the HTTP response status code was not 200 (OK)
*/
public OAuthMessage invoke(OAuthMessage request, ParameterStyle style)
throws IOException, OAuthException {
final boolean isPost = POST.equalsIgnoreCase(request.method);
InputStream body = request.getBodyAsStream();
if (style == ParameterStyle.BODY && !(isPost && body == null)) {
style = ParameterStyle.QUERY_STRING;
}
String url = request.URL;
final List<Map.Entry<String, String>> headers =
new ArrayList<Map.Entry<String, String>>(request.getHeaders());
switch (style) {
case QUERY_STRING:
url = OAuth.addParameters(url, request.getParameters());
break;
case BODY: {
byte[] form = OAuth.formEncode(request.getParameters()).getBytes(
request.getBodyEncoding());
headers.add(new OAuth.Parameter(HttpMessage.CONTENT_TYPE,
OAuth.FORM_ENCODED));
headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length + ""));
body = new ByteArrayInputStream(form);
break;
}
case AUTHORIZATION_HEADER:
headers.add(new OAuth.Parameter("Authorization", request.getAuthorizationHeader(null)));
// Find the non-OAuth parameters:
List<Map.Entry<String, String>> others = request.getParameters();
if (others != null && !others.isEmpty()) {
others = new ArrayList<Map.Entry<String, String>>(others);
for (Iterator<Map.Entry<String, String>> p = others.iterator(); p
.hasNext();) {
if (p.next().getKey().startsWith("oauth_")) {
p.remove();
}
}
// Place the non-OAuth parameters elsewhere in the request:
if (isPost && body == null) {
byte[] form = OAuth.formEncode(others).getBytes(
request.getBodyEncoding());
headers.add(new OAuth.Parameter(HttpMessage.CONTENT_TYPE,
OAuth.FORM_ENCODED));
headers.add(new OAuth.Parameter(CONTENT_LENGTH, form.length
+ ""));
body = new ByteArrayInputStream(form);
} else {
url = OAuth.addParameters(url, others);
}
}
break;
}
final HttpMessage httpRequest = new HttpMessage(request.method, new URL(url), body);
httpRequest.headers.addAll(headers);
HttpResponseMessage httpResponse = http.execute(httpRequest);
httpResponse = HttpMessageDecoder.decode(httpResponse);
OAuthMessage response = new OAuthResponseMessage(httpResponse);
if (httpResponse.getStatusCode() != HttpResponseMessage.STATUS_OK) {
OAuthProblemException problem = new OAuthProblemException();
try {
response.getParameters(); // decode the response body
} catch (IOException ignored) {
}
problem.getParameters().putAll(response.getDump());
try {
InputStream b = response.getBodyAsStream();
if (b != null) {
b.close(); // release resources
}
} catch (IOException ignored) {
}
throw problem;
}
return response;
}
/** Where to place parameters in an HTTP message. */
public enum ParameterStyle {
AUTHORIZATION_HEADER, BODY, QUERY_STRING;
};
protected static final String PUT = OAuthMessage.PUT;
protected static final String POST = OAuthMessage.POST;
protected static final String DELETE = OAuthMessage.DELETE;
protected static final String CONTENT_LENGTH = HttpMessage.CONTENT_LENGTH;
}