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