/**
* Copyright 2015-2016 The OpenZipkin Authors
*
* Licensed 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 zipkin.autoconfigure.storage.elasticsearch.aws;
import com.squareup.moshi.JsonReader;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.ByteString;
import static java.lang.String.format;
import static zipkin.internal.Util.checkNotNull;
import static zipkin.moshi.JsonReaders.enterPath;
// http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
final class AWSSignatureVersion4 implements Interceptor {
static final String EMPTY_STRING_HASH =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
static final String HOST = "host";
static final String X_AMZ_DATE = "x-amz-date";
static final String X_AMZ_SECURITY_TOKEN = "x-amz-security-token";
static final String[] CANONICAL_HEADERS = {HOST, X_AMZ_DATE, X_AMZ_SECURITY_TOKEN};
static final String HOST_DATE = HOST + ";" + X_AMZ_DATE;
static final String HOST_DATE_TOKEN = HOST_DATE + ";" + X_AMZ_SECURITY_TOKEN;
// SimpleDateFormat isn't thread-safe
static final ThreadLocal<SimpleDateFormat> iso8601 = new ThreadLocal<SimpleDateFormat>() {
@Override protected SimpleDateFormat initialValue() {
SimpleDateFormat result = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
result.setTimeZone(TimeZone.getTimeZone("UTC"));
return result;
}
};
final String region;
final String service;
final AWSCredentials.Provider credentials;
AWSSignatureVersion4(String region, String service, AWSCredentials.Provider credentials) {
this.region = checkNotNull(region, "region");
this.service = checkNotNull(service, "service");
this.credentials = checkNotNull(credentials, "credentials");
}
@Override public Response intercept(Chain chain) throws IOException {
Request input = chain.request();
Request signed = sign(input);
Response response = chain.proceed(signed);
if (response.code() == 403) {
try (ResponseBody body = response.body()) {
JsonReader message = enterPath(JsonReader.of(body.source()), "message");
if (message != null) throw new IllegalStateException(message.nextString());
}
throw new IllegalStateException(response.toString());
}
return response;
}
static Buffer canonicalString(Request input) throws IOException {
Buffer result = new Buffer();
// HTTPRequestMethod + '\n' +
result.writeUtf8(input.method()).writeByte('\n');
// CanonicalURI + '\n' +
// TODO: make this more efficient
result.writeUtf8(input.url().encodedPath()
.replace("*", "%2A").replace(",", "%2C")
).writeByte('\n');
// CanonicalQueryString + '\n' +
String query = input.url().encodedQuery();
result.writeUtf8(query == null ? "" : query).writeByte('\n');
// CanonicalHeaders + '\n' +
Buffer signedHeaders = new Buffer();
for (String canonicalHeader: CANONICAL_HEADERS) {
String value = input.header(canonicalHeader);
if (value != null) {
result.writeUtf8(canonicalHeader).writeByte(':').writeUtf8(value).writeByte('\n');
signedHeaders.writeByte(';').writeUtf8(canonicalHeader);
}
}
result.writeByte('\n'); // end headers
// SignedHeaders + '\n' +
signedHeaders.readByte(); // throw away the first semicolon
result.writeAll(signedHeaders);
result.writeByte('\n');
// HexEncode(Hash(Payload))
if (input.body() != null && input.body().contentLength() != 0) {
Buffer body = new Buffer();
input.body().writeTo(body);
result.writeUtf8(body.sha256().hex());
} else {
result.writeUtf8(EMPTY_STRING_HASH);
}
return result;
}
static Buffer toSign(String timestamp, String credentialScope, Buffer canonicalRequest) {
Buffer result = new Buffer();
// Algorithm + '\n' +
result.writeUtf8("AWS4-HMAC-SHA256\n");
// RequestDate + '\n' +
result.writeUtf8(timestamp).writeByte('\n');
// CredentialScope + '\n' +
result.writeUtf8(credentialScope).writeByte('\n');
// HexEncode(Hash(CanonicalRequest))
result.writeUtf8(canonicalRequest.sha256().hex());
return result;
}
Request sign(Request input) throws IOException {
AWSCredentials credentials = checkNotNull(this.credentials.get(), "awsCredentials");
String timestamp = iso8601.get().format(new Date());
String yyyyMMdd = timestamp.substring(0, 8);
String credentialScope = format("%s/%s/%s/%s", yyyyMMdd, region, service, "aws4_request");
Request.Builder builder = input.newBuilder();
builder.header(HOST, input.url().host());
builder.header(X_AMZ_DATE, timestamp);
if (credentials.sessionToken != null) {
builder.header(X_AMZ_SECURITY_TOKEN, credentials.sessionToken);
}
Buffer canonicalString = canonicalString(builder.build());
String signedHeaders = credentials.sessionToken == null ? HOST_DATE : HOST_DATE_TOKEN;
Buffer toSign = toSign(timestamp, credentialScope, canonicalString);
// TODO: this key is invalid when the secret key or the date change. both are very infrequent
ByteString signatureKey = signatureKey(credentials.secretKey, yyyyMMdd);
String signature = toSign.readByteString().hmacSha256(signatureKey).hex();
String authorization = new StringBuilder().append("AWS4-HMAC-SHA256 Credential=")
.append(credentials.accessKey).append('/').append(credentialScope)
.append(", SignedHeaders=").append(signedHeaders)
.append(", Signature=").append(signature).toString();
return builder.header("authorization", authorization).build();
}
ByteString signatureKey(String secretKey, String yyyyMMdd) {
ByteString kSecret = ByteString.encodeUtf8("AWS4" + secretKey);
ByteString kDate = ByteString.encodeUtf8(yyyyMMdd).hmacSha256(kSecret);
ByteString kRegion = ByteString.encodeUtf8(region).hmacSha256(kDate);
ByteString kService = ByteString.encodeUtf8(service).hmacSha256(kRegion);
ByteString kSigning = ByteString.encodeUtf8("aws4_request").hmacSha256(kService);
return kSigning;
}
}