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.signature;
18
19import java.io.IOException;
20import java.net.URI;
21import java.net.URISyntaxException;
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.Collections;
25import java.util.List;
26import java.util.Map;
27import java.util.concurrent.ConcurrentHashMap;
28import net.oauth.OAuth;
29import net.oauth.OAuthAccessor;
30import net.oauth.OAuthConsumer;
31import net.oauth.OAuthException;
32import net.oauth.OAuthMessage;
33import net.oauth.OAuthProblemException;
34// BEGIN android-changed
35// import org.apache.commons.codec.binary.Base64;
36import android.util.Base64;
37// END android-changed
38
39/**
40 * A pair of algorithms for computing and verifying an OAuth digital signature.
41 *
42 * @author John Kristian
43 * @hide
44 */
45public abstract class OAuthSignatureMethod {
46
47    /** Add a signature to the message.
48     * @throws URISyntaxException
49     * @throws IOException */
50    public void sign(OAuthMessage message)
51    throws OAuthException, IOException, URISyntaxException {
52        message.addParameter(new OAuth.Parameter("oauth_signature",
53                getSignature(message)));
54    }
55
56    /**
57     * Check whether the message has a valid signature.
58     * @throws URISyntaxException
59     *
60     * @throws OAuthProblemException
61     *             the signature is invalid
62     */
63    public void validate(OAuthMessage message)
64    throws IOException, OAuthException, URISyntaxException {
65        message.requireParameters("oauth_signature");
66        String signature = message.getSignature();
67        String baseString = getBaseString(message);
68        if (!isValid(signature, baseString)) {
69            OAuthProblemException problem = new OAuthProblemException(
70                    "signature_invalid");
71            problem.setParameter("oauth_signature", signature);
72            problem.setParameter("oauth_signature_base_string", baseString);
73            problem.setParameter("oauth_signature_method", message
74                    .getSignatureMethod());
75            throw problem;
76        }
77    }
78
79    protected String getSignature(OAuthMessage message)
80    throws OAuthException, IOException, URISyntaxException {
81        String baseString = getBaseString(message);
82        String signature = getSignature(baseString);
83        // Logger log = Logger.getLogger(getClass().getName());
84        // if (log.isLoggable(Level.FINE)) {
85        // log.fine(signature + "=getSignature(" + baseString + ")");
86        // }
87        return signature;
88    }
89
90    protected void initialize(String name, OAuthAccessor accessor)
91            throws OAuthException {
92        String secret = accessor.consumer.consumerSecret;
93        if (name.endsWith(_ACCESSOR)) {
94            // This code supports the 'Accessor Secret' extensions
95            // described in http://oauth.pbwiki.com/AccessorSecret
96            final String key = OAuthConsumer.ACCESSOR_SECRET;
97            Object accessorSecret = accessor.getProperty(key);
98            if (accessorSecret == null) {
99                accessorSecret = accessor.consumer.getProperty(key);
100            }
101            if (accessorSecret != null) {
102                secret = accessorSecret.toString();
103            }
104        }
105        if (secret == null) {
106            secret = "";
107        }
108        setConsumerSecret(secret);
109    }
110
111    public static final String _ACCESSOR = "-Accessor";
112
113    /** Compute the signature for the given base string. */
114    protected abstract String getSignature(String baseString) throws OAuthException;
115
116    /** Decide whether the signature is valid. */
117    protected abstract boolean isValid(String signature, String baseString)
118            throws OAuthException;
119
120    private String consumerSecret;
121
122    private String tokenSecret;
123
124    protected String getConsumerSecret() {
125        return consumerSecret;
126    }
127
128    protected void setConsumerSecret(String consumerSecret) {
129        this.consumerSecret = consumerSecret;
130    }
131
132    public String getTokenSecret() {
133        return tokenSecret;
134    }
135
136    public void setTokenSecret(String tokenSecret) {
137        this.tokenSecret = tokenSecret;
138    }
139
140    public static String getBaseString(OAuthMessage message)
141            throws IOException, URISyntaxException {
142        List<Map.Entry<String, String>> parameters;
143        String url = message.URL;
144        int q = url.indexOf('?');
145        if (q < 0) {
146            parameters = message.getParameters();
147        } else {
148            // Combine the URL query string with the other parameters:
149            parameters = new ArrayList<Map.Entry<String, String>>();
150            parameters.addAll(OAuth.decodeForm(message.URL.substring(q + 1)));
151            parameters.addAll(message.getParameters());
152            url = url.substring(0, q);
153        }
154        return OAuth.percentEncode(message.method.toUpperCase()) + '&'
155                + OAuth.percentEncode(normalizeUrl(url)) + '&'
156                + OAuth.percentEncode(normalizeParameters(parameters));
157    }
158
159    protected static String normalizeUrl(String url) throws URISyntaxException {
160        URI uri = new URI(url);
161        String scheme = uri.getScheme().toLowerCase();
162        String authority = uri.getAuthority().toLowerCase();
163        boolean dropPort = (scheme.equals("http") && uri.getPort() == 80)
164                           || (scheme.equals("https") && uri.getPort() == 443);
165        if (dropPort) {
166            // find the last : in the authority
167            int index = authority.lastIndexOf(":");
168            if (index >= 0) {
169                authority = authority.substring(0, index);
170            }
171        }
172        String path = uri.getRawPath();
173        if (path == null || path.length() <= 0) {
174            path = "/"; // conforms to RFC 2616 section 3.2.2
175        }
176        // we know that there is no query and no fragment here.
177        return scheme + "://" + authority + path;
178    }
179
180    protected static String normalizeParameters(
181            Collection<? extends Map.Entry> parameters) throws IOException {
182        if (parameters == null) {
183            return "";
184        }
185        List<ComparableParameter> p = new ArrayList<ComparableParameter>(
186                parameters.size());
187        for (Map.Entry parameter : parameters) {
188            if (!"oauth_signature".equals(parameter.getKey())) {
189                p.add(new ComparableParameter(parameter));
190            }
191        }
192        Collections.sort(p);
193        return OAuth.formEncode(getParameters(p));
194    }
195
196    // BEGIN android-changed
197    public static byte[] decodeBase64(String s) {
198        return Base64.decode(s, Base64.DEFAULT);
199    }
200
201    public static String base64Encode(byte[] b) {
202        return Base64.encodeToString(b, Base64.DEFAULT);
203    }
204    // END android-changed
205
206    public static OAuthSignatureMethod newSigner(OAuthMessage message,
207            OAuthAccessor accessor) throws IOException, OAuthException {
208        message.requireParameters(OAuth.OAUTH_SIGNATURE_METHOD);
209        OAuthSignatureMethod signer = newMethod(message.getSignatureMethod(),
210                accessor);
211        signer.setTokenSecret(accessor.tokenSecret);
212        return signer;
213    }
214
215    /** The factory for signature methods. */
216    public static OAuthSignatureMethod newMethod(String name,
217            OAuthAccessor accessor) throws OAuthException {
218        try {
219            Class methodClass = NAME_TO_CLASS.get(name);
220            if (methodClass != null) {
221                OAuthSignatureMethod method = (OAuthSignatureMethod) methodClass
222                .newInstance();
223                method.initialize(name, accessor);
224                return method;
225            }
226            OAuthProblemException problem = new OAuthProblemException(
227            "signature_method_rejected");
228            String acceptable = OAuth.percentEncode(NAME_TO_CLASS.keySet());
229            if (acceptable.length() > 0) {
230                problem.setParameter("oauth_acceptable_signature_methods",
231                        acceptable.toString());
232            }
233            throw problem;
234        } catch (InstantiationException e) {
235            throw new OAuthException(e);
236        } catch (IllegalAccessException e) {
237            throw new OAuthException(e);
238        }
239    }
240
241    /**
242     * Subsequently, newMethod(name) will attempt to instantiate the given
243     * class, with no constructor parameters.
244     */
245    public static void registerMethodClass(String name, Class clazz) {
246        NAME_TO_CLASS.put(name, clazz);
247    }
248
249    private static final Map<String, Class> NAME_TO_CLASS = new ConcurrentHashMap<String, Class>();
250    static {
251        registerMethodClass("HMAC-SHA1", HMAC_SHA1.class);
252        registerMethodClass("PLAINTEXT", PLAINTEXT.class);
253        registerMethodClass("RSA-SHA1", RSA_SHA1.class);
254        registerMethodClass("HMAC-SHA1" + _ACCESSOR, HMAC_SHA1.class);
255        registerMethodClass("PLAINTEXT" + _ACCESSOR, PLAINTEXT.class);
256    }
257
258    /** An efficiently sortable wrapper around a parameter. */
259    private static class ComparableParameter implements
260            Comparable<ComparableParameter> {
261
262        ComparableParameter(Map.Entry value) {
263            this.value = value;
264            String n = toString(value.getKey());
265            String v = toString(value.getValue());
266            this.key = OAuth.percentEncode(n) + ' ' + OAuth.percentEncode(v);
267            // ' ' is used because it comes before any character
268            // that can appear in a percentEncoded string.
269        }
270
271        final Map.Entry value;
272
273        private final String key;
274
275        private static String toString(Object from) {
276            return (from == null) ? null : from.toString();
277        }
278
279        public int compareTo(ComparableParameter that) {
280            return this.key.compareTo(that.key);
281        }
282
283        @Override
284        public String toString() {
285            return key;
286        }
287
288    }
289
290    /** Retrieve the original parameters from a sorted collection. */
291    private static List<Map.Entry> getParameters(
292            Collection<ComparableParameter> parameters) {
293        if (parameters == null) {
294            return null;
295        }
296        List<Map.Entry> list = new ArrayList<Map.Entry>(parameters.size());
297        for (ComparableParameter parameter : parameters) {
298            list.add(parameter.value);
299        }
300        return list;
301    }
302
303}
304