HTTPCLIENT-926: Amazon S3 authentication support

Contributed by Jean-Philippe Steinmetz <caskater47 at gmail.com>


git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@934160 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2010-04-14 19:55:44 +00:00
parent a603d9b60c
commit 056b249b0b
2 changed files with 299 additions and 0 deletions

View File

@ -0,0 +1,255 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.http.contrib.auth;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.impl.cookie.DateUtils;
import org.apache.http.message.BasicHeader;
/**
* Implementation of Amazon S3 authentication. This scheme must be used
* preemptively only.
* <p>
* Reference Document: {@link http
* ://docs.amazonwebservices.com/AmazonS3/latest/index
* .html?RESTAuthentication.html}
*/
public class AWSScheme implements AuthScheme {
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
public static final String NAME = "AWS";
public AWSScheme() {
}
public Header authenticate(
final Credentials credentials,
final HttpRequest request) throws AuthenticationException {
// If the Date header has not been provided add it as it is required
if (request.getFirstHeader("Date") == null) {
Header dateHeader = new BasicHeader("Date", DateUtils.formatDate(new Date()));
request.addHeader(dateHeader);
}
String canonicalizedAmzHeaders = getCanonicalizedAmzHeaders(request.getAllHeaders());
String canonicalizedResource = getCanonicalizedResource(request.getRequestLine().getUri(),
(request.getFirstHeader("Host") != null ? request.getFirstHeader("Host").getValue()
: null));
String contentMD5 = request.getFirstHeader("Content-MD5") != null ? request.getFirstHeader(
"Content-MD5").getValue() : "";
String contentType = request.getFirstHeader("Content-Type") != null ? request
.getFirstHeader("Content-Type").getValue() : "";
String date = request.getFirstHeader("Date").getValue();
String method = request.getRequestLine().getMethod();
StringBuilder toSign = new StringBuilder();
toSign.append(method).append("\n");
toSign.append(contentMD5).append("\n");
toSign.append(contentType).append("\n");
toSign.append(date).append("\n");
toSign.append(canonicalizedAmzHeaders);
toSign.append(canonicalizedResource);
String signature = calculateRFC2104HMAC(toSign.toString(), credentials.getPassword());
String headerValue = NAME + " " + credentials.getUserPrincipal().getName() + ":"
+ signature;
return new BasicHeader("Authorization", headerValue);
}
/**
* Computes RFC 2104-compliant HMAC signature.
*
* @param data
* The data to be signed.
* @param key
* The signing key.
* @return The Base64-encoded RFC 2104-compliant HMAC signature.
* @throws RuntimeException
* when signature generation fails
*/
private static String calculateRFC2104HMAC(
final String data,
final String key) throws AuthenticationException {
try {
// get an hmac_sha1 key from the raw key bytes
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
// get an hmac_sha1 Mac instance and initialize with the signing key
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
// compute the hmac on input data bytes
byte[] rawHmac = mac.doFinal(data.getBytes());
// base64-encode the hmac
return Base64.encodeBase64String(rawHmac);
} catch (InvalidKeyException ex) {
throw new AuthenticationException("Failed to generate HMAC: " + ex.getMessage(), ex);
} catch (NoSuchAlgorithmException ex) {
throw new AuthenticationException(HMAC_SHA1_ALGORITHM +
" algorithm is not supported", ex);
}
}
/**
* Returns the canonicalized AMZ headers.
*
* @param headers
* The list of request headers.
* @return The canonicalized AMZ headers.
*/
private static String getCanonicalizedAmzHeaders(final Header[] headers) {
StringBuilder sb = new StringBuilder();
Pattern spacePattern = Pattern.compile("\\s+");
// Create a lexographically sorted list of headers that begin with x-amz
SortedMap<String, String> amzHeaders = new TreeMap<String, String>();
for (Header header : headers) {
String name = header.getName().toLowerCase();
if (name.startsWith("x-amz-")) {
String value = "";
if (amzHeaders.containsKey(name))
value = amzHeaders.get(name) + "," + header.getValue();
else
value = header.getValue();
// All newlines and multiple spaces must be replaced with a
// single space character.
Matcher m = spacePattern.matcher(value);
value = m.replaceAll(" ");
amzHeaders.put(name, value);
}
}
// Concatenate all AMZ headers
for (Entry<String, String> entry : amzHeaders.entrySet()) {
sb.append(entry.getKey()).append(':').append(entry.getValue()).append("\n");
}
return sb.toString();
}
/**
* Returns the canonicalized resource.
*
* @param uri
* The resource uri
* @param hostName
* the host name
* @return The canonicalized resource.
*/
private static String getCanonicalizedResource(String uri, String hostName) {
StringBuilder sb = new StringBuilder();
// Append the bucket if there is one
if (hostName != null) {
// If the host name contains a port number remove it
if (hostName.contains(":"))
hostName = hostName.substring(0, hostName.indexOf(":"));
// Now extract the bucket if there is one
if (hostName.endsWith(".s3.amazonaws.com")) {
String bucketName = hostName.substring(0, hostName.length() - 17);
sb.append("/" + bucketName);
}
}
int queryIdx = uri.indexOf("?");
// Append the resource path
if (queryIdx >= 0)
sb.append(uri.substring(0, queryIdx));
else
sb.append(uri.substring(0, uri.length()));
// Append the AWS sub-resource
if (queryIdx >= 0) {
String query = uri.substring(queryIdx - 1, uri.length());
if (query.contains("?acl"))
sb.append("?acl");
else if (query.contains("?location"))
sb.append("?location");
else if (query.contains("?logging"))
sb.append("?logging");
else if (query.contains("?torrent"))
sb.append("?torrent");
}
return sb.toString();
}
public String getParameter(String name) {
return null;
}
public String getRealm() {
return null;
}
public String getSchemeName() {
return NAME;
}
public boolean isComplete() {
return true;
}
public boolean isConnectionBased() {
return false;
}
public void processChallenge(final Header header) throws MalformedChallengeException {
// Nothing to do here
}
}

View File

@ -0,0 +1,44 @@
/*
* ====================================================================
* 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.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.http.contrib.auth;
import org.apache.http.auth.AuthScheme;
import org.apache.http.auth.AuthSchemeFactory;
import org.apache.http.params.HttpParams;
/**
* {@link AuthSchemeFactory} implementation that creates and initializes
* {@link AWSScheme} instances.
*/
public class AWSSchemeFactory implements AuthSchemeFactory {
public AuthScheme newInstance(final HttpParams params) {
return new AWSScheme();
}
}