com.terremark.handlers.CloudApiAuthenticationHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.terremark.handlers.CloudApiAuthenticationHandler.java

Source

/**
 * Copyright 2012 Terremark Worldwide Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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.terremark.handlers;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.core.HttpHeaders;

import org.apache.commons.codec.binary.Base64;
import org.apache.wink.client.ClientRequest;
import org.apache.wink.client.ClientResponse;
import org.apache.wink.client.handlers.ClientHandler;
import org.apache.wink.client.handlers.HandlerContext;
import org.apache.wink.client.internal.handlers.ClientRequestImpl;

import com.terremark.annotations.AuthenticationNotRequired;
import com.terremark.config.SignatureAlgorithm;

/**
 * Cloud API authentication handler. If the client is configured for Cloud API authentication this handler should be
 * added to the chain. This handler will add the {@code x-tmrk-authorization} header before the request is sent to the
 * server.
 *
 * @author <a href="mailto:spasam@terremark.com">Seshu Pasam</a>
 */
public class CloudApiAuthenticationHandler implements ClientHandler {
    /** Terremark specific headers prefix */
    private static final String TERREMARK_HEADER_PREFIX = "x-tmrk-";
    /** Terremark specific authorization header */
    private static final String TERREMARK_AUTHORIZATION_HEADER = "x-tmrk-authorization";
    /** Access key for Cloud API signing */
    private final String accessKey;
    /** Private key for Cloud API signing */
    private transient final String privateKey;
    /** Signing algorithm */
    private final SignatureAlgorithm algorithm;

    /**
     * Default constructor.
     *
     * @param accessKey Access key.
     * @param privateKey Private key.
     * @param algorithm Signing algorithm.
     */
    public CloudApiAuthenticationHandler(final String accessKey, final String privateKey,
            final SignatureAlgorithm algorithm) {
        this.accessKey = accessKey;
        this.privateKey = privateKey;
        this.algorithm = algorithm;
    }

    /**
     * Method invoked in the chain. If the request entity class is not annotated with
     * {@link com.terremark.annotations.AuthenticationNotRequired AuthenticationNotRequired} this method will add the
     * {@code x-tmrk-authorization} header.
     *
     * @param request Client request.
     * @param context Request context.
     * @return Client response.
     * @throws Exception If there is a problem invoking the chain.
     */
    @Override
    @SuppressWarnings("PMD.SignatureDeclareThrowsException")
    public final ClientResponse handle(final ClientRequest request, final HandlerContext context) throws Exception {
        final Class<?> responseEntityClass = (Class<?>) request.getAttributes()
                .get(ClientRequestImpl.RESPONSE_ENTITY_CLASS_TYPE);
        if (responseEntityClass == null
                || !responseEntityClass.isAnnotationPresent(AuthenticationNotRequired.class)) {
            request.getHeaders().putSingle(TERREMARK_AUTHORIZATION_HEADER,
                    getTerremarkAuthorizationHeaderValue(request));
        }

        return context.doChain(request);
    }

    /**
     * Builds a formatted string a Terremark specific headers (excluding x-tmrk-authorization).
     *
     * @param request Client request.
     * @return Formatted string of Terremark specific headers and values.
     */
    private static String getCanonicalizedHeaders(final ClientRequest request) {
        final Map<String, String> headers = new TreeMap<String, String>();

        for (final Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
            final String headerName = entry.getKey().toLowerCase();

            if (!headerName.startsWith(TERREMARK_HEADER_PREFIX)
                    || TERREMARK_AUTHORIZATION_HEADER.equals(headerName)) {
                continue;
            }

            if (entry.getValue() != null && entry.getValue().size() > 0) {
                headers.put(headerName, entry.getValue().get(0));
            }
        }

        final StringBuilder builder = new StringBuilder();
        for (final Map.Entry<String, String> entry : headers.entrySet()) {
            builder.append(entry.getKey()).append(':').append(entry.getValue()).append('\n');
        }

        if (builder.length() < 1) {
            builder.append('\n');
        }

        return builder.toString();
    }

    /**
     * Builds a formatted string for the URI and the query string.
     *
     * @param request Client request.
     * @return Formatted URI and query arguments.
     */
    private static String getCanonicalizedResource(final ClientRequest request) {
        final Map<String, String> queryMap = new TreeMap<String, String>();
        final URI uri = request.getURI();
        final String query = uri.getQuery();

        if (query != null && query.length() > 0) {
            final String[] parts = query.split("&");

            for (final String part : parts) {
                final String[] nameValue = part.split("=");
                if (nameValue.length == 2) {
                    queryMap.put(nameValue[0].toLowerCase(), nameValue[1]);
                }
            }
        }

        final StringBuilder builder = new StringBuilder();
        builder.append(uri.getPath().toLowerCase()).append('\n');

        for (final Map.Entry<String, String> entry : queryMap.entrySet()) {
            builder.append(entry.getKey()).append(':').append(entry.getValue()).append('\n');
        }

        return builder.toString();
    }

    /**
     * Returns the content length header value.
     *
     * @param request Client request.
     * @return Content length.
     */
    private static String getContentLength(final ClientRequest request) {
        return getHeaderValue(request, HttpHeaders.CONTENT_LENGTH);
    }

    /**
     * Returns the content type header value.
     *
     * @param request Client request.
     * @return Content type.
     */
    private static String getContentType(final ClientRequest request) {
        return getHeaderValue(request, HttpHeaders.CONTENT_TYPE);
    }

    /**
     * Returns the date header value.
     *
     * @param request Client request.
     * @return Date.
     */
    private static String getDate(final ClientRequest request) {
        return getHeaderValue(request, HttpHeaders.DATE);
    }

    /**
     * Generic method to return header value. If the header is not set, a new line is returned.
     *
     * @param request Client request.
     * @param headerName Header name.
     * @return Header value with new line appended.
     */
    private static String getHeaderValue(final ClientRequest request, final String headerName) {
        final List<String> value = request.getHeaders().get(headerName);

        return (value == null || value.isEmpty()) ? "\n" : (value.get(0) + '\n');
    }

    /**
     * Returns HMAC instance initialized with the private key.
     *
     * @return HMAC instance.
     * @throws NoSuchAlgorithmException If the HMAC algorithm is not available.
     * @throws InvalidKeyException If the private key is malformed.
     * @throws UnsupportedEncodingException If UTF-8 character encoding is not supported.
     */
    private Mac getMac() throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
        String algo;
        switch (algorithm) {
        case HMAC_SHA1:
            algo = "HmacSHA1";
            break;
        case HMAC_SHA256:
            algo = "HmacSHA256";
            break;
        case HMAC_SHA512:
            algo = "HmacSHA512";
            break;
        default:
            throw new UnsupportedOperationException("Not implemented: " + algorithm.toString());
        }

        final Mac mac = Mac.getInstance(algo);
        mac.init(new SecretKeySpec(privateKey.getBytes("UTF-8"), algo));

        return mac;
    }

    /**
     * Builds the string to sign and signs it.
     *
     * @param request Client request.
     * @return Signature.
     * @throws NoSuchAlgorithmException If the HMAC algorithm is not available.
     * @throws InvalidKeyException If the private key is malformed.
     * @throws UnsupportedEncodingException If UTF-8 character encoding is not supported.
     */
    private String getSignature(final ClientRequest request)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        final StringBuilder stringToSign = new StringBuilder().append(getVerb(request))
                .append(getContentLength(request)).append(getContentType(request)).append(getDate(request))
                .append(getCanonicalizedHeaders(request)).append(getCanonicalizedResource(request));

        final byte[] digest = getMac().doFinal(stringToSign.toString().getBytes("UTF-8"));

        return Base64.encodeBase64String(digest);
    }

    /**
     * Returns the signature type that should be specified in the {@code x-tmrk-authorization} header.
     *
     * @return Signature type used.
     */
    private String getSignatureType() {
        switch (algorithm) {
        case HMAC_SHA1:
            return "HmacSha1";
        case HMAC_SHA256:
            return "HmacSha256";
        case HMAC_SHA512:
            return "HmacSha512";
        default:
            throw new UnsupportedOperationException("Not implemented: " + algorithm.toString());
        }
    }

    /**
     * Returns the value of the {@code x-tmrk-authorization} header.
     *
     * @param request Client request.
     * @return {@code x-tmrk-authorization} header value.
     * @throws NoSuchAlgorithmException If the HMAC algorithm is not available.
     * @throws InvalidKeyException If the private key is malformed.
     * @throws UnsupportedEncodingException If UTF-8 character encoding is not supported.
     */
    private String getTerremarkAuthorizationHeaderValue(final ClientRequest request) throws InvalidKeyException,
            NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException {
        return "CloudApi AccessKey=\"" + accessKey + "\" SignatureType=\"" + getSignatureType() + "\" Signature=\""
                + getSignature(request) + "\"";
    }

    /**
     * The HTTP request type.
     *
     * @param request Client request.
     * @return Request verb
     */
    private static String getVerb(final ClientRequest request) {
        return request.getMethod().toUpperCase() + '\n';
    }
}