blob: 82ffa0c5c43034bbc57a5c1a00a6cd9ed811d54a [file] [log] [blame]
/*
* 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 org.apache.hadoop.ozone.s3;
import com.google.common.annotations.VisibleForTesting;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
import org.apache.hadoop.ozone.s3.header.AuthorizationHeaderV4;
import org.apache.hadoop.ozone.s3.header.Credential;
import org.apache.kerby.util.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedMap;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.time.temporal.ChronoUnit.SECONDS;
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.S3_TOKEN_CREATION_ERROR;
/**
* Parser to process AWS v4 auth request. Creates string to sign and auth
* header. For more details refer to AWS documentation https://docs.aws
* .amazon.com/general/latest/gr/sigv4-create-canonical-request.html.
**/
public class AWSV4AuthParser implements AWSAuthParser {
private final static Logger LOG =
LoggerFactory.getLogger(AWSV4AuthParser.class);
private MultivaluedMap<String, String> headerMap;
private MultivaluedMap<String, String> queryMap;
private String uri;
private String method;
private AuthorizationHeaderV4 v4Header;
private String stringToSign;
private String amzContentPayload;
public AWSV4AuthParser(ContainerRequestContext context)
throws OS3Exception {
this.headerMap = context.getHeaders();
this.queryMap = context.getUriInfo().getQueryParameters();
try {
this.uri = new URI(context.getUriInfo().getRequestUri()
.getPath().replaceAll("\\/+",
"/")).normalize().getPath();
} catch (URISyntaxException e) {
throw S3_TOKEN_CREATION_ERROR;
}
this.method = context.getMethod();
v4Header = new AuthorizationHeaderV4(
headerMap.getFirst(AUTHORIZATION_HEADER));
}
public void parse() throws Exception {
StringBuilder strToSign = new StringBuilder();
// According to AWS sigv4 documentation, authorization header should be
// in following format.
// Authorization: algorithm Credential=access key ID/credential scope,
// SignedHeaders=SignedHeaders, Signature=signature
// Construct String to sign in below format.
// StringToSign =
// Algorithm + \n +
// RequestDateTime + \n +
// CredentialScope + \n +
// HashedCanonicalRequest
String algorithm, requestDateTime, credentialScope, canonicalRequest;
algorithm = v4Header.getAlgorithm();
requestDateTime = headerMap.getFirst(X_AMAZ_DATE);
Credential credential = v4Header.getCredentialObj();
credentialScope = String.format("%s/%s/%s/%s", credential.getDate(),
credential.getAwsRegion(), credential.getAwsService(),
credential.getAwsRequest());
// If the absolute path is empty, use a forward slash (/)
uri = (uri.trim().length() > 0) ? uri : "/";
// Encode URI and preserve forward slashes
strToSign.append(algorithm + NEWLINE);
strToSign.append(requestDateTime + NEWLINE);
strToSign.append(credentialScope + NEWLINE);
canonicalRequest = buildCanonicalRequest();
strToSign.append(hash(canonicalRequest));
if (LOG.isDebugEnabled()) {
LOG.debug("canonicalRequest:[{}]", canonicalRequest);
}
if (LOG.isTraceEnabled()) {
headerMap.keySet().forEach(k -> LOG.trace("Header:{},value:{}", k,
headerMap.get(k)));
}
LOG.debug("StringToSign:[{}]", strToSign);
stringToSign = strToSign.toString();
}
private String buildCanonicalRequest() throws OS3Exception {
Iterable<String> parts = split("/", uri);
List<String> encParts = new ArrayList<>();
for (String p : parts) {
encParts.add(urlEncode(p));
}
String canonicalUri = join("/", encParts);
String canonicalQueryStr = getQueryParamString();
StringBuilder canonicalHeaders = new StringBuilder();
for (String header : v4Header.getSignedHeaders()) {
List<String> headerValue = new ArrayList<>();
canonicalHeaders.append(header.toLowerCase());
canonicalHeaders.append(":");
for (String originalHeader : headerMap.keySet()) {
if (originalHeader.toLowerCase().equals(header)) {
headerValue.add(headerMap.getFirst(originalHeader).trim());
}
}
if (headerValue.size() == 0) {
throw new RuntimeException("Header " + header + " not present in " +
"request");
}
if (headerValue.size() > 1) {
Collections.sort(headerValue);
}
// Set for testing purpose only to skip date and host validation.
validateSignedHeader(header, headerValue.get(0));
canonicalHeaders.append(join(",", headerValue));
canonicalHeaders.append(NEWLINE);
}
String payloadHash;
if (UNSIGNED_PAYLOAD.equals(
headerMap.get(X_AMZ_CONTENT_SHA256))) {
payloadHash = UNSIGNED_PAYLOAD;
} else {
payloadHash = headerMap.getFirst(X_AMZ_CONTENT_SHA256);
}
String signedHeaderStr = v4Header.getSignedHeaderString();
String canonicalRequest = method + NEWLINE
+ canonicalUri + NEWLINE
+ canonicalQueryStr + NEWLINE
+ canonicalHeaders + NEWLINE
+ signedHeaderStr + NEWLINE
+ payloadHash;
return canonicalRequest;
}
@VisibleForTesting
void validateSignedHeader(String header, String headerValue)
throws OS3Exception {
switch (header) {
case HOST:
try {
URI hostUri = new URI(headerValue);
InetAddress.getByName(hostUri.getHost());
// TODO: Validate if current request is coming from same host.
} catch (UnknownHostException|URISyntaxException e) {
LOG.error("Host value mentioned in signed header is not valid. " +
"Host:{}", headerValue);
throw S3_TOKEN_CREATION_ERROR;
}
break;
case X_AMAZ_DATE:
LocalDate date = LocalDate.parse(headerValue, TIME_FORMATTER);
LocalDate now = LocalDate.now();
if (date.isBefore(now.minus(PRESIGN_URL_MAX_EXPIRATION_SECONDS, SECONDS))
|| date.isAfter(now.plus(PRESIGN_URL_MAX_EXPIRATION_SECONDS,
SECONDS))) {
LOG.error("AWS date not in valid range. Request timestamp:{} should " +
"not be older than {} seconds.", headerValue,
PRESIGN_URL_MAX_EXPIRATION_SECONDS);
throw S3_TOKEN_CREATION_ERROR;
}
break;
case X_AMZ_CONTENT_SHA256:
// TODO: Construct request payload and match HEX(SHA256(requestPayload))
break;
default:
break;
}
}
/**
* String join that also works with empty strings.
*
* @return joined string
*/
private static String join(String glue, List<String> parts) {
StringBuilder result = new StringBuilder();
boolean addSeparator = false;
for (String p : parts) {
if (addSeparator) {
result.append(glue);
}
result.append(p);
addSeparator = true;
}
return result.toString();
}
/**
* Returns matching strings.
*
* @param regex Regular expression to split by
* @param whole The string to split
* @return pieces
*/
private static Iterable<String> split(String regex, String whole) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(whole);
List<String> result = new ArrayList<>();
int pos = 0;
while (m.find()) {
result.add(whole.substring(pos, m.start()));
pos = m.end();
}
result.add(whole.substring(pos));
return result;
}
private String urlEncode(String str) {
try {
return URLEncoder.encode(str, UTF_8.name())
.replaceAll("\\+", "%20")
.replaceAll("%7E", "~");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private String getQueryParamString() {
List<String> params = new ArrayList<>(queryMap.keySet());
// Sort by name, then by value
Collections.sort(params, (o1, o2) -> o1.equals(o2) ?
queryMap.getFirst(o1).compareTo(queryMap.getFirst(o2)) :
o1.compareTo(o2));
StringBuilder result = new StringBuilder();
for (String p : params) {
if (result.length() > 0) {
result.append("&");
}
result.append(urlEncode(p));
result.append('=');
result.append(urlEncode(queryMap.getFirst(p)));
}
return result.toString();
}
public static String hash(String payload) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(payload.getBytes(UTF_8));
return Hex.encode(md.digest()).toLowerCase();
}
public String getAwsAccessId() {
return v4Header.getAccessKeyID();
}
public String getSignature() {
return v4Header.getSignature();
}
public String getStringToSign() throws Exception {
return stringToSign;
}
}