mirror of https://github.com/apache/cloudstack.git
285 lines
9.8 KiB
Java
285 lines
9.8 KiB
Java
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you 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.cloud.bridge.util;
|
|
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.net.URLDecoder;
|
|
import java.security.SignatureException;
|
|
import java.text.DateFormat;
|
|
import java.text.SimpleDateFormat;
|
|
import java.util.Calendar;
|
|
import java.util.Collection;
|
|
import java.util.Iterator;
|
|
import java.util.TreeMap;
|
|
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
import org.apache.commons.codec.binary.Base64;
|
|
import org.apache.log4j.Logger;
|
|
|
|
public class EC2RestAuth {
|
|
protected final static Logger logger = Logger.getLogger(RestAuth.class);
|
|
|
|
// TreeMap: used to Sort the UTF-8 query string components by parameter name with natural byte ordering
|
|
protected TreeMap<String, String> queryParts = null; // used to generate a CanonicalizedQueryString
|
|
protected String canonicalizedQueryString = null;
|
|
protected String hostHeader = null;
|
|
protected String httpRequestURI = null;
|
|
|
|
public EC2RestAuth() {
|
|
// these must be lexicographically sorted
|
|
queryParts = new TreeMap<String, String>();
|
|
}
|
|
|
|
public static Calendar parseDateString(String created) {
|
|
DateFormat formatter = null;
|
|
Calendar cal = Calendar.getInstance();
|
|
|
|
// -> for some unknown reason SimpleDateFormat does not properly handle the 'Z' timezone
|
|
if (created.endsWith("Z"))
|
|
created = created.replace("Z", "+0000");
|
|
|
|
try {
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
try {
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
// -> the time zone is GMT if not defined
|
|
try {
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
|
cal.setTime(formatter.parse(created));
|
|
|
|
created = created + "+0000";
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
// -> including millseconds?
|
|
try {
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.Sz");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
try {
|
|
formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SZ");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
// -> the CloudStack API used to return this format for some calls
|
|
try {
|
|
formatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy");
|
|
cal.setTime(formatter.parse(created));
|
|
return cal;
|
|
} catch (Exception e) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Assuming that a port number is to be included.
|
|
*
|
|
* @param header - contents of the "Host:" header, skipping the 'Host:' preamble.
|
|
*/
|
|
public void setHostHeader(String hostHeader) {
|
|
if (null == hostHeader)
|
|
this.hostHeader = null;
|
|
else
|
|
this.hostHeader = hostHeader.trim().toLowerCase();
|
|
}
|
|
|
|
public void setHTTPRequestURI(String uri) {
|
|
if (null == uri || 0 == uri.length())
|
|
this.httpRequestURI = "/";
|
|
else
|
|
this.httpRequestURI = uri.trim();
|
|
}
|
|
|
|
/**
|
|
* The given query string needs to be pulled apart, sorted by paramter name, and reconstructed.
|
|
* We sort the query string values via a TreeMap.
|
|
*
|
|
* @param query - this string still has all URL encoding in place.
|
|
*/
|
|
public void setQueryString(String query) {
|
|
String parameter = null;
|
|
|
|
if (null == query) {
|
|
this.canonicalizedQueryString = null;
|
|
return;
|
|
}
|
|
|
|
// -> sort by paramter name
|
|
String[] parts = query.split("&");
|
|
if (null != parts) {
|
|
for (int i = 0; i < parts.length; i++) {
|
|
|
|
parameter = parts[i];
|
|
|
|
if (parameter.startsWith("?"))
|
|
parameter = parameter.substring(1);
|
|
|
|
// -> don't include a 'Signature=' parameter
|
|
if (parameter.startsWith("Signature="))
|
|
continue;
|
|
|
|
int offset = parameter.indexOf("=");
|
|
if (-1 == offset)
|
|
queryParts.put(parameter, parameter + "=");
|
|
else
|
|
queryParts.put(parameter.substring(0, offset), parameter);
|
|
}
|
|
}
|
|
|
|
// -> reconstruct into a canonicalized format
|
|
Collection<String> headers = queryParts.values();
|
|
Iterator<String> itr = headers.iterator();
|
|
StringBuffer reconstruct = new StringBuffer();
|
|
int count = 0;
|
|
|
|
while (itr.hasNext()) {
|
|
if (0 < count)
|
|
reconstruct.append("&");
|
|
reconstruct.append(itr.next());
|
|
count++;
|
|
}
|
|
canonicalizedQueryString = reconstruct.toString();
|
|
}
|
|
|
|
/**
|
|
* The request is authenticated if we can regenerate the same signature given
|
|
* on the request. Before calling this function make sure to set the header values
|
|
* defined by the public values above.
|
|
*
|
|
* @param httpVerb - the type of HTTP request (e.g., GET, PUT)
|
|
* @param secretKey - value obtained from the AWSAccessKeyId
|
|
* @param signature - the signature we are trying to recreate, note can be URL-encoded
|
|
* @param method - { "HmacSHA1", "HmacSHA256" }
|
|
*
|
|
* @throws SignatureException
|
|
*
|
|
* @return true if request has been authenticated, false otherwise
|
|
* @throws UnsupportedEncodingException
|
|
*/
|
|
public boolean verifySignature(String httpVerb, String secretKey, String signature, String method) throws SignatureException, UnsupportedEncodingException {
|
|
|
|
if (null == httpVerb || null == secretKey || null == signature)
|
|
return false;
|
|
|
|
httpVerb = httpVerb.trim();
|
|
secretKey = secretKey.trim();
|
|
signature = signature.trim();
|
|
|
|
// -> first calculate the StringToSign after the caller has initialized all the header values
|
|
String StringToSign = genStringToSign(httpVerb);
|
|
String calSig = calculateRFC2104HMAC(StringToSign, secretKey, method.equalsIgnoreCase("HmacSHA1"));
|
|
|
|
// -> the passed in signature is defined to be URL encoded? (and it must be base64 encoded)
|
|
int offset = signature.indexOf("%");
|
|
if (-1 != offset)
|
|
signature = URLDecoder.decode(signature, "UTF-8");
|
|
|
|
boolean match = signature.equals(calSig);
|
|
if (!match)
|
|
logger.error("Signature mismatch, [" + signature + "] [" + calSig + "] over [" + StringToSign + "]");
|
|
return match;
|
|
}
|
|
|
|
/**
|
|
* This function generates the single string that will be used to sign with a users
|
|
* secret key.
|
|
*
|
|
* StringToSign = HTTP-Verb + "\n" +
|
|
* ValueOfHostHeaderInLowercase + "\n" +
|
|
* HTTPRequestURI + "\n" +
|
|
* CanonicalizedQueryString
|
|
*
|
|
* @return The single StringToSign or null.
|
|
*/
|
|
private String genStringToSign(String httpVerb) {
|
|
StringBuffer stringToSign = new StringBuffer();
|
|
|
|
stringToSign.append(httpVerb).append("\n");
|
|
|
|
if (null != this.hostHeader)
|
|
stringToSign.append(this.hostHeader);
|
|
stringToSign.append("\n");
|
|
|
|
if (null != this.httpRequestURI)
|
|
stringToSign.append(this.httpRequestURI);
|
|
stringToSign.append("\n");
|
|
|
|
if (null != this.canonicalizedQueryString)
|
|
stringToSign.append(this.canonicalizedQueryString);
|
|
|
|
if (0 == stringToSign.length())
|
|
return null;
|
|
else
|
|
return stringToSign.toString();
|
|
}
|
|
|
|
/**
|
|
* Create a signature by the following method:
|
|
* new String( Base64( SHA1 or SHA256 ( key, byte array )))
|
|
*
|
|
* @param signIt - the data to generate a keyed HMAC over
|
|
* @param secretKey - the user's unique key for the HMAC operation
|
|
* @param useSHA1 - if false use SHA256
|
|
* @return String - the recalculated string
|
|
* @throws SignatureException
|
|
*/
|
|
private String calculateRFC2104HMAC(String signIt, String secretKey, boolean useSHA1) throws SignatureException {
|
|
SecretKeySpec key = null;
|
|
Mac hmacShaAlg = null;
|
|
String result = null;
|
|
|
|
try {
|
|
if (useSHA1) {
|
|
key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
|
|
hmacShaAlg = Mac.getInstance("HmacSHA1");
|
|
} else {
|
|
key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
|
|
hmacShaAlg = Mac.getInstance("HmacSHA256");
|
|
}
|
|
|
|
hmacShaAlg.init(key);
|
|
byte[] rawHmac = hmacShaAlg.doFinal(signIt.getBytes());
|
|
result = new String(Base64.encodeBase64(rawHmac));
|
|
|
|
} catch (Exception e) {
|
|
throw new SignatureException("Failed to generate keyed HMAC on REST request: " + e.getMessage());
|
|
}
|
|
return result.trim();
|
|
}
|
|
}
|