All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.emc.atmos.api.RestUtil Maven / Gradle / Ivy

Go to download

EMC Atmos Client for Java - provides REST access to object data on EMC platforms using the Atmos API.

There is a newer version: 3.2.1
Show newest version
/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2013-2018, Dell EMC
 * All rights reserved.
 *
 * Redistribution and use 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.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * 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 HOLDER 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.
 *
 */
package com.emc.atmos.api;

import com.emc.atmos.AtmosException;
import com.emc.atmos.api.bean.Metadata;
import com.emc.atmos.api.bean.Permission;
import com.emc.util.HttpUtil;
import org.apache.log4j.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class RestUtil {
    public static final String HEADER_CONTENT_TYPE = "Content-Type";
    public static final String HEADER_DATE = "Date";
    public static final String HEADER_EXPECT = "Expect";
    public static final String HEADER_RANGE = "Range";
    public static final String HEADER_ETAG = "ETag";
    public static final String HEADER_LAST_MODIFIED = "Last-Modified";

    public static final String XHEADER_CONTENT_CHECKSUM = "x-emc-content-checksum";
    public static final String XHEADER_DATE = "x-emc-date";
    public static final String XHEADER_EXPIRES = "x-emc-expires";
    public static final String XHEADER_FEATURES = "x-emc-features";
    public static final String XHEADER_FORCE = "x-emc-force";
    public static final String XHEADER_GENERATE_CHECKSUM = "x-emc-generate-checksum";
    public static final String XHEADER_GROUP_ACL = "x-emc-groupacl";
    public static final String XHEADER_INCLUDE_META = "x-emc-include-meta";
    public static final String XHEADER_LIMIT = "x-emc-limit";
    public static final String XHEADER_LISTABLE_META = "x-emc-listable-meta";
    public static final String XHEADER_LISTABLE_TAGS = "x-emc-listable-tags";
    public static final String XHEADER_META = "x-emc-meta";
    public static final String XHEADER_OBJECT_ID = "x-emc-object-id";
    public static final String XHEADER_OBJECTID = "x-emc-objectid";
    public static final String XHEADER_PATH = "x-emc-path";
    public static final String XHEADER_POOL = "x-emc-pool";
    public static final String XHEADER_RETENTION_PERIOD = "x-emc-retention-period";
    public static final String XHEADER_RETENTION_POLICY = "x-emc-retention-policy";
    public static final String XHEADER_SIGNATURE = "x-emc-signature";
    public static final String XHEADER_SUBTENANT_ID = "x-emc-subtenant-id";
    public static final String XHEADER_SUPPORT_UTF8 = "x-emc-support-utf8";
    public static final String XHEADER_SYSTEM_TAGS = "x-emc-system-tags";
    public static final String XHEADER_TAGS = "x-emc-tags";
    public static final String XHEADER_TOKEN = "x-emc-token";
    public static final String XHEADER_UID = "x-emc-uid";
    public static final String XHEADER_USER_ACL = "x-emc-useracl";
    public static final String XHEADER_USER_TAGS = "x-emc-user-tags";
    public static final String XHEADER_UTF8 = "x-emc-utf8";
    public static final String XHEADER_VERSION_OID = "x-emc-version-oid";
    public static final String XHEADER_WSCHECKSUM = "x-emc-wschecksum";
    public static final String XHEADER_OBJECT_VPOOL = "x-emc-vpool";

    private static final String USER_MAUI_PREFIX = "user.maui.";
    public static final String METADATA_KEY_EXPIRATION_ENABLE = USER_MAUI_PREFIX + "expirationEnable";
    public static final String METADATA_KEY_EXPIRATION_END = USER_MAUI_PREFIX + "expirationEnd";
    public static final String METADATA_KEY_RETENTION_ENABLE = USER_MAUI_PREFIX + "retentionEnable";
    public static final String METADATA_KEY_RETENTION_END = USER_MAUI_PREFIX + "retentionEnd";

    public static final String TYPE_MULTIPART = "multipart";
    public static final String TYPE_MULTIPART_BYTE_RANGES = "multipart/byteranges";
    public static final String TYPE_APPLICATION_OCTET_STREAM = "application/octet-stream";

    public static final String TYPE_DEFAULT = TYPE_APPLICATION_OCTET_STREAM;

    public static final String TYPE_PARAM_BOUNDARY = "boundary";

    public static final String PROP_ENABLE_EXPECT_100_CONTINUE = "com.emc.atmos.api.expect100Continue";

    private static final Logger l4j = Logger.getLogger( RestUtil.class );

    private static final Pattern OBJECTID_PATTERN = Pattern.compile( "/\\w+/objects/([0-9a-f-]{44,})" );

    public static String sign( String string, byte[] hashKey ) {
        try {
            // Compute the signature hash
            l4j.debug( "Hashing: \n" + string );

            byte[] input = string.getBytes( "UTF-8" );

            Mac mac = Mac.getInstance( "HmacSHA1" );
            SecretKeySpec key = new SecretKeySpec( hashKey, "HmacSHA1" );
            mac.init( key );

            byte[] hashBytes = mac.doFinal( input );

            // Encode the hash in Base64.
            String hash = DatatypeConverter.printBase64Binary(hashBytes);

            l4j.debug( "Hash: " + hash );

            return hash;
        } catch ( Exception e ) {
            throw new RuntimeException( "Error signing string:\n" + string + "\n", e );
        }
    }

    /**
     * Generates the HMAC-SHA1 signature used to authenticate the request using
     * the Java security APIs, then adds the uid and signature to the headers.
     *
     * @param method  the HTTP method used
     * @param path    the resource path including any querystring
     * @param headers the HTTP headers for the request
     * @param hashKey the secret key to use when signing
     */
    public static void signRequest( String method, String path, String query, Map> headers,
                                    String uid, byte[] hashKey, long serverClockSkew ) {

        // Add date header
        Date serverTime = new Date( System.currentTimeMillis() - serverClockSkew );
        headers.put( HEADER_DATE, Arrays.asList( (Object) HttpUtil.headerFormat( serverTime ) ) );
        headers.put( XHEADER_DATE, Arrays.asList( (Object) HttpUtil.headerFormat( serverTime ) ) );

        // Add uid to headers
        if ( !headers.containsKey( XHEADER_UID ) )
            headers.put( XHEADER_UID, Arrays.asList( (Object) uid ) );

        // Build the string to hash.
        StringBuilder builder = new StringBuilder();

        builder.append( method ).append( "\n" );

        // Add the following header values or blank lines if they aren't present
        builder.append( generateHashLine( headers, HEADER_CONTENT_TYPE ) );
        builder.append( generateHashLine( headers, HEADER_RANGE ) );
        builder.append( generateHashLine( headers, HEADER_DATE ) );

        // Add the resource
        builder.append( path.toLowerCase() );
        if ( query != null ) builder.append( "?" ).append( query );
        builder.append( "\n" );

        // Do the 'x-emc' headers. The headers must be hashed in alphabetic
        // order and the values must be stripped of whitespace and newlines.
        // TreeMap will automatically sort by key.
        Map emcHeaders = new TreeMap();
        for ( String key : headers.keySet() ) {
            String lowerKey = key.toLowerCase();
            if ( lowerKey.indexOf( "x-emc" ) == 0 )
                emcHeaders.put( lowerKey, join( headers.get( key ), "," ) );
        }
        for ( Iterator i = emcHeaders.keySet().iterator(); i.hasNext(); ) {
            String key = i.next();
            builder.append( key ).append( ':' ).append( normalizeSpace( emcHeaders.get( key ) ) );
            if ( i.hasNext() ) builder.append( "\n" );
        }

        String hash = sign( builder.toString(), hashKey );

        // Add signature to headers
        headers.put( XHEADER_SIGNATURE, Arrays.asList( (Object) hash ) );
    }

    public static String normalizeSpace( String str ) {
        int length;
        do {
            length = str.length();
            str = str.replace( "  ", " " );
        } while ( length != str.length() );

        return str.replace( "\n", "" ).trim();
    }

    public static String join( Iterable list, String delimiter ) {
        if ( list == null ) return null;
        StringBuilder builder = new StringBuilder();
        for ( Iterator i = list.iterator(); i.hasNext(); ) {
            Object value = i.next();
            builder.append( value );
            if ( i.hasNext() ) builder.append( delimiter );
        }
        return builder.toString();
    }

    /**
     * Initializes new keys with an empty ArrayList. Convenience method for generating a header map.
     */
    public static void addValue( Map> multiValueMap, String key, Object value ) {
        List values = multiValueMap.get( key );
        if ( values == null ) {
            values = new ArrayList();
            multiValueMap.put( key, values );
        }
        values.add( value );
    }

    public static String lastPathElement( String path ) {
        if ( path == null ) return null;
        String[] elements = path.split( "/" );
        return elements[elements.length - 1];
    }

    public static ObjectId parseObjectId( String path ) {
        Matcher matcher = OBJECTID_PATTERN.matcher( path );
        if ( matcher.find() )
            return new ObjectId( matcher.group( 1 ) );
        else
            throw new AtmosException( "Cannot find object ID in path" + path );
    }

    public static Map parseMetadataHeader( String headerValue, boolean listable, boolean decodeUtf8 ) {
        Map metadataMap = new TreeMap();
        if ( headerValue == null ) return metadataMap;
        String[] pairs = headerValue.split( ",(?=[^,]+=)" ); // comma with key as look-ahead (not part of match)
        for ( String pair : pairs ) {
            String[] components = pair.split( "=", 2 );
            String name = components[0].trim();
            if ( decodeUtf8 ) name = HttpUtil.decodeUtf8( name );
            String value = components.length > 1 ? components[1] : null;
            if ( value != null && decodeUtf8 ) value = HttpUtil.decodeUtf8( value );
            Metadata metadata = new Metadata( name, value, listable );
            metadataMap.put( name, metadata );
        }
        return metadataMap;
    }

    public static Map parseAclHeader( String headerValue ) {
        Map acl = new TreeMap();
        if ( headerValue == null || headerValue.trim().length() == 0 ) return acl;
        for ( String pair : headerValue.split( "," ) ) {
            String[] components = pair.split( "=", 2 );
            String name = components[0].trim();
            String permission = components[1];

            // Currently, the server returns "FULL" instead of "FULL_CONTROL".
            // For consistency, change this to the value used in the request
            if ( "FULL".equals( permission ) ) {
                permission = "FULL_CONTROL";
            }

            acl.put( name, Permission.valueOf( permission ) );
        }
        return acl;
    }

    private static String generateHashLine( Map> headers, String headerName ) {
        String value = join( headers.get( headerName ), "," );
        l4j.debug( headerName + ": " + value );
        if ( value != null ) return value + "\n";
        return "\n";
    }

    private RestUtil() {
    }
}