1 /* 2 * Copyright 2007 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; 18 19 import java.io.ByteArrayOutputStream; 20 import java.io.IOException; 21 import java.io.OutputStream; 22 import java.io.UnsupportedEncodingException; 23 import java.net.URLDecoder; 24 import java.net.URLEncoder; 25 import java.util.ArrayList; 26 import java.util.HashMap; 27 import java.util.List; 28 import java.util.Map; 29 30 /** 31 * @author John Kristian 32 * @hide 33 */ 34 public class OAuth { 35 36 public static final String VERSION_1_0 = "1.0"; 37 38 /** The encoding used to represent characters as bytes. */ 39 public static final String ENCODING = "UTF-8"; 40 41 /** The MIME type for a sequence of OAuth parameters. */ 42 public static final String FORM_ENCODED = "application/x-www-form-urlencoded"; 43 44 public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key"; 45 public static final String OAUTH_TOKEN = "oauth_token"; 46 public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret"; 47 public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; 48 public static final String OAUTH_SIGNATURE = "oauth_signature"; 49 public static final String OAUTH_TIMESTAMP = "oauth_timestamp"; 50 public static final String OAUTH_NONCE = "oauth_nonce"; 51 public static final String OAUTH_VERSION = "oauth_version"; 52 53 public static final String HMAC_SHA1 = "HMAC-SHA1"; 54 public static final String RSA_SHA1 = "RSA-SHA1"; 55 56 public static class Problems { 57 public static final String TOKEN_NOT_AUTHORIZED = "token_not_authorized"; 58 public static final String INVALID_USED_NONCE = "invalid_used_nonce"; 59 public static final String SIGNATURE_INVALID = "signature_invalid"; 60 public static final String INVALID_EXPIRED_TOKEN = "invalid_expired_token"; 61 public static final String INVALID_CONSUMER_KEY = "invalid_consumer_key"; 62 public static final String CONSUMER_KEY_REFUSED = "consumer_key_refused"; 63 public static final String TIMESTAMP_REFUSED = "timestamp_refused"; 64 public static final String PARAMETER_REJECTED = "parameter_rejected"; 65 public static final String PARAMETER_ABSENT = "parameter_absent"; 66 public static final String VERSION_REJECTED = "version_rejected"; 67 public static final String SIGNATURE_METHOD_REJECTED = "signature_method_rejected"; 68 69 public static final String OAUTH_PARAMETERS_ABSENT = "oauth_parameters_absent"; 70 public static final String OAUTH_PARAMETERS_REJECTED = "oauth_parameters_rejected"; 71 public static final String OAUTH_ACCEPTABLE_TIMESTAMPS = "oauth_acceptable_timestamps"; 72 public static final String OAUTH_ACCEPTABLE_VERSIONS = "oauth_acceptable_versions"; 73 } 74 75 /** Return true if the given Content-Type header means FORM_ENCODED. */ 76 public static boolean isFormEncoded(String contentType) { 77 if (contentType == null) { 78 return false; 79 } 80 int semi = contentType.indexOf(";"); 81 if (semi >= 0) { 82 contentType = contentType.substring(0, semi); 83 } 84 return FORM_ENCODED.equalsIgnoreCase(contentType.trim()); 85 } 86 87 /** 88 * Construct a form-urlencoded document containing the given sequence of 89 * name/value pairs. Use OAuth percent encoding (not exactly the encoding 90 * mandated by HTTP). 91 */ 92 public static String formEncode(Iterable<? extends Map.Entry> parameters) 93 throws IOException { 94 ByteArrayOutputStream b = new ByteArrayOutputStream(); 95 formEncode(parameters, b); 96 return new String(b.toByteArray()); 97 } 98 99 /** 100 * Write a form-urlencoded document into the given stream, containing the 101 * given sequence of name/value pairs. 102 */ 103 public static void formEncode(Iterable<? extends Map.Entry> parameters, 104 OutputStream into) throws IOException { 105 if (parameters != null) { 106 boolean first = true; 107 for (Map.Entry parameter : parameters) { 108 if (first) { 109 first = false; 110 } else { 111 into.write('&'); 112 } 113 into.write(percentEncode(toString(parameter.getKey())) 114 .getBytes()); 115 into.write('='); 116 into.write(percentEncode(toString(parameter.getValue())) 117 .getBytes()); 118 } 119 } 120 } 121 122 /** Parse a form-urlencoded document. */ 123 public static List<Parameter> decodeForm(String form) { 124 List<Parameter> list = new ArrayList<Parameter>(); 125 if (!isEmpty(form)) { 126 for (String nvp : form.split("\\&")) { 127 int equals = nvp.indexOf('='); 128 String name; 129 String value; 130 if (equals < 0) { 131 name = decodePercent(nvp); 132 value = null; 133 } else { 134 name = decodePercent(nvp.substring(0, equals)); 135 value = decodePercent(nvp.substring(equals + 1)); 136 } 137 list.add(new Parameter(name, value)); 138 } 139 } 140 return list; 141 } 142 143 /** Construct a &-separated list of the given values, percentEncoded. */ 144 public static String percentEncode(Iterable values) { 145 StringBuilder p = new StringBuilder(); 146 for (Object v : values) { 147 if (p.length() > 0) { 148 p.append("&"); 149 } 150 p.append(OAuth.percentEncode(toString(v))); 151 } 152 return p.toString(); 153 } 154 155 public static String percentEncode(String s) { 156 if (s == null) { 157 return ""; 158 } 159 try { 160 return URLEncoder.encode(s, ENCODING) 161 // OAuth encodes some characters differently: 162 .replace("+", "%20").replace("*", "%2A") 163 .replace("%7E", "~"); 164 // This could be done faster with more hand-crafted code. 165 } catch (UnsupportedEncodingException wow) { 166 throw new RuntimeException(wow.getMessage(), wow); 167 } 168 } 169 170 public static String decodePercent(String s) { 171 try { 172 return URLDecoder.decode(s, ENCODING); 173 // This implements http://oauth.pbwiki.com/FlexibleDecoding 174 } catch (java.io.UnsupportedEncodingException wow) { 175 throw new RuntimeException(wow.getMessage(), wow); 176 } 177 } 178 179 /** 180 * Construct a Map containing a copy of the given parameters. If several 181 * parameters have the same name, the Map will contain the first value, 182 * only. 183 */ 184 public static Map<String, String> newMap(Iterable<? extends Map.Entry> from) { 185 Map<String, String> map = new HashMap<String, String>(); 186 if (from != null) { 187 for (Map.Entry f : from) { 188 String key = toString(f.getKey()); 189 if (!map.containsKey(key)) { 190 map.put(key, toString(f.getValue())); 191 } 192 } 193 } 194 return map; 195 } 196 197 /** Construct a list of Parameters from name, value, name, value... */ 198 public static List<Parameter> newList(String... parameters) { 199 List<Parameter> list = new ArrayList<Parameter>(parameters.length / 2); 200 for (int p = 0; p + 1 < parameters.length; p += 2) { 201 list.add(new Parameter(parameters[p], parameters[p + 1])); 202 } 203 return list; 204 } 205 206 /** A name/value pair. */ 207 public static class Parameter implements Map.Entry<String, String> { 208 209 public Parameter(String key, String value) { 210 this.key = key; 211 this.value = value; 212 } 213 214 private final String key; 215 216 private String value; 217 218 public String getKey() { 219 return key; 220 } 221 222 public String getValue() { 223 return value; 224 } 225 226 public String setValue(String value) { 227 try { 228 return this.value; 229 } finally { 230 this.value = value; 231 } 232 } 233 234 @Override 235 public String toString() { 236 return percentEncode(getKey()) + '=' + percentEncode(getValue()); 237 } 238 239 @Override 240 public int hashCode() 241 { 242 final int prime = 31; 243 int result = 1; 244 result = prime * result + ((key == null) ? 0 : key.hashCode()); 245 result = prime * result + ((value == null) ? 0 : value.hashCode()); 246 return result; 247 } 248 249 @Override 250 public boolean equals(Object obj) 251 { 252 if (this == obj) 253 return true; 254 if (obj == null) 255 return false; 256 if (getClass() != obj.getClass()) 257 return false; 258 final Parameter that = (Parameter) obj; 259 if (key == null) { 260 if (that.key != null) 261 return false; 262 } else if (!key.equals(that.key)) 263 return false; 264 if (value == null) { 265 if (that.value != null) 266 return false; 267 } else if (!value.equals(that.value)) 268 return false; 269 return true; 270 } 271 } 272 273 private static final String toString(Object from) { 274 return (from == null) ? null : from.toString(); 275 } 276 277 /** 278 * Construct a URL like the given one, but with the given parameters added 279 * to its query string. 280 */ 281 public static String addParameters(String url, String... parameters) 282 throws IOException { 283 return addParameters(url, newList(parameters)); 284 } 285 286 public static String addParameters(String url, 287 Iterable<? extends Map.Entry<String, String>> parameters) 288 throws IOException { 289 String form = formEncode(parameters); 290 if (form == null || form.length() <= 0) { 291 return url; 292 } else { 293 return url + ((url.indexOf("?") < 0) ? '?' : '&') + form; 294 } 295 } 296 297 public static boolean isEmpty(String str) { 298 return (str == null) || (str.length() == 0); 299 } 300 } 301