blob: d9da04c2c0ed0610a7c1cbbbb03f02aa0a42939b [file] [log] [blame]
/*
* Copyright 2007 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.signature;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import net.oauth.OAuthProblemException;
// BEGIN android-changed
// import org.apache.commons.codec.binary.Base64;
import android.util.Base64;
// END android-changed
/**
* A pair of algorithms for computing and verifying an OAuth digital signature.
*
* @author John Kristian
* @hide
*/
public abstract class OAuthSignatureMethod {
/** Add a signature to the message.
* @throws URISyntaxException
* @throws IOException */
public void sign(OAuthMessage message)
throws OAuthException, IOException, URISyntaxException {
message.addParameter(new OAuth.Parameter("oauth_signature",
getSignature(message)));
}
/**
* Check whether the message has a valid signature.
* @throws URISyntaxException
*
* @throws OAuthProblemException
* the signature is invalid
*/
public void validate(OAuthMessage message)
throws IOException, OAuthException, URISyntaxException {
message.requireParameters("oauth_signature");
String signature = message.getSignature();
String baseString = getBaseString(message);
if (!isValid(signature, baseString)) {
OAuthProblemException problem = new OAuthProblemException(
"signature_invalid");
problem.setParameter("oauth_signature", signature);
problem.setParameter("oauth_signature_base_string", baseString);
problem.setParameter("oauth_signature_method", message
.getSignatureMethod());
throw problem;
}
}
protected String getSignature(OAuthMessage message)
throws OAuthException, IOException, URISyntaxException {
String baseString = getBaseString(message);
String signature = getSignature(baseString);
// Logger log = Logger.getLogger(getClass().getName());
// if (log.isLoggable(Level.FINE)) {
// log.fine(signature + "=getSignature(" + baseString + ")");
// }
return signature;
}
protected void initialize(String name, OAuthAccessor accessor)
throws OAuthException {
String secret = accessor.consumer.consumerSecret;
if (name.endsWith(_ACCESSOR)) {
// This code supports the 'Accessor Secret' extensions
// described in http://oauth.pbwiki.com/AccessorSecret
final String key = OAuthConsumer.ACCESSOR_SECRET;
Object accessorSecret = accessor.getProperty(key);
if (accessorSecret == null) {
accessorSecret = accessor.consumer.getProperty(key);
}
if (accessorSecret != null) {
secret = accessorSecret.toString();
}
}
if (secret == null) {
secret = "";
}
setConsumerSecret(secret);
}
public static final String _ACCESSOR = "-Accessor";
/** Compute the signature for the given base string. */
protected abstract String getSignature(String baseString) throws OAuthException;
/** Decide whether the signature is valid. */
protected abstract boolean isValid(String signature, String baseString)
throws OAuthException;
private String consumerSecret;
private String tokenSecret;
protected String getConsumerSecret() {
return consumerSecret;
}
protected void setConsumerSecret(String consumerSecret) {
this.consumerSecret = consumerSecret;
}
public String getTokenSecret() {
return tokenSecret;
}
public void setTokenSecret(String tokenSecret) {
this.tokenSecret = tokenSecret;
}
public static String getBaseString(OAuthMessage message)
throws IOException, URISyntaxException {
List<Map.Entry<String, String>> parameters;
String url = message.URL;
int q = url.indexOf('?');
if (q < 0) {
parameters = message.getParameters();
} else {
// Combine the URL query string with the other parameters:
parameters = new ArrayList<Map.Entry<String, String>>();
parameters.addAll(OAuth.decodeForm(message.URL.substring(q + 1)));
parameters.addAll(message.getParameters());
url = url.substring(0, q);
}
return OAuth.percentEncode(message.method.toUpperCase()) + '&'
+ OAuth.percentEncode(normalizeUrl(url)) + '&'
+ OAuth.percentEncode(normalizeParameters(parameters));
}
protected static String normalizeUrl(String url) throws URISyntaxException {
URI uri = new URI(url);
String scheme = uri.getScheme().toLowerCase();
String authority = uri.getAuthority().toLowerCase();
boolean dropPort = (scheme.equals("http") && uri.getPort() == 80)
|| (scheme.equals("https") && uri.getPort() == 443);
if (dropPort) {
// find the last : in the authority
int index = authority.lastIndexOf(":");
if (index >= 0) {
authority = authority.substring(0, index);
}
}
String path = uri.getRawPath();
if (path == null || path.length() <= 0) {
path = "/"; // conforms to RFC 2616 section 3.2.2
}
// we know that there is no query and no fragment here.
return scheme + "://" + authority + path;
}
protected static String normalizeParameters(
Collection<? extends Map.Entry> parameters) throws IOException {
if (parameters == null) {
return "";
}
List<ComparableParameter> p = new ArrayList<ComparableParameter>(
parameters.size());
for (Map.Entry parameter : parameters) {
if (!"oauth_signature".equals(parameter.getKey())) {
p.add(new ComparableParameter(parameter));
}
}
Collections.sort(p);
return OAuth.formEncode(getParameters(p));
}
// BEGIN android-changed
public static byte[] decodeBase64(String s) {
return Base64.decode(s, Base64.DEFAULT);
}
public static String base64Encode(byte[] b) {
return Base64.encodeToString(b, Base64.DEFAULT);
}
// END android-changed
public static OAuthSignatureMethod newSigner(OAuthMessage message,
OAuthAccessor accessor) throws IOException, OAuthException {
message.requireParameters(OAuth.OAUTH_SIGNATURE_METHOD);
OAuthSignatureMethod signer = newMethod(message.getSignatureMethod(),
accessor);
signer.setTokenSecret(accessor.tokenSecret);
return signer;
}
/** The factory for signature methods. */
public static OAuthSignatureMethod newMethod(String name,
OAuthAccessor accessor) throws OAuthException {
try {
Class methodClass = NAME_TO_CLASS.get(name);
if (methodClass != null) {
OAuthSignatureMethod method = (OAuthSignatureMethod) methodClass
.newInstance();
method.initialize(name, accessor);
return method;
}
OAuthProblemException problem = new OAuthProblemException(
"signature_method_rejected");
String acceptable = OAuth.percentEncode(NAME_TO_CLASS.keySet());
if (acceptable.length() > 0) {
problem.setParameter("oauth_acceptable_signature_methods",
acceptable.toString());
}
throw problem;
} catch (InstantiationException e) {
throw new OAuthException(e);
} catch (IllegalAccessException e) {
throw new OAuthException(e);
}
}
/**
* Subsequently, newMethod(name) will attempt to instantiate the given
* class, with no constructor parameters.
*/
public static void registerMethodClass(String name, Class clazz) {
NAME_TO_CLASS.put(name, clazz);
}
private static final Map<String, Class> NAME_TO_CLASS = new ConcurrentHashMap<String, Class>();
static {
registerMethodClass("HMAC-SHA1", HMAC_SHA1.class);
registerMethodClass("PLAINTEXT", PLAINTEXT.class);
registerMethodClass("RSA-SHA1", RSA_SHA1.class);
registerMethodClass("HMAC-SHA1" + _ACCESSOR, HMAC_SHA1.class);
registerMethodClass("PLAINTEXT" + _ACCESSOR, PLAINTEXT.class);
}
/** An efficiently sortable wrapper around a parameter. */
private static class ComparableParameter implements
Comparable<ComparableParameter> {
ComparableParameter(Map.Entry value) {
this.value = value;
String n = toString(value.getKey());
String v = toString(value.getValue());
this.key = OAuth.percentEncode(n) + ' ' + OAuth.percentEncode(v);
// ' ' is used because it comes before any character
// that can appear in a percentEncoded string.
}
final Map.Entry value;
private final String key;
private static String toString(Object from) {
return (from == null) ? null : from.toString();
}
public int compareTo(ComparableParameter that) {
return this.key.compareTo(that.key);
}
@Override
public String toString() {
return key;
}
}
/** Retrieve the original parameters from a sorted collection. */
private static List<Map.Entry> getParameters(
Collection<ComparableParameter> parameters) {
if (parameters == null) {
return null;
}
List<Map.Entry> list = new ArrayList<Map.Entry>(parameters.size());
for (ComparableParameter parameter : parameters) {
list.add(parameter.value);
}
return list;
}
}