/*
* Copyright 2014 EMC Corporation. 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://www.apache.org/licenses/LICENSE-2.0.txt
*
* 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.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.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
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 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_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_SIGNATURE = "x-emc-signature";
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_PROJECT = "x-emc-project-id";
public static final String XHEADER_OBJECT_VPOOL = "x-emc-vpool";
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 = new String( Base64.encodeBase64( hashBytes ), "UTF-8" );
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<String, List<Object>> 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<String, String> emcHeaders = new TreeMap<String, String>();
for ( String key : headers.keySet() ) {
String lowerKey = key.toLowerCase();
if ( lowerKey.indexOf( "x-emc" ) == 0 )
emcHeaders.put( lowerKey, join( headers.get( key ), "," ) );
}
for ( Iterator<String> 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<String, List<Object>> multiValueMap, String key, Object value ) {
List<Object> values = multiValueMap.get( key );
if ( values == null ) {
values = new ArrayList<Object>();
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<String, Metadata> parseMetadataHeader( String headerValue, boolean listable ) {
Map<String, Metadata> metadataMap = new TreeMap<String, Metadata>();
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 = HttpUtil.decodeUtf8( components[0].trim() );
String value = components.length > 1 ? HttpUtil.decodeUtf8( components[1] ) : null;
Metadata metadata = new Metadata( name, value, listable );
metadataMap.put( name, metadata );
}
return metadataMap;
}
public static Map<String, Permission> parseAclHeader( String headerValue ) {
Map<String, Permission> acl = new TreeMap<String, Permission>();
if ( headerValue == null ) 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<String, List<Object>> headers, String headerName ) {
String value = join( headers.get( headerName ), "," );
l4j.debug( headerName + ": " + value );
if ( value != null ) return value + "\n";
return "\n";
}
private RestUtil() {
}
}