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