View Javadoc

1   /**********************************************************************************************
2    * Copyright 2009 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
5    * except in compliance with the License. A copy of the License is located at
6    *
7    *       http://aws.amazon.com/apache2.0/
8    *
9    * or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS"
10   * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11   * License for the specific language governing permissions and limitations under the License.
12   *
13   * ********************************************************************************************
14   *
15   *  Amazon Product Advertising API
16   *  Signed Requests Sample Code
17   *
18   *  API Version: 2009-03-31
19   *  
20   *  AZ Development 2010
21   */
22  
23  package com.melloware.jukes.ws;
24  
25  import java.io.UnsupportedEncodingException;
26  
27  import java.net.URLDecoder;
28  import java.net.URLEncoder;
29  
30  import java.security.InvalidKeyException;
31  import java.security.NoSuchAlgorithmException;
32  
33  import java.text.DateFormat;
34  import java.text.SimpleDateFormat;
35  
36  import java.util.Calendar;
37  import java.util.HashMap;
38  import java.util.Iterator;
39  import java.util.Map;
40  import java.util.SortedMap;
41  import java.util.TimeZone;
42  import java.util.TreeMap;
43  
44  import javax.crypto.Mac;
45  import javax.crypto.spec.SecretKeySpec;
46  
47  import org.apache.commons.codec.binary.Base64;
48  
49  /**
50   * This class contains all the logic for signing requests
51   * to the Amazon Product Advertising API.
52   */
53  public class SignedRequestsHelper {
54      /**
55       * All strings are handled as UTF-8
56       */
57      private static final String UTF8_CHARSET = "UTF-8";
58     
59      /**
60       * The HMAC algorithm required by Amazon
61       */
62      private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
63     
64      /**
65       * This is the URI for the service, don't change unless you really know
66       * what you're doing.
67       */
68      private static final String REQUEST_URI = "/onca/xml";
69     
70      /**
71       * The sample uses HTTP GET to fetch the response. If you changed the sample
72       * to use HTTP POST instead, change the value below to POST.
73       */
74      private static final String REQUEST_METHOD = "GET";
75  
76      private String endpoint = null;
77      private String awsAccessKeyId = null;
78      private String awsSecretKey = null;
79  
80      private SecretKeySpec secretKeySpec = null;
81      private Mac mac = null;
82  
83      /**
84       * You must provide the three values below to initialize the helper.
85       *  
86       * @param endpoint          Destination for the requests.
87       * @param awsAccessKeyId    Your AWS Access Key ID
88       * @param awsSecretKey      Your AWS Secret Key
89       */
90      public static SignedRequestsHelper getInstance(
91              String endpoint,
92              String awsAccessKeyId,
93              String awsSecretKey
94      ) throws IllegalArgumentException, UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException
95      {
96          if (null == endpoint || endpoint.length() == 0)
97              { throw new IllegalArgumentException("endpoint is null or empty"); }
98          if (null == awsAccessKeyId || awsAccessKeyId.length() == 0)
99              { throw new IllegalArgumentException("awsAccessKeyId is null or empty"); }
100         if (null == awsSecretKey || awsSecretKey.length() == 0)  
101             { throw new IllegalArgumentException("awsSecretKey is null or empty"); }
102        
103         SignedRequestsHelper instance = new SignedRequestsHelper();
104         instance.endpoint = endpoint.toLowerCase();
105         instance.awsAccessKeyId = awsAccessKeyId;
106         instance.awsSecretKey = awsSecretKey;
107         byte[] secretyKeyBytes = instance.awsSecretKey.getBytes(UTF8_CHARSET);
108         instance.secretKeySpec = new SecretKeySpec(secretyKeyBytes, HMAC_SHA256_ALGORITHM);
109         instance.mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
110         instance.mac.init(instance.secretKeySpec);
111 
112         return instance;
113     }
114    
115     /**
116      * The construct is private since we'd rather use getInstance()
117      */
118     private SignedRequestsHelper() {}
119 
120     /**
121      * This method signs requests in hashmap form. It returns a URL that should
122      * be used to fetch the response. The URL returned should not be modified in
123      * any way, doing so will invalidate the signature and Amazon will reject
124      * the request.
125      */
126     public String sign(Map<String, String> params) {
127         // Let's add the AWSAccessKeyId and Timestamp parameters to the request.
128         params.put("AWSAccessKeyId", this.awsAccessKeyId);
129         params.put("Timestamp", this.timestamp());
130 
131         // The parameters need to be processed in lexicographical order, so we'll
132         // use a TreeMap implementation for that.
133         SortedMap<String, String> sortedParamMap = new TreeMap<String, String>(params);
134        
135         // get the canonical form the query string
136         String canonicalQS = this.canonicalize(sortedParamMap);
137        
138         // create the string upon which the signature is calculated
139         String toSign =
140             REQUEST_METHOD + "\n"
141             + this.endpoint + "\n"
142             + REQUEST_URI + "\n"
143             + canonicalQS;
144 
145         // get the signature
146         String hmac = this.hmac(toSign);
147         String sig = this.percentEncodeRfc3986(hmac);
148 
149         // construct the URL
150         String url =
151             "http://" + this.endpoint + REQUEST_URI + "?" + canonicalQS + "&Signature=" + sig;
152 
153         return url;
154     }
155 
156     /**
157      * This method signs requests in query-string form. It returns a URL that
158      * should be used to fetch the response. The URL returned should not be
159      * modified in any way, doing so will invalidate the signature and Amazon
160      * will reject the request.
161      */
162     public String sign(String queryString) {
163         // let's break the query string into it's constituent name-value pairs
164         Map<String, String> params = this.createParameterMap(queryString);
165        
166         // then we can sign the request as before
167         return this.sign(params);
168     }
169 
170     /**
171      * Compute the HMAC.
172      *  
173      * @param stringToSign  String to compute the HMAC over.
174      * @return              base64-encoded hmac value.
175      */
176     private String hmac(String stringToSign) {
177         String signature = null;
178         byte[] data;
179         byte[] rawHmac;
180         try {
181             data = stringToSign.getBytes(UTF8_CHARSET);
182             rawHmac = mac.doFinal(data);
183             Base64 encoder = new Base64();
184             signature = new String(encoder.encode(rawHmac));
185         } catch (UnsupportedEncodingException e) {
186             throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
187         }
188         return signature;
189     }
190 
191     /**
192      * Generate a ISO-8601 format timestamp as required by Amazon.
193      *  
194      * @return  ISO-8601 format timestamp.
195      */
196     private String timestamp() {
197         String timestamp = null;
198         Calendar cal = Calendar.getInstance();
199         DateFormat dfm = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
200         dfm.setTimeZone(TimeZone.getTimeZone("GMT"));
201         timestamp = dfm.format(cal.getTime());
202         return timestamp;
203     }
204 
205 
206     /**
207      * Canonicalize the query string as required by Amazon.
208      *
209      * @param sortedParamMap    Parameter name-value pairs in lexicographical order.
210      * @return                  Canonical form of query string.
211      */
212     private String canonicalize(SortedMap<String, String> sortedParamMap) {
213         if (sortedParamMap.isEmpty()) {
214             return "";
215         }
216 
217         StringBuffer buffer = new StringBuffer();
218         Iterator<Map.Entry<String, String>> iter = sortedParamMap.entrySet().iterator();
219 
220         while (iter.hasNext()) {
221             Map.Entry<String, String> kvpair = iter.next();
222             buffer.append(percentEncodeRfc3986(kvpair.getKey()));
223             buffer.append("=");
224             buffer.append(percentEncodeRfc3986(kvpair.getValue()));
225             if (iter.hasNext()) {
226                 buffer.append("&");
227             }
228         }
229         String cannoical = buffer.toString();
230         return cannoical;
231     }
232 
233     /**
234      * Percent-encode values according the RFC 3986. The built-in Java
235      * URLEncoder does not encode according to the RFC, so we make the
236      * extra replacements.
237      *
238      * @param s decoded string
239      * @return  encoded string per RFC 3986
240      */
241     private String percentEncodeRfc3986(String s) {
242         String out;
243         try {
244             out = URLEncoder.encode(s, UTF8_CHARSET)
245                 .replace("+", "%20")
246                 .replace("*", "%2A")
247                 .replace("%7E", "~");
248         } catch (UnsupportedEncodingException e) {
249             out = s;
250         }
251         return out;
252     }
253 
254     /**
255      * Takes a query string, separates the constituent name-value pairs
256      * and stores them in a hashmap.
257      *
258      * @param queryString
259      * @return
260      */
261     private Map<String, String> createParameterMap(String queryString) {
262         Map<String, String> map = new HashMap<String, String>();
263         String[] pairs = queryString.split("&");
264 
265         for (String pair: pairs) {
266             if (pair.length() < 1) {
267                 continue;
268             }
269 
270             String[] tokens = pair.split("=",2);
271             for(int j=0; j<tokens.length; j++)
272             {
273                 try {
274                     tokens[j] = URLDecoder.decode(tokens[j], UTF8_CHARSET);
275                 } catch (UnsupportedEncodingException e) {
276                 }
277             }
278             switch (tokens.length) {
279                 case 1: {
280                     if (pair.charAt(0) == '=') {
281                         map.put("", tokens[0]);
282                     } else {
283                         map.put(tokens[0], "");
284                     }
285                     break;
286                 }
287                 case 2: {
288                     map.put(tokens[0], tokens[1]);
289                     break;
290                 }
291             }
292         }
293         return map;
294     }
295 }
296