EMMA Coverage Report (generated Mon Oct 17 05:41:20 EDT 2011)
[all classes][org.jclouds.s3.filters]

COVERAGE SUMMARY FOR SOURCE FILE [RequestAuthorizeSignature.java]

nameclass, %method, %block, %line, %
RequestAuthorizeSignature.java100% (2/2)100% (17/17)94%  (571/607)91%  (88.8/97)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class RequestAuthorizeSignature100% (1/1)100% (15/15)94%  (563/599)91%  (87.8/96)
calculateSignature (String): String 100% (1/1)62%  (10/16)75%  (3/4)
sign (String): String 100% (1/1)67%  (14/21)60%  (3/5)
createStringToSign (HttpRequest): String 100% (1/1)86%  (61/71)87%  (13/15)
appendUriPath (HttpRequest, StringBuilder): void 100% (1/1)88%  (53/60)86%  (12/14)
valueOrEmpty (Collection): String 100% (1/1)92%  (12/13)92%  (0.9/1)
appendBucketName (HttpRequest, StringBuilder): void 100% (1/1)96%  (45/47)99%  (9.9/10)
appendPayloadMetadata (HttpRequest, StringBuilder): void 100% (1/1)97%  (73/75)99%  (4.9/5)
appendHttpHeaders (HttpRequest, SortedSetMultimap): void 100% (1/1)98%  (58/59)89%  (8/9)
<static initializer> 100% (1/1)100% (71/71)100% (2/2)
RequestAuthorizeSignature (SignatureWire, String, boolean, String, String, St... 100% (1/1)100% (44/44)100% (14/14)
appendAmzHeaders (SortedSetMultimap, StringBuilder): void 100% (1/1)100% (48/48)100% (6/6)
appendMethod (HttpRequest, StringBuilder): void 100% (1/1)100% (8/8)100% (2/2)
filter (HttpRequest): HttpRequest 100% (1/1)100% (24/24)100% (5/5)
replaceAuthorizationHeader (HttpRequest, String): HttpRequest 100% (1/1)100% (27/27)100% (2/2)
replaceDateHeader (HttpRequest): HttpRequest 100% (1/1)100% (15/15)100% (2/2)
     
class RequestAuthorizeSignature$1100% (1/1)100% (2/2)100% (8/8)100% (2/2)
RequestAuthorizeSignature$1 (): void 100% (1/1)100% (3/3)100% (1/1)
apply (Annotation): boolean 100% (1/1)100% (5/5)100% (1/1)

1/**
2 * Licensed to jclouds, Inc. (jclouds) under one or more
3 * contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  jclouds licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19package org.jclouds.s3.filters;
20 
21import static com.google.common.base.Preconditions.checkArgument;
22import static com.google.common.collect.Iterables.any;
23import static com.google.common.collect.Iterables.get;
24import static org.jclouds.Constants.PROPERTY_CREDENTIAL;
25import static org.jclouds.Constants.PROPERTY_IDENTITY;
26import static org.jclouds.aws.reference.AWSConstants.PROPERTY_AUTH_TAG;
27import static org.jclouds.aws.reference.AWSConstants.PROPERTY_HEADER_TAG;
28import static org.jclouds.http.utils.ModifyRequest.parseQueryToMap;
29import static org.jclouds.http.utils.ModifyRequest.replaceHeader;
30import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_SERVICE_PATH;
31import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_VIRTUAL_HOST_BUCKETS;
32import static org.jclouds.util.Strings2.toInputStream;
33 
34import java.lang.annotation.Annotation;
35import java.util.Arrays;
36import java.util.Collection;
37import java.util.Locale;
38import java.util.Set;
39import java.util.Map.Entry;
40 
41import javax.annotation.Resource;
42import javax.inject.Inject;
43import javax.inject.Named;
44import javax.inject.Provider;
45import javax.inject.Singleton;
46import javax.ws.rs.core.HttpHeaders;
47 
48import org.jclouds.Constants;
49import org.jclouds.crypto.Crypto;
50import org.jclouds.crypto.CryptoStreams;
51import org.jclouds.date.TimeStamp;
52import org.jclouds.http.HttpException;
53import org.jclouds.http.HttpRequest;
54import org.jclouds.http.HttpRequestFilter;
55import org.jclouds.http.HttpUtils;
56import org.jclouds.http.internal.SignatureWire;
57import org.jclouds.io.InputSuppliers;
58import org.jclouds.logging.Logger;
59import org.jclouds.rest.RequestSigner;
60import org.jclouds.rest.internal.GeneratedHttpRequest;
61import org.jclouds.s3.Bucket;
62 
63import com.google.common.annotations.VisibleForTesting;
64import com.google.common.base.Predicate;
65import com.google.common.collect.ImmutableSet;
66import com.google.common.collect.Multimap;
67import com.google.common.collect.Ordering;
68import com.google.common.collect.SortedSetMultimap;
69import com.google.common.collect.TreeMultimap;
70 
71/**
72 * Signs the S3 request.
73 * 
74 * @see <a href=
75 *      "http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?RESTAuthentication.html"
76 *      />
77 * @author Adrian Cole
78 * 
79 */
80@Singleton
81public class RequestAuthorizeSignature implements HttpRequestFilter, RequestSigner {
82   private static final Predicate<Annotation> ANNOTATIONTYPE_BUCKET = new Predicate<Annotation>() {
83      public boolean apply(Annotation input) {
84         return input.annotationType().equals(Bucket.class);
85      }
86   };
87 
88   private final String[] firstHeadersToSign = new String[] { HttpHeaders.DATE };
89 
90   public static Set<String> SIGNED_PARAMETERS = ImmutableSet.of("acl", "torrent", "logging", "location", "policy",
91            "requestPayment", "versioning", "versions", "versionId", "notification", "uploadId", "uploads",
92            "partNumber", "website", "response-content-type", "response-content-language", "response-expires",
93            "response-cache-control", "response-content-disposition", "response-content-encoding");
94 
95   private final SignatureWire signatureWire;
96   private final String accessKey;
97   private final String secretKey;
98   private final Provider<String> timeStampProvider;
99   private final Crypto crypto;
100   private final HttpUtils utils;
101 
102   @Resource
103   @Named(Constants.LOGGER_SIGNATURE)
104   Logger signatureLog = Logger.NULL;
105 
106   private final String authTag;
107   private final String headerTag;
108   private final String servicePath;
109   private final boolean isVhostStyle;
110 
111   @Inject
112   public RequestAuthorizeSignature(SignatureWire signatureWire, @Named(PROPERTY_AUTH_TAG) String authTag,
113            @Named(PROPERTY_S3_VIRTUAL_HOST_BUCKETS) boolean isVhostStyle,
114            @Named(PROPERTY_S3_SERVICE_PATH) String servicePath, @Named(PROPERTY_HEADER_TAG) String headerTag,
115            @Named(PROPERTY_IDENTITY) String accessKey, @Named(PROPERTY_CREDENTIAL) String secretKey,
116            @TimeStamp Provider<String> timeStampProvider, Crypto crypto, HttpUtils utils) {
117      this.isVhostStyle = isVhostStyle;
118      this.servicePath = servicePath;
119      this.headerTag = headerTag;
120      this.authTag = authTag;
121      this.signatureWire = signatureWire;
122      this.accessKey = accessKey;
123      this.secretKey = secretKey;
124      this.timeStampProvider = timeStampProvider;
125      this.crypto = crypto;
126      this.utils = utils;
127   }
128 
129   public HttpRequest filter(HttpRequest request) throws HttpException {
130      request = replaceDateHeader(request);
131      String signature = calculateSignature(createStringToSign(request));
132      request = replaceAuthorizationHeader(request, signature);
133      utils.logRequest(signatureLog, request, "<<");
134      return request;
135   }
136 
137   HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
138      request = replaceHeader(request, HttpHeaders.AUTHORIZATION, authTag + " " + accessKey + ":" + signature);
139      return request;
140   }
141 
142   HttpRequest replaceDateHeader(HttpRequest request) {
143      request = replaceHeader(request, HttpHeaders.DATE, timeStampProvider.get());
144      return request;
145   }
146 
147   public String createStringToSign(HttpRequest request) {
148      utils.logRequest(signatureLog, request, ">>");
149      SortedSetMultimap<String, String> canonicalizedHeaders = TreeMultimap.create();
150      StringBuilder buffer = new StringBuilder();
151      // re-sign the request
152      appendMethod(request, buffer);
153      appendPayloadMetadata(request, buffer);
154      appendHttpHeaders(request, canonicalizedHeaders);
155 
156      // Remove default date timestamp if "x-amz-date" is set.
157      if (canonicalizedHeaders.containsKey("x-" + headerTag + "-date")) {
158         canonicalizedHeaders.removeAll("date");
159      }
160 
161      appendAmzHeaders(canonicalizedHeaders, buffer);
162      if (isVhostStyle)
163         appendBucketName(request, buffer);
164      appendUriPath(request, buffer);
165      if (signatureWire.enabled())
166         signatureWire.output(buffer.toString());
167      return buffer.toString();
168   }
169 
170   String calculateSignature(String toSign) throws HttpException {
171      String signature = sign(toSign);
172      if (signatureWire.enabled())
173         signatureWire.input(toInputStream(signature));
174      return signature;
175   }
176 
177   public String sign(String toSign) {
178      String signature;
179      try {
180         signature = CryptoStreams.base64(CryptoStreams.mac(InputSuppliers.of(toSign), crypto.hmacSHA1(secretKey
181                  .getBytes())));
182      } catch (Exception e) {
183         throw new HttpException("error signing request", e);
184      }
185      return signature;
186   }
187 
188   void appendMethod(HttpRequest request, StringBuilder toSign) {
189      toSign.append(request.getMethod()).append("\n");
190   }
191 
192   @VisibleForTesting
193   void appendAmzHeaders(SortedSetMultimap<String, String> canonicalizedHeaders, StringBuilder toSign) {
194      for (Entry<String, String> header : canonicalizedHeaders.entries()) {
195         String key = header.getKey();
196         if (key.startsWith("x-" + headerTag + "-")) {
197            toSign.append(String.format("%s:%s\n", key.toLowerCase(), header.getValue()));
198         }
199      }
200   }
201 
202   void appendPayloadMetadata(HttpRequest request, StringBuilder buffer) {
203      // note that we fall back to headers, and some requests such as ?uploads do not have a
204      // payload, yet specify payload related parameters
205      buffer.append(
206               request.getPayload() == null ? utils.valueOrEmpty(request.getFirstHeaderOrNull("Content-MD5")) : utils
207                        .valueOrEmpty(request.getPayload() == null ? null : request.getPayload().getContentMetadata()
208                                 .getContentMD5())).append("\n");
209      buffer.append(
210               utils.valueOrEmpty(request.getPayload() == null ? request.getFirstHeaderOrNull(HttpHeaders.CONTENT_TYPE)
211                        : request.getPayload().getContentMetadata().getContentType())).append("\n");
212      for (String header : firstHeadersToSign)
213         buffer.append(valueOrEmpty(request.getHeaders().get(header))).append("\n");
214   }
215 
216   @VisibleForTesting
217   void appendHttpHeaders(HttpRequest request, SortedSetMultimap<String, String> canonicalizedHeaders) {
218      Multimap<String, String> headers = request.getHeaders();
219      for (Entry<String, String> header : headers.entries()) {
220         if (header.getKey() == null)
221            continue;
222         String key = header.getKey().toString().toLowerCase(Locale.getDefault());
223         // Ignore any headers that are not particularly interesting.
224         if (key.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE) || key.equalsIgnoreCase("Content-MD5")
225                  || key.equalsIgnoreCase(HttpHeaders.DATE) || key.startsWith("x-" + headerTag + "-")) {
226            canonicalizedHeaders.put(key, header.getValue());
227         }
228      }
229   }
230 
231   @VisibleForTesting
232   void appendBucketName(HttpRequest req, StringBuilder toSign) {
233      checkArgument(req instanceof GeneratedHttpRequest<?>, "this should be a generated http request");
234      GeneratedHttpRequest<?> request = GeneratedHttpRequest.class.cast(req);
235 
236      String bucketName = null;
237 
238      for (int i = 0; i < request.getJavaMethod().getParameterAnnotations().length; i++) {
239         if (any(Arrays.asList(request.getJavaMethod().getParameterAnnotations()[i]), ANNOTATIONTYPE_BUCKET)) {
240            bucketName = (String) request.getArgs().get(i);
241            break;
242         }
243      }
244 
245      if (bucketName != null)
246         toSign.append(servicePath).append(bucketName);
247   }
248 
249   @VisibleForTesting
250   void appendUriPath(HttpRequest request, StringBuilder toSign) {
251 
252      toSign.append(request.getEndpoint().getRawPath());
253 
254      // ...however, there are a few exceptions that must be included in the
255      // signed URI.
256      if (request.getEndpoint().getQuery() != null) {
257         Multimap<String, String> params = parseQueryToMap(request.getEndpoint().getQuery());
258         char separator = '?';
259         for (String paramName : Ordering.natural().sortedCopy(params.keySet())) {
260            // Skip any parameters that aren't part of the canonical signed string
261            if (SIGNED_PARAMETERS.contains(paramName) == false)
262               continue;
263            toSign.append(separator).append(paramName);
264            String paramValue = get(params.get(paramName), 0);
265            if (paramValue != null) {
266               toSign.append("=").append(paramValue);
267            }
268            separator = '&';
269         }
270      }
271   }
272 
273   private String valueOrEmpty(Collection<String> collection) {
274      return (collection != null && collection.size() >= 1) ? collection.iterator().next() : "";
275   }
276}

[all classes][org.jclouds.s3.filters]
EMMA 2.0.5312 (C) Vladimir Roubtsov