mirror of https://github.com/apache/cloudstack.git
409 lines
16 KiB
Java
409 lines
16 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.InvalidKeyException;
|
|
import java.security.SignatureException;
|
|
import java.util.Collection;
|
|
import java.util.HashSet;
|
|
import java.util.Iterator;
|
|
import java.util.Set;
|
|
import java.util.TreeMap;
|
|
import java.util.TreeSet;
|
|
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
import org.apache.commons.codec.binary.Base64;
|
|
import org.apache.log4j.Logger;
|
|
|
|
/**
|
|
* This class expects that the caller pulls the required headers from the standard
|
|
* HTTPServeletRequest structure. This class is responsible for providing the
|
|
* RFC2104 calculation to ensure that the signature is valid for the signing string.
|
|
* The signing string is a representation of the request.
|
|
* Notes are given below on what values are expected.
|
|
* This class is used for the Authentication check for REST requests and Query String
|
|
* Authentication requests.
|
|
*
|
|
*/
|
|
|
|
public class RestAuth {
|
|
protected final static Logger logger = Logger.getLogger(RestAuth.class);
|
|
|
|
// TreeMap: used when constructing the CanonicalizedAmzHeaders Element of the StringToSign
|
|
protected TreeMap<String, String> AmazonHeaders = null; // not always present
|
|
protected String bucketName = null; // not always present
|
|
protected String queryString = null; // for CanonicalizedResource - only interested in a string starting with particular values
|
|
protected String uriPath = null; // only interested in the resource path
|
|
protected String date = null; // only if x-amz-date is not set
|
|
protected String contentType = null; // not always present
|
|
protected String contentMD5 = null; // not always present
|
|
protected boolean amzDateSet = false;
|
|
protected boolean useSubDomain = false;
|
|
|
|
protected Set<String> allowedQueryParams;
|
|
|
|
public RestAuth() {
|
|
// these must be lexicographically sorted
|
|
AmazonHeaders = new TreeMap<String, String>();
|
|
allowedQueryParams = new HashSet<String>() {
|
|
{
|
|
add("acl");
|
|
add("lifecycle");
|
|
add("location");
|
|
add("logging");
|
|
add("notification");
|
|
add("partNumber");
|
|
add("policy");
|
|
add("requestPayment");
|
|
add("torrent");
|
|
add("uploadId");
|
|
add("uploads");
|
|
add("versionId");
|
|
add("versioning");
|
|
add("versions");
|
|
add("website");
|
|
add("delete");
|
|
}
|
|
};
|
|
}
|
|
|
|
public RestAuth(boolean useSubDomain) {
|
|
//invoke the other constructor
|
|
this();
|
|
this.useSubDomain = useSubDomain;
|
|
}
|
|
|
|
public void setUseSubDomain(boolean value) {
|
|
useSubDomain = value;
|
|
}
|
|
|
|
public boolean getUseSubDomain() {
|
|
return useSubDomain;
|
|
}
|
|
|
|
/**
|
|
* This header is used iff the "x-amz-date:" header is not defined.
|
|
* Value is used in constructing the StringToSign for signature verification.
|
|
*
|
|
* @param date - the contents of the "Date:" header, skipping the 'Date:' preamble.
|
|
* OR pass in the value of the "Expires=" query string parameter passed in
|
|
* for "Query String Authentication".
|
|
*/
|
|
public void setDateHeader(String date) {
|
|
if (this.amzDateSet)
|
|
return;
|
|
if (null != date)
|
|
date = date.trim();
|
|
this.date = date;
|
|
}
|
|
|
|
/**
|
|
* Value is used in constructing the StringToSign for signature verification.
|
|
*
|
|
* @param type - the contents of the "Content-Type:" header, skipping the 'Content-Type:' preamble.
|
|
*/
|
|
public void setContentTypeHeader(String type) {
|
|
if (null != type)
|
|
type = type.trim();
|
|
this.contentType = type;
|
|
}
|
|
|
|
/**
|
|
* Value is used in constructing the StringToSign for signature verification.
|
|
* @param type - the contents of the "Content-MD5:" header, skipping the 'Content-MD5:' preamble.
|
|
*/
|
|
public void setContentMD5Header(String md5) {
|
|
if (null != md5)
|
|
md5 = md5.trim();
|
|
this.contentMD5 = md5;
|
|
}
|
|
|
|
/**
|
|
* The bucket name can be in the "Host:" header but it does not have to be. It can
|
|
* instead be in the uriPath as the first step in the path.
|
|
*
|
|
* Used as part of the CanonalizedResource element of the StringToSign.
|
|
* If we get "Host: static.johnsmith.net:8080", then the bucket name is "static.johnsmith.net"
|
|
*
|
|
* @param header - contents of the "Host:" header, skipping the 'Host:' preamble.
|
|
*/
|
|
public void setHostHeader(String header) {
|
|
if (null == header) {
|
|
this.bucketName = null;
|
|
return;
|
|
}
|
|
|
|
// -> is there a port on the name?
|
|
header = header.trim();
|
|
int offset = header.indexOf(":");
|
|
if (-1 != offset)
|
|
header = header.substring(0, offset);
|
|
this.bucketName = header;
|
|
}
|
|
|
|
/**
|
|
* Used as part of the CanonalizedResource element of the StringToSign.
|
|
* CanonicalizedResource = [ "/" + Bucket ] +
|
|
* <HTTP-Request-URI, from the protocol name up to the query string> + [sub-resource]
|
|
* The list of sub-resources that must be included when constructing the CanonicalizedResource Element are: acl, lifecycle, location,
|
|
* logging, notification, partNumber, policy, requestPayment, torrent, uploadId, uploads, versionId, versioning, versions and website.
|
|
* (http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html)
|
|
* @param query - results from calling "HttpServletRequest req.getQueryString()"
|
|
*/
|
|
public void setQueryString(String query) {
|
|
if (null == query) {
|
|
this.queryString = null;
|
|
return;
|
|
}
|
|
|
|
// Sub-resources (i.e.: query params) must be lex sorted
|
|
Set<String> subResources = new TreeSet<String>();
|
|
|
|
String[] queryParams = query.split("&");
|
|
StringBuffer builtQuery = new StringBuffer();
|
|
for (String queryParam : queryParams) {
|
|
// lookup parameter name
|
|
String paramName = queryParam.split("=")[0];
|
|
if (allowedQueryParams.contains(paramName)) {
|
|
subResources.add(queryParam);
|
|
}
|
|
}
|
|
for (String subResource : subResources) {
|
|
builtQuery.append(subResource + "&");
|
|
}
|
|
// If anything inside the string buffer, add a "?" at the beginning,
|
|
// and then remove the last '&'
|
|
if (builtQuery.length() > 0) {
|
|
builtQuery.insert(0, "?");
|
|
builtQuery.deleteCharAt(builtQuery.length() - 1);
|
|
}
|
|
this.queryString = builtQuery.toString();
|
|
}
|
|
|
|
/**
|
|
* Used as part of the CanonalizedResource element of the StringToSign.
|
|
* Append the path part of the un-decoded HTTP Request-URI, up-to but not including the query string.
|
|
*
|
|
* @param path - - results from calling "HttpServletRequest req.getPathInfo()"
|
|
*/
|
|
public void addUriPath(String path) {
|
|
if (null != path)
|
|
path = path.trim();
|
|
this.uriPath = path;
|
|
}
|
|
|
|
/**
|
|
* Pass in each complete Amazon header found in the HTTP request one at a time.
|
|
* Each Amazon header added will become part of the signature calculation.
|
|
* We are using a TreeMap here because of the S3 definition:
|
|
* "Sort the collection of headers lexicographically by header name."
|
|
*
|
|
* @param headerAndValue - needs to be the complete amazon header (i.e., starts with "x-amz").
|
|
*/
|
|
public void addAmazonHeader(String headerAndValue) {
|
|
if (null == headerAndValue)
|
|
return;
|
|
|
|
String canonicalized = null;
|
|
|
|
// [A] First Canonicalize the header and its value
|
|
// -> we use the header 'name' as the key since we have to sort on that
|
|
int offset = headerAndValue.indexOf(":");
|
|
String header = headerAndValue.substring(0, offset + 1).toLowerCase();
|
|
String value = headerAndValue.substring(offset + 1).trim();
|
|
|
|
// -> RFC 2616, Section 4.2: unfold the header's value by replacing linear white space with a single space character
|
|
// -> does the HTTPServeletReq already do this for us?
|
|
value = value.replaceAll(" ", " "); // -> multiple spaces to one space
|
|
value = value.replaceAll("(\r\n|\t|\n)", " "); // -> CRLF, tab, and LF to one space
|
|
|
|
// [B] Does this header already exist?
|
|
if (AmazonHeaders.containsKey(header)) {
|
|
// -> combine header fields with the same name into one "header-name:comma-separated-value-list" pair as prescribed by RFC 2616, section 4.2, without any white-space between values.
|
|
canonicalized = AmazonHeaders.get(header);
|
|
canonicalized = new String(canonicalized + "," + value + "\n");
|
|
canonicalized = canonicalized.replaceAll("\n,", ","); // remove the '\n' from the first stored value
|
|
} else
|
|
canonicalized = new String(header + value + "\n"); // -> as per spec, no space between header and its value
|
|
|
|
AmazonHeaders.put(header, canonicalized);
|
|
|
|
// [C] "x-amz-date:" takes precedence over the "Date:" header
|
|
if (header.equals("x-amz-date:")) {
|
|
this.amzDateSet = true;
|
|
if (null != this.date)
|
|
this.date = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @throws SignatureException
|
|
*
|
|
* @return true if request has been authenticated, false otherwise
|
|
* @throws UnsupportedEncodingException
|
|
*/
|
|
|
|
public boolean verifySignature(String httpVerb, String secretKey, String signature) 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);
|
|
// Was the passed in signature URL encoded? (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" +
|
|
* Content-MD5 + "\n" +
|
|
* Content-Type + "\n" +
|
|
* Date + "\n" +
|
|
* CanonicalizedAmzHeaders +
|
|
* CanonicalizedResource;
|
|
*
|
|
* @return The single StringToSign or null.
|
|
*/
|
|
private String genStringToSign(String httpVerb) {
|
|
StringBuffer canonicalized = new StringBuffer();
|
|
String temp = null;
|
|
String canonicalizedResourceElement = genCanonicalizedResourceElement();
|
|
canonicalized.append(httpVerb).append("\n");
|
|
if ((null != this.contentMD5))
|
|
canonicalized.append(this.contentMD5);
|
|
canonicalized.append("\n");
|
|
|
|
if ((null != this.contentType))
|
|
canonicalized.append(this.contentType);
|
|
canonicalized.append("\n");
|
|
|
|
if (null != this.date)
|
|
canonicalized.append(this.date);
|
|
|
|
canonicalized.append("\n");
|
|
|
|
if (null != (temp = genCanonicalizedAmzHeadersElement()))
|
|
canonicalized.append(temp);
|
|
if (null != canonicalizedResourceElement)
|
|
canonicalized.append(canonicalizedResourceElement);
|
|
|
|
if (0 == canonicalized.length())
|
|
return null;
|
|
|
|
return canonicalized.toString();
|
|
}
|
|
|
|
/**
|
|
* CanonicalizedResource represents the Amazon S3 resource targeted by the request.
|
|
* CanonicalizedResource = [ "/" + Bucket ] +
|
|
* <HTTP-Request-URI, from the protocol name up to the query string> +
|
|
* [ sub-resource, if present. For example "?acl", "?location", "?logging", or "?torrent"];
|
|
*
|
|
* @return A single string representing CanonicalizedResource or null.
|
|
*/
|
|
private String genCanonicalizedResourceElement() {
|
|
StringBuffer canonicalized = new StringBuffer();
|
|
|
|
if (this.useSubDomain && this.bucketName != null)
|
|
canonicalized.append("/").append(this.bucketName);
|
|
|
|
if (null != this.uriPath)
|
|
canonicalized.append(this.uriPath);
|
|
if (null != this.queryString)
|
|
canonicalized.append(this.queryString);
|
|
|
|
if (0 == canonicalized.length())
|
|
return null;
|
|
|
|
return canonicalized.toString();
|
|
}
|
|
|
|
/**
|
|
* Construct the Canonicalized Amazon headers element of the StringToSign by
|
|
* concatenating all headers in the TreeMap into a single string.
|
|
*
|
|
* @return A single string with all the Amazon headers glued together, or null
|
|
* if no Amazon headers appeared in the request.
|
|
*/
|
|
private String genCanonicalizedAmzHeadersElement() {
|
|
Collection<String> headers = AmazonHeaders.values();
|
|
Iterator<String> itr = headers.iterator();
|
|
StringBuffer canonicalized = new StringBuffer();
|
|
|
|
while (itr.hasNext())
|
|
canonicalized.append(itr.next());
|
|
|
|
if (0 == canonicalized.length())
|
|
return null;
|
|
|
|
return canonicalized.toString();
|
|
}
|
|
|
|
/**
|
|
* Create a signature by the following method:
|
|
* new String( Base64( SHA1( key, byte array )))
|
|
*
|
|
* @param signIt - the data to generate a keyed HMAC over
|
|
* @param secretKey - the user's unique key for the HMAC operation
|
|
* @return String - the recalculated string
|
|
* @throws SignatureException
|
|
*/
|
|
private String calculateRFC2104HMAC(String signIt, String secretKey) throws SignatureException {
|
|
String result = null;
|
|
try {
|
|
SecretKeySpec key = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
|
|
Mac hmacSha1 = Mac.getInstance("HmacSHA1");
|
|
hmacSha1.init(key);
|
|
byte[] rawHmac = hmacSha1.doFinal(signIt.getBytes());
|
|
result = new String(Base64.encodeBase64(rawHmac));
|
|
} catch (InvalidKeyException e) {
|
|
throw new SignatureException("Failed to generate keyed HMAC on REST request because key " + secretKey + " is invalid" + e.getMessage());
|
|
} catch (Exception e) {
|
|
throw new SignatureException("Failed to generate keyed HMAC on REST request: " + e.getMessage());
|
|
}
|
|
return result.trim();
|
|
}
|
|
}
|