Merge branch 'stable-5.13' into stable-6.0
* stable-5.13: Prepare 5.13.2-SNAPSHOT builds JGit v5.13.1.202206130422-r AmazonS3: Add support for AWS API signature version 4 Change-Id: Ibd663a1d874d1aac274abc3dd44354fd99f64c39
This commit is contained in:
commit
d961bb6502
|
@ -40,6 +40,15 @@
|
||||||
# * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html
|
# * https://docs.aws.amazon.com/AmazonS3/latest/dev/manage-lifecycle-using-console.html
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# AWS API signature version (defaults to 2)
|
||||||
|
# aws.api.signature.version=4
|
||||||
|
|
||||||
|
# AWS S3 Region Domain (defaults to s3.amazonaws.com)
|
||||||
|
# domain: s3-us-east-2.amazonaws.com
|
||||||
|
|
||||||
|
# AWS S3 Region (required if aws.api.signature.version=4, must match domain)
|
||||||
|
# region: us-east-2
|
||||||
|
|
||||||
# Test bucket name
|
# Test bucket name
|
||||||
test.bucket=jgit.eclipse.org
|
test.bucket=jgit.eclipse.org
|
||||||
|
|
||||||
|
|
|
@ -209,12 +209,14 @@ couldNotGetAdvertisedRef=Remote {0} did not advertise Ref for branch {1}. This R
|
||||||
couldNotGetRepoStatistics=Could not get repository statistics
|
couldNotGetRepoStatistics=Could not get repository statistics
|
||||||
couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format.
|
couldNotFindTabInLine=Could not find tab in line {0}. Tab is the mandatory separator for the Netscape Cookie File Format.
|
||||||
couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format.
|
couldNotFindSixTabsInLine=Could not find 6 tabs but only {0} in line '{1}'. 7 tab separated columns per line are mandatory for the Netscape Cookie File Format.
|
||||||
|
couldNotHashByteArrayWithSha256=Could not hash byte array with SHA-256 algorithm.
|
||||||
couldNotLockHEAD=Could not lock HEAD
|
couldNotLockHEAD=Could not lock HEAD
|
||||||
couldNotPersistCookies=Could not persist received cookies in file ''{0}''
|
couldNotPersistCookies=Could not persist received cookies in file ''{0}''
|
||||||
couldNotReadCookieFile=Could not read cookie file ''{0}''
|
couldNotReadCookieFile=Could not read cookie file ''{0}''
|
||||||
couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read
|
couldNotReadIndexInOneGo=Could not read index in one go, only {0} out of {1} read
|
||||||
couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0}
|
couldNotReadObjectWhileParsingCommit=Could not read an object while parsing commit {0}
|
||||||
couldNotRewindToUpstreamCommit=Could not rewind to upstream commit
|
couldNotRewindToUpstreamCommit=Could not rewind to upstream commit
|
||||||
|
couldNotSignStringWithKey=Could not sign string with key.
|
||||||
couldNotURLEncodeToUTF8=Could not URL encode to UTF-8
|
couldNotURLEncodeToUTF8=Could not URL encode to UTF-8
|
||||||
countingObjects=Counting objects
|
countingObjects=Counting objects
|
||||||
corruptPack=Pack file {0} is corrupt, removing it from pack list
|
corruptPack=Pack file {0} is corrupt, removing it from pack list
|
||||||
|
@ -361,6 +363,7 @@ interruptedWriting=Interrupted writing {0}
|
||||||
inTheFuture=in the future
|
inTheFuture=in the future
|
||||||
invalidAdvertisementOf=invalid advertisement of {0}
|
invalidAdvertisementOf=invalid advertisement of {0}
|
||||||
invalidAncestryLength=Invalid ancestry length
|
invalidAncestryLength=Invalid ancestry length
|
||||||
|
invalidAwsApiSignatureVersion=Invalid aws.api.signature.version: {0}
|
||||||
invalidBooleanValue=Invalid boolean value: {0}.{1}={2}
|
invalidBooleanValue=Invalid boolean value: {0}.{1}={2}
|
||||||
invalidChannel=Invalid channel {0}
|
invalidChannel=Invalid channel {0}
|
||||||
invalidCommitParentNumber=Invalid commit parent number
|
invalidCommitParentNumber=Invalid commit parent number
|
||||||
|
@ -458,6 +461,7 @@ minutesAgo={0} minutes ago
|
||||||
mismatchOffset=mismatch offset for object {0}
|
mismatchOffset=mismatch offset for object {0}
|
||||||
mismatchCRC=mismatch CRC for object {0}
|
mismatchCRC=mismatch CRC for object {0}
|
||||||
missingAccesskey=Missing accesskey.
|
missingAccesskey=Missing accesskey.
|
||||||
|
missingAwsRegion=Missing region (e.g. us-west-2).
|
||||||
missingConfigurationForKey=No value for key {0} found in configuration
|
missingConfigurationForKey=No value for key {0} found in configuration
|
||||||
missingCookieFile=Configured http.cookieFile ''{0}'' is missing
|
missingCookieFile=Configured http.cookieFile ''{0}'' is missing
|
||||||
missingCRC=missing CRC for object {0}
|
missingCRC=missing CRC for object {0}
|
||||||
|
@ -739,6 +743,7 @@ unableToWrite=Unable to write {0}
|
||||||
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
|
unableToSignCommitNoSecretKey=Unable to sign commit. Signing key not available.
|
||||||
unauthorized=Unauthorized
|
unauthorized=Unauthorized
|
||||||
unencodeableFile=Unencodable file: {0}
|
unencodeableFile=Unencodable file: {0}
|
||||||
|
unexpectedAwsApiSignatureVersion=Unexpected AWS API Signature Version: {0}
|
||||||
unexpectedCompareResult=Unexpected metadata comparison result: {0}
|
unexpectedCompareResult=Unexpected metadata comparison result: {0}
|
||||||
unexpectedEndOfConfigFile=Unexpected end of config file
|
unexpectedEndOfConfigFile=Unexpected end of config file
|
||||||
unexpectedEndOfInput=Unexpected end of input
|
unexpectedEndOfInput=Unexpected end of input
|
||||||
|
|
|
@ -238,12 +238,14 @@ public static JGitText get() {
|
||||||
/***/ public String couldNotFindSixTabsInLine;
|
/***/ public String couldNotFindSixTabsInLine;
|
||||||
/***/ public String couldNotGetAdvertisedRef;
|
/***/ public String couldNotGetAdvertisedRef;
|
||||||
/***/ public String couldNotGetRepoStatistics;
|
/***/ public String couldNotGetRepoStatistics;
|
||||||
|
/***/ public String couldNotHashByteArrayWithSha256;
|
||||||
/***/ public String couldNotLockHEAD;
|
/***/ public String couldNotLockHEAD;
|
||||||
/***/ public String couldNotPersistCookies;
|
/***/ public String couldNotPersistCookies;
|
||||||
/***/ public String couldNotReadCookieFile;
|
/***/ public String couldNotReadCookieFile;
|
||||||
/***/ public String couldNotReadIndexInOneGo;
|
/***/ public String couldNotReadIndexInOneGo;
|
||||||
/***/ public String couldNotReadObjectWhileParsingCommit;
|
/***/ public String couldNotReadObjectWhileParsingCommit;
|
||||||
/***/ public String couldNotRewindToUpstreamCommit;
|
/***/ public String couldNotRewindToUpstreamCommit;
|
||||||
|
/***/ public String couldNotSignStringWithKey;
|
||||||
/***/ public String couldNotURLEncodeToUTF8;
|
/***/ public String couldNotURLEncodeToUTF8;
|
||||||
/***/ public String countingObjects;
|
/***/ public String countingObjects;
|
||||||
/***/ public String createBranchFailedUnknownReason;
|
/***/ public String createBranchFailedUnknownReason;
|
||||||
|
@ -389,6 +391,7 @@ public static JGitText get() {
|
||||||
/***/ public String inTheFuture;
|
/***/ public String inTheFuture;
|
||||||
/***/ public String invalidAdvertisementOf;
|
/***/ public String invalidAdvertisementOf;
|
||||||
/***/ public String invalidAncestryLength;
|
/***/ public String invalidAncestryLength;
|
||||||
|
/***/ public String invalidAwsApiSignatureVersion;
|
||||||
/***/ public String invalidBooleanValue;
|
/***/ public String invalidBooleanValue;
|
||||||
/***/ public String invalidChannel;
|
/***/ public String invalidChannel;
|
||||||
/***/ public String invalidCommitParentNumber;
|
/***/ public String invalidCommitParentNumber;
|
||||||
|
@ -486,6 +489,7 @@ public static JGitText get() {
|
||||||
/***/ public String mismatchOffset;
|
/***/ public String mismatchOffset;
|
||||||
/***/ public String mismatchCRC;
|
/***/ public String mismatchCRC;
|
||||||
/***/ public String missingAccesskey;
|
/***/ public String missingAccesskey;
|
||||||
|
/***/ public String missingAwsRegion;
|
||||||
/***/ public String missingConfigurationForKey;
|
/***/ public String missingConfigurationForKey;
|
||||||
/***/ public String missingCookieFile;
|
/***/ public String missingCookieFile;
|
||||||
/***/ public String missingCRC;
|
/***/ public String missingCRC;
|
||||||
|
@ -767,6 +771,7 @@ public static JGitText get() {
|
||||||
/***/ public String unableToSignCommitNoSecretKey;
|
/***/ public String unableToSignCommitNoSecretKey;
|
||||||
/***/ public String unauthorized;
|
/***/ public String unauthorized;
|
||||||
/***/ public String unencodeableFile;
|
/***/ public String unencodeableFile;
|
||||||
|
/***/ public String unexpectedAwsApiSignatureVersion;
|
||||||
/***/ public String unexpectedCompareResult;
|
/***/ public String unexpectedCompareResult;
|
||||||
/***/ public String unexpectedEndOfConfigFile;
|
/***/ public String unexpectedEndOfConfigFile;
|
||||||
/***/ public String unexpectedEndOfInput;
|
/***/ public String unexpectedEndOfInput;
|
||||||
|
|
|
@ -86,6 +86,12 @@
|
||||||
public class AmazonS3 {
|
public class AmazonS3 {
|
||||||
private static final Set<String> SIGNED_HEADERS;
|
private static final Set<String> SIGNED_HEADERS;
|
||||||
|
|
||||||
|
private static final String AWS_API_V2 = "2"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private static final String AWS_API_V4 = "4"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private static final String AWS_S3_SERVICE_NAME = "s3"; //$NON-NLS-1$
|
||||||
|
|
||||||
private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$
|
private static final String HMAC = "HmacSHA1"; //$NON-NLS-1$
|
||||||
|
|
||||||
private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$
|
private static final String X_AMZ_ACL = "x-amz-acl"; //$NON-NLS-1$
|
||||||
|
@ -135,11 +141,17 @@ private static MessageDigest newMD5() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** AWS API Signature Version. */
|
||||||
|
private final String awsApiSignatureVersion;
|
||||||
|
|
||||||
/** AWSAccessKeyId, public string that identifies the user's account. */
|
/** AWSAccessKeyId, public string that identifies the user's account. */
|
||||||
private final String publicKey;
|
private final String publicKey;
|
||||||
|
|
||||||
/** Decoded form of the private AWSSecretAccessKey, to sign requests. */
|
/** Decoded form of the private AWSSecretAccessKey, to sign requests. */
|
||||||
private final SecretKeySpec privateKey;
|
private final SecretKeySpec secretKeySpec;
|
||||||
|
|
||||||
|
/** AWSSecretAccessKey, private string used to access a user's account. */
|
||||||
|
private final char[] secretKey; // store as char[] for security
|
||||||
|
|
||||||
/** Our HTTP proxy support, in case we are behind a firewall. */
|
/** Our HTTP proxy support, in case we are behind a firewall. */
|
||||||
private final ProxySelector proxySelector;
|
private final ProxySelector proxySelector;
|
||||||
|
@ -159,8 +171,12 @@ private static MessageDigest newMD5() {
|
||||||
/** S3 Bucket Domain. */
|
/** S3 Bucket Domain. */
|
||||||
private final String domain;
|
private final String domain;
|
||||||
|
|
||||||
|
/** S3 Region. */
|
||||||
|
private final String region;
|
||||||
|
|
||||||
/** Property names used in amazon connection configuration file. */
|
/** Property names used in amazon connection configuration file. */
|
||||||
interface Keys {
|
interface Keys {
|
||||||
|
String AWS_API_SIGNATURE_VERSION = "aws.api.signature.version"; //$NON-NLS-1$
|
||||||
String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
|
String ACCESS_KEY = "accesskey"; //$NON-NLS-1$
|
||||||
String SECRET_KEY = "secretkey"; //$NON-NLS-1$
|
String SECRET_KEY = "secretkey"; //$NON-NLS-1$
|
||||||
String PASSWORD = "password"; //$NON-NLS-1$
|
String PASSWORD = "password"; //$NON-NLS-1$
|
||||||
|
@ -168,6 +184,7 @@ interface Keys {
|
||||||
String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
|
String CRYPTO_VER = "crypto.version"; //$NON-NLS-1$
|
||||||
String ACL = "acl"; //$NON-NLS-1$
|
String ACL = "acl"; //$NON-NLS-1$
|
||||||
String DOMAIN = "domain"; //$NON-NLS-1$
|
String DOMAIN = "domain"; //$NON-NLS-1$
|
||||||
|
String REGION = "region"; //$NON-NLS-1$
|
||||||
String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
|
String HTTP_RETRY = "httpclient.retry-max"; //$NON-NLS-1$
|
||||||
String TMP_DIR = "tmpdir"; //$NON-NLS-1$
|
String TMP_DIR = "tmpdir"; //$NON-NLS-1$
|
||||||
}
|
}
|
||||||
|
@ -180,6 +197,12 @@ interface Keys {
|
||||||
* For example:
|
* For example:
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
|
* # AWS API signature version, must be one of:
|
||||||
|
* # 2 - deprecated (not supported in all AWS regions)
|
||||||
|
* # 4 - latest (supported in all AWS regions)
|
||||||
|
* # Defaults to 2.
|
||||||
|
* aws.api.signature.version: 4
|
||||||
|
*
|
||||||
* # AWS Access and Secret Keys (required)
|
* # AWS Access and Secret Keys (required)
|
||||||
* accesskey: <YourAWSAccessKey>
|
* accesskey: <YourAWSAccessKey>
|
||||||
* secretkey: <YourAWSSecretKey>
|
* secretkey: <YourAWSSecretKey>
|
||||||
|
@ -192,6 +215,9 @@ interface Keys {
|
||||||
* # AWS S3 Region Domain (defaults to s3.amazonaws.com)
|
* # AWS S3 Region Domain (defaults to s3.amazonaws.com)
|
||||||
* domain: s3.amazonaws.com
|
* domain: s3.amazonaws.com
|
||||||
*
|
*
|
||||||
|
* # AWS S3 Region (required if aws.api.signature.version = 4)
|
||||||
|
* region: us-west-2
|
||||||
|
*
|
||||||
* # Number of times to retry after internal error from S3.
|
* # Number of times to retry after internal error from S3.
|
||||||
* httpclient.retry-max: 3
|
* httpclient.retry-max: 3
|
||||||
*
|
*
|
||||||
|
@ -204,16 +230,34 @@ interface Keys {
|
||||||
* connection properties.
|
* connection properties.
|
||||||
*/
|
*/
|
||||||
public AmazonS3(final Properties props) {
|
public AmazonS3(final Properties props) {
|
||||||
|
awsApiSignatureVersion = props
|
||||||
|
.getProperty(Keys.AWS_API_SIGNATURE_VERSION, AWS_API_V2);
|
||||||
|
if (awsApiSignatureVersion.equals(AWS_API_V4)) {
|
||||||
|
region = props.getProperty(Keys.REGION);
|
||||||
|
if (region == null) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
JGitText.get().missingAwsRegion);
|
||||||
|
}
|
||||||
|
} else if (awsApiSignatureVersion.equals(AWS_API_V2)) {
|
||||||
|
region = null;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(MessageFormat.format(
|
||||||
|
JGitText.get().invalidAwsApiSignatureVersion,
|
||||||
|
awsApiSignatureVersion));
|
||||||
|
}
|
||||||
|
|
||||||
domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$
|
domain = props.getProperty(Keys.DOMAIN, "s3.amazonaws.com"); //$NON-NLS-1$
|
||||||
|
|
||||||
publicKey = props.getProperty(Keys.ACCESS_KEY);
|
publicKey = props.getProperty(Keys.ACCESS_KEY);
|
||||||
if (publicKey == null)
|
if (publicKey == null)
|
||||||
throw new IllegalArgumentException(JGitText.get().missingAccesskey);
|
throw new IllegalArgumentException(JGitText.get().missingAccesskey);
|
||||||
|
|
||||||
final String secret = props.getProperty(Keys.SECRET_KEY);
|
final String secretKeyStr = props.getProperty(Keys.SECRET_KEY);
|
||||||
if (secret == null)
|
if (secretKeyStr == null) {
|
||||||
throw new IllegalArgumentException(JGitText.get().missingSecretkey);
|
throw new IllegalArgumentException(JGitText.get().missingSecretkey);
|
||||||
privateKey = new SecretKeySpec(Constants.encodeASCII(secret), HMAC);
|
}
|
||||||
|
secretKeySpec = new SecretKeySpec(Constants.encodeASCII(secretKeyStr), HMAC);
|
||||||
|
secretKey = secretKeyStr.toCharArray();
|
||||||
|
|
||||||
final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$
|
final String pacl = props.getProperty(Keys.ACL, "PRIVATE"); //$NON-NLS-1$
|
||||||
if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
|
if (StringUtils.equalsIgnoreCase("PRIVATE", pacl)) //$NON-NLS-1$
|
||||||
|
@ -258,7 +302,7 @@ public URLConnection get(String bucket, String key)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
||||||
final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$
|
final HttpURLConnection c = open("GET", bucket, key); //$NON-NLS-1$
|
||||||
authorize(c);
|
authorize(c, Collections.emptyMap(), 0, null);
|
||||||
switch (HttpSupport.response(c)) {
|
switch (HttpSupport.response(c)) {
|
||||||
case HttpURLConnection.HTTP_OK:
|
case HttpURLConnection.HTTP_OK:
|
||||||
encryption.validate(c, X_AMZ_META);
|
encryption.validate(c, X_AMZ_META);
|
||||||
|
@ -339,7 +383,7 @@ public void delete(String bucket, String key)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
||||||
final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$
|
final HttpURLConnection c = open("DELETE", bucket, key); //$NON-NLS-1$
|
||||||
authorize(c);
|
authorize(c, Collections.emptyMap(), 0, null);
|
||||||
switch (HttpSupport.response(c)) {
|
switch (HttpSupport.response(c)) {
|
||||||
case HttpURLConnection.HTTP_NO_CONTENT:
|
case HttpURLConnection.HTTP_NO_CONTENT:
|
||||||
return;
|
return;
|
||||||
|
@ -385,13 +429,16 @@ public void put(String bucket, String key, byte[] data)
|
||||||
}
|
}
|
||||||
|
|
||||||
final String md5str = Base64.encodeBytes(newMD5().digest(data));
|
final String md5str = Base64.encodeBytes(newMD5().digest(data));
|
||||||
|
final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
|
||||||
|
? AwsRequestSignerV4.calculateBodyHash(data)
|
||||||
|
: null;
|
||||||
final String lenstr = String.valueOf(data.length);
|
final String lenstr = String.valueOf(data.length);
|
||||||
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
||||||
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
|
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
|
||||||
c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
|
c.setRequestProperty("Content-Length", lenstr); //$NON-NLS-1$
|
||||||
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
|
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
|
||||||
c.setRequestProperty(X_AMZ_ACL, acl);
|
c.setRequestProperty(X_AMZ_ACL, acl);
|
||||||
authorize(c);
|
authorize(c, Collections.emptyMap(), data.length, bodyHash);
|
||||||
c.setDoOutput(true);
|
c.setDoOutput(true);
|
||||||
c.setFixedLengthStreamingMode(data.length);
|
c.setFixedLengthStreamingMode(data.length);
|
||||||
try (OutputStream os = c.getOutputStream()) {
|
try (OutputStream os = c.getOutputStream()) {
|
||||||
|
@ -466,6 +513,9 @@ void putImpl(final String bucket, final String key,
|
||||||
monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
|
monitorTask = MessageFormat.format(JGitText.get().progressMonUploading, key);
|
||||||
|
|
||||||
final String md5str = Base64.encodeBytes(csum);
|
final String md5str = Base64.encodeBytes(csum);
|
||||||
|
final String bodyHash = awsApiSignatureVersion.equals(AWS_API_V4)
|
||||||
|
? AwsRequestSignerV4.calculateBodyHash(buf.toByteArray())
|
||||||
|
: null;
|
||||||
final long len = buf.length();
|
final long len = buf.length();
|
||||||
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
||||||
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
|
final HttpURLConnection c = open("PUT", bucket, key); //$NON-NLS-1$
|
||||||
|
@ -473,7 +523,7 @@ void putImpl(final String bucket, final String key,
|
||||||
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
|
c.setRequestProperty("Content-MD5", md5str); //$NON-NLS-1$
|
||||||
c.setRequestProperty(X_AMZ_ACL, acl);
|
c.setRequestProperty(X_AMZ_ACL, acl);
|
||||||
encryption.request(c, X_AMZ_META);
|
encryption.request(c, X_AMZ_META);
|
||||||
authorize(c);
|
authorize(c, Collections.emptyMap(), len, bodyHash);
|
||||||
c.setDoOutput(true);
|
c.setDoOutput(true);
|
||||||
monitor.beginTask(monitorTask, (int) (len / 1024));
|
monitor.beginTask(monitorTask, (int) (len / 1024));
|
||||||
try (OutputStream os = c.getOutputStream()) {
|
try (OutputStream os = c.getOutputStream()) {
|
||||||
|
@ -545,8 +595,13 @@ HttpURLConnection open(final String method, final String bucket,
|
||||||
urlstr.append('.');
|
urlstr.append('.');
|
||||||
urlstr.append(domain);
|
urlstr.append(domain);
|
||||||
urlstr.append('/');
|
urlstr.append('/');
|
||||||
if (key.length() > 0)
|
if (key.length() > 0) {
|
||||||
|
if (awsApiSignatureVersion.equals(AWS_API_V2)) {
|
||||||
HttpSupport.encode(urlstr, key);
|
HttpSupport.encode(urlstr, key);
|
||||||
|
} else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
|
||||||
|
urlstr.append(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!args.isEmpty()) {
|
if (!args.isEmpty()) {
|
||||||
final Iterator<Map.Entry<String, String>> i;
|
final Iterator<Map.Entry<String, String>> i;
|
||||||
|
|
||||||
|
@ -573,7 +628,18 @@ HttpURLConnection open(final String method, final String bucket,
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
void authorize(HttpURLConnection c) throws IOException {
|
void authorize(HttpURLConnection httpURLConnection,
|
||||||
|
Map<String, String> queryParams, long contentLength,
|
||||||
|
final String bodyHash) throws IOException {
|
||||||
|
if (awsApiSignatureVersion.equals(AWS_API_V2)) {
|
||||||
|
authorizeV2(httpURLConnection);
|
||||||
|
} else if (awsApiSignatureVersion.equals(AWS_API_V4)) {
|
||||||
|
AwsRequestSignerV4.sign(httpURLConnection, queryParams, contentLength, bodyHash, AWS_S3_SERVICE_NAME,
|
||||||
|
region, publicKey, secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void authorizeV2(HttpURLConnection c) throws IOException {
|
||||||
final Map<String, List<String>> reqHdr = c.getRequestProperties();
|
final Map<String, List<String>> reqHdr = c.getRequestProperties();
|
||||||
final SortedMap<String, String> sigHdr = new TreeMap<>();
|
final SortedMap<String, String> sigHdr = new TreeMap<>();
|
||||||
for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
|
for (Map.Entry<String, List<String>> entry : reqHdr.entrySet()) {
|
||||||
|
@ -610,7 +676,7 @@ void authorize(HttpURLConnection c) throws IOException {
|
||||||
final String sec;
|
final String sec;
|
||||||
try {
|
try {
|
||||||
final Mac m = Mac.getInstance(HMAC);
|
final Mac m = Mac.getInstance(HMAC);
|
||||||
m.init(privateKey);
|
m.init(secretKeySpec);
|
||||||
sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8)));
|
sec = Base64.encodeBytes(m.doFinal(s.toString().getBytes(UTF_8)));
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
|
throw new IOException(MessageFormat.format(JGitText.get().noHMACsupport, HMAC, e.getMessage()));
|
||||||
|
@ -674,7 +740,7 @@ void list() throws IOException {
|
||||||
|
|
||||||
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
for (int curAttempt = 0; curAttempt < maxAttempts; curAttempt++) {
|
||||||
final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
|
final HttpURLConnection c = open("GET", bucket, "", args); //$NON-NLS-1$ //$NON-NLS-2$
|
||||||
authorize(c);
|
authorize(c, args, 0, null);
|
||||||
switch (HttpSupport.response(c)) {
|
switch (HttpSupport.response(c)) {
|
||||||
case HttpURLConnection.HTTP_OK:
|
case HttpURLConnection.HTTP_OK:
|
||||||
truncated = false;
|
truncated = false;
|
||||||
|
|
|
@ -0,0 +1,313 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022, Workday Inc.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Distribution License v. 1.0 which is available at
|
||||||
|
* https://www.eclipse.org/org/documents/edl-v10.php.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.eclipse.jgit.transport;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.internal.JGitText;
|
||||||
|
import org.eclipse.jgit.util.Hex;
|
||||||
|
import org.eclipse.jgit.util.HttpSupport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for signing requests to AWS service endpoints using the V4
|
||||||
|
* signing protocol.
|
||||||
|
*
|
||||||
|
* Reference implementation: <a href=
|
||||||
|
* "https://docs.aws.amazon.com/AmazonS3/latest/API/samples/AWSS3SigV4JavaSamples.zip">AWSS3SigV4JavaSamples.zip</a>
|
||||||
|
*
|
||||||
|
* @see <a href=
|
||||||
|
* "https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html">AWS
|
||||||
|
* Signature Version 4</a>
|
||||||
|
*
|
||||||
|
* @since 5.13
|
||||||
|
*/
|
||||||
|
public final class AwsRequestSignerV4 {
|
||||||
|
|
||||||
|
/** AWS version 4 signing algorithm (for authorization header). **/
|
||||||
|
private static final String ALGORITHM = "HMAC-SHA256"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** Java Message Authentication Code (MAC) algorithm name. **/
|
||||||
|
private static final String MAC_ALGORITHM = "HmacSHA256"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** AWS version 4 signing scheme. **/
|
||||||
|
private static final String SCHEME = "AWS4"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** AWS version 4 terminator string. **/
|
||||||
|
private static final String TERMINATOR = "aws4_request"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** SHA-256 hash of an empty request body. **/
|
||||||
|
private static final String EMPTY_BODY_SHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** Date format for the 'x-amz-date' header. **/
|
||||||
|
private static final DateTimeFormatter AMZ_DATE_FORMAT = DateTimeFormatter
|
||||||
|
.ofPattern("yyyyMMdd'T'HHmmss'Z'"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** Date format for the string-to-sign's scope. **/
|
||||||
|
private static final DateTimeFormatter SCOPE_DATE_FORMAT = DateTimeFormatter
|
||||||
|
.ofPattern("yyyyMMdd"); //$NON-NLS-1$
|
||||||
|
|
||||||
|
private AwsRequestSignerV4() {
|
||||||
|
// Don't instantiate utility class
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign the provided request with an AWS4 signature as the 'Authorization'
|
||||||
|
* header.
|
||||||
|
*
|
||||||
|
* @param httpURLConnection
|
||||||
|
* The request to sign.
|
||||||
|
* @param queryParameters
|
||||||
|
* The query parameters being sent in the request.
|
||||||
|
* @param contentLength
|
||||||
|
* The content length of the data being sent in the request
|
||||||
|
* @param bodyHash
|
||||||
|
* Hex-encoded SHA-256 hash of the data being sent in the request
|
||||||
|
* @param serviceName
|
||||||
|
* The signing name of the AWS service (e.g. "s3").
|
||||||
|
* @param regionName
|
||||||
|
* The name of the AWS region that will handle the request (e.g.
|
||||||
|
* "us-east-1").
|
||||||
|
* @param awsAccessKey
|
||||||
|
* The user's AWS Access Key.
|
||||||
|
* @param awsSecretKey
|
||||||
|
* The user's AWS Secret Key.
|
||||||
|
*/
|
||||||
|
public static void sign(HttpURLConnection httpURLConnection,
|
||||||
|
Map<String, String> queryParameters, long contentLength,
|
||||||
|
String bodyHash, String serviceName, String regionName,
|
||||||
|
String awsAccessKey, char[] awsSecretKey) {
|
||||||
|
// get request headers
|
||||||
|
Map<String, String> headers = new HashMap<>();
|
||||||
|
httpURLConnection.getRequestProperties()
|
||||||
|
.forEach((headerName, headerValues) -> headers.put(headerName,
|
||||||
|
String.join(",", headerValues))); //$NON-NLS-1$
|
||||||
|
|
||||||
|
// add required content headers
|
||||||
|
if (contentLength > 0) {
|
||||||
|
headers.put(HttpSupport.HDR_CONTENT_LENGTH,
|
||||||
|
String.valueOf(contentLength));
|
||||||
|
} else {
|
||||||
|
bodyHash = EMPTY_BODY_SHA256;
|
||||||
|
}
|
||||||
|
headers.put("x-amz-content-sha256", bodyHash); //$NON-NLS-1$
|
||||||
|
|
||||||
|
// add the 'x-amz-date' header
|
||||||
|
OffsetDateTime now = Instant.now().atOffset(ZoneOffset.UTC);
|
||||||
|
String amzDate = now.format(AMZ_DATE_FORMAT);
|
||||||
|
headers.put("x-amz-date", amzDate); //$NON-NLS-1$
|
||||||
|
|
||||||
|
// add the 'host' header
|
||||||
|
URL endpointUrl = httpURLConnection.getURL();
|
||||||
|
int port = endpointUrl.getPort();
|
||||||
|
String hostHeader = (port > -1)
|
||||||
|
? endpointUrl.getHost().concat(":" + port) //$NON-NLS-1$
|
||||||
|
: endpointUrl.getHost();
|
||||||
|
headers.put("Host", hostHeader); //$NON-NLS-1$
|
||||||
|
|
||||||
|
// construct the canonicalized request
|
||||||
|
String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
|
||||||
|
String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
|
||||||
|
String canonicalizedQueryParameters = getCanonicalizedQueryString(
|
||||||
|
queryParameters);
|
||||||
|
String httpMethod = httpURLConnection.getRequestMethod();
|
||||||
|
String canonicalRequest = httpMethod + '\n'
|
||||||
|
+ getCanonicalizedResourcePath(endpointUrl) + '\n'
|
||||||
|
+ canonicalizedQueryParameters + '\n' + canonicalizedHeaders
|
||||||
|
+ '\n' + canonicalizedHeaderNames + '\n' + bodyHash;
|
||||||
|
|
||||||
|
// construct the string-to-sign
|
||||||
|
String scopeDate = now.format(SCOPE_DATE_FORMAT);
|
||||||
|
String scope = scopeDate + '/' + regionName + '/' + serviceName + '/'
|
||||||
|
+ TERMINATOR;
|
||||||
|
String stringToSign = SCHEME + '-' + ALGORITHM + '\n' + amzDate + '\n'
|
||||||
|
+ scope + '\n' + Hex.toHexString(hash(
|
||||||
|
canonicalRequest.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
|
||||||
|
// compute the signing key
|
||||||
|
byte[] secretKey = (SCHEME + new String(awsSecretKey)).getBytes();
|
||||||
|
byte[] dateKey = signStringWithKey(scopeDate, secretKey);
|
||||||
|
byte[] regionKey = signStringWithKey(regionName, dateKey);
|
||||||
|
byte[] serviceKey = signStringWithKey(serviceName, regionKey);
|
||||||
|
byte[] signingKey = signStringWithKey(TERMINATOR, serviceKey);
|
||||||
|
byte[] signature = signStringWithKey(stringToSign, signingKey);
|
||||||
|
|
||||||
|
// construct the authorization header
|
||||||
|
String credentialsAuthorizationHeader = "Credential=" + awsAccessKey //$NON-NLS-1$
|
||||||
|
+ '/' + scope;
|
||||||
|
String signedHeadersAuthorizationHeader = "SignedHeaders=" //$NON-NLS-1$
|
||||||
|
+ canonicalizedHeaderNames;
|
||||||
|
String signatureAuthorizationHeader = "Signature=" //$NON-NLS-1$
|
||||||
|
+ Hex.toHexString(signature);
|
||||||
|
String authorizationHeader = SCHEME + '-' + ALGORITHM + ' '
|
||||||
|
+ credentialsAuthorizationHeader + ", " //$NON-NLS-1$
|
||||||
|
+ signedHeadersAuthorizationHeader + ", " //$NON-NLS-1$
|
||||||
|
+ signatureAuthorizationHeader;
|
||||||
|
|
||||||
|
// Copy back the updated request headers
|
||||||
|
headers.forEach(httpURLConnection::setRequestProperty);
|
||||||
|
|
||||||
|
// Add the 'authorization' header
|
||||||
|
httpURLConnection.setRequestProperty(HttpSupport.HDR_AUTHORIZATION,
|
||||||
|
authorizationHeader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the hex-encoded SHA-256 hash of the provided byte array.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* Byte array to hash
|
||||||
|
*
|
||||||
|
* @return Hex-encoded SHA-256 hash of the provided byte array.
|
||||||
|
*/
|
||||||
|
public static String calculateBodyHash(final byte[] data) {
|
||||||
|
return (data == null || data.length < 1) ? EMPTY_BODY_SHA256
|
||||||
|
: Hex.toHexString(hash(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a string listing all request headers in sorted case-insensitive
|
||||||
|
* order, separated by a ';'.
|
||||||
|
*
|
||||||
|
* @param headers
|
||||||
|
* Map containing all request headers.
|
||||||
|
*
|
||||||
|
* @return String that lists all request headers in sorted case-insensitive
|
||||||
|
* order, separated by a ';'.
|
||||||
|
*/
|
||||||
|
private static String getCanonicalizeHeaderNames(
|
||||||
|
Map<String, String> headers) {
|
||||||
|
return headers.keySet().stream().map(String::toLowerCase).sorted()
|
||||||
|
.collect(Collectors.joining(";")); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the canonical header string for a request.
|
||||||
|
*
|
||||||
|
* @param headers
|
||||||
|
* Map containing all request headers.
|
||||||
|
*
|
||||||
|
* @return The canonical headers with values for the request.
|
||||||
|
*/
|
||||||
|
private static String getCanonicalizedHeaderString(
|
||||||
|
Map<String, String> headers) {
|
||||||
|
if (headers == null || headers.isEmpty()) {
|
||||||
|
return ""; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
headers.keySet().stream().sorted(String.CASE_INSENSITIVE_ORDER)
|
||||||
|
.forEach(key -> {
|
||||||
|
String header = key.toLowerCase().replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
|
||||||
|
String value = headers.get(key).replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
|
||||||
|
sb.append(header).append(':').append(value).append('\n');
|
||||||
|
});
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the canonicalized resource path for an AWS service endpoint.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The AWS service endpoint URL, including the path to any
|
||||||
|
* resource.
|
||||||
|
*
|
||||||
|
* @return The canonicalized resource path for the AWS service endpoint.
|
||||||
|
*/
|
||||||
|
private static String getCanonicalizedResourcePath(URL url) {
|
||||||
|
if (url == null) {
|
||||||
|
return "/"; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
String path = url.getPath();
|
||||||
|
if (path == null || path.isEmpty()) {
|
||||||
|
return "/"; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
String encodedPath = HttpSupport.urlEncode(path, true);
|
||||||
|
if (encodedPath.startsWith("/")) { //$NON-NLS-1$
|
||||||
|
return encodedPath;
|
||||||
|
}
|
||||||
|
return "/".concat(encodedPath); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs the canonicalized query string for a request.
|
||||||
|
*
|
||||||
|
* @param queryParameters
|
||||||
|
* The query parameters in the request.
|
||||||
|
*
|
||||||
|
* @return The canonicalized query string for the request.
|
||||||
|
*/
|
||||||
|
public static String getCanonicalizedQueryString(
|
||||||
|
Map<String, String> queryParameters) {
|
||||||
|
if (queryParameters == null || queryParameters.isEmpty()) {
|
||||||
|
return ""; //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
return queryParameters
|
||||||
|
.keySet().stream().sorted().map(
|
||||||
|
key -> HttpSupport.urlEncode(key, false) + '='
|
||||||
|
+ HttpSupport.urlEncode(
|
||||||
|
queryParameters.get(key), false))
|
||||||
|
.collect(Collectors.joining("&")); //$NON-NLS-1$
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hashes the provided byte array using the SHA-256 algorithm.
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* The byte array to hash.
|
||||||
|
*
|
||||||
|
* @return Hashed string contents of the provided byte array.
|
||||||
|
*/
|
||||||
|
public static byte[] hash(byte[] data) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256"); //$NON-NLS-1$
|
||||||
|
md.update(data);
|
||||||
|
return md.digest();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
JGitText.get().couldNotHashByteArrayWithSha256, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signs the provided string data using the specified key.
|
||||||
|
*
|
||||||
|
* @param stringToSign
|
||||||
|
* The string data to sign.
|
||||||
|
* @param key
|
||||||
|
* The key material of the secret key.
|
||||||
|
*
|
||||||
|
* @return Signed string data.
|
||||||
|
*/
|
||||||
|
private static byte[] signStringWithKey(String stringToSign, byte[] key) {
|
||||||
|
try {
|
||||||
|
byte[] data = stringToSign.getBytes(StandardCharsets.UTF_8);
|
||||||
|
Mac mac = Mac.getInstance(MAC_ALGORITHM);
|
||||||
|
mac.init(new SecretKeySpec(key, MAC_ALGORITHM));
|
||||||
|
return mac.doFinal(data);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(JGitText.get().couldNotSignStringWithKey,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -185,6 +185,33 @@ public static void encode(StringBuilder urlstr, String key) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the provided URL into application/x-www-form-urlencoded
|
||||||
|
* format.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The URL to translate.
|
||||||
|
* @param keepPathSlash
|
||||||
|
* Whether or not to keep "/" in the URL (i.e. don't translate
|
||||||
|
* them to "%2F").
|
||||||
|
*
|
||||||
|
* @return The translated URL.
|
||||||
|
* @since 5.13
|
||||||
|
*/
|
||||||
|
public static String urlEncode(String url, boolean keepPathSlash) {
|
||||||
|
String encoded;
|
||||||
|
try {
|
||||||
|
encoded = URLEncoder.encode(url, UTF_8.name());
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
throw new RuntimeException(JGitText.get().couldNotURLEncodeToUTF8,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
if (keepPathSlash) {
|
||||||
|
encoded = encoded.replace("%2F", "/"); //$NON-NLS-1$ //$NON-NLS-2$
|
||||||
|
}
|
||||||
|
return encoded;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the HTTP response code from the request.
|
* Get the HTTP response code from the request.
|
||||||
* <p>
|
* <p>
|
||||||
|
|
Loading…
Reference in New Issue