001package com.pusher.rest;
002
003import java.io.UnsupportedEncodingException;
004import java.net.URI;
005import java.net.URISyntaxException;
006import java.security.InvalidKeyException;
007import java.security.MessageDigest;
008import java.security.NoSuchAlgorithmException;
009import java.util.Arrays;
010import java.util.HashMap;
011import java.util.Map;
012import java.util.Map.Entry;
013
014import javax.crypto.Mac;
015import javax.crypto.spec.SecretKeySpec;
016
017import org.apache.commons.codec.binary.Hex;
018import org.apache.http.client.utils.URIBuilder;
019
020import com.pusher.rest.util.Prerequisites;
021
022public class SignatureUtil {
023
024    public static URI uri(final String method,
025                          final String scheme,
026                          final String host,
027                          final String path,
028                          final String body,
029                          final String key,
030                          final String secret,
031                          final Map<String, String> extraParams) {
032
033        Prerequisites.noReservedKeys(extraParams);
034
035        try {
036            final Map<String, String> allParams = new HashMap<String, String>(extraParams);
037            allParams.put("auth_key", key);
038            allParams.put("auth_version", "1.0");
039            allParams.put("auth_timestamp", new Long(System.currentTimeMillis() / 1000).toString());
040            if (body != null) {
041                allParams.put("body_md5", bodyMd5(body));
042            }
043
044            // This is where the auth gets a bit weird. The query params for the request must include
045            // the auth signature which is a signature over all the params except itself.
046            allParams.put("auth_signature", sign(buildSignatureString(method, path, allParams), secret));
047
048            final URIBuilder b = new URIBuilder()
049                    .setScheme(scheme)
050                    .setHost(host)
051                    .setPath(path);
052
053            for (final Entry<String, String> e : allParams.entrySet()) {
054                b.setParameter(e.getKey(), e.getValue());
055            }
056
057            return b.build();
058        }
059        catch (final URISyntaxException e) {
060            throw new RuntimeException("Could not build URI", e);
061        }
062    }
063
064    private static String bodyMd5(final String body) {
065        try {
066            final MessageDigest md = MessageDigest.getInstance("MD5");
067            final byte[] digest = md.digest(body.getBytes("UTF-8"));
068            return Hex.encodeHexString(digest);
069        }
070        // If this doesn't exist, we're pretty much out of luck.
071        catch (final NoSuchAlgorithmException e) {
072            throw new RuntimeException("The Pusher HTTP client requires MD5 support", e);
073        }
074        catch (final UnsupportedEncodingException e) {
075            throw new RuntimeException("The Pusher HTTP client needs UTF-8 support", e);
076        }
077    }
078
079    public static String sign(final String input, final String secret) {
080        try {
081            final Mac mac = Mac.getInstance("HmacSHA256");
082            mac.init(new SecretKeySpec(secret.getBytes(), "SHA256"));
083
084            final byte[] digest = mac.doFinal(input.getBytes("UTF-8"));
085            return Hex.encodeHexString(digest);
086        }
087        catch (final InvalidKeyException e) {
088            /// We validate this when the key is first provided, so we should never encounter it here.
089            throw new RuntimeException("Invalid secret key", e);
090        }
091        // If either of these doesn't exist, we're pretty much out of luck.
092        catch (final NoSuchAlgorithmException e) {
093            throw new RuntimeException("The Pusher HTTP client requires HmacSHA256 support", e);
094        }
095        catch (final UnsupportedEncodingException e) {
096            throw new RuntimeException("The Pusher HTTP client needs UTF-8 support", e);
097        }
098    }
099
100    // Visible for testing
101    static String buildSignatureString(final String method, final String path, final Map<String, String> queryParams) {
102        final StringBuilder sb = new StringBuilder();
103        sb.append(method)
104            .append('\n')
105            .append(path)
106            .append('\n');
107
108        final String[] keys = queryParams.keySet().toArray(new String[0]);
109        Arrays.sort(keys);
110
111        boolean first = true;
112        for (final String key : keys) {
113            if (!first) sb.append('&');
114            else first = false;
115
116            sb.append(key)
117                .append('=')
118                .append(queryParams.get(key));
119        }
120
121        return sb.toString();
122    }
123}