com.kolich.aws.services.s3.impl.KolichS3Signer.java Source code

Java tutorial

Introduction

Here is the source code for com.kolich.aws.services.s3.impl.KolichS3Signer.java

Source

/**
 * Copyright (c) 2014 Mark S. Kolich
 * http://mark.koli.ch
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kolich.aws.services.s3.impl;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.kolich.aws.services.AbstractAwsSigner;
import com.kolich.aws.signing.AwsCredentials;
import com.kolich.aws.signing.AwsSigner;
import com.kolich.aws.signing.impl.KolichAwsSigner;
import com.kolich.aws.transport.AwsHttpRequest;
import com.kolich.aws.transport.SortableBasicNameValuePair;
import com.kolich.common.date.RFC822DateFormat;
import org.apache.http.Header;
import org.apache.http.NameValuePair;
import org.apache.http.client.utils.URLEncodedUtils;

import java.util.*;

import static com.kolich.aws.signing.impl.KolichAwsSigner.AwsSigningAlgorithm.HmacSHA1;
import static com.kolich.aws.transport.AwsHeaders.AMAZON_PREFIX;
import static com.kolich.aws.transport.AwsHeaders.S3_ALTERNATE_DATE;
import static com.kolich.aws.transport.SortableBasicNameValuePair.sortParams;
import static com.kolich.common.DefaultCharacterEncoding.UTF_8;
import static org.apache.commons.io.IOUtils.LINE_SEPARATOR_UNIX;
import static org.apache.http.HttpHeaders.*;
import static org.apache.http.entity.ContentType.APPLICATION_FORM_URLENCODED;

public final class KolichS3Signer extends AbstractAwsSigner {

    private static final String APPLICATION_FORM_URLENCODED_TYPE = APPLICATION_FORM_URLENCODED.toString();

    public KolichS3Signer(final AwsCredentials credentials, final AwsSigner signer) {
        super(credentials, signer);
    }

    public KolichS3Signer(final AwsCredentials credentials) {
        this(credentials, new KolichAwsSigner(HmacSHA1));
    }

    public KolichS3Signer(final String key, final String secret) {
        this(new AwsCredentials(key, secret));
    }

    /**
      * The set of request parameters which must be included in
      * the canonical string to sign.  Note that these must be
      * sorted alphabetically, something that the AWS documentation
      * does not make very clear upfront.
      */
    private static final List<String> INTERESTING_PARAMETERS = Arrays.asList("acl", "torrent", "logging",
            "location", "policy", "requestPayment", "versioning", "versions", "versionId", "notification");

    @Override
    public final void signHttpRequest(final AwsHttpRequest request) throws Exception {
        // Add a Date header to the request.
        request.addHeader(DATE, RFC822DateFormat.format(new Date()));
        // Only add a Content-Type header to the request if one is not
        // already there. AWS expects something useful here.
        if (request.getFirstHeader(CONTENT_TYPE) == null) {
            request.addHeader(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_TYPE);
        }
        final String toSign = getS3CanonicalString(request);
        final String signature = signer_.sign(credentials_, toSign);
        // Add the resulting Authorization header to the request.
        request.addHeader(AUTHORIZATION,
                // The format of the AWS required Authorization header.
                String.format("AWS %s:%s",
                        // The Access Key ID uniquely identifies an AWS account. You
                        // include it in AWS service requests to identify yourself as
                        // the sender of the request.
                        credentials_.getKey(),
                        // The computed S3 signature for this request.
                        signature));
    }

    /**
      * Calculate the canonical string for a REST/HTTP request to S3.
      */
    private static final String getS3CanonicalString(final AwsHttpRequest request) {
        // A few standard headers we extract for conveinence.
        final String contentType = CONTENT_TYPE.toLowerCase(), contentMd5 = CONTENT_MD5.toLowerCase(),
                date = DATE.toLowerCase();
        // Start with the empty string ("").
        final StringBuilder buf = new StringBuilder();
        // Next is the HTTP verb and a newline.
        buf.append(request.getMethod() + LINE_SEPARATOR_UNIX);
        // Add all interesting headers to a list, then sort them.
        // "Interesting" is defined as Content-MD5, Content-Type, Date,
        // and x-amz-... headers.
        final Map<String, String> headersMap = getHeadersAsMap(request);
        final SortedMap<String, String> interesting = Maps.newTreeMap();
        if (!headersMap.isEmpty()) {
            Iterator<Map.Entry<String, String>> it = headersMap.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, String> entry = it.next();
                final String key = entry.getKey(), value = entry.getValue();
                if (key == null) {
                    continue;
                }
                final String lk = key.toLowerCase(Locale.getDefault());
                // Ignore any headers that are not interesting.
                if (lk.equals(contentType) || lk.equals(contentMd5) || lk.equals(date)
                        || lk.startsWith(AMAZON_PREFIX)) {
                    interesting.put(lk, value);
                }
            }
        }
        // Remove default date timestamp if "x-amz-date" is set.
        if (interesting.containsKey(S3_ALTERNATE_DATE)) {
            interesting.put(date, "");
        }
        // These headers require that we still put a new line in after them,
        // even if they don't exist.
        if (!interesting.containsKey(contentType)) {
            interesting.put(contentType, "");
        }
        if (!interesting.containsKey(contentMd5)) {
            interesting.put(contentMd5, "");
        }
        // Add all the interesting headers
        for (Iterator<Map.Entry<String, String>> i = interesting.entrySet().iterator(); i.hasNext();) {
            final Map.Entry<String, String> entry = i.next();
            final String key = entry.getKey();
            final Object value = entry.getValue();
            if (key.startsWith(AMAZON_PREFIX)) {
                buf.append(key).append(':').append(value);
            } else {
                buf.append(value);
            }
            buf.append(LINE_SEPARATOR_UNIX);
        }
        // The CanonicalizedResource this request is working with.
        // If the request specifies a bucket using the HTTP Host header
        // (virtual hosted-style), append the bucket name preceded by a
        // "/" (e.g., "/bucketname"). For path-style requests and requests
        // that don't address a bucket, do nothing.
        if (request.getResource() != null) {
            buf.append("/" + request.getResource() + request.getURI().getRawPath());
        } else {
            buf.append(request.getURI().getRawPath());
        }
        // Amazon requires us to sort the query string parameters.
        final List<SortableBasicNameValuePair> params = sortParams(URLEncodedUtils.parse(request.getURI(), UTF_8));
        String separator = "?";
        for (final NameValuePair pair : params) {
            final String name = pair.getName(), value = pair.getValue();
            // Skip any parameters that aren't part of the
            // canonical signed string.
            if (!INTERESTING_PARAMETERS.contains(name)) {
                continue;
            }
            buf.append(separator).append(name);
            if (value != null) {
                buf.append("=").append(value);
            }
            separator = "&";
        }
        return buf.toString();
    }

    /**
     * Given an HttpRequestBase, extracts a Map containing each header to
     * value pair.  The returned Map is unmodifiable.
     */
    private static final ImmutableMap<String, String> getHeadersAsMap(final AwsHttpRequest request) {
        final ImmutableMap.Builder<String, String> map = ImmutableMap.builder();
        for (final Header h : request.getRequestBase().getAllHeaders()) {
            map.put(h.getName(), h.getValue());
        }
        return map.build();
    }

    @Override
    public final String toString() {
        return String.format("%s(%s, %s)", getClass().getSimpleName(), credentials_.toString(), signer_.toString());
    }

}