com.eucalyptus.objectstorage.pipeline.handlers.ObjectStorageAuthenticationHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.objectstorage.pipeline.handlers.ObjectStorageAuthenticationHandler.java

Source

/*************************************************************************
 * Copyright 2009-2012 Eucalyptus Systems, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 *
 * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
 * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
 * additional information or have any questions.
 *
 * This file may incorporate work covered under the following copyright
 * and permission notice:
 *
 *   Software License Agreement (BSD License)
 *
 *   Copyright (c) 2008, Regents of the University of California
 *   All rights reserved.
 *
 *   Redistribution and use of this software in source and binary forms,
 *   with or without modification, are permitted provided that the
 *   following conditions are met:
 *
 *     Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *     Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer
 *     in the documentation and/or other materials provided with the
 *     distribution.
 *
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *   "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *   LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 *   FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *   COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *   BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *   CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *   LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 *   ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *   POSSIBILITY OF SUCH DAMAGE. USERS OF THIS SOFTWARE ACKNOWLEDGE
 *   THE POSSIBLE PRESENCE OF OTHER OPEN SOURCE LICENSED MATERIAL,
 *   COPYRIGHTED MATERIAL OR PATENTED MATERIAL IN THIS SOFTWARE,
 *   AND IF ANY SUCH MATERIAL IS DISCOVERED THE PARTY DISCOVERING
 *   IT MAY INFORM DR. RICH WOLSKI AT THE UNIVERSITY OF CALIFORNIA,
 *   SANTA BARBARA WHO WILL THEN ASCERTAIN THE MOST APPROPRIATE REMEDY,
 *   WHICH IN THE REGENTS' DISCRETION MAY INCLUDE, WITHOUT LIMITATION,
 *   REPLACEMENT OF THE CODE SO IDENTIFIED, LICENSING OF THE CODE SO
 *   IDENTIFIED, OR WITHDRAWAL OF THE CODE CAPABILITY TO THE EXTENT
 *   NEEDED TO COMPLY WITH ANY SUCH LICENSES OR RIGHTS.
 ************************************************************************/

package com.eucalyptus.objectstorage.pipeline.handlers;

import com.eucalyptus.auth.login.SecurityContext;
import com.eucalyptus.auth.principal.Principals;
import com.eucalyptus.component.ComponentIds;
import com.eucalyptus.context.Context;
import com.eucalyptus.context.Contexts;
import com.eucalyptus.context.NoSuchContextException;
import com.eucalyptus.http.MappingHttpRequest;
import com.eucalyptus.objectstorage.ObjectStorage;
import com.eucalyptus.objectstorage.exceptions.s3.AccessDeniedException;
import com.eucalyptus.objectstorage.exceptions.s3.InternalErrorException;
import com.eucalyptus.objectstorage.exceptions.s3.InvalidAccessKeyIdException;
import com.eucalyptus.objectstorage.exceptions.s3.InvalidAddressingHeaderException;
import com.eucalyptus.objectstorage.exceptions.s3.InvalidSecurityException;
import com.eucalyptus.objectstorage.exceptions.s3.MissingSecurityHeaderException;
import com.eucalyptus.objectstorage.exceptions.s3.S3Exception;
import com.eucalyptus.objectstorage.exceptions.s3.SignatureDoesNotMatchException;
import com.eucalyptus.objectstorage.pipeline.auth.ObjectStorageWrappedCredentials;
import com.eucalyptus.objectstorage.util.OSGUtil;
import com.eucalyptus.objectstorage.util.ObjectStorageProperties;
import com.eucalyptus.ws.handlers.MessageStackHandler;
import com.eucalyptus.ws.server.Statistics;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.apache.commons.httpclient.util.DateUtil;
import org.apache.log4j.Logger;
import org.jboss.netty.channel.ChannelEvent;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.HttpHeaders;

import javax.security.auth.login.LoginException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;

@ChannelPipelineCoverage("one")
public class ObjectStorageAuthenticationHandler extends MessageStackHandler {
    private static Logger LOG = Logger.getLogger(ObjectStorageAuthenticationHandler.class);
    private static final String AWS_AUTH_TYPE = "AWS";
    private static final String EUCA_AUTH_TYPE = "EUCA2-RSA-SHA256";
    private static final String EUCA_OLD_AUTH_TYPE = "Euca";
    protected static final String ISO_8601_FORMAT = "yyyyMMdd'T'HHmmss'Z'"; //Use the ISO8601 format

    public static enum SecurityParameter {
        AWSAccessKeyId, Timestamp, Expires, Signature, Authorization, Date, Content_MD5, Content_Type, SecurityToken,
    }

    /* The possible fields in an authorization header */
    private static enum AuthorizationField {
        Type, AccessKeyId, CertFingerPrint, SignedHeaders, Signature
    }

    /**
     * Ensure that only one header for each name exists (i.e. not 2 Authorization headers)
     * Accomplish this by comma-delimited concatenating any duplicates found as per HTTP 1.1 RFC 2616 section 4.2
     * <p/>
     * TODO: Also, should convert all headers to lower-case for consistent processing later. This is okay since headers are case-insensitive.
     * <p/>
     * in HTTP
     *
     * @param httpRequest
     */
    private static void canonicalizeHeaders(MappingHttpRequest httpRequest) {
        //Iterate through headers and find duplicates, concatenate their values together and remove from
        // request as we find them.
        TreeMap<String, String> headerMap = new TreeMap<String, String>();
        String value = null;

        //Construct a map of the normalized headers, cannot modify in-place since
        // conconcurrent-modify exception may result
        for (String header : httpRequest.getHeaderNames()) {
            //TODO: zhill, put in the map in lower-case form.
            headerMap.put(header, Joiner.on(',').join(httpRequest.getHeaders(header)));
        }

        //Remove *all* headers
        httpRequest.clearHeaders();

        //Add the normalized headers back into the request
        for (String foundHeader : headerMap.keySet()) {
            httpRequest.addHeader(foundHeader, headerMap.get(foundHeader).toString());
        }
    }

    /**
     * This method exists to clean up a problem encountered periodically where the HTTP
     * headers are duplicated
     *
     * @param httpRequest
     */
    private static void removeDuplicateHeaderValues(MappingHttpRequest httpRequest) {
        List<String> hdrList = null;
        HashMap<String, List<String>> fixedHeaders = new HashMap<String, List<String>>();
        boolean foundDup = false;
        for (String header : httpRequest.getHeaderNames()) {
            hdrList = httpRequest.getHeaders(header);

            //Only address the specific case where there is exactly one identical copy of the header
            if (hdrList != null && hdrList.size() == 2 && hdrList.get(0).equals(hdrList.get(1))) {
                foundDup = true;
                fixedHeaders.put(header, Lists.newArrayList(hdrList.get(0)));
            } else {
                fixedHeaders.put(header, hdrList);
            }
        }

        if (foundDup) {
            LOG.debug("Found duplicate headers in: " + httpRequest.logMessage());
            httpRequest.clearHeaders();

            for (Map.Entry<String, List<String>> e : fixedHeaders.entrySet()) {
                for (String v : e.getValue()) {
                    httpRequest.addHeader(e.getKey(), v);
                }
            }
        }
    }

    @Override
    public void incomingMessage(ChannelHandlerContext ctx, MessageEvent event) throws Exception {
        if (event.getMessage() instanceof MappingHttpRequest) {
            MappingHttpRequest httpRequest = (MappingHttpRequest) event.getMessage();

            removeDuplicateHeaderValues(httpRequest);
            // Consolidate duplicates, etc.

            canonicalizeHeaders(httpRequest);
            handle(httpRequest);
        }
    }

    // Overriding this method to ensure that the message is passed to the next stage in the pipeline only if it passes authentication.
    @Override
    public void handleUpstream(final ChannelHandlerContext ctx, final ChannelEvent channelEvent) throws Exception {
        if (channelEvent instanceof MessageEvent) {
            try {
                final MessageEvent msgEvent = (MessageEvent) channelEvent;
                Callable<Long> stat = Statistics.startUpstream(ctx.getChannel(), this);
                this.incomingMessage(ctx, msgEvent);
                stat.call();
                ctx.sendUpstream(channelEvent);
            } catch (Throwable e) {
                Channels.fireExceptionCaught(ctx, e);
            }
        } else {
            ctx.sendUpstream(channelEvent);
        }
    }

    /**
     * Process the authorization header
     *
     * @param authorization
     * @return
     * @throws AccessDeniedException
     */
    public static Map<AuthorizationField, String> processAuthorizationHeader(String authorization)
            throws AccessDeniedException {
        if (Strings.isNullOrEmpty(authorization)) {
            return null;
        }

        HashMap<AuthorizationField, String> authMap = new HashMap<AuthorizationField, String>();
        String[] components = authorization.split(" ");

        if (components.length < 2) {
            throw new AccessDeniedException("Invalid authoriztion header");
        }

        if (AWS_AUTH_TYPE.equals(components[0]) && components.length == 2) {
            //Expect: components[1] = <AccessKeyId>:<Signature>
            authMap.put(AuthorizationField.Type, AWS_AUTH_TYPE);
            String[] signatureElements = components[1].split(":");
            authMap.put(AuthorizationField.AccessKeyId, signatureElements[0]);
            authMap.put(AuthorizationField.Signature, signatureElements[1]);
        } else if (EUCA_AUTH_TYPE.equals(components[0]) && components.length == 4) {
            //Expect: components[0] = EUCA2-RSA-SHA256 components[1] = <fingerprint of signing certificate> components[2] = <list of signed headers> components[3] = <Signature>
            authMap.put(AuthorizationField.Type, EUCA_AUTH_TYPE);
            authMap.put(AuthorizationField.CertFingerPrint, components[1].trim());
            authMap.put(AuthorizationField.SignedHeaders, components[2].trim());
            authMap.put(AuthorizationField.Signature, components[3].trim());
        } else if (EUCA_OLD_AUTH_TYPE.equals(components[0]) && components.length == 1) {
            authMap.put(AuthorizationField.Type, EUCA_OLD_AUTH_TYPE);
        } else {
            throw new AccessDeniedException("Invalid authorization header");
        }

        return authMap;
    }

    static class S3Authentication {
        /**
         * Authenticate using S3-spec REST authentication
         *
         * @param httpRequest
         * @param authMap
         * @throws AccessDeniedException
         */

        static void authenticate(MappingHttpRequest httpRequest, Map<AuthorizationField, String> authMap)
                throws S3Exception {
            if (!authMap.get(AuthorizationField.Type).equals(AWS_AUTH_TYPE)) {
                throw new InvalidSecurityException("Mismatch between expected and found authentication types");
            }

            //Standard S3 authentication signed by SecretKeyID
            String verb = httpRequest.getMethod().getName();
            String date = getDate(httpRequest);
            String addrString = getS3AddressString(httpRequest, true);
            String content_md5 = httpRequest.getHeader("Content-MD5");
            content_md5 = content_md5 == null ? "" : content_md5;
            String content_type = httpRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE);
            content_type = content_type == null ? "" : content_type;
            String securityToken = httpRequest.getHeader(ObjectStorageProperties.X_AMZ_SECURITY_TOKEN);
            String canonicalizedAmzHeaders = getCanonicalizedAmzHeaders(httpRequest);
            String data = verb + "\n" + content_md5 + "\n" + content_type + "\n" + date + "\n"
                    + canonicalizedAmzHeaders + addrString;
            String accessKeyId = authMap.get(AuthorizationField.AccessKeyId);
            String signature = authMap.get(AuthorizationField.Signature);

            try {
                SecurityContext.getLoginContext(new ObjectStorageWrappedCredentials(httpRequest.getCorrelationId(),
                        data, accessKeyId, signature, securityToken)).login();
            } catch (LoginException ex) {
                if (ex.getMessage().contains("The AWS Access Key Id you provided does not exist in our records")) {
                    throw new InvalidAccessKeyIdException(accessKeyId);
                }

                //Try using the '/services/ObjectStorage' portion of the addrString and retry the signature calc
                if (httpRequest.getUri().startsWith(ComponentIds.lookup(ObjectStorage.class).getServicePath())
                        || httpRequest.getUri().startsWith(ObjectStorageProperties.LEGACY_WALRUS_SERVICE_PATH)) {
                    try {
                        String modifiedAddrString = getS3AddressString(httpRequest, false);
                        data = verb + "\n" + content_md5 + "\n" + content_type + "\n" + date + "\n"
                                + canonicalizedAmzHeaders + modifiedAddrString;
                        SecurityContext
                                .getLoginContext(new ObjectStorageWrappedCredentials(httpRequest.getCorrelationId(),
                                        data, accessKeyId, signature, securityToken))
                                .login();
                    } catch (S3Exception ex2) {
                        LOG.debug("CorrelationId: " + httpRequest.getCorrelationId()
                                + " Authentication failed due to signature match issue:", ex2);
                        throw ex2;
                    } catch (Exception ex2) {
                        LOG.debug("CorrelationId: " + httpRequest.getCorrelationId()
                                + " Authentication failed due to signature match issue:", ex2);
                        throw new SignatureDoesNotMatchException(data);
                    }
                } else {
                    LOG.debug("CorrelationId: " + httpRequest.getCorrelationId()
                            + " Authentication failed due to signature mismatch:", ex);
                    throw new SignatureDoesNotMatchException(data);
                }
            } catch (Exception e) {
                LOG.warn("CorrelationId: " + httpRequest.getCorrelationId()
                        + " Unexpected failure trying to authenticate request", e);
                throw new InternalErrorException(e);
            }
        }

        /**
         * Authenticate using S3-spec query string authentication
         *
         * @param httpRequest
         * @throws AccessDeniedException
         */
        static void authenticateQueryString(MappingHttpRequest httpRequest) throws S3Exception {
            //Standard S3 query string authentication
            Map<String, String> parameters = httpRequest.getParameters();
            String verb = httpRequest.getMethod().getName();
            String content_md5 = httpRequest.getHeader("Content-MD5");
            content_md5 = content_md5 == null ? "" : content_md5;
            String content_type = httpRequest.getHeader(HttpHeaders.Names.CONTENT_TYPE);
            content_type = content_type == null ? "" : content_type;
            String addrString = getS3AddressString(httpRequest, true);
            String accesskeyid = parameters.remove(SecurityParameter.AWSAccessKeyId.toString());

            try {
                //Parameter url decode happens during MappingHttpRequest construction.
                String signature = parameters.remove(SecurityParameter.Signature.toString());
                if (signature == null) {
                    throw new InvalidSecurityException("No signature found");
                }
                String expires = parameters.remove(SecurityParameter.Expires.toString());
                if (expires == null) {
                    throw new InvalidSecurityException("Expiration parameter must be specified.");
                }
                String securityToken = parameters.get(SecurityParameter.SecurityToken.toString());

                if (checkExpires(expires)) {
                    String canonicalizedAmzHeaders = getCanonicalizedAmzHeaders(httpRequest);
                    String stringToSign = verb + "\n" + content_md5 + "\n" + content_type + "\n"
                            + Long.parseLong(expires) + "\n" + canonicalizedAmzHeaders + addrString;
                    try {
                        SecurityContext
                                .getLoginContext(new ObjectStorageWrappedCredentials(httpRequest.getCorrelationId(),
                                        stringToSign, accesskeyid, signature, securityToken))
                                .login();
                    } catch (Exception ex) {
                        //Try adding back the '/services/objectStorage' portion of the addrString and retry the signature calc
                        if (httpRequest.getUri()
                                .startsWith(ComponentIds.lookup(ObjectStorage.class).getServicePath())
                                || httpRequest.getUri()
                                        .startsWith(ObjectStorageProperties.LEGACY_WALRUS_SERVICE_PATH)) {
                            try {
                                String modifiedAddrString = getS3AddressString(httpRequest, false);
                                stringToSign = verb + "\n" + content_md5 + "\n" + content_type + "\n"
                                        + Long.parseLong(expires) + "\n" + canonicalizedAmzHeaders
                                        + modifiedAddrString;
                                SecurityContext
                                        .getLoginContext(
                                                new ObjectStorageWrappedCredentials(httpRequest.getCorrelationId(),
                                                        stringToSign, accesskeyid, signature, securityToken))
                                        .login();
                            } catch (Exception ex2) {
                                LOG.error("CorrelationId: " + httpRequest.getCorrelationId()
                                        + " authentication failed due to signature mismatch:", ex2);
                                throw new SignatureDoesNotMatchException(stringToSign);
                            }
                        } else {
                            LOG.error("CorrelationId: " + httpRequest.getCorrelationId()
                                    + " authentication failed due to signature mismatch:", ex);
                            throw new SignatureDoesNotMatchException(stringToSign);
                        }
                    }
                } else {
                    throw new AccessDeniedException("Cannot process request. Expired.");
                }
            } catch (Exception ex) {
                throw new AccessDeniedException("Could not verify request " + ex.getMessage());
            }
        }

        /**
         * See if the expires string indicates the message is expired.
         *
         * @param expires
         * @return
         */
        static boolean checkExpires(String expires) {
            Long expireTime = Long.parseLong(expires);
            Long currentTime = new Date().getTime() / 1000;
            if (currentTime > expireTime)
                return false;
            return true;
        }

        /**
         * Gets the date for S3-spec authentication
         *
         * @param httpRequest
         * @return
         * @throws AccessDeniedException
         */
        static String getDate(MappingHttpRequest httpRequest) throws AccessDeniedException {
            String date;
            String verifyDate;
            if (httpRequest.containsHeader("x-amz-date")) {
                date = "";
                verifyDate = httpRequest.getHeader("x-amz-date");
            } else {
                date = httpRequest.getAndRemoveHeader(SecurityParameter.Date.toString());
                verifyDate = date;
                if (date == null || date.length() <= 0)
                    throw new AccessDeniedException("User authentication failed. Date must be specified.");
            }

            try {
                Date dateToVerify = DateUtil.parseDate(verifyDate);
                Date currentDate = new Date();
                if (Math.abs(currentDate.getTime()
                        - dateToVerify.getTime()) > ObjectStorageProperties.EXPIRATION_LIMIT) {
                    LOG.error("Incoming ObjectStorage message is expired. Current date: " + currentDate.toString()
                            + " Message's Verification Date: " + dateToVerify.toString());
                    throw new AccessDeniedException("Message expired. Sorry.");
                }
            } catch (Exception ex) {
                LOG.error("Cannot parse date: " + verifyDate);
                throw new AccessDeniedException("Unable to parse date.");
            }

            return date;
        }

        private static String getCanonicalizedAmzHeaders(MappingHttpRequest httpRequest) {
            String result = "";
            Set<String> headerNames = httpRequest.getHeaderNames();

            TreeMap<String, String> amzHeaders = new TreeMap<String, String>();
            for (String headerName : headerNames) {
                String headerNameString = headerName.toLowerCase().trim();
                if (headerNameString.startsWith("x-amz-")) {
                    String value = httpRequest.getHeader(headerName).trim();
                    String[] parts = value.split("\n");
                    value = "";
                    for (String part : parts) {
                        part = part.trim();
                        value += part + " ";
                    }
                    value = value.trim();
                    if (amzHeaders.containsKey(headerNameString)) {
                        String oldValue = (String) amzHeaders.remove(headerNameString);
                        oldValue += "," + value;
                        amzHeaders.put(headerNameString, oldValue);
                    } else {
                        amzHeaders.put(headerNameString, value);
                    }
                }
            }

            Iterator<String> iterator = amzHeaders.keySet().iterator();
            while (iterator.hasNext()) {
                String key = iterator.next();
                String value = (String) amzHeaders.get(key);
                result += key + ":" + value + "\n";
            }
            return result;
        }

        //Old method for getting signature info from Auth header
        static String[] getSigInfo(String auth_part) {
            int index = auth_part.lastIndexOf(" ");
            String sigString = auth_part.substring(index + 1);
            return sigString.split(":");
        }

        /**
         * AWS S3-spec address string, which includes the query parameters
         *
         * @param httpRequest
         * @param removeServicePath if true, removes the service path from the address string if found and if the request is path-style
         * @return
         * @throws AccessDeniedException
         */
        static String getS3AddressString(MappingHttpRequest httpRequest, boolean removeServicePath)
                throws S3Exception {
            /*
             * There are two modes: dns-style and path-style.
             * dns-style has the bucket name in the HOST header
             * path-style has the bucket name in the request path.
             *
             * If using DNS-style, we assume the key is the path, no service path necessary or allowed
             * If using path-style, there may be service path as well that prefixes the bucket name (e.g. /services/objectstorage/bucket/key)
             */
            try {
                String addr = httpRequest.getUri();
                String targetHost = httpRequest.getHeader(HttpHeaders.Names.HOST);
                String osgServicePath = ComponentIds.lookup(ObjectStorage.class).getServicePath();
                String bucket, key;

                StringBuilder addrString = new StringBuilder();

                //Normalize the URI
                if (targetHost.contains(".objectstorage")) {
                    //dns-style request
                    String hostBucket = targetHost.substring(0, targetHost.indexOf(".objectstorage"));
                    if (hostBucket.length() == 0) {
                        throw new InvalidAddressingHeaderException("Invalid Host header: " + targetHost);
                    } else {
                        addrString.append("/" + hostBucket);
                    }
                } else if (targetHost.contains(".walrus")) {
                    //dns-style request with walrus
                    String hostBucket = targetHost.substring(0, targetHost.indexOf(".walrus"));
                    if (hostBucket.length() == 0) {
                        throw new InvalidAddressingHeaderException("Invalid Host header: " + targetHost);
                    } else {
                        addrString.append("/" + hostBucket);
                    }
                } else {
                    //path-style request (or service request that won't have a bucket anyway)
                    if (removeServicePath) {
                        if (addr.startsWith(osgServicePath)) {
                            addr = addr.substring(osgServicePath.length(), addr.length());
                        } else if (addr.startsWith(ObjectStorageProperties.LEGACY_WALRUS_SERVICE_PATH)) {
                            addr = addr.substring(ObjectStorageProperties.LEGACY_WALRUS_SERVICE_PATH.length(),
                                    addr.length());
                        }
                    }
                }

                //Get the path part, up to the ?
                key = addr.split("\\?", 2)[0];
                if (!Strings.isNullOrEmpty(key)) {
                    addrString.append(key);
                } else {
                    addrString.append("/");
                }

                List<String> canonicalSubresources = new ArrayList<>();
                String resource;
                for (String queryParam : httpRequest.getParameters().keySet()) {
                    try {
                        if (ObjectStorageProperties.SubResource.valueOf(queryParam) != null) {
                            canonicalSubresources.add(queryParam);
                        }
                    } catch (IllegalArgumentException e) {
                        //Skip. Not in the set.
                    }
                }

                if (canonicalSubresources.size() > 0) {
                    Collections.sort(canonicalSubresources);
                    String value;
                    addrString.append("?");
                    //Add resources to canonical string
                    for (String subResource : canonicalSubresources) {
                        value = httpRequest.getParameters().get(subResource);
                        addrString.append(subResource);
                        //Query values are not URL-decoded, the signature should have them exactly as in the URI
                        if (!Strings.isNullOrEmpty(value)) {
                            addrString.append("=").append(value);
                        }
                        addrString.append("&");
                    }

                    //Remove trailng '&' if found
                    if (addrString.charAt(addrString.length() - 1) == '&') {
                        addrString.deleteCharAt(addrString.length() - 1);
                    }
                }

                return addrString.toString();
            } catch (S3Exception e) {
                throw e;
            } catch (Exception e) {
                //Anything unexpected...
                throw new InternalErrorException(e);
            }
        }

    } //End class S3Authentication

    /**
     * Authentication Handler for ObjectStorage REST requests (POST method and SOAP are processed using different handlers)
     *
     * @param httpRequest
     * @throws AccessDeniedException
     */
    public void handle(MappingHttpRequest httpRequest) throws S3Exception {
        //Clean up the headers such that no duplicates may exist etc.
        //sanitizeHeaders(httpRequest);
        Map<String, String> parameters = httpRequest.getParameters();

        if (httpRequest.containsHeader(SecurityParameter.Authorization.toString())) {
            String authHeader = httpRequest.getAndRemoveHeader(SecurityParameter.Authorization.toString());
            Map<AuthorizationField, String> authMap = processAuthorizationHeader(authHeader);

            if (AWS_AUTH_TYPE.equals(authMap.get(AuthorizationField.Type))) {
                //Normally signed request using AccessKeyId/SecretKeyId pair
                S3Authentication.authenticate(httpRequest, authMap);
            } else {
                throw new MissingSecurityHeaderException(
                        "Malformed or unexpected format for Authentication header");
            }
        } else {
            if (parameters.containsKey(SecurityParameter.AWSAccessKeyId.toString())) {
                //Query String Auth
                S3Authentication.authenticateQueryString(httpRequest);
            } else {
                //Anonymous request, no query string, no Authorization header
                try {
                    Context ctx = Contexts.lookup(httpRequest.getCorrelationId());
                    ctx.setUser(Principals.nobodyUser());
                } catch (NoSuchContextException e) {
                    LOG.error(e, e);
                    throw new AccessDeniedException();
                }
            }
        }
    }
}