com.emc.vipr.services.s3.ViPRS3Signer.java Source code

Java tutorial

Introduction

Here is the source code for com.emc.vipr.services.s3.ViPRS3Signer.java

Source

/*
 * Copyright 2013 EMC Corporation. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.emc.vipr.services.s3;

import com.amazonaws.AmazonClientException;
import com.amazonaws.Request;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSSessionCredentials;
import com.amazonaws.auth.SigningAlgorithm;
import com.amazonaws.services.s3.Headers;
import com.amazonaws.services.s3.internal.S3Signer;
import com.amazonaws.services.s3.internal.ServiceUtils;
import com.amazonaws.services.s3.model.ResponseHeaderOverrides;
import com.amazonaws.util.HttpUtils;
import com.emc.vipr.services.s3.model.ViPRConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.*;

/**
 * This class overrides S3Signer.sign and replaces RestUtils.makeS3CanonicalString for the sole purpose of adding
 * ViPR-specific headers and parameters to the canonical string when signing.
 */
public class ViPRS3Signer extends S3Signer {
    private static final Log log = LogFactory.getLog(ViPRS3Signer.class);

    protected String httpVerb, resourcePath;

    public ViPRS3Signer(String httpVerb, String resourcePath) {
        super(httpVerb, resourcePath);
        this.httpVerb = httpVerb;
        this.resourcePath = resourcePath;
    }

    @Override
    public void sign(Request<?> request, AWSCredentials credentials) throws AmazonClientException {
        if (credentials == null || credentials.getAWSSecretKey() == null) {
            log.debug("Canonical string will not be signed, as no AWS Secret Key was provided");
            return;
        }

        AWSCredentials sanitizedCredentials = sanitizeCredentials(credentials);
        if (sanitizedCredentials instanceof AWSSessionCredentials) {
            addSessionCredentials(request, (AWSSessionCredentials) sanitizedCredentials);
        }

        /*
         * In s3 sigv2, the way slash characters are encoded should be
         * consistent in both the request url and the encoded resource path.
         * Since we have to encode "//" to "/%2F" in the request url to make
         * httpclient works, we need to do the same encoding here for the
         * resource path.
         */
        String encodedResourcePath = HttpUtils.appendUri(request.getEndpoint().getPath(), resourcePath, true);

        Date date = getSignatureDate(request.getTimeOffset());
        request.addHeader(Headers.DATE, ServiceUtils.formatRfc822Date(date));
        String canonicalString = makeS3CanonicalString(httpVerb, encodedResourcePath, request, null);
        log.debug("Calculated string to sign:\n\"" + canonicalString + "\"");

        String signature = super.signAndBase64Encode(canonicalString, sanitizedCredentials.getAWSSecretKey(),
                SigningAlgorithm.HmacSHA1);
        request.addHeader("Authorization", "AWS " + sanitizedCredentials.getAWSAccessKeyId() + ":" + signature);
    }

    /**
     * The set of request parameters which must be included in the canonical
     * string to sign.
     */
    protected static final List<String> SIGNED_PARAMETERS = Arrays.asList("acl", "torrent", "logging", "location",
            "policy", "requestPayment", "versioning", "versions", "versionId", "notification", "uploadId",
            "uploads", "partNumber", "website", "delete", "lifecycle", "tagging", "cors", "restore",
            ResponseHeaderOverrides.RESPONSE_HEADER_CACHE_CONTROL,
            ResponseHeaderOverrides.RESPONSE_HEADER_CONTENT_DISPOSITION,
            ResponseHeaderOverrides.RESPONSE_HEADER_CONTENT_ENCODING,
            ResponseHeaderOverrides.RESPONSE_HEADER_CONTENT_LANGUAGE,
            ResponseHeaderOverrides.RESPONSE_HEADER_CONTENT_TYPE, ResponseHeaderOverrides.RESPONSE_HEADER_EXPIRES,
            ViPRConstants.ACCESS_MODE_PARAMETER, ViPRConstants.FILE_ACCESS_PARAMETER,
            ViPRConstants.ENDPOINT_PARAMETER);

    /**
     * Calculate the canonical string for a REST/HTTP request to S3.
     * <p/>
     * When expires is non-null, it will be used instead of the Date header.
     */
    protected String makeS3CanonicalString(String method, String resource, Request<?> request, String expires) {
        StringBuilder buf = new StringBuilder();
        buf.append(method).append("\n");

        // Add all interesting headers to a list, then sort them.  "Interesting"
        // is defined as Content-MD5, Content-Type, Date, and x-amz- and x-emc-
        Map<String, String> headersMap = request.getHeaders();
        SortedMap<String, String> interestingHeaders = new TreeMap<String, String>();
        if (headersMap != null && headersMap.size() > 0) {
            for (Map.Entry<String, String> entry : headersMap.entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();

                if (key == null)
                    continue;
                String lk = key.toLowerCase(Locale.getDefault());

                // Ignore any headers that are not particularly interesting.
                if (lk.equals("content-type") || lk.equals("content-md5") || lk.equals("date")
                        || lk.startsWith(Headers.AMAZON_PREFIX) || lk.startsWith(ViPRConstants.EMC_PREFIX)) {
                    interestingHeaders.put(lk, value);
                }
            }
        }

        // Remove default date timestamp if "x-amz-date" is set.
        if (interestingHeaders.containsKey(Headers.S3_ALTERNATE_DATE)) {
            interestingHeaders.put("date", "");
        }

        // Use the expires value as the timestamp if it is available. This trumps both the default
        // "date" timestamp, and the "x-amz-date" header.
        if (expires != null) {
            interestingHeaders.put("date", expires);
        }

        // These headers require that we still put a new line in after them,
        // even if they don't exist.
        if (!interestingHeaders.containsKey("content-type")) {
            interestingHeaders.put("content-type", "");
        }
        if (!interestingHeaders.containsKey("content-md5")) {
            interestingHeaders.put("content-md5", "");
        }

        // Any parameters that are prefixed with "x-amz-" need to be included
        // in the headers section of the canonical string to sign
        for (Map.Entry<String, String> parameter : request.getParameters().entrySet()) {
            if (parameter.getKey().startsWith("x-amz-")) {
                interestingHeaders.put(parameter.getKey(), parameter.getValue());
            }
        }

        // Add all the interesting headers (i.e.: all that startwith x-amz- or x-emc- ;-))
        for (Map.Entry<String, String> entry : interestingHeaders.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();

            if (key.startsWith(Headers.AMAZON_PREFIX) || key.startsWith(ViPRConstants.EMC_PREFIX)) {
                buf.append(key).append(':').append(value);
            } else {
                buf.append(value);
            }
            buf.append("\n");
        }

        // Add all the interesting parameters
        buf.append(resource);
        String[] parameterNames = request.getParameters().keySet()
                .toArray(new String[request.getParameters().size()]);
        Arrays.sort(parameterNames);
        char separator = '?';
        for (String parameterName : parameterNames) {
            // Skip any parameters that aren't part of the canonical signed string
            if (!SIGNED_PARAMETERS.contains(parameterName))
                continue;

            buf.append(separator);
            buf.append(parameterName);
            String parameterValue = request.getParameters().get(parameterName);
            if (parameterValue != null) {
                buf.append("=").append(parameterValue);
            }

            separator = '&';
        }

        return buf.toString();
    }
}