/*
* 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.
*
* 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>();
} else {
this.parameters = new ArrayList>(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> parameters;
private Map parameterMap;
private boolean parametersAreComplete = false;
private final List> headers = new ArrayList>();
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> 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 parameter) {
parameters.add(parameter);
parameterMap = null;
}
public void addParameters(
Collection extends Map.Entry> 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 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 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> 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 getDump() throws IOException {
Map into = new HashMap();
dump(into);
return into;
}
protected void dump(Map 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 present = getParameterMap().keySet();
List absent = new ArrayList();
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 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 decodeAuthorization(String authorization) {
List into = new ArrayList();
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();
}
}