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
17package net.oauth;
18
19import java.io.IOException;
20import java.io.InputStream;
21import java.io.InputStreamReader;
22import java.io.Reader;
23import java.net.URISyntaxException;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.Collections;
27import java.util.HashMap;
28import java.util.List;
29import java.util.Map;
30import java.util.Set;
31import java.util.regex.Matcher;
32import java.util.regex.Pattern;
33import net.oauth.http.HttpMessage;
34import 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 */
47public 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