diff options
author | Nico Sallembien <nsallembien@google.com> | 2010-01-26 16:54:17 -0800 |
---|---|---|
committer | Nico Sallembien <nsallembien@google.com> | 2010-01-26 16:54:17 -0800 |
commit | b852fcf48a8909164d7f323dd02a35d2a8056a61 (patch) | |
tree | f48cf3fca303a8d814b61b26470cccc5c9d0e5ed /core | |
parent | ab1c87260e23a28960b7ebc8319d40722a06711a (diff) | |
download | oauth-b852fcf48a8909164d7f323dd02a35d2a8056a61.tar.gz |
Checking oauth.net external library
Diffstat (limited to 'core')
27 files changed, 3456 insertions, 0 deletions
diff --git a/core/src/main/java/net/oauth/ConsumerProperties.java b/core/src/main/java/net/oauth/ConsumerProperties.java new file mode 100755 index 0000000..42d22f9 --- /dev/null +++ b/core/src/main/java/net/oauth/ConsumerProperties.java @@ -0,0 +1,131 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * A pool of OAuthConsumers that are constructed from Properties. Each consumer + * has a name, which is a property of the OAuthConsumer. Other properties come + * from Properties whose names are prefixed with the consumer's name. For + * example, a consumer's credentials come from properties named + * [name].consumerKey and [name].consumerSecret. + * + * @author John Kristian + * @hide + */ +public class ConsumerProperties { + + public static URL getResource(String name, ClassLoader loader) + throws IOException { + URL resource = loader.getResource(name); + if (resource == null) { + throw new IOException("resource not found: " + name); + } + return resource; + } + + public static Properties getProperties(URL source) throws IOException { + InputStream input = source.openStream(); + try { + Properties p = new Properties(); + p.load(input); + return p; + } finally { + input.close(); + } + } + + public ConsumerProperties(String resourceName, ClassLoader loader) + throws IOException { + this(getProperties(getResource(resourceName, loader))); + } + + public ConsumerProperties(Properties consumerProperties) { + this.consumerProperties = consumerProperties; + } + + private final Properties consumerProperties; + + private final Map<String, OAuthConsumer> pool = new HashMap<String, OAuthConsumer>(); + + /** Get the consumer with the given name. */ + public OAuthConsumer getConsumer(String name) throws MalformedURLException { + OAuthConsumer consumer; + synchronized (pool) { + consumer = pool.get(name); + } + if (consumer == null) { + consumer = newConsumer(name); + } + synchronized (pool) { + OAuthConsumer first = pool.get(name); + if (first == null) { + pool.put(name, consumer); + } else { + /* + * Another thread just constructed an identical OAuthConsumer. + * Use that one (and discard the one we just constructed). + */ + consumer = first; + } + } + return consumer; + } + + protected OAuthConsumer newConsumer(String name) + throws MalformedURLException { + String base = consumerProperties.getProperty(name + + ".serviceProvider.baseURL"); + URL baseURL = (base == null) ? null : new URL(base); + OAuthServiceProvider serviceProvider = new OAuthServiceProvider(getURL( + baseURL, name + ".serviceProvider.requestTokenURL"), getURL( + baseURL, name + ".serviceProvider.userAuthorizationURL"), + getURL(baseURL, name + ".serviceProvider.accessTokenURL")); + OAuthConsumer consumer = new OAuthConsumer(consumerProperties + .getProperty(name + ".callbackURL"), consumerProperties + .getProperty(name + ".consumerKey"), consumerProperties + .getProperty(name + ".consumerSecret"), serviceProvider); + consumer.setProperty("name", name); + if (baseURL != null) { + consumer.setProperty("serviceProvider.baseURL", baseURL); + } + for (Map.Entry prop : consumerProperties.entrySet()) { + String propName = (String) prop.getKey(); + if (propName.startsWith(name + ".consumer.")) { + String c = propName.substring(name.length() + 10); + consumer.setProperty(c, prop.getValue()); + } + } + return consumer; + } + + private String getURL(URL base, String name) throws MalformedURLException { + String url = consumerProperties.getProperty(name); + if (base != null) { + url = (new URL(base, url)).toExternalForm(); + } + return url; + } + +} diff --git a/core/src/main/java/net/oauth/OAuth.java b/core/src/main/java/net/oauth/OAuth.java new file mode 100755 index 0000000..aed4c5e --- /dev/null +++ b/core/src/main/java/net/oauth/OAuth.java @@ -0,0 +1,300 @@ +/* + * 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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author John Kristian + * @hide + */ +public class OAuth { + + public static final String VERSION_1_0 = "1.0"; + + /** The encoding used to represent characters as bytes. */ + public static final String ENCODING = "UTF-8"; + + /** The MIME type for a sequence of OAuth parameters. */ + public static final String FORM_ENCODED = "application/x-www-form-urlencoded"; + + public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key"; + public static final String OAUTH_TOKEN = "oauth_token"; + public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret"; + public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method"; + public static final String OAUTH_SIGNATURE = "oauth_signature"; + public static final String OAUTH_TIMESTAMP = "oauth_timestamp"; + public static final String OAUTH_NONCE = "oauth_nonce"; + public static final String OAUTH_VERSION = "oauth_version"; + + public static final String HMAC_SHA1 = "HMAC-SHA1"; + public static final String RSA_SHA1 = "RSA-SHA1"; + + public static class Problems { + public static final String TOKEN_NOT_AUTHORIZED = "token_not_authorized"; + public static final String INVALID_USED_NONCE = "invalid_used_nonce"; + public static final String SIGNATURE_INVALID = "signature_invalid"; + public static final String INVALID_EXPIRED_TOKEN = "invalid_expired_token"; + public static final String INVALID_CONSUMER_KEY = "invalid_consumer_key"; + public static final String CONSUMER_KEY_REFUSED = "consumer_key_refused"; + public static final String TIMESTAMP_REFUSED = "timestamp_refused"; + public static final String PARAMETER_REJECTED = "parameter_rejected"; + public static final String PARAMETER_ABSENT = "parameter_absent"; + public static final String VERSION_REJECTED = "version_rejected"; + public static final String SIGNATURE_METHOD_REJECTED = "signature_method_rejected"; + + public static final String OAUTH_PARAMETERS_ABSENT = "oauth_parameters_absent"; + public static final String OAUTH_PARAMETERS_REJECTED = "oauth_parameters_rejected"; + public static final String OAUTH_ACCEPTABLE_TIMESTAMPS = "oauth_acceptable_timestamps"; + public static final String OAUTH_ACCEPTABLE_VERSIONS = "oauth_acceptable_versions"; + } + + /** Return true if the given Content-Type header means FORM_ENCODED. */ + public static boolean isFormEncoded(String contentType) { + if (contentType == null) { + return false; + } + int semi = contentType.indexOf(";"); + if (semi >= 0) { + contentType = contentType.substring(0, semi); + } + return FORM_ENCODED.equalsIgnoreCase(contentType.trim()); + } + + /** + * Construct a form-urlencoded document containing the given sequence of + * name/value pairs. Use OAuth percent encoding (not exactly the encoding + * mandated by HTTP). + */ + public static String formEncode(Iterable<? extends Map.Entry> parameters) + throws IOException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + formEncode(parameters, b); + return new String(b.toByteArray()); + } + + /** + * Write a form-urlencoded document into the given stream, containing the + * given sequence of name/value pairs. + */ + public static void formEncode(Iterable<? extends Map.Entry> parameters, + OutputStream into) throws IOException { + if (parameters != null) { + boolean first = true; + for (Map.Entry parameter : parameters) { + if (first) { + first = false; + } else { + into.write('&'); + } + into.write(percentEncode(toString(parameter.getKey())) + .getBytes()); + into.write('='); + into.write(percentEncode(toString(parameter.getValue())) + .getBytes()); + } + } + } + + /** Parse a form-urlencoded document. */ + public static List<Parameter> decodeForm(String form) { + List<Parameter> list = new ArrayList<Parameter>(); + if (!isEmpty(form)) { + for (String nvp : form.split("\\&")) { + int equals = nvp.indexOf('='); + String name; + String value; + if (equals < 0) { + name = decodePercent(nvp); + value = null; + } else { + name = decodePercent(nvp.substring(0, equals)); + value = decodePercent(nvp.substring(equals + 1)); + } + list.add(new Parameter(name, value)); + } + } + return list; + } + + /** Construct a &-separated list of the given values, percentEncoded. */ + public static String percentEncode(Iterable values) { + StringBuilder p = new StringBuilder(); + for (Object v : values) { + if (p.length() > 0) { + p.append("&"); + } + p.append(OAuth.percentEncode(toString(v))); + } + return p.toString(); + } + + public static String percentEncode(String s) { + if (s == null) { + return ""; + } + try { + return URLEncoder.encode(s, ENCODING) + // OAuth encodes some characters differently: + .replace("+", "%20").replace("*", "%2A") + .replace("%7E", "~"); + // This could be done faster with more hand-crafted code. + } catch (UnsupportedEncodingException wow) { + throw new RuntimeException(wow.getMessage(), wow); + } + } + + public static String decodePercent(String s) { + try { + return URLDecoder.decode(s, ENCODING); + // This implements http://oauth.pbwiki.com/FlexibleDecoding + } catch (java.io.UnsupportedEncodingException wow) { + throw new RuntimeException(wow.getMessage(), wow); + } + } + + /** + * Construct a Map containing a copy of the given parameters. If several + * parameters have the same name, the Map will contain the first value, + * only. + */ + public static Map<String, String> newMap(Iterable<? extends Map.Entry> from) { + Map<String, String> map = new HashMap<String, String>(); + if (from != null) { + for (Map.Entry f : from) { + String key = toString(f.getKey()); + if (!map.containsKey(key)) { + map.put(key, toString(f.getValue())); + } + } + } + return map; + } + + /** Construct a list of Parameters from name, value, name, value... */ + public static List<Parameter> newList(String... parameters) { + List<Parameter> list = new ArrayList<Parameter>(parameters.length / 2); + for (int p = 0; p + 1 < parameters.length; p += 2) { + list.add(new Parameter(parameters[p], parameters[p + 1])); + } + return list; + } + + /** A name/value pair. */ + public static class Parameter implements Map.Entry<String, String> { + + public Parameter(String key, String value) { + this.key = key; + this.value = value; + } + + private final String key; + + private String value; + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + + public String setValue(String value) { + try { + return this.value; + } finally { + this.value = value; + } + } + + @Override + public String toString() { + return percentEncode(getKey()) + '=' + percentEncode(getValue()); + } + + @Override + public int hashCode() + { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final Parameter that = (Parameter) obj; + if (key == null) { + if (that.key != null) + return false; + } else if (!key.equals(that.key)) + return false; + if (value == null) { + if (that.value != null) + return false; + } else if (!value.equals(that.value)) + return false; + return true; + } + } + + private static final String toString(Object from) { + return (from == null) ? null : from.toString(); + } + + /** + * Construct a URL like the given one, but with the given parameters added + * to its query string. + */ + public static String addParameters(String url, String... parameters) + throws IOException { + return addParameters(url, newList(parameters)); + } + + public static String addParameters(String url, + Iterable<? extends Map.Entry<String, String>> parameters) + throws IOException { + String form = formEncode(parameters); + if (form == null || form.length() <= 0) { + return url; + } else { + return url + ((url.indexOf("?") < 0) ? '?' : '&') + form; + } + } + + public static boolean isEmpty(String str) { + return (str == null) || (str.length() == 0); + } +} diff --git a/core/src/main/java/net/oauth/OAuthAccessor.java b/core/src/main/java/net/oauth/OAuthAccessor.java new file mode 100755 index 0000000..043d6d6 --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthAccessor.java @@ -0,0 +1,92 @@ +/* + * 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; + +import java.io.IOException; +import java.io.Serializable; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import net.oauth.http.HttpMessage; + +/** + * Properties of one User of an OAuthConsumer. Properties may be added freely, + * e.g. to support extensions. + * + * @author John Kristian + * @hide + */ +public class OAuthAccessor implements Serializable { + + private static final long serialVersionUID = 5590788443138352999L; + + public final OAuthConsumer consumer; + public String requestToken; + public String accessToken; + public String tokenSecret; + + public OAuthAccessor(OAuthConsumer consumer) { + this.consumer = consumer; + this.requestToken = null; + this.accessToken = null; + this.tokenSecret = null; + } + + private final Map<String, Object> properties = new HashMap<String, Object>(); + + public Object getProperty(String name) { + return properties.get(name); + } + + public void setProperty(String name, Object value) { + properties.put(name, value); + } + + /** + * Construct a request message containing the given parameters but no body. + * Don't send the message, merely construct it. The caller will ordinarily + * send it, for example by calling OAuthClient.invoke. + * + * @param method + * the HTTP request method. If this is null, use the default + * method; that is getProperty("httpMethod") or (if that's null) + * consumer.getProperty("httpMethod") or (if that's null) + * OAuthMessage.GET. + */ + public OAuthMessage newRequestMessage(String method, String url, + Collection<? extends Map.Entry> parameters) + throws OAuthException, IOException, URISyntaxException { + if (method == null) { + method = (String) this.getProperty("httpMethod"); + if (method == null) { + method = (String) this.consumer.getProperty("httpMethod"); + if (method == null) { + method = OAuthMessage.GET; + } + } + } + OAuthMessage message = new OAuthMessage(method, url, parameters); + message.addRequiredParameters(this); + Object accepted = consumer.getProperty(OAuthConsumer.ACCEPT_ENCODING); + if (accepted != null) { + message.getHeaders().add(new OAuth.Parameter(HttpMessage.ACCEPT_ENCODING, accepted.toString())); + } + return message; + } + +} diff --git a/core/src/main/java/net/oauth/OAuthConsumer.java b/core/src/main/java/net/oauth/OAuthConsumer.java new file mode 100755 index 0000000..31cc9ff --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthConsumer.java @@ -0,0 +1,70 @@ +/* + * 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; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import net.oauth.http.HttpMessage; + +/** + * Properties of an OAuth Consumer. Properties may be added freely, e.g. to + * support extensions. + * + * @author John Kristian + * @hide + */ +public class OAuthConsumer implements Serializable { + + private static final long serialVersionUID = -2258581186977818580L; + + public final String callbackURL; + public final String consumerKey; + public final String consumerSecret; + public final OAuthServiceProvider serviceProvider; + + public OAuthConsumer(String callbackURL, String consumerKey, + String consumerSecret, OAuthServiceProvider serviceProvider) { + this.callbackURL = callbackURL; + this.consumerKey = consumerKey; + this.consumerSecret = consumerSecret; + this.serviceProvider = serviceProvider; + } + + private final Map<String, Object> properties = new HashMap<String, Object>(); + + public Object getProperty(String name) { + return properties.get(name); + } + + public void setProperty(String name, Object value) { + properties.put(name, value); + } + + /** + * The name of the property whose value is the Accept-Encoding header in + * HTTP requests. + */ + public static final String ACCEPT_ENCODING = "HTTP.header." + HttpMessage.ACCEPT_ENCODING; + + /** + * The name of the property whose value is the <a + * href="http://oauth.pbwiki.com/AccessorSecret">Accessor Secret</a>. + */ + public static final String ACCESSOR_SECRET = "oauth_accessor_secret"; + +} diff --git a/core/src/main/java/net/oauth/OAuthException.java b/core/src/main/java/net/oauth/OAuthException.java new file mode 100755 index 0000000..94f6451 --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthException.java @@ -0,0 +1,54 @@ +/* + * Copyright 2008 Google, 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; + +/** + * Superclass for extensions thrown by the OAuth library. + * @hide + */ +public class OAuthException extends Exception { + + /** + * For subclasses only. + */ + protected OAuthException() { + } + + /** + * @param message + */ + public OAuthException(String message) { + super(message); + } + + /** + * @param cause + */ + public OAuthException(Throwable cause) { + super(cause); + } + + /** + * @param message + * @param cause + */ + public OAuthException(String message, Throwable cause) { + super(message, cause); + } + + private static final long serialVersionUID = 1L; + +} diff --git a/core/src/main/java/net/oauth/OAuthMessage.java b/core/src/main/java/net/oauth/OAuthMessage.java new file mode 100755 index 0000000..e16ca9b --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthMessage.java @@ -0,0 +1,394 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.oauth.http.HttpMessage; +import net.oauth.signature.OAuthSignatureMethod; + +/** + * A request or response message used in the OAuth protocol. + * <p> + * The parameters in this class are not percent-encoded. Methods like + * OAuthClient.invoke and OAuthResponseMessage.completeParameters are + * responsible for percent-encoding parameters before transmission and decoding + * them after reception. + * + * @author John Kristian + * @hide + */ +public class OAuthMessage { + + public OAuthMessage(String method, String URL, + Collection<? extends Map.Entry> parameters) { + this.method = method; + this.URL = URL; + if (parameters == null) { + this.parameters = new ArrayList<Map.Entry<String, String>>(); + } else { + this.parameters = new ArrayList<Map.Entry<String, String>>(parameters.size()); + for (Map.Entry p : parameters) { + this.parameters.add(new OAuth.Parameter( + toString(p.getKey()), toString(p.getValue()))); + } + } + } + + public String method; + public String URL; + + private final List<Map.Entry<String, String>> parameters; + private Map<String, String> parameterMap; + private boolean parametersAreComplete = false; + private final List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(); + + public String toString() { + return "OAuthMessage(" + method + ", " + URL + ", " + parameters + ")"; + } + + /** A caller is about to get a parameter. */ + private void beforeGetParameter() throws IOException { + if (!parametersAreComplete) { + completeParameters(); + parametersAreComplete = true; + } + } + + /** + * Finish adding parameters; for example read an HTTP response body and + * parse parameters from it. + */ + protected void completeParameters() throws IOException { + } + + public List<Map.Entry<String, String>> getParameters() throws IOException { + beforeGetParameter(); + return Collections.unmodifiableList(parameters); + } + + public void addParameter(String key, String value) { + addParameter(new OAuth.Parameter(key, value)); + } + + public void addParameter(Map.Entry<String, String> parameter) { + parameters.add(parameter); + parameterMap = null; + } + + public void addParameters( + Collection<? extends Map.Entry<String, String>> parameters) { + this.parameters.addAll(parameters); + parameterMap = null; + } + + public String getParameter(String name) throws IOException { + return getParameterMap().get(name); + } + + public String getConsumerKey() throws IOException { + return getParameter(OAuth.OAUTH_CONSUMER_KEY); + } + + public String getToken() throws IOException { + return getParameter(OAuth.OAUTH_TOKEN); + } + + public String getSignatureMethod() throws IOException { + return getParameter(OAuth.OAUTH_SIGNATURE_METHOD); + } + + public String getSignature() throws IOException { + return getParameter(OAuth.OAUTH_SIGNATURE); + } + + protected Map<String, String> getParameterMap() throws IOException { + beforeGetParameter(); + if (parameterMap == null) { + parameterMap = OAuth.newMap(parameters); + } + return parameterMap; + } + + /** + * The MIME type of the body of this message. + * + * @return the MIME type, or null to indicate the type is unknown. + */ + public String getBodyType() { + return getHeader(HttpMessage.CONTENT_TYPE); + } + + /** + * The character encoding of the body of this message. + * + * @return the name of an encoding, or "ISO-8859-1" if no charset has been + * specified. + */ + public String getBodyEncoding() { + return HttpMessage.DEFAULT_CHARSET; + } + + /** + * The value of the last HTTP header with the given name. The name is case + * insensitive. + * + * @return the value of the last header, or null to indicate that there is + * no such header in this message. + */ + public final String getHeader(String name) { + String value = null; // no such header + for (Map.Entry<String, String> header : getHeaders()) { + if (name.equalsIgnoreCase(header.getKey())) { + value = header.getValue(); + } + } + return value; + } + + /** All HTTP headers. You can add headers to this list. */ + public final List<Map.Entry<String, String>> getHeaders() { + return headers; + } + + /** + * Read the body of the HTTP request or response and convert it to a String. + * This method isn't repeatable, since it consumes and closes getBodyAsStream. + * + * @return the body, or null to indicate there is no body. + */ + public final String readBodyAsString() throws IOException + { + InputStream body = getBodyAsStream(); + return readAll(body, getBodyEncoding()); + } + + /** + * Get a stream from which to read the body of the HTTP request or response. + * This is designed to support efficient streaming of a large message. + * The caller must close the returned stream, to release the underlying + * resources such as the TCP connection for an HTTP response. + * + * @return a stream from which to read the body, or null to indicate there + * is no body. + */ + public InputStream getBodyAsStream() throws IOException { + return null; + } + + /** Construct a verbose description of this message and its origins. */ + public Map<String, Object> getDump() throws IOException { + Map<String, Object> into = new HashMap<String, Object>(); + dump(into); + return into; + } + + protected void dump(Map<String, Object> into) throws IOException { + into.put("URL", URL); + if (parametersAreComplete) { + try { + into.putAll(getParameterMap()); + } catch (Exception ignored) { + } + } + } + + /** + * Verify that the required parameter names are contained in the actual + * collection. + * + * @throws OAuthProblemException + * one or more parameters are absent. + * @throws IOException + */ + public void requireParameters(String... names) + throws OAuthProblemException, IOException { + Set<String> present = getParameterMap().keySet(); + List<String> absent = new ArrayList<String>(); + for (String required : names) { + if (!present.contains(required)) { + absent.add(required); + } + } + if (!absent.isEmpty()) { + OAuthProblemException problem = new OAuthProblemException(OAuth.Problems.PARAMETER_ABSENT); + problem.setParameter(OAuth.Problems.OAUTH_PARAMETERS_ABSENT, OAuth.percentEncode(absent)); + throw problem; + } + } + + /** + * Add some of the parameters needed to request access to a protected + * resource, if they aren't already in the message. + * + * @throws IOException + * @throws URISyntaxException + */ + public void addRequiredParameters(OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException { + final Map<String, String> pMap = OAuth.newMap(parameters); + if (pMap.get(OAuth.OAUTH_TOKEN) == null && accessor.accessToken != null) { + addParameter(OAuth.OAUTH_TOKEN, accessor.accessToken); + } + final OAuthConsumer consumer = accessor.consumer; + if (pMap.get(OAuth.OAUTH_CONSUMER_KEY) == null) { + addParameter(OAuth.OAUTH_CONSUMER_KEY, consumer.consumerKey); + } + String signatureMethod = pMap.get(OAuth.OAUTH_SIGNATURE_METHOD); + if (signatureMethod == null) { + signatureMethod = (String) consumer.getProperty(OAuth.OAUTH_SIGNATURE_METHOD); + if (signatureMethod == null) { + signatureMethod = OAuth.HMAC_SHA1; + } + addParameter(OAuth.OAUTH_SIGNATURE_METHOD, signatureMethod); + } + if (pMap.get(OAuth.OAUTH_TIMESTAMP) == null) { + addParameter(OAuth.OAUTH_TIMESTAMP, (System.currentTimeMillis() / 1000) + ""); + } + if (pMap.get(OAuth.OAUTH_NONCE) == null) { + addParameter(OAuth.OAUTH_NONCE, System.nanoTime() + ""); + } + if (pMap.get(OAuth.OAUTH_VERSION) == null) { + addParameter(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0); + } + this.sign(accessor); + } + + /** + * Add a signature to the message. + * + * @throws URISyntaxException + */ + public void sign(OAuthAccessor accessor) throws IOException, + OAuthException, URISyntaxException { + OAuthSignatureMethod.newSigner(this, accessor).sign(this); + } + + /** + * Check that the message is valid. + * + * @throws IOException + * @throws URISyntaxException + * + * @throws OAuthProblemException + * the message is invalid + */ + public void validateMessage(OAuthAccessor accessor, OAuthValidator validator) + throws OAuthException, IOException, URISyntaxException { + validator.validateMessage(this, accessor); + } + + /** + * Construct a WWW-Authenticate or Authentication header value, containing + * the given realm plus all the parameters whose names begin with "oauth_". + */ + public String getAuthorizationHeader(String realm) throws IOException { + StringBuilder into = new StringBuilder(); + if (realm != null) { + into.append(" realm=\"").append(OAuth.percentEncode(realm)).append('"'); + } + beforeGetParameter(); + if (parameters != null) { + for (Map.Entry parameter : parameters) { + String name = toString(parameter.getKey()); + if (name.startsWith("oauth_")) { + if (into.length() > 0) into.append(","); + into.append(" "); + into.append(OAuth.percentEncode(name)).append("=\""); + into.append(OAuth.percentEncode(toString(parameter.getValue()))).append('"'); + } + } + } + return AUTH_SCHEME + into.toString(); + } + + /** + * Read all the data from the given stream, and close it. + * + * @return null if from is null, or the data from the stream converted to a + * String + */ + public static String readAll(InputStream from, String encoding) throws IOException + { + if (from == null) { + return null; + } + try { + StringBuilder into = new StringBuilder(); + Reader r = new InputStreamReader(from, encoding); + char[] s = new char[512]; + for (int n; 0 < (n = r.read(s));) { + into.append(s, 0, n); + } + return into.toString(); + } finally { + from.close(); + } + } + + /** + * Parse the parameters from an OAuth Authorization or WWW-Authenticate + * header. The realm is included as a parameter. If the given header doesn't + * start with "OAuth ", return an empty list. + */ + public static List<OAuth.Parameter> decodeAuthorization(String authorization) { + List<OAuth.Parameter> into = new ArrayList<OAuth.Parameter>(); + if (authorization != null) { + Matcher m = AUTHORIZATION.matcher(authorization); + if (m.matches()) { + if (AUTH_SCHEME.equalsIgnoreCase(m.group(1))) { + for (String nvp : m.group(2).split("\\s*,\\s*")) { + m = NVP.matcher(nvp); + if (m.matches()) { + String name = OAuth.decodePercent(m.group(1)); + String value = OAuth.decodePercent(m.group(2)); + into.add(new OAuth.Parameter(name, value)); + } + } + } + } + } + return into; + } + + public static final String AUTH_SCHEME = "OAuth"; + + public static final String GET = "GET"; + public static final String POST = "POST"; + public static final String PUT = "PUT"; + public static final String DELETE = "DELETE"; + + private static final Pattern AUTHORIZATION = Pattern.compile("\\s*(\\w*)\\s+(.*)"); + private static final Pattern NVP = Pattern.compile("(\\S*)\\s*\\=\\s*\"([^\"]*)\""); + + private static final String toString(Object from) { + return (from == null) ? null : from.toString(); + } + +} diff --git a/core/src/main/java/net/oauth/OAuthProblemException.java b/core/src/main/java/net/oauth/OAuthProblemException.java new file mode 100755 index 0000000..7ef0f41 --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthProblemException.java @@ -0,0 +1,106 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Map; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; + +/** + * Describes an OAuth-related problem, using a set of named parameters. One + * parameter identifies the basic problem, and the others provide supplementary + * diagnostic information. This can be used to capture information from a + * response that conforms to the OAuth <a + * href="http://wiki.oauth.net/ProblemReporting">Problem Reporting + * extension</a>. + * + * @author John Kristian + * @hide + */ +public class OAuthProblemException extends OAuthException { + + public static final String OAUTH_PROBLEM = "oauth_problem"; + + public OAuthProblemException() { + } + + public OAuthProblemException(String problem) { + super(problem); + if (problem != null) { + parameters.put(OAUTH_PROBLEM, problem); + } + } + + private final Map<String, Object> parameters = new HashMap<String, Object>(); + + @Override + public String getMessage() { + String msg = super.getMessage(); + if (msg != null) + return msg; + msg = getProblem(); + if (msg != null) + return msg; + Object response = getParameters().get(HttpMessage.RESPONSE); + if (response != null) { + msg = response.toString(); + int eol = msg.indexOf("\n"); + if (eol < 0) { + eol = msg.indexOf("\r"); + } + if (eol >= 0) { + msg = msg.substring(0, eol); + } + msg = msg.trim(); + if (msg.length() > 0) { + return msg; + } + } + response = getHttpStatusCode(); + if (response != null) { + return HttpResponseMessage.STATUS_CODE + " " + response; + } + return null; + } + + public void setParameter(String name, Object value) { + getParameters().put(name, value); + } + + public Map<String, Object> getParameters() { + return parameters; + } + + public String getProblem() { + return (String) getParameters().get(OAUTH_PROBLEM); + } + + public int getHttpStatusCode() { + Object code = getParameters().get(HttpResponseMessage.STATUS_CODE); + if (code == null) { + return 200; + } else if (code instanceof Number) { // the usual case + return ((Number) code).intValue(); + } else { + return Integer.parseInt(code.toString()); + } + } + + private static final long serialVersionUID = 1L; + +} diff --git a/core/src/main/java/net/oauth/OAuthServiceProvider.java b/core/src/main/java/net/oauth/OAuthServiceProvider.java new file mode 100755 index 0000000..2061449 --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthServiceProvider.java @@ -0,0 +1,42 @@ +/* + * 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; + +import java.io.Serializable; + +/** + * Properties of an OAuth Service Provider. + * + * @author John Kristian + * @hide + */ +public class OAuthServiceProvider implements Serializable { + + private static final long serialVersionUID = 3306534392621038574L; + + public final String requestTokenURL; + public final String userAuthorizationURL; + public final String accessTokenURL; + + public OAuthServiceProvider(String requestTokenURL, + String userAuthorizationURL, String accessTokenURL) { + this.requestTokenURL = requestTokenURL; + this.userAuthorizationURL = userAuthorizationURL; + this.accessTokenURL = accessTokenURL; + } + +} diff --git a/core/src/main/java/net/oauth/OAuthValidator.java b/core/src/main/java/net/oauth/OAuthValidator.java new file mode 100755 index 0000000..8d272a8 --- /dev/null +++ b/core/src/main/java/net/oauth/OAuthValidator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2008 Google, 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; + +import java.io.IOException; +import java.net.URISyntaxException; + +/** + * An algorithm to determine whether a message has a valid signature, a correct + * version number, a fresh timestamp, etc. + * + * @author Dirk Balfanz + * @author John Kristian + * @hide + */ +public interface OAuthValidator { + + /** + * Check that the given message from the given accessor is valid. + * @throws OAuthException TODO + * @throws IOException TODO + * @throws URISyntaxException + * @throws OAuthProblemException the message is invalid. + * The implementation should throw exceptions that conform to the OAuth + * <a href="http://wiki.oauth.net/ProblemReporting">Problem Reporting extension</a>. + */ + public void validateMessage(OAuthMessage message, OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException; + +} diff --git a/core/src/main/java/net/oauth/SimpleOAuthValidator.java b/core/src/main/java/net/oauth/SimpleOAuthValidator.java new file mode 100755 index 0000000..80a7d42 --- /dev/null +++ b/core/src/main/java/net/oauth/SimpleOAuthValidator.java @@ -0,0 +1,111 @@ +/* + * Copyright 2008 Google, 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; + +import java.io.IOException; +import java.net.URISyntaxException; + +import net.oauth.signature.OAuthSignatureMethod; + +/** + * A simple OAuthValidator, which checks the version, whether the timestamp + * is close to now and the signature is valid. Each check may be overridden. + * + * @author Dirk Balfanz + * @author John Kristian + * @hide + */ +public class SimpleOAuthValidator implements OAuthValidator { + + // default window for timestamps is 5 minutes + public static final long DEFAULT_TIMESTAMP_WINDOW = 5 * 60 * 1000L; + + /** + * Construct a validator that rejects messages more than five minutes out + * of date, or with a OAuth version other than 1.0, or with an invalid + * signature. + */ + public SimpleOAuthValidator() { + this(DEFAULT_TIMESTAMP_WINDOW, Double.parseDouble(OAuth.VERSION_1_0)); + } + + /** + * Public constructor. + * + * @param timestampWindowSec + * specifies, in seconds, the windows (into the past and + * into the future) in which we'll accept timestamps. + * @param maxVersion + * the maximum acceptable oauth_version + */ + public SimpleOAuthValidator(long timestampWindowMsec, double maxVersion) { + this.timestampWindow = timestampWindowMsec; + this.maxVersion = maxVersion; + } + + protected final double minVersion = 1.0; + protected final double maxVersion; + protected final long timestampWindow; + + /** {@inherit} + * @throws URISyntaxException */ + public void validateMessage(OAuthMessage message, OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException { + validateVersion(message); + validateTimestampAndNonce(message); + validateSignature(message, accessor); + } + + protected void validateVersion(OAuthMessage message) + throws OAuthException, IOException { + String versionString = message.getParameter(OAuth.OAUTH_VERSION); + if (versionString != null) { + double version = Double.parseDouble(versionString); + if (version < minVersion || maxVersion < version) { + OAuthProblemException problem = new OAuthProblemException("version_rejected"); + problem.setParameter("oauth_acceptable_versions", minVersion + "-" + maxVersion); + throw problem; + } + } + } + + /** This implementation doesn't check the nonce value. */ + protected void validateTimestampAndNonce(OAuthMessage message) + throws IOException, OAuthProblemException { + message.requireParameters(OAuth.OAUTH_TIMESTAMP, OAuth.OAUTH_NONCE); + long timestamp = Long.parseLong(message.getParameter(OAuth.OAUTH_TIMESTAMP)) * 1000L; + long now = currentTimeMsec(); + long min = now - timestampWindow; + long max = now + timestampWindow; + if (timestamp < min || max < timestamp) { + OAuthProblemException problem = new OAuthProblemException("timestamp_refused"); + problem.setParameter("oauth_acceptable_timestamps", min + "-" + max); + throw problem; + } + } + + protected void validateSignature(OAuthMessage message, OAuthAccessor accessor) + throws OAuthException, IOException, URISyntaxException { + message.requireParameters(OAuth.OAUTH_CONSUMER_KEY, + OAuth.OAUTH_SIGNATURE_METHOD, OAuth.OAUTH_SIGNATURE); + OAuthSignatureMethod.newSigner(message, accessor).validate(message); + } + + protected long currentTimeMsec() { + return System.currentTimeMillis(); + } + +} diff --git a/core/src/main/java/net/oauth/client/ExcerptInputStream.java b/core/src/main/java/net/oauth/client/ExcerptInputStream.java new file mode 100755 index 0000000..0dae3c3 --- /dev/null +++ b/core/src/main/java/net/oauth/client/ExcerptInputStream.java @@ -0,0 +1,45 @@ +package net.oauth.client; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A decorator that retains a copy of the first few bytes of data. + * @hide + */ +public class ExcerptInputStream extends BufferedInputStream +{ + /** + * A marker that's appended to the excerpt if it's less than the complete + * stream. + */ + public static final byte[] ELLIPSIS = " ...".getBytes(); + + public ExcerptInputStream(InputStream in) throws IOException { + super(in); + mark(LIMIT); + int total = 0; + int read; + while ((read = read(excerpt, total, LIMIT - total)) != -1 && ((total += read) < LIMIT)); + if (total == LIMIT) { + // Only add the ellipsis if there are at least LIMIT bytes + System.arraycopy(ELLIPSIS, 0, excerpt, total, ELLIPSIS.length); + } else { + byte[] tmp = new byte[total]; + System.arraycopy(excerpt, 0, tmp, 0, total); + excerpt = tmp; + } + reset(); + } + + private static final int LIMIT = 1024; + private byte[] excerpt = new byte[LIMIT + ELLIPSIS.length]; + + /** The first few bytes of data, plus ELLIPSIS if there are more bytes. */ + public byte[] getExcerpt() + { + return excerpt; + } + +} diff --git a/core/src/main/java/net/oauth/client/OAuthClient.java b/core/src/main/java/net/oauth/client/OAuthClient.java new file mode 100755 index 0000000..4be9e7f --- /dev/null +++ b/core/src/main/java/net/oauth/client/OAuthClient.java @@ -0,0 +1,348 @@ +/* + * 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; + +} diff --git a/core/src/main/java/net/oauth/client/OAuthResponseMessage.java b/core/src/main/java/net/oauth/client/OAuthResponseMessage.java new file mode 100755 index 0000000..88a1417 --- /dev/null +++ b/core/src/main/java/net/oauth/client/OAuthResponseMessage.java @@ -0,0 +1,93 @@ +/* + * Copyright 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.IOException; +import java.io.InputStream; +import java.util.Map; +import net.oauth.OAuth; +import net.oauth.OAuthMessage; +import net.oauth.OAuthProblemException; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; + +/** + * An HTTP response, encapsulated as an OAuthMessage. + * + * @author John Kristian + * @hide + */ +final class OAuthResponseMessage extends OAuthMessage +{ + OAuthResponseMessage(HttpResponseMessage http) throws IOException + { + super(http.method, http.url.toExternalForm(), null); + this.http = http; + getHeaders().addAll(http.headers); + for (Map.Entry<String, String> header : http.headers) { + if ("WWW-Authenticate".equalsIgnoreCase(header.getKey())) { + for (OAuth.Parameter parameter : decodeAuthorization(header.getValue())) { + if (!"realm".equalsIgnoreCase(parameter.getKey())) { + addParameter(parameter); + } + } + } + } + } + + private final HttpMessage http; + + @Override + public InputStream getBodyAsStream() throws IOException + { + return http.getBody(); + } + + @Override + public String getBodyEncoding() + { + return http.getContentCharset(); + } + + @Override + protected void completeParameters() throws IOException + { + super.completeParameters(); + String body = readBodyAsString(); + if (body != null) { + addParameters(OAuth.decodeForm(body.trim())); + } + } + + @Override + protected void dump(Map<String, Object> into) throws IOException + { + super.dump(into); + http.dump(into); + } + + @Override + public void requireParameters(String... names) throws OAuthProblemException, IOException { + try { + super.requireParameters(names); + } catch (OAuthProblemException problem) { + problem.getParameters().putAll(getDump()); + throw problem; + } + } + +} diff --git a/core/src/main/java/net/oauth/client/URLConnectionClient.java b/core/src/main/java/net/oauth/client/URLConnectionClient.java new file mode 100755 index 0000000..18bdf47 --- /dev/null +++ b/core/src/main/java/net/oauth/client/URLConnectionClient.java @@ -0,0 +1,113 @@ +/* + * Copyright 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.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.oauth.http.HttpClient; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; + +/** + * An HttpClient based on HttpURLConnection. + * <p> + * HttpClient3 or HttpClient4 perform better than this class, as a rule; since + * they do things like connection pooling. They also support reading the body + * of an HTTP response whose status code isn't 200 (OK), which can enable your + * application to handle problems better. + * + * @author John Kristian + * @hide + */ +public class URLConnectionClient implements HttpClient { + + /** Send a message to the service provider and get the response. */ + public HttpResponseMessage execute(HttpMessage request) throws IOException { + final String httpMethod = request.method; + final Collection<Map.Entry<String, String>> addHeaders = request.headers; + final URL url = request.url; + final URLConnection connection = url.openConnection(); + connection.setDoInput(true); + if (connection instanceof HttpURLConnection) { + HttpURLConnection http = (HttpURLConnection) connection; + http.setRequestMethod(httpMethod); + http.setInstanceFollowRedirects(false); + } + StringBuilder headers = new StringBuilder(httpMethod); + { + headers.append(" ").append(url.getPath()); + String query = url.getQuery(); + if (query != null && query.length() > 0) { + headers.append("?").append(query); + } + headers.append(EOL); + for (Map.Entry<String, List<String>> header : connection + .getRequestProperties().entrySet()) { + String key = header.getKey(); + for (String value : header.getValue()) { + headers.append(key).append(": ").append(value).append(EOL); + } + } + } + String contentLength = null; + for (Map.Entry<String, String> header : addHeaders) { + String key = header.getKey(); + if (HttpMessage.CONTENT_LENGTH.equalsIgnoreCase(key) + && connection instanceof HttpURLConnection) { + contentLength = header.getValue(); + } else { + connection.setRequestProperty(key, header.getValue()); + } + headers.append(key).append(": ").append(header.getValue()).append(EOL); + } + byte[] excerpt = null; + final InputStream body = request.getBody(); + if (body != null) { + try { + if (contentLength != null) { + ((HttpURLConnection) connection) + .setFixedLengthStreamingMode(Integer.parseInt(contentLength)); + } + connection.setDoOutput(true); + OutputStream output = connection.getOutputStream(); + try { + final ExcerptInputStream ex = new ExcerptInputStream(body); + byte[] b = new byte[1024]; + for (int n; 0 < (n = ex.read(b));) { + output.write(b, 0, n); + } + excerpt = ex.getExcerpt(); + } finally { + output.close(); + } + } finally { + body.close(); + } + } + return new URLConnectionResponse(request, headers.toString(), excerpt, connection); + } + + private static final String EOL = HttpResponseMessage.EOL; + +} diff --git a/core/src/main/java/net/oauth/client/URLConnectionResponse.java b/core/src/main/java/net/oauth/client/URLConnectionResponse.java new file mode 100755 index 0000000..4061ca8 --- /dev/null +++ b/core/src/main/java/net/oauth/client/URLConnectionResponse.java @@ -0,0 +1,136 @@ +/* + * Copyright 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.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import net.oauth.OAuth; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; + +/** + * The response part of a URLConnection, encapsulated as an OAuthMessage. + * + * @author John Kristian + * @hide + */ +public class URLConnectionResponse extends HttpResponseMessage { + + /** + * Construct an OAuthMessage from the HTTP response, including parameters + * from OAuth WWW-Authenticate headers and the body. The header parameters + * come first, followed by the ones from the response body. + */ + public URLConnectionResponse(HttpMessage request, String requestHeaders, + byte[] requestExcerpt, URLConnection connection) throws IOException { + super(request.method, request.url); + this.requestHeaders = requestHeaders; + this.requestExcerpt = requestExcerpt; + this.requestEncoding = request.getContentCharset(); + this.connection = connection; + this.headers.addAll(getHeaders()); + } + + private final String requestHeaders; + private final byte[] requestExcerpt; + private final String requestEncoding; + private final URLConnection connection; + + @Override + public int getStatusCode() throws IOException { + if (connection instanceof HttpURLConnection) { + return ((HttpURLConnection) connection).getResponseCode(); + } + return STATUS_OK; + } + + @Override + public InputStream openBody() { + try { + return connection.getInputStream(); + } catch (IOException ohWell) { + } + return null; + } + + private List<Map.Entry<String, String>> getHeaders() { + List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(); + boolean foundContentType = false; + String value; + for (int i = 0; (value = connection.getHeaderField(i)) != null; ++i) { + String name = connection.getHeaderFieldKey(i); + if (name != null) { + headers.add(new OAuth.Parameter(name, value)); + if (CONTENT_TYPE.equalsIgnoreCase(name)) { + foundContentType = true; + } + } + } + if (!foundContentType) { + headers.add(new OAuth.Parameter(CONTENT_TYPE, connection + .getContentType())); + } + return headers; + } + /** Return a complete description of the HTTP exchange. */ + @Override + public void dump(Map<String, Object> into) throws IOException { + super.dump(into); + { + StringBuilder request = new StringBuilder(requestHeaders); + request.append(EOL); + if (requestExcerpt != null) { + request.append(new String(requestExcerpt, requestEncoding)); + } + into.put(REQUEST, request.toString()); + } + { + HttpURLConnection http = (connection instanceof HttpURLConnection) ? (HttpURLConnection) connection + : null; + StringBuilder response = new StringBuilder(); + String value; + for (int i = 0; (value = connection.getHeaderField(i)) != null; ++i) { + String name = connection.getHeaderFieldKey(i); + if (i == 0 && name != null && http != null) { + String firstLine = "HTTP " + getStatusCode(); + String message = http.getResponseMessage(); + if (message != null) { + firstLine += (" " + message); + } + response.append(firstLine).append(EOL); + } + if (name != null) { + response.append(name).append(": "); + name = name.toLowerCase(); + } + response.append(value).append(EOL); + } + response.append(EOL); + if (body != null) { + response.append(new String(((ExcerptInputStream) body) + .getExcerpt(), getContentCharset())); + } + into.put(HttpMessage.RESPONSE, response.toString()); + } + } + +} diff --git a/core/src/main/java/net/oauth/client/httpclient4/HttpClient4.java b/core/src/main/java/net/oauth/client/httpclient4/HttpClient4.java new file mode 100755 index 0000000..3376cc8 --- /dev/null +++ b/core/src/main/java/net/oauth/client/httpclient4/HttpClient4.java @@ -0,0 +1,125 @@ +/* + * Copyright 2008 Sean Sullivan + * + * 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.httpclient4; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Map; +import net.oauth.client.ExcerptInputStream; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.params.ClientPNames; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.HttpParams; + +/** + * Utility methods for an OAuth client based on the <a + * href="http://hc.apache.org">Apache HttpClient</a>. + * + * @author Sean Sullivan + * @hide + */ +public class HttpClient4 implements net.oauth.http.HttpClient { + + public HttpClient4() { + this(SHARED_CLIENT); + } + + public HttpClient4(HttpClientPool clientPool) { + this.clientPool = clientPool; + } + + private final HttpClientPool clientPool; + + public HttpResponseMessage execute(HttpMessage request) throws IOException { + final String method = request.method; + final String url = request.url.toExternalForm(); + final InputStream body = request.getBody(); + final boolean isDelete = DELETE.equalsIgnoreCase(method); + final boolean isPost = POST.equalsIgnoreCase(method); + final boolean isPut = PUT.equalsIgnoreCase(method); + byte[] excerpt = null; + HttpRequestBase httpRequest; + if (isPost || isPut) { + HttpEntityEnclosingRequestBase entityEnclosingMethod = + isPost ? new HttpPost(url) : new HttpPut(url); + if (body != null) { + ExcerptInputStream e = new ExcerptInputStream(body); + excerpt = e.getExcerpt(); + String length = request.removeHeaders(HttpMessage.CONTENT_LENGTH); + entityEnclosingMethod.setEntity(new InputStreamEntity(e, + (length == null) ? -1 : Long.parseLong(length))); + } + httpRequest = entityEnclosingMethod; + } else if (isDelete) { + httpRequest = new HttpDelete(url); + } else { + httpRequest = new HttpGet(url); + } + for (Map.Entry<String, String> header : request.headers) { + httpRequest.addHeader(header.getKey(), header.getValue()); + } + HttpClient client = clientPool.getHttpClient(new URL(httpRequest.getURI().toString())); + client.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false); + HttpResponse httpResponse = client.execute(httpRequest); + return new HttpMethodResponse(httpRequest, httpResponse, excerpt, request.getContentCharset()); + } + + private static final HttpClientPool SHARED_CLIENT = new SingleClient(); + + /** + * A pool that simply shares a single HttpClient. An HttpClient owns a pool + * of TCP connections. So, callers that share an HttpClient will share + * connections. Sharing improves performance (by avoiding the overhead of + * creating connections) and uses fewer resources in the client and its + * servers. + */ + private static class SingleClient implements HttpClientPool + { + SingleClient() + { + HttpClient client = new DefaultHttpClient(); + ClientConnectionManager mgr = client.getConnectionManager(); + if (!(mgr instanceof ThreadSafeClientConnManager)) { + HttpParams params = client.getParams(); + client = new DefaultHttpClient(new ThreadSafeClientConnManager(params, + mgr.getSchemeRegistry()), params); + } + this.client = client; + } + + private final HttpClient client; + + public HttpClient getHttpClient(URL server) + { + return client; + } + } + +} diff --git a/core/src/main/java/net/oauth/client/httpclient4/HttpClientPool.java b/core/src/main/java/net/oauth/client/httpclient4/HttpClientPool.java new file mode 100755 index 0000000..90f9ade --- /dev/null +++ b/core/src/main/java/net/oauth/client/httpclient4/HttpClientPool.java @@ -0,0 +1,36 @@ +/* + * Copyright 2008 Sean Sullivan + * + * 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.httpclient4; + +import java.net.URL; + +/** + * + * A source of Apache HttpClient 4 objects. + * + * This class relies on <a href="http://hc.apache.org">Apache HttpClient</a> + * version 4. + * + * @author Sean Sullivan + * @hide + */ +public interface HttpClientPool { + + /** Get the appropriate HttpClient for sending a request to the given URL. */ + public org.apache.http.client.HttpClient getHttpClient(URL server); + +} diff --git a/core/src/main/java/net/oauth/client/httpclient4/HttpMethodResponse.java b/core/src/main/java/net/oauth/client/httpclient4/HttpMethodResponse.java new file mode 100755 index 0000000..1f7b747 --- /dev/null +++ b/core/src/main/java/net/oauth/client/httpclient4/HttpMethodResponse.java @@ -0,0 +1,137 @@ +/* + * Copyright 2008 Sean Sullivan + * + * 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.httpclient4; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import net.oauth.OAuth; +import net.oauth.client.ExcerptInputStream; +import net.oauth.http.HttpMessage; +import net.oauth.http.HttpResponseMessage; +import org.apache.http.Header; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; + +/** + * An HttpResponse, encapsulated as an OAuthMessage. + * + * This class relies on <a href="http://hc.apache.org">Apache HttpClient</a> + * version 4. + * + * @author Sean Sullivan + * @hide + */ +public class HttpMethodResponse extends HttpResponseMessage +{ + + /** + * Construct an OAuthMessage from the HTTP response, including parameters + * from OAuth WWW-Authenticate headers and the body. The header parameters + * come first, followed by the ones from the response body. + */ + public HttpMethodResponse(HttpRequestBase request, HttpResponse response, byte[] requestBody, + String requestEncoding) throws IOException + { + super(request.getMethod(), new URL(request.getURI().toString())); + this.httpRequest = request; + this.httpResponse = response; + this.requestBody = requestBody; + this.requestEncoding = requestEncoding; + this.headers.addAll(getHeaders()); + } + + private final HttpRequestBase httpRequest; + private final HttpResponse httpResponse; + private final byte[] requestBody; + private final String requestEncoding; + + @Override + public int getStatusCode() + { + return httpResponse.getStatusLine().getStatusCode(); + } + + @Override + public InputStream openBody() throws IOException + { + return httpResponse.getEntity().getContent(); + } + + private List<Map.Entry<String, String>> getHeaders() + { + List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(); + Header[] allHeaders = httpResponse.getAllHeaders(); + if (allHeaders != null) { + for (Header header : allHeaders) { + headers.add(new OAuth.Parameter(header.getName(), header.getValue())); + } + } + return headers; + } + + /** Return a complete description of the HTTP exchange. */ + @Override + public void dump(Map<String, Object> into) throws IOException + { + super.dump(into); + { + StringBuilder request = new StringBuilder(httpRequest.getMethod()); + request.append(" ").append(httpRequest.getURI().getPath()); + String query = httpRequest.getURI().getQuery(); + if (query != null && query.length() > 0) { + request.append("?").append(query); + } + request.append(EOL); + for (Header header : httpRequest.getAllHeaders()) { + request.append(header.getName()).append(": ").append(header.getValue()).append(EOL); + } + if (httpRequest instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest r = (HttpEntityEnclosingRequest) httpRequest; + long contentLength = r.getEntity().getContentLength(); + if (contentLength >= 0) { + request.append("Content-Length: ").append(contentLength).append(EOL); + } + } + request.append(EOL); + if (requestBody != null) { + request.append(new String(requestBody, requestEncoding)); + } + into.put(REQUEST, request.toString()); + } + { + StringBuilder response = new StringBuilder(); + String value = httpResponse.getStatusLine().toString(); + response.append(value).append(EOL); + for (Header header : httpResponse.getAllHeaders()) { + String name = header.getName(); + value = header.getValue(); + response.append(name).append(": ").append(value).append(EOL); + } + response.append(EOL); + if (body != null) { + response.append(new String(((ExcerptInputStream) body).getExcerpt(), + getContentCharset())); + } + into.put(HttpMessage.RESPONSE, response.toString()); + } + } +} diff --git a/core/src/main/java/net/oauth/consumer.properties.sample b/core/src/main/java/net/oauth/consumer.properties.sample new file mode 100755 index 0000000..ea26c90 --- /dev/null +++ b/core/src/main/java/net/oauth/consumer.properties.sample @@ -0,0 +1,16 @@ +# NamedConsumerPool can gets consumer configuration parameters from a file like this. + +ma.gnolia.consumerKey: - Your key here - +ma.gnolia.consumerSecret: - Your secret here - +ma.gnolia.serviceProvider.requestTokenURL: http://ma.gnolia.com/oauth/get_request_token +ma.gnolia.serviceProvider.userAuthorizationURL: http://ma.gnolia.com/oauth/authorize +ma.gnolia.serviceProvider.accessTokenURL: http://ma.gnolia.com/oauth/get_access_token + +twitter.consumerKey: - Your key here - +twitter.consumerSecret: - Your secret here - +twitter.callbackURL: - Your URL here - +twitter.consumer.oauth_signature_method: PLAINTEXT +# There can be more consumer properties. +twitter.serviceProvider.requestTokenURL: http://twitter.com/oauth/request_token +twitter.serviceProvider.userAuthorizationURL: http://twitter.com/oauth/authorize +twitter.serviceProvider.accessTokenURL: http://twitter.com/oauth/access_token diff --git a/core/src/main/java/net/oauth/http/HttpClient.java b/core/src/main/java/net/oauth/http/HttpClient.java new file mode 100755 index 0000000..b964f96 --- /dev/null +++ b/core/src/main/java/net/oauth/http/HttpClient.java @@ -0,0 +1,40 @@ +/* + * Copyright 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.http; + +import java.io.IOException; +import net.oauth.OAuthMessage; + +/** + * @hide + */ +public interface HttpClient +{ + /** + * Send an HTTP request and return the response. + * <p> + * Don't follow redirects. If a redirect response is received, simply return + * it (with a statusCode and LOCATION header). + */ + HttpResponseMessage execute(HttpMessage request) throws IOException; + + static final String GET = OAuthMessage.GET; + static final String POST = OAuthMessage.POST; + static final String PUT = OAuthMessage.PUT; + static final String DELETE = OAuthMessage.DELETE; + +} diff --git a/core/src/main/java/net/oauth/http/HttpMessage.java b/core/src/main/java/net/oauth/http/HttpMessage.java new file mode 100755 index 0000000..8f3ee5f --- /dev/null +++ b/core/src/main/java/net/oauth/http/HttpMessage.java @@ -0,0 +1,161 @@ +/* + * Copyright 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.http; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import net.oauth.client.ExcerptInputStream; + +/** + * An HTTP request or response. + * + * @author John Kristian + * @hide + */ +public class HttpMessage +{ + + public HttpMessage() + { + this(null, null); + } + + public HttpMessage(String method, URL url) + { + this(method, url, null); + } + + public HttpMessage(String method, URL url, InputStream body) + { + this.method = method; + this.url = url; + this.body = body; + } + + public String method; + public URL url; + public final List<Map.Entry<String, String>> headers = new ArrayList<Map.Entry<String, String>>(); + protected InputStream body = null; + + /** + * Get the value of the last header of the given name. The name is + * case-insensitive. + */ + public final String getHeader(String name) + { + String value = null; + for (Map.Entry<String, String> header : headers) { + if (equalsIgnoreCase(name, header.getKey())) { + value = header.getValue(); + } + } + return value; + } + + /** + * Remove all headers of the given name. The name is case insensitive. + * + * @return the value of the last header with that name, or null to indicate + * there was no such header + */ + public String removeHeaders(String name) + { + String value = null; + for (Iterator<Map.Entry<String, String>> i = headers.iterator(); i.hasNext();) { + Map.Entry<String, String> header = i.next(); + if (equalsIgnoreCase(name, header.getKey())) { + value = header.getValue(); + i.remove(); + } + } + return value; + } + + public final String getContentCharset() + { + return getCharset(getHeader(CONTENT_TYPE)); + } + + public final InputStream getBody() throws IOException + { + if (body == null) { + InputStream raw = openBody(); + if (raw != null) { + body = new ExcerptInputStream(raw); + } + } + return body; + } + + protected InputStream openBody() throws IOException + { + return null; + } + + /** Put a description of this message and its origins into the given Map. */ + public void dump(Map<String, Object> into) throws IOException + { + } + + private static boolean equalsIgnoreCase(String x, String y) + { + if (x == null) + return y == null; + else + return x.equalsIgnoreCase(y); + } + + private static final String getCharset(String mimeType) + { + if (mimeType != null) { + Matcher m = CHARSET.matcher(mimeType); + if (m.find()) { + String charset = m.group(1); + if (charset.length() >= 2 && charset.charAt(0) == '"' + && charset.charAt(charset.length() - 1) == '"') { + charset = charset.substring(1, charset.length() - 1); + charset = charset.replace("\\\"", "\""); + } + return charset; + } + } + return DEFAULT_CHARSET; + } + + /** The name of a dump entry whose value is the HTTP request. */ + public static final String REQUEST = "HTTP request"; + + /** The name of a dump entry whose value is the HTTP response. */ + public static final String RESPONSE = "HTTP response"; + + public static final String ACCEPT_ENCODING = "Accept-Encoding"; + public static final String CONTENT_ENCODING = "Content-Encoding"; + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String DEFAULT_CHARSET = "ISO-8859-1"; + + private static final Pattern CHARSET = Pattern + .compile("; *charset *= *([^;\"]*|\"([^\"]|\\\\\")*\")(;|$)"); + +} diff --git a/core/src/main/java/net/oauth/http/HttpMessageDecoder.java b/core/src/main/java/net/oauth/http/HttpMessageDecoder.java new file mode 100755 index 0000000..0db1a42 --- /dev/null +++ b/core/src/main/java/net/oauth/http/HttpMessageDecoder.java @@ -0,0 +1,97 @@ +/* + * Copyright 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.http; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; + +/** + * A decorator that handles Content-Encoding. + * @hide + */ +public class HttpMessageDecoder extends HttpResponseMessage { + + /** + * Decode the given message if necessary and possible. + * + * @return a decorator that decodes the body of the given message; or the + * given message if this class can't decode it. + */ + public static HttpResponseMessage decode(HttpResponseMessage message) + throws IOException { + if (message != null) { + String encoding = getEncoding(message); + if (encoding != null) { + return new HttpMessageDecoder(message, encoding); + } + } + return message; + } + + public static final String GZIP = "gzip"; + public static final String DEFLATE = "deflate"; + public static final String ACCEPTED = GZIP + "," + DEFLATE; + + private static String getEncoding(HttpMessage message) { + String encoding = message.getHeader(CONTENT_ENCODING); + if (encoding == null) { + // That's easy. + } else if (GZIP.equalsIgnoreCase(encoding) + || ("x-" + GZIP).equalsIgnoreCase(encoding)) { + return GZIP; + } else if (DEFLATE.equalsIgnoreCase(encoding)) { + return DEFLATE; + } + return null; + } + + private HttpMessageDecoder(HttpResponseMessage in, String encoding) + throws IOException { + super(in.method, in.url); + this.headers.addAll(in.headers); + removeHeaders(CONTENT_ENCODING); // handled here + removeHeaders(CONTENT_LENGTH); // unpredictable + InputStream body = in.getBody(); + if (body != null) { + if (encoding == GZIP) { + body = new GZIPInputStream(body); + } else if (encoding == DEFLATE) { + body = new InflaterInputStream(body); + } else { + assert false; + } + } + this.body = body; + this.in = in; + } + + private final HttpResponseMessage in; + + @Override + public void dump(Map<String, Object> into) throws IOException { + in.dump(into); + } + + @Override + public int getStatusCode() throws IOException { + return in.getStatusCode(); + } + +} diff --git a/core/src/main/java/net/oauth/http/HttpResponseMessage.java b/core/src/main/java/net/oauth/http/HttpResponseMessage.java new file mode 100755 index 0000000..8ad5816 --- /dev/null +++ b/core/src/main/java/net/oauth/http/HttpResponseMessage.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.http; + +import java.io.IOException; +import java.net.URL; +import java.util.Map; + +/** + * An HTTP response. + * + * @author John Kristian + * @hide + */ +public abstract class HttpResponseMessage extends HttpMessage { + + protected HttpResponseMessage(String method, URL url) { + super(method, url); + } + + @Override + public void dump(Map<String, Object> into) throws IOException { + super.dump(into); + into.put(STATUS_CODE, Integer.valueOf(getStatusCode())); + String location = getHeader(LOCATION); + if (location != null) { + into.put(LOCATION, location); + } + } + + public abstract int getStatusCode() throws IOException; + + /** The name of a dump entry whose value is the response Location header. */ + public static final String LOCATION = "Location"; + + /** The name of a dump entry whose value is the HTTP status code. */ + public static final String STATUS_CODE = "HTTP status"; + + /** The statusCode that indicates a normal outcome. */ + public static final int STATUS_OK = 200; + + /** The standard end-of-line marker in an HTTP message. */ + public static final String EOL = "\r\n"; + +} diff --git a/core/src/main/java/net/oauth/signature/HMAC_SHA1.java b/core/src/main/java/net/oauth/signature/HMAC_SHA1.java new file mode 100755 index 0000000..dee72fd --- /dev/null +++ b/core/src/main/java/net/oauth/signature/HMAC_SHA1.java @@ -0,0 +1,103 @@ +/* + * 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.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import net.oauth.OAuth; +import net.oauth.OAuthException; + +/** + * @author John Kristian + * @hide + */ +class HMAC_SHA1 extends OAuthSignatureMethod { + + @Override + protected String getSignature(String baseString) throws OAuthException { + try { + String signature = base64Encode(computeSignature(baseString)); + return signature; + } catch (GeneralSecurityException e) { + throw new OAuthException(e); + } catch (UnsupportedEncodingException e) { + throw new OAuthException(e); + } + } + + @Override + protected boolean isValid(String signature, String baseString) + throws OAuthException { + try { + byte[] expected = computeSignature(baseString); + byte[] actual = decodeBase64(signature); + return Arrays.equals(expected, actual); + } catch (GeneralSecurityException e) { + throw new OAuthException(e); + } catch (UnsupportedEncodingException e) { + throw new OAuthException(e); + } + } + + private byte[] computeSignature(String baseString) + throws GeneralSecurityException, UnsupportedEncodingException { + SecretKey key = null; + synchronized (this) { + if (this.key == null) { + String keyString = OAuth.percentEncode(getConsumerSecret()) + + '&' + OAuth.percentEncode(getTokenSecret()); + byte[] keyBytes = keyString.getBytes(ENCODING); + this.key = new SecretKeySpec(keyBytes, MAC_NAME); + } + key = this.key; + } + Mac mac = Mac.getInstance(MAC_NAME); + mac.init(key); + byte[] text = baseString.getBytes(ENCODING); + return mac.doFinal(text); + } + + /** ISO-8859-1 or US-ASCII would work, too. */ + private static final String ENCODING = OAuth.ENCODING; + + private static final String MAC_NAME = "HmacSHA1"; + + private SecretKey key = null; + + @Override + public void setConsumerSecret(String consumerSecret) { + synchronized (this) { + key = null; + } + super.setConsumerSecret(consumerSecret); + } + + @Override + public void setTokenSecret(String tokenSecret) { + synchronized (this) { + key = null; + } + super.setTokenSecret(tokenSecret); + } + +} diff --git a/core/src/main/java/net/oauth/signature/OAuthSignatureMethod.java b/core/src/main/java/net/oauth/signature/OAuthSignatureMethod.java new file mode 100755 index 0000000..967153d --- /dev/null +++ b/core/src/main/java/net/oauth/signature/OAuthSignatureMethod.java @@ -0,0 +1,300 @@ +/* + * 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; +import org.apache.commons.codec.binary.Base64; + +/** + * 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)); + } + + public static byte[] decodeBase64(String s) { + return BASE64.decode(s.getBytes()); + } + + public static String base64Encode(byte[] b) { + return new String(BASE64.encode(b)); + } + + private static final Base64 BASE64 = new Base64(); + + 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; + } + +} diff --git a/core/src/main/java/net/oauth/signature/PLAINTEXT.java b/core/src/main/java/net/oauth/signature/PLAINTEXT.java new file mode 100755 index 0000000..910f903 --- /dev/null +++ b/core/src/main/java/net/oauth/signature/PLAINTEXT.java @@ -0,0 +1,65 @@ +/* + * 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 net.oauth.OAuth; +import net.oauth.OAuthException; + +/** + * @author John Kristian + * @hide + */ +class PLAINTEXT extends OAuthSignatureMethod { + + @Override + public String getSignature(String baseString) { + return getSignature(); + } + + @Override + protected boolean isValid(String signature, String baseString) + throws OAuthException { + return signature.equals(getSignature()); + } + + private synchronized String getSignature() { + if (signature == null) { + signature = OAuth.percentEncode(getConsumerSecret()) + '&' + + OAuth.percentEncode(getTokenSecret()); + } + return signature; + } + + private String signature = null; + + @Override + public void setConsumerSecret(String consumerSecret) { + synchronized (this) { + signature = null; + } + super.setConsumerSecret(consumerSecret); + } + + @Override + public void setTokenSecret(String tokenSecret) { + synchronized (this) { + signature = null; + } + super.setTokenSecret(tokenSecret); + } + +} diff --git a/core/src/main/java/net/oauth/signature/RSA_SHA1.java b/core/src/main/java/net/oauth/signature/RSA_SHA1.java new file mode 100755 index 0000000..0aa99f1 --- /dev/null +++ b/core/src/main/java/net/oauth/signature/RSA_SHA1.java @@ -0,0 +1,239 @@ +/* + * Copyright 2007 Google, 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.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.EncodedKeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +import net.oauth.OAuth; +import net.oauth.OAuthAccessor; +import net.oauth.OAuthException; + +/** + * Class to handle RSA-SHA1 signatures on OAuth requests. A consumer + * that wishes to use public-key signatures on messages does not need + * a shared secret with the service provider, but it needs a private + * RSA signing key. You create it like this: + * + * OAuthConsumer c = new OAuthConsumer(callback_url, consumer_key, + * null, provider); + * c.setProperty(RSA_SHA1.PRIVATE_KEY, consumer_privateRSAKey); + * + * consumer_privateRSAKey must be an RSA signing key and + * of type java.security.PrivateKey, String, or byte[]. In the latter two + * cases, the key must be PKCS#8-encoded (byte[]) or PKCS#8-encoded and + * then Base64-encoded (String). + * + * A service provider that wishes to verify signatures made by such a + * consumer does not need a shared secret with the consumer, but it needs + * to know the consumer's public key. You create the necessary + * OAuthConsumer object (on the service provider's side) like this: + * + * OAuthConsumer c = new OAuthConsumer(callback_url, consumer_key, + * null, provider); + * c.setProperty(RSA_SHA1.PUBLIC_KEY, consumer_publicRSAKey); + * + * consumer_publicRSAKey must be the consumer's public RSAkey and + * of type java.security.PublicKey, String, or byte[]. In the latter two + * cases, the key must be X509-encoded (byte[]) or X509-encoded and + * then Base64-encoded (String). + * + * Alternatively, a service provider that wishes to verify signatures made + * by such a consumer can use a X509 certificate containing the consumer's + * public key. You create the necessary OAuthConsumer object (on the service + * provider's side) like this: + * + * OAuthConsumer c = new OAuthConsumer(callback_url, consumer_key, + * null, provider); + * c.setProperty(RSA_SHA1.X509_CERTIFICATE, consumer_cert); + * + * consumer_cert must be a X509 Certificate containing the consumer's public + * key and be of type java.security.cert.X509Certificate, String, + * or byte[]. In the latter two cases, the certificate must be DER-encoded + * (byte[]) or PEM-encoded (String). + * + * @author Dirk Balfanz + * @hide + * + */ +public class RSA_SHA1 extends OAuthSignatureMethod { + + final static public String PRIVATE_KEY = "RSA-SHA1.PrivateKey"; + final static public String PUBLIC_KEY = "RSA-SHA1.PublicKey"; + final static public String X509_CERTIFICATE = "RSA-SHA1.X509Certificate"; + + private PrivateKey privateKey = null; + private PublicKey publicKey = null; + + @Override + protected void initialize(String name, OAuthAccessor accessor) + throws OAuthException { + super.initialize(name, accessor); + + Object privateKeyObject = accessor.consumer.getProperty(PRIVATE_KEY); + try { + if (privateKeyObject != null) { + if (privateKeyObject instanceof PrivateKey) { + privateKey = (PrivateKey)privateKeyObject; + } else if (privateKeyObject instanceof String) { + privateKey = getPrivateKeyFromPem((String)privateKeyObject); + } else if (privateKeyObject instanceof byte[]) { + privateKey = getPrivateKeyFromDer((byte[])privateKeyObject); + } else { + throw new IllegalArgumentException( + "Private key set through RSA_SHA1.PRIVATE_KEY must be of " + + "type PrivateKey, String, or byte[], and not " + + privateKeyObject.getClass().getName()); + } + } + + Object publicKeyObject = accessor.consumer.getProperty(PUBLIC_KEY); + if (publicKeyObject != null) { + if (publicKeyObject instanceof PublicKey) { + publicKey = (PublicKey)publicKeyObject; + } else if (publicKeyObject instanceof String) { + publicKey = getPublicKeyFromPem((String)publicKeyObject); + } else if (publicKeyObject instanceof byte[]) { + publicKey = getPublicKeyFromDer((byte[])publicKeyObject); + } else { + throw new IllegalArgumentException( + "Public key set through RSA_SHA1.PRIVATE_KEY must be of " + + "type PublicKey, String, or byte[], and not " + + publicKeyObject.getClass().getName()); + } + } else { // public key was null. perhaps they gave us a X509 cert. + Object certObject = accessor.consumer.getProperty(X509_CERTIFICATE); + if (certObject != null) { + if (certObject instanceof X509Certificate) { + publicKey = ((X509Certificate) certObject).getPublicKey(); + } else if (certObject instanceof String) { + publicKey = getPublicKeyFromPemCert((String)certObject); + } else if (certObject instanceof byte[]) { + publicKey = getPublicKeyFromDerCert((byte[])certObject); + } else { + throw new IllegalArgumentException( + "X509Certificate set through RSA_SHA1.X509_CERTIFICATE" + + " must be of type X509Certificate, String, or byte[]," + + " and not " + certObject.getClass().getName()); + } + } + } + } catch (GeneralSecurityException e) { + throw new OAuthException(e); + } + } + + private PublicKey getPublicKeyFromPemCert(String certObject) + throws GeneralSecurityException { + CertificateFactory fac = CertificateFactory.getInstance("X509"); + ByteArrayInputStream in = new ByteArrayInputStream(certObject.getBytes()); + X509Certificate cert = (X509Certificate)fac.generateCertificate(in); + return cert.getPublicKey(); + } + + private PublicKey getPublicKeyFromDerCert(byte[] certObject) + throws GeneralSecurityException { + CertificateFactory fac = CertificateFactory.getInstance("X509"); + ByteArrayInputStream in = new ByteArrayInputStream(certObject); + X509Certificate cert = (X509Certificate)fac.generateCertificate(in); + return cert.getPublicKey(); + } + + private PublicKey getPublicKeyFromDer(byte[] publicKeyObject) + throws GeneralSecurityException { + KeyFactory fac = KeyFactory.getInstance("RSA"); + EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(publicKeyObject); + return fac.generatePublic(pubKeySpec); + } + + private PublicKey getPublicKeyFromPem(String publicKeyObject) + throws GeneralSecurityException { + return getPublicKeyFromDer(decodeBase64(publicKeyObject)); + } + + private PrivateKey getPrivateKeyFromDer(byte[] privateKeyObject) + throws GeneralSecurityException { + KeyFactory fac = KeyFactory.getInstance("RSA"); + EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(privateKeyObject); + return fac.generatePrivate(privKeySpec); + } + + private PrivateKey getPrivateKeyFromPem(String privateKeyObject) + throws GeneralSecurityException { + return getPrivateKeyFromDer(decodeBase64(privateKeyObject)); + } + + @Override + protected String getSignature(String baseString) throws OAuthException { + try { + byte[] signature = sign(baseString.getBytes(OAuth.ENCODING)); + return base64Encode(signature); + } catch (UnsupportedEncodingException e) { + throw new OAuthException(e); + } catch (GeneralSecurityException e) { + throw new OAuthException(e); + } + } + + @Override + protected boolean isValid(String signature, String baseString) + throws OAuthException { + try { + return verify(decodeBase64(signature), + baseString.getBytes(OAuth.ENCODING)); + } catch (UnsupportedEncodingException e) { + throw new OAuthException(e); + } catch (GeneralSecurityException e) { + throw new OAuthException(e); + } + } + + private byte[] sign(byte[] message) throws GeneralSecurityException { + if (privateKey == null) { + throw new IllegalStateException("need to set private key with " + + "OAuthConsumer.setProperty when " + + "generating RSA-SHA1 signatures."); + } + Signature signer = Signature.getInstance("SHA1withRSA"); + signer.initSign(privateKey); + signer.update(message); + return signer.sign(); + } + + private boolean verify(byte[] signature, byte[] message) + throws GeneralSecurityException { + if (publicKey == null) { + throw new IllegalStateException("need to set public key with " + + " OAuthConsumer.setProperty when " + + "verifying RSA-SHA1 signatures."); + } + Signature verifier = Signature.getInstance("SHA1withRSA"); + verifier.initVerify(publicKey); + verifier.update(message); + return verifier.verify(signature); + } +} |