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; 18 19import java.io.IOException; 20import java.io.InputStream; 21import java.io.InputStreamReader; 22import java.io.Reader; 23import java.net.URISyntaxException; 24import java.util.ArrayList; 25import java.util.Collection; 26import java.util.Collections; 27import java.util.HashMap; 28import java.util.List; 29import java.util.Map; 30import java.util.Set; 31import java.util.regex.Matcher; 32import java.util.regex.Pattern; 33import net.oauth.http.HttpMessage; 34import net.oauth.signature.OAuthSignatureMethod; 35 36/** 37 * A request or response message used in the OAuth protocol. 38 * <p> 39 * The parameters in this class are not percent-encoded. Methods like 40 * OAuthClient.invoke and OAuthResponseMessage.completeParameters are 41 * responsible for percent-encoding parameters before transmission and decoding 42 * them after reception. 43 * 44 * @author John Kristian 45 * @hide 46 */ 47public class OAuthMessage { 48 49 public OAuthMessage(String method, String URL, 50 Collection<? extends Map.Entry> parameters) { 51 this.method = method; 52 this.URL = URL; 53 if (parameters == null) { 54 this.parameters = new ArrayList<Map.Entry<String, String>>(); 55 } else { 56 this.parameters = new ArrayList<Map.Entry<String, String>>(parameters.size()); 57 for (Map.Entry p : parameters) { 58 this.parameters.add(new OAuth.Parameter( 59 toString(p.getKey()), toString(p.getValue()))); 60 } 61 } 62 } 63 64 public String method; 65 public String URL; 66 67 private final List<Map.Entry<String, String>> parameters; 68 private Map<String, String> parameterMap; 69 private boolean parametersAreComplete = false; 70 private final List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(); 71 72 public String toString() { 73 return "OAuthMessage(" + method + ", " + URL + ", " + parameters + ")"; 74 } 75 76 /** A caller is about to get a parameter. */ 77 private void beforeGetParameter() throws IOException { 78 if (!parametersAreComplete) { 79 completeParameters(); 80 parametersAreComplete = true; 81 } 82 } 83 84 /** 85 * Finish adding parameters; for example read an HTTP response body and 86 * parse parameters from it. 87 */ 88 protected void completeParameters() throws IOException { 89 } 90 91 public List<Map.Entry<String, String>> getParameters() throws IOException { 92 beforeGetParameter(); 93 return Collections.unmodifiableList(parameters); 94 } 95 96 public void addParameter(String key, String value) { 97 addParameter(new OAuth.Parameter(key, value)); 98 } 99 100 public void addParameter(Map.Entry<String, String> parameter) { 101 parameters.add(parameter); 102 parameterMap = null; 103 } 104 105 public void addParameters( 106 Collection<? extends Map.Entry<String, String>> parameters) { 107 this.parameters.addAll(parameters); 108 parameterMap = null; 109 } 110 111 public String getParameter(String name) throws IOException { 112 return getParameterMap().get(name); 113 } 114 115 public String getConsumerKey() throws IOException { 116 return getParameter(OAuth.OAUTH_CONSUMER_KEY); 117 } 118 119 public String getToken() throws IOException { 120 return getParameter(OAuth.OAUTH_TOKEN); 121 } 122 123 public String getSignatureMethod() throws IOException { 124 return getParameter(OAuth.OAUTH_SIGNATURE_METHOD); 125 } 126 127 public String getSignature() throws IOException { 128 return getParameter(OAuth.OAUTH_SIGNATURE); 129 } 130 131 protected Map<String, String> getParameterMap() throws IOException { 132 beforeGetParameter(); 133 if (parameterMap == null) { 134 parameterMap = OAuth.newMap(parameters); 135 } 136 return parameterMap; 137 } 138 139 /** 140 * The MIME type of the body of this message. 141 * 142 * @return the MIME type, or null to indicate the type is unknown. 143 */ 144 public String getBodyType() { 145 return getHeader(HttpMessage.CONTENT_TYPE); 146 } 147 148 /** 149 * The character encoding of the body of this message. 150 * 151 * @return the name of an encoding, or "ISO-8859-1" if no charset has been 152 * specified. 153 */ 154 public String getBodyEncoding() { 155 return HttpMessage.DEFAULT_CHARSET; 156 } 157 158 /** 159 * The value of the last HTTP header with the given name. The name is case 160 * insensitive. 161 * 162 * @return the value of the last header, or null to indicate that there is 163 * no such header in this message. 164 */ 165 public final String getHeader(String name) { 166 String value = null; // no such header 167 for (Map.Entry<String, String> header : getHeaders()) { 168 if (name.equalsIgnoreCase(header.getKey())) { 169 value = header.getValue(); 170 } 171 } 172 return value; 173 } 174 175 /** All HTTP headers. You can add headers to this list. */ 176 public final List<Map.Entry<String, String>> getHeaders() { 177 return headers; 178 } 179 180 /** 181 * Read the body of the HTTP request or response and convert it to a String. 182 * This method isn't repeatable, since it consumes and closes getBodyAsStream. 183 * 184 * @return the body, or null to indicate there is no body. 185 */ 186 public final String readBodyAsString() throws IOException 187 { 188 InputStream body = getBodyAsStream(); 189 return readAll(body, getBodyEncoding()); 190 } 191 192 /** 193 * Get a stream from which to read the body of the HTTP request or response. 194 * This is designed to support efficient streaming of a large message. 195 * The caller must close the returned stream, to release the underlying 196 * resources such as the TCP connection for an HTTP response. 197 * 198 * @return a stream from which to read the body, or null to indicate there 199 * is no body. 200 */ 201 public InputStream getBodyAsStream() throws IOException { 202 return null; 203 } 204 205 /** Construct a verbose description of this message and its origins. */ 206 public Map<String, Object> getDump() throws IOException { 207 Map<String, Object> into = new HashMap<String, Object>(); 208 dump(into); 209 return into; 210 } 211 212 protected void dump(Map<String, Object> into) throws IOException { 213 into.put("URL", URL); 214 if (parametersAreComplete) { 215 try { 216 into.putAll(getParameterMap()); 217 } catch (Exception ignored) { 218 } 219 } 220 } 221 222 /** 223 * Verify that the required parameter names are contained in the actual 224 * collection. 225 * 226 * @throws OAuthProblemException 227 * one or more parameters are absent. 228 * @throws IOException 229 */ 230 public void requireParameters(String... names) 231 throws OAuthProblemException, IOException { 232 Set<String> present = getParameterMap().keySet(); 233 List<String> absent = new ArrayList<String>(); 234 for (String required : names) { 235 if (!present.contains(required)) { 236 absent.add(required); 237 } 238 } 239 if (!absent.isEmpty()) { 240 OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.PARAMETER_ABSENT); 241 problem.setParameter(OAuth.Problems.OAUTH_PARAMETERS_ABSENT, OAuth.percentEncode(absent)); 242 throw problem; 243 } 244 } 245 246 /** 247 * Add some of the parameters needed to request access to a protected 248 * resource, if they aren't already in the message. 249 * 250 * @throws IOException 251 * @throws URISyntaxException 252 */ 253 public void addRequiredParameters(OAuthAccessor accessor) 254 throws OAuthException, IOException, URISyntaxException { 255 final Map<String, String> pMap = OAuth.newMap(parameters); 256 if (pMap.get(OAuth.OAUTH_TOKEN) == null && accessor.accessToken != null) { 257 addParameter(OAuth.OAUTH_TOKEN, accessor.accessToken); 258 } 259 final OAuthConsumer consumer = accessor.consumer; 260 if (pMap.get(OAuth.OAUTH_CONSUMER_KEY) == null) { 261 addParameter(OAuth.OAUTH_CONSUMER_KEY, consumer.consumerKey); 262 } 263 String signatureMethod = pMap.get(OAuth.OAUTH_SIGNATURE_METHOD); 264 if (signatureMethod == null) { 265 signatureMethod = (String) consumer.getProperty(OAuth.OAUTH_SIGNATURE_METHOD); 266 if (signatureMethod == null) { 267 signatureMethod = OAuth.HMAC_SHA1; 268 } 269 addParameter(OAuth.OAUTH_SIGNATURE_METHOD, signatureMethod); 270 } 271 if (pMap.get(OAuth.OAUTH_TIMESTAMP) == null) { 272 addParameter(OAuth.OAUTH_TIMESTAMP, (System.currentTimeMillis() / 1000) + ""); 273 } 274 if (pMap.get(OAuth.OAUTH_NONCE) == null) { 275 addParameter(OAuth.OAUTH_NONCE, System.nanoTime() + ""); 276 } 277 if (pMap.get(OAuth.OAUTH_VERSION) == null) { 278 addParameter(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0); 279 } 280 this.sign(accessor); 281 } 282 283 /** 284 * Add a signature to the message. 285 * 286 * @throws URISyntaxException 287 */ 288 public void sign(OAuthAccessor accessor) throws IOException, 289 OAuthException, URISyntaxException { 290 OAuthSignatureMethod.newSigner(this, accessor).sign(this); 291 } 292 293 /** 294 * Check that the message is valid. 295 * 296 * @throws IOException 297 * @throws URISyntaxException 298 * 299 * @throws OAuthProblemException 300 * the message is invalid 301 */ 302 public void validateMessage(OAuthAccessor accessor, OAuthValidator validator) 303 throws OAuthException, IOException, URISyntaxException { 304 validator.validateMessage(this, accessor); 305 } 306 307 /** 308 * Construct a WWW-Authenticate or Authentication header value, containing 309 * the given realm plus all the parameters whose names begin with "oauth_". 310 */ 311 public String getAuthorizationHeader(String realm) throws IOException { 312 StringBuilder into = new StringBuilder(); 313 if (realm != null) { 314 into.append(" realm=\"").append(OAuth.percentEncode(realm)).append('"'); 315 } 316 beforeGetParameter(); 317 if (parameters != null) { 318 for (Map.Entry parameter : parameters) { 319 String name = toString(parameter.getKey()); 320 if (name.startsWith("oauth_")) { 321 if (into.length() > 0) into.append(","); 322 into.append(" "); 323 into.append(OAuth.percentEncode(name)).append("=\""); 324 into.append(OAuth.percentEncode(toString(parameter.getValue()))).append('"'); 325 } 326 } 327 } 328 return AUTH_SCHEME + into.toString(); 329 } 330 331 /** 332 * Read all the data from the given stream, and close it. 333 * 334 * @return null if from is null, or the data from the stream converted to a 335 * String 336 */ 337 public static String readAll(InputStream from, String encoding) throws IOException 338 { 339 if (from == null) { 340 return null; 341 } 342 try { 343 StringBuilder into = new StringBuilder(); 344 Reader r = new InputStreamReader(from, encoding); 345 char[] s = new char[512]; 346 for (int n; 0 < (n = r.read(s));) { 347 into.append(s, 0, n); 348 } 349 return into.toString(); 350 } finally { 351 from.close(); 352 } 353 } 354 355 /** 356 * Parse the parameters from an OAuth Authorization or WWW-Authenticate 357 * header. The realm is included as a parameter. If the given header doesn't 358 * start with "OAuth ", return an empty list. 359 */ 360 public static List<OAuth.Parameter> decodeAuthorization(String authorization) { 361 List<OAuth.Parameter> into = new ArrayList<OAuth.Parameter>(); 362 if (authorization != null) { 363 Matcher m = AUTHORIZATION.matcher(authorization); 364 if (m.matches()) { 365 if (AUTH_SCHEME.equalsIgnoreCase(m.group(1))) { 366 for (String nvp : m.group(2).split("\\s*,\\s*")) { 367 m = NVP.matcher(nvp); 368 if (m.matches()) { 369 String name = OAuth.decodePercent(m.group(1)); 370 String value = OAuth.decodePercent(m.group(2)); 371 into.add(new OAuth.Parameter(name, value)); 372 } 373 } 374 } 375 } 376 } 377 return into; 378 } 379 380 public static final String AUTH_SCHEME = "OAuth"; 381 382 public static final String GET = "GET"; 383 public static final String POST = "POST"; 384 public static final String PUT = "PUT"; 385 public static final String DELETE = "DELETE"; 386 387 private static final Pattern AUTHORIZATION = Pattern.compile("\\s*(\\w*)\\s+(.*)"); 388 private static final Pattern NVP = Pattern.compile("(\\S*)\\s*\\=\\s*\"([^\"]*)\""); 389 390 private static final String toString(Object from) { 391 return (from == null) ? null : from.toString(); 392 } 393 394} 395