com.amazonaws.services.s3.internal.AWSS3V4Signer.java Source code

Java tutorial

Introduction

Here is the source code for com.amazonaws.services.s3.internal.AWSS3V4Signer.java

Source

/*
 * Copyright 2013-2014 Amazon.com, Inc. or its affiliates. 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://aws.amazon.com/apache2.0
 *
 * 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.amazonaws.services.s3.internal;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.amazonaws.AmazonClientException;
import com.amazonaws.Request;
import com.amazonaws.auth.AWS4Signer;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSSessionCredentials;
import com.amazonaws.auth.AnonymousAWSCredentials;
import com.amazonaws.auth.AwsChunkedEncodingInputStream;
import com.amazonaws.auth.Presigner;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.amazonaws.services.s3.model.UploadPartRequest;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.Headers;
import com.amazonaws.util.BinaryUtils;

/**
 * AWS4 signer implementation for AWS S3
 */
public class AWSS3V4Signer extends AWS4Signer implements Presigner {

    /** We use the logger for AmazonS3Client to */
    private static Log clientLog = LogFactory.getLog(AmazonS3Client.class);

    /** Seconds in a week, which is the max expiration time Sig-v4 accepts */
    private final static long MAX_EXPIRATION_TIME_IN_SECONDS = 60 * 60 * 24 * 7;

    /**
     * Don't double-url-encode path elements; S3 expects path elements to be
     * encoded only once in the canonical URI.
     */
    public AWSS3V4Signer() {
        super(false);
    }

    /**
     * Sign the URL using V4, it will compute the signature and add it to the
     * query string as value of parameter 'X-Amz-Signature'.
     */
    public void presignRequest(Request<?> request, AWSCredentials credentials, Date expiration) {

        addHostHeader(request);

        // annonymous credentials, don't sign
        if (credentials instanceof AnonymousAWSCredentials) {
            return;
        }

        AWSCredentials sanitizedCredentials = sanitizeCredentials(credentials);
        if (sanitizedCredentials instanceof AWSSessionCredentials) {
            addSessionCredentials(request, (AWSSessionCredentials) sanitizedCredentials);
            // For SigV4 presigning URL, we need to add "x-amz-security-token"
            // as a query string parameter, before constructing the canonical
            // request.
            request.addParameter(Headers.SECURITY_TOKEN,
                    ((AWSSessionCredentials) sanitizedCredentials).getSessionToken());
        }

        long dateMilli = getDateFromRequest(request);
        final String dateStamp = getDateStamp(dateMilli);

        String scope = getScope(request, dateStamp);

        String signingCredentials = sanitizedCredentials.getAWSAccessKeyId() + "/" + scope;
        long expirationInSeconds = (expiration.getTime() - System.currentTimeMillis()) / 1000L;
        if (expirationInSeconds > MAX_EXPIRATION_TIME_IN_SECONDS) {
            throw new AmazonClientException(
                    "Requests that are pre-signed by SigV4 algorithm are valid for at most 7 days. "
                            + "The expiration date set on the current request ["
                            + getTimeStamp(expiration.getTime()) + "] has exceeded this limit.");
        }

        // Add the important parameters for v4 signing
        long now = System.currentTimeMillis();
        final String timeStamp = getTimeStamp(now);
        request.addParameter("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
        request.addParameter("X-Amz-Date", timeStamp);
        request.addParameter("X-Amz-SignedHeaders", getSignedHeadersString(request));
        request.addParameter("X-Amz-Expires", Long.toString(expirationInSeconds));
        request.addParameter("X-Amz-Credential", signingCredentials);

        String contentSha256 = "UNSIGNED-PAYLOAD";

        HeaderSigningResult headerSigningResult = computeSignature(request, dateStamp, timeStamp, ALGORITHM,
                contentSha256, sanitizedCredentials);
        request.addParameter("X-Amz-Signature", BinaryUtils.toHex(headerSigningResult.getSignature()));
    }

    /**
     * If necessary, creates a chunk-encoding wrapper on the request payload.
     */
    @Override
    protected void processRequestPayload(Request<?> request, HeaderSigningResult headerSigningResult) {
        if (useChunkEncoding(request)) {
            InputStream payloadStream = request.getContent();
            String dateTime = headerSigningResult.getDateTime();
            String keyPath = headerSigningResult.getScope();
            byte[] kSigning = headerSigningResult.getKSigning();
            String signature = BinaryUtils.toHex(headerSigningResult.getSignature());
            AwsChunkedEncodingInputStream chunkEncodededStream = new AwsChunkedEncodingInputStream(payloadStream,
                    kSigning, dateTime, keyPath, signature, this);
            request.setContent(chunkEncodededStream);
        }
    }

    /**
     * Returns the pre-defined header value and set other necessary headers
     * if the request needs to be chunk-encoded. Otherwise calls the superclass
     * method which calculates the hash of the whole content for signing.
     */
    @Override
    protected String calculateContentHash(Request<?> request) {
        // To be consistent with other service clients using sig-v4,
        // we just set the header as "required", and AWS4Signer.sign() will be notified
        // to pick up the header value returned by this method.
        request.addHeader("x-amz-content-sha256", "required");
        if (useChunkEncoding(request)) {
            String contentSha256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
            long originalContentLength;
            if (request.getHeaders().containsKey(Headers.CONTENT_LENGTH)) {
                originalContentLength = Long.parseLong(request.getHeaders().get(Headers.CONTENT_LENGTH));
            } else {
                /**
                 * "Content-Length" header could be missing if the caller is uploading a stream
                 * without setting Content-Length in ObjectMetadata.
                 * Before using sigv4, we rely on HttpClient to add this header by using BufferedHttpEntity
                 * when creating the HttpRequest object. But now, we need this information immediately
                 * for the signing process, so we have to cache the stream here.
                 */
                try {
                    originalContentLength = getContentLength(request);
                } catch (IOException e) {
                    throw new AmazonClientException("Cannot get the content-lenght of the request content.", e);
                }
            }
            request.addHeader("x-amz-decoded-content-length", Long.toString(originalContentLength));
            // Make sure "Content-Length" header is not empty so that HttpClient won't cache the stream again to recover Content-Length
            request.addHeader(Headers.CONTENT_LENGTH, Long
                    .toString(AwsChunkedEncodingInputStream.calculateStreamContentLength(originalContentLength)));
            return contentSha256;
        } else
            return super.calculateContentHash(request);
    }

    /**
     * Determine whether to use aws-chunked for signing
     */
    private static boolean useChunkEncoding(Request<?> request) {
        // Whether to use chunked encoding for signing the request
        boolean chunkedEncodingEnabled = false;
        if (request.getOriginalRequest() instanceof PutObjectRequest
                || request.getOriginalRequest() instanceof UploadPartRequest) {
            chunkedEncodingEnabled = true;
        }
        return chunkedEncodingEnabled;
    }

    /**
     * Read the content of the request to get the length of the stream.
     * This method will wrap the stream by RepeatableInputStream if it is 
     * not mark-supported.
     */
    private static long getContentLength(Request<?> request) throws IOException {
        InputStream content = request.getContent();
        if (!content.markSupported()) {
            int streamBufferSize = Constants.getStreamBufferSize();
            content = new RepeatableInputStream(content, streamBufferSize);
            request.setContent(content);
        }

        long contentLength = 0;
        byte[] tmp = new byte[4096];
        int read;
        try {
            content.mark(-1);
            while ((read = content.read(tmp)) != -1) {
                contentLength += read;
            }
            content.reset();
        } finally {
            content.close();
        }

        return contentLength;
    }
}