1/*
2 * Copyright 2007 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.ByteArrayOutputStream;
20import java.io.IOException;
21import java.io.OutputStream;
22import java.io.UnsupportedEncodingException;
23import java.net.URLDecoder;
24import java.net.URLEncoder;
25import java.util.ArrayList;
26import java.util.HashMap;
27import java.util.List;
28import java.util.Map;
29
30/**
31 * @author John Kristian
32 * @hide
33 */
34public class OAuth {
35
36    public static final String VERSION_1_0 = "1.0";
37
38    /** The encoding used to represent characters as bytes. */
39    public static final String ENCODING = "UTF-8";
40
41    /** The MIME type for a sequence of OAuth parameters. */
42    public static final String FORM_ENCODED = "application/x-www-form-urlencoded";
43
44    public static final String OAUTH_CONSUMER_KEY = "oauth_consumer_key";
45    public static final String OAUTH_TOKEN = "oauth_token";
46    public static final String OAUTH_TOKEN_SECRET = "oauth_token_secret";
47    public static final String OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
48    public static final String OAUTH_SIGNATURE = "oauth_signature";
49    public static final String OAUTH_TIMESTAMP = "oauth_timestamp";
50    public static final String OAUTH_NONCE = "oauth_nonce";
51    public static final String OAUTH_VERSION = "oauth_version";
52
53    public static final String HMAC_SHA1 = "HMAC-SHA1";
54    public static final String RSA_SHA1 = "RSA-SHA1";
55
56    public static class Problems {
57        public static final String TOKEN_NOT_AUTHORIZED = "token_not_authorized";
58        public static final String INVALID_USED_NONCE = "invalid_used_nonce";
59        public static final String SIGNATURE_INVALID = "signature_invalid";
60        public static final String INVALID_EXPIRED_TOKEN = "invalid_expired_token";
61        public static final String INVALID_CONSUMER_KEY = "invalid_consumer_key";
62        public static final String CONSUMER_KEY_REFUSED = "consumer_key_refused";
63        public static final String TIMESTAMP_REFUSED = "timestamp_refused";
64        public static final String PARAMETER_REJECTED = "parameter_rejected";
65        public static final String PARAMETER_ABSENT = "parameter_absent";
66        public static final String VERSION_REJECTED = "version_rejected";
67        public static final String SIGNATURE_METHOD_REJECTED = "signature_method_rejected";
68
69        public static final String OAUTH_PARAMETERS_ABSENT = "oauth_parameters_absent";
70        public static final String OAUTH_PARAMETERS_REJECTED = "oauth_parameters_rejected";
71        public static final String OAUTH_ACCEPTABLE_TIMESTAMPS = "oauth_acceptable_timestamps";
72        public static final String OAUTH_ACCEPTABLE_VERSIONS = "oauth_acceptable_versions";
73    }
74
75    /** Return true if the given Content-Type header means FORM_ENCODED. */
76    public static boolean isFormEncoded(String contentType) {
77        if (contentType == null) {
78            return false;
79        }
80        int semi = contentType.indexOf(";");
81        if (semi >= 0) {
82            contentType = contentType.substring(0, semi);
83        }
84        return FORM_ENCODED.equalsIgnoreCase(contentType.trim());
85    }
86
87    /**
88     * Construct a form-urlencoded document containing the given sequence of
89     * name/value pairs. Use OAuth percent encoding (not exactly the encoding
90     * mandated by HTTP).
91     */
92    public static String formEncode(Iterable<? extends Map.Entry> parameters)
93            throws IOException {
94        ByteArrayOutputStream b = new ByteArrayOutputStream();
95        formEncode(parameters, b);
96        return new String(b.toByteArray());
97    }
98
99    /**
100     * Write a form-urlencoded document into the given stream, containing the
101     * given sequence of name/value pairs.
102     */
103    public static void formEncode(Iterable<? extends Map.Entry> parameters,
104            OutputStream into) throws IOException {
105        if (parameters != null) {
106            boolean first = true;
107            for (Map.Entry parameter : parameters) {
108                if (first) {
109                    first = false;
110                } else {
111                    into.write('&');
112                }
113                into.write(percentEncode(toString(parameter.getKey()))
114                        .getBytes());
115                into.write('=');
116                into.write(percentEncode(toString(parameter.getValue()))
117                        .getBytes());
118            }
119        }
120    }
121
122    /** Parse a form-urlencoded document. */
123    public static List<Parameter> decodeForm(String form) {
124        List<Parameter> list = new ArrayList<Parameter>();
125        if (!isEmpty(form)) {
126            for (String nvp : form.split("\\&")) {
127                int equals = nvp.indexOf('=');
128                String name;
129                String value;
130                if (equals < 0) {
131                    name = decodePercent(nvp);
132                    value = null;
133                } else {
134                    name = decodePercent(nvp.substring(0, equals));
135                    value = decodePercent(nvp.substring(equals + 1));
136                }
137                list.add(new Parameter(name, value));
138            }
139        }
140        return list;
141    }
142
143    /** Construct a &-separated list of the given values, percentEncoded. */
144    public static String percentEncode(Iterable values) {
145        StringBuilder p = new StringBuilder();
146        for (Object v : values) {
147            if (p.length() > 0) {
148                p.append("&");
149            }
150            p.append(OAuth.percentEncode(toString(v)));
151        }
152        return p.toString();
153    }
154
155    public static String percentEncode(String s) {
156        if (s == null) {
157            return "";
158        }
159        try {
160            return URLEncoder.encode(s, ENCODING)
161                    // OAuth encodes some characters differently:
162                    .replace("+", "%20").replace("*", "%2A")
163                    .replace("%7E", "~");
164            // This could be done faster with more hand-crafted code.
165        } catch (UnsupportedEncodingException wow) {
166            throw new RuntimeException(wow.getMessage(), wow);
167        }
168    }
169
170    public static String decodePercent(String s) {
171        try {
172            return URLDecoder.decode(s, ENCODING);
173            // This implements http://oauth.pbwiki.com/FlexibleDecoding
174        } catch (java.io.UnsupportedEncodingException wow) {
175            throw new RuntimeException(wow.getMessage(), wow);
176        }
177    }
178
179    /**
180     * Construct a Map containing a copy of the given parameters. If several
181     * parameters have the same name, the Map will contain the first value,
182     * only.
183     */
184    public static Map<String, String> newMap(Iterable<? extends Map.Entry> from) {
185        Map<String, String> map = new HashMap<String, String>();
186        if (from != null) {
187            for (Map.Entry f : from) {
188                String key = toString(f.getKey());
189                if (!map.containsKey(key)) {
190                    map.put(key, toString(f.getValue()));
191                }
192            }
193        }
194        return map;
195    }
196
197    /** Construct a list of Parameters from name, value, name, value... */
198    public static List<Parameter> newList(String... parameters) {
199        List<Parameter> list = new ArrayList<Parameter>(parameters.length / 2);
200        for (int p = 0; p + 1 < parameters.length; p += 2) {
201            list.add(new Parameter(parameters[p], parameters[p + 1]));
202        }
203        return list;
204    }
205
206    /** A name/value pair. */
207    public static class Parameter implements Map.Entry<String, String> {
208
209        public Parameter(String key, String value) {
210            this.key = key;
211            this.value = value;
212        }
213
214        private final String key;
215
216        private String value;
217
218        public String getKey() {
219            return key;
220        }
221
222        public String getValue() {
223            return value;
224        }
225
226        public String setValue(String value) {
227            try {
228                return this.value;
229            } finally {
230                this.value = value;
231            }
232        }
233
234        @Override
235        public String toString() {
236            return percentEncode(getKey()) + '=' + percentEncode(getValue());
237        }
238
239        @Override
240        public int hashCode()
241        {
242            final int prime = 31;
243            int result = 1;
244            result = prime * result + ((key == null) ? 0 : key.hashCode());
245            result = prime * result + ((value == null) ? 0 : value.hashCode());
246            return result;
247        }
248
249        @Override
250        public boolean equals(Object obj)
251        {
252            if (this == obj)
253                return true;
254            if (obj == null)
255                return false;
256            if (getClass() != obj.getClass())
257                return false;
258            final Parameter that = (Parameter) obj;
259            if (key == null) {
260                if (that.key != null)
261                    return false;
262            } else if (!key.equals(that.key))
263                return false;
264            if (value == null) {
265                if (that.value != null)
266                    return false;
267            } else if (!value.equals(that.value))
268                return false;
269            return true;
270        }
271    }
272
273    private static final String toString(Object from) {
274        return (from == null) ? null : from.toString();
275    }
276
277    /**
278     * Construct a URL like the given one, but with the given parameters added
279     * to its query string.
280     */
281    public static String addParameters(String url, String... parameters)
282            throws IOException {
283        return addParameters(url, newList(parameters));
284    }
285
286    public static String addParameters(String url,
287            Iterable<? extends Map.Entry<String, String>> parameters)
288            throws IOException {
289        String form = formEncode(parameters);
290        if (form == null || form.length() <= 0) {
291            return url;
292        } else {
293            return url + ((url.indexOf("?") < 0) ? '?' : '&') + form;
294        }
295    }
296
297    public static boolean isEmpty(String str) {
298	return (str == null) || (str.length() == 0);
299    }
300}
301