From 69e21d206b49a30c2163ad2ba7847bba37e87931 Mon Sep 17 00:00:00 2001 From: David Negrete Date: Fri, 30 Sep 2022 09:08:17 -0600 Subject: [PATCH 1/2] Use RFC 822 format for marshalling date shapes --- .../bugfix-AWSSDKforJavav2-83d35fd.json | 6 +++ .../exception/AwsServiceExceptionTest.java | 4 +- .../awscore/retry/AwsRetryPolicyTest.java | 2 +- .../marshall/SimpleTypeJsonMarshaller.java | 2 +- .../protocols/core/InstantToString.java | 2 +- .../protocols/core/StringToInstant.java | 2 +- .../amazon/awssdk/core/retry/ClockSkew.java | 2 +- .../internal/retry/ClockSkewAdjusterTest.java | 2 +- .../clockskew/ClockSkewAdjustmentTest.java | 4 +- .../amazon/awssdk/utils/DateUtils.java | 40 ++++++++++++++++++ .../amazon/awssdk/utils/DateUtilsTest.java | 41 ++++++++++++++++++- 11 files changed, 96 insertions(+), 11 deletions(-) create mode 100644 .changes/next-release/bugfix-AWSSDKforJavav2-83d35fd.json diff --git a/.changes/next-release/bugfix-AWSSDKforJavav2-83d35fd.json b/.changes/next-release/bugfix-AWSSDKforJavav2-83d35fd.json new file mode 100644 index 000000000000..655f8b594f9f --- /dev/null +++ b/.changes/next-release/bugfix-AWSSDKforJavav2-83d35fd.json @@ -0,0 +1,6 @@ +{ + "category": "AWS SDK for Java v2", + "contributor": "dave-fn", + "type": "bugfix", + "description": "Build headers with two-digit day of month to meet RFC 822 reporting requirement" +} diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/exception/AwsServiceExceptionTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/exception/AwsServiceExceptionTest.java index 463bd7d15c53..154a6bc10af2 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/exception/AwsServiceExceptionTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/exception/AwsServiceExceptionTest.java @@ -87,7 +87,7 @@ public void assertSkewed(int clientSideTimeOffset, int statusCode, Instant serverDate) { AwsServiceException exception = exception(clientSideTimeOffset, errorCode, statusCode, - DateUtils.formatRfc1123Date(serverDate)); + DateUtils.formatRfc822Date(serverDate)); assertThat(exception.isClockSkewException()).isTrue(); } @@ -96,7 +96,7 @@ public void assertNotSkewed(int clientSideTimeOffset, int statusCode, Instant serverDate) { AwsServiceException exception = exception(clientSideTimeOffset, errorCode, statusCode, - DateUtils.formatRfc1123Date(serverDate)); + DateUtils.formatRfc822Date(serverDate)); assertThat(exception.isClockSkewException()).isFalse(); } diff --git a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicyTest.java b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicyTest.java index 38c306d1c4de..b318381ca41f 100644 --- a/core/aws-core/src/test/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicyTest.java +++ b/core/aws-core/src/test/java/software/amazon/awssdk/awscore/retry/AwsRetryPolicyTest.java @@ -125,7 +125,7 @@ private Consumer applyErrorCode(String errorCode) { private Consumer applyErrorCode(String errorCode, Duration clockSkew, Instant dateHeader) { SdkHttpFullResponse response = SdkHttpFullResponse.builder() - .putHeader("Date", DateUtils.formatRfc1123Date(dateHeader)) + .putHeader("Date", DateUtils.formatRfc822Date(dateHeader)) .build(); AwsErrorDetails errorDetails = AwsErrorDetails.builder() diff --git a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/SimpleTypeJsonMarshaller.java b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/SimpleTypeJsonMarshaller.java index 026687b82148..e58a8ec66be5 100644 --- a/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/SimpleTypeJsonMarshaller.java +++ b/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/SimpleTypeJsonMarshaller.java @@ -113,7 +113,7 @@ public void marshall(Boolean val, StructuredJsonGenerator jsonGenerator, JsonMar jsonGenerator.writeNumber(DateUtils.formatUnixTimestampInstant(val)); break; case RFC_822: - jsonGenerator.writeValue(DateUtils.formatRfc1123Date(val)); + jsonGenerator.writeValue(DateUtils.formatRfc822Date(val)); break; case ISO_8601: jsonGenerator.writeValue(DateUtils.formatIso8601Date(val)); diff --git a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/InstantToString.java b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/InstantToString.java index 96eaa4164932..96a0878a894d 100644 --- a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/InstantToString.java +++ b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/InstantToString.java @@ -51,7 +51,7 @@ public String convert(Instant val, SdkField sdkField) { case ISO_8601: return DateUtils.formatIso8601Date(val); case RFC_822: - return DateUtils.formatRfc1123Date(val); + return DateUtils.formatRfc822Date(val); case UNIX_TIMESTAMP: return DateUtils.formatUnixTimestampInstant(val); default: diff --git a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/StringToInstant.java b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/StringToInstant.java index a9888f076c1d..8d44e4adf104 100644 --- a/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/StringToInstant.java +++ b/core/protocols/protocol-core/src/main/java/software/amazon/awssdk/protocols/core/StringToInstant.java @@ -55,7 +55,7 @@ public Instant convert(String value, SdkField field) { case UNIX_TIMESTAMP_MILLIS: return safeParseDate(DateUtils::parseUnixTimestampMillisInstant).apply(value); case RFC_822: - return DateUtils.parseRfc1123Date(value); + return DateUtils.parseRfc822Date(value); default: throw SdkClientException.create("Unrecognized timestamp format - " + format); } diff --git a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/ClockSkew.java b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/ClockSkew.java index 2c76b5261e78..a7b4ef974b7b 100644 --- a/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/ClockSkew.java +++ b/core/sdk-core/src/main/java/software/amazon/awssdk/core/retry/ClockSkew.java @@ -76,7 +76,7 @@ public static Optional getServerTime(SdkHttpResponse serviceResponse) { log.debug(() -> "Reported service date: " + serverDate); try { - return Optional.of(DateUtils.parseRfc1123Date(serverDate)); + return Optional.of(DateUtils.parseRfc822Date(serverDate)); } catch (RuntimeException e) { log.warn(() -> "Unable to parse clock skew offset from response: " + serverDate, e); return Optional.empty(); diff --git a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java index d18e48ce1d3d..11050022cfea 100644 --- a/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java +++ b/core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/retry/ClockSkewAdjusterTest.java @@ -47,7 +47,7 @@ public void badDateTranslatesToZero() { private SdkHttpFullResponse responseWithDateOffset(int value, ChronoUnit unit) { return SdkHttpFullResponse.builder() - .putHeader("Date", DateUtils.formatRfc1123Date(Instant.now().plus(value, unit))) + .putHeader("Date", DateUtils.formatRfc822Date(Instant.now().plus(value, unit))) .build(); } diff --git a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/clockskew/ClockSkewAdjustmentTest.java b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/clockskew/ClockSkewAdjustmentTest.java index 30858d40d893..21be8a282843 100644 --- a/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/clockskew/ClockSkewAdjustmentTest.java +++ b/test/protocol-tests/src/test/java/software/amazon/awssdk/protocol/tests/clockskew/ClockSkewAdjustmentTest.java @@ -166,7 +166,7 @@ private void stubForResponse(Instant serviceTime, int statusCode, String errorCo .willReturn(aResponse() .withStatus(statusCode) .withHeader("x-amzn-ErrorType", errorCode) - .withHeader("Date", DateUtils.formatRfc1123Date(serviceTime)) + .withHeader("Date", DateUtils.formatRfc822Date(serviceTime)) .withBody("{}"))); } @@ -180,7 +180,7 @@ private void stubForClockSkewFailureThenSuccess(Instant serviceTime, int statusC .willReturn(aResponse() .withStatus(statusCode) .withHeader("x-amzn-ErrorType", errorCode) - .withHeader("Date", DateUtils.formatRfc1123Date(serviceTime)) + .withHeader("Date", DateUtils.formatRfc822Date(serviceTime)) .withBody("{}"))); stubFor(post(urlEqualTo(PATH)) diff --git a/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java b/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java index b10e245f4e2d..3e2a40d5b019 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java @@ -30,6 +30,7 @@ import java.time.format.DateTimeParseException; import java.util.Arrays; import java.util.List; +import java.util.Locale; import software.amazon.awssdk.annotations.SdkProtectedApi; import software.amazon.awssdk.annotations.ThreadSafe; @@ -48,6 +49,18 @@ public final class DateUtils { .toFormatter() .withZone(UTC); + /** + * RFC 822 date/time formatter. + */ + static final DateTimeFormatter RFC_822_DATE_TIME = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .parseLenient() + .appendPattern("EEE, dd MMM yyyy HH:mm:ss") + .appendLiteral(' ') + .appendOffset("+HHMM", "GMT") + .toFormatter() + .withLocale(Locale.US); + // ISO_INSTANT does not handle offsets in Java 12-. See https://bugs.openjdk.java.net/browse/JDK-8166138 private static final List ALTERNATE_ISO_8601_FORMATTERS = Arrays.asList(ISO_INSTANT, ALTERNATE_ISO_8601_DATE_FORMAT, ISO_OFFSET_DATE_TIME); @@ -102,6 +115,33 @@ public static String formatIso8601Date(Instant date) { return ISO_INSTANT.format(date); } + /** + * Parses the specified date string as an RFC 822 date and returns the Date object. + * + * @param dateString + * The date string to parse. + * + * @return The parsed Date object. + */ + public static Instant parseRfc822Date(String dateString) { + if (dateString == null) { + return null; + } + return parseInstant(dateString, RFC_822_DATE_TIME); + } + + /** + * Formats the specified date as an RFC 822 string. + * + * @param instant + * The instant to format. + * + * @return The RFC 822 string representing the specified date. + */ + public static String formatRfc822Date(Instant instant) { + return RFC_822_DATE_TIME.format(ZonedDateTime.ofInstant(instant, UTC)); + } + /** * Parses the specified date string as an RFC 1123 date and returns the Date * object. diff --git a/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java b/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java index cadf10240d99..1b612231a0ed 100644 --- a/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java +++ b/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java @@ -18,6 +18,7 @@ import static java.time.ZoneOffset.UTC; import static java.time.format.DateTimeFormatter.ISO_INSTANT; import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static software.amazon.awssdk.utils.DateUtils.ALTERNATE_ISO_8601_DATE_FORMAT; @@ -72,6 +73,44 @@ public void formatIso8601Date() throws ParseException { assertEquals(expectedDate, actualDate); } + @Test + public void formatRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsFormattedString() throws ParseException { + String string = DateUtils.formatRfc822Date(INSTANT); + Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant(); + assertThat(parsedDateAsInstant).isEqualTo(INSTANT); + } + + @Test + public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsFormattedString() throws ParseException { + Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; + String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); + Instant parsedDateAsInstant = LONG_DATE_FORMAT.parse(string).toInstant(); + assertThat(parsedDateAsInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); + } + + @Test + public void formatRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsStringWithZeroLeadingDayOfMonth() throws ParseException { + final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; + String string = DateUtils.formatRfc822Date(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); + String expectedString = "Wed, 07 May 2014 17:43:26 GMT"; + assertThat(string).isEqualTo(expectedString); + } + + @Test + public void parseRfc822Date_DateWithTwoDigitDayOfMonth_ReturnsInstantObject() throws ParseException { + String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT)); + Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate); + assertThat(parsedInstant).isEqualTo(INSTANT); + } + + @Test + public void parseRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsInstantObject() throws ParseException { + final Instant INSTANT_SINGLE_DIGIT_DAY_OF_MONTH = Instant.ofEpochMilli(1399484606000L);; + String formattedDate = LONG_DATE_FORMAT.format(Date.from(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH)); + Instant parsedInstant = DateUtils.parseRfc822Date(formattedDate); + assertThat(parsedInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); + } + @Test public void formatRfc1123Date() throws ParseException { String string = DateUtils.formatRfc1123Date(INSTANT); @@ -84,7 +123,7 @@ public void formatRfc1123Date() throws ParseException { } @Test - public void parseRfc822Date() throws ParseException { + public void parseRfc1123Date() throws ParseException { String formatted = LONG_DATE_FORMAT.format(Date.from(INSTANT)); Instant expected = LONG_DATE_FORMAT.parse(formatted).toInstant(); Instant actual = DateUtils.parseRfc1123Date(formatted); From 2fc5e5c458689d0599862eb7048902dc134d86b1 Mon Sep 17 00:00:00 2001 From: David Negrete Date: Mon, 3 Oct 2022 13:44:59 -0600 Subject: [PATCH 2/2] Explicilty set ResolverStyle and Chronology --- .../amazon/awssdk/utils/DateUtils.java | 6 ++- .../amazon/awssdk/utils/DateUtilsTest.java | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java b/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java index 3e2a40d5b019..6dda13367d1a 100644 --- a/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java +++ b/utils/src/main/java/software/amazon/awssdk/utils/DateUtils.java @@ -25,9 +25,11 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.time.chrono.IsoChronology; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -59,7 +61,9 @@ public final class DateUtils { .appendLiteral(' ') .appendOffset("+HHMM", "GMT") .toFormatter() - .withLocale(Locale.US); + .withLocale(Locale.US) + .withResolverStyle(ResolverStyle.SMART) + .withChronology(IsoChronology.INSTANCE); // ISO_INSTANT does not handle offsets in Java 12-. See https://bugs.openjdk.java.net/browse/JDK-8166138 private static final List ALTERNATE_ISO_8601_FORMATTERS = diff --git a/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java b/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java index 1b612231a0ed..20d21a01d0ba 100644 --- a/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java +++ b/utils/src/test/java/software/amazon/awssdk/utils/DateUtilsTest.java @@ -18,10 +18,12 @@ import static java.time.ZoneOffset.UTC; import static java.time.format.DateTimeFormatter.ISO_INSTANT; import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; +import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static software.amazon.awssdk.utils.DateUtils.ALTERNATE_ISO_8601_DATE_FORMAT; +import static software.amazon.awssdk.utils.DateUtils.RFC_822_DATE_TIME; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -111,6 +113,42 @@ public void parseRfc822Date_DateWithSingleDigitDayOfMonth_ReturnsInstantObject() assertThat(parsedInstant).isEqualTo(INSTANT_SINGLE_DIGIT_DAY_OF_MONTH); } + @Test + public void parseRfc822Date_DateWithInvalidDayOfMonth_IsParsedWithSmartResolverStyle() { + String badDateString = "Wed, 31 Apr 2014 17:43:26 GMT"; + String validDateString = "Wed, 30 Apr 2014 17:43:26 GMT"; + Instant badDateParsedInstant = DateUtils.parseRfc822Date(badDateString); + Instant validDateParsedInstant = DateUtils.parseRfc1123Date(validDateString); + assertThat(badDateParsedInstant).isEqualTo(validDateParsedInstant); + } + + @Test + public void parseRfc822Date_DateWithInvalidDayOfMonth_MatchesRfc1123Behavior() { + String dateString = "Wed, 31 Apr 2014 17:43:26 GMT"; + Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(dateString); + Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(dateString); + assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser); + } + + @Test + public void parseRfc822Date_DateWithDayOfMonthLessThan10th_MatchesRfc1123Behavior() { + String rfc822DateString = "Wed, 02 Apr 2014 17:43:26 GMT"; + String rfc1123DateString = "Wed, 2 Apr 2014 17:43:26 GMT"; + Instant parsedInstantFromRfc822Parser = DateUtils.parseRfc822Date(rfc822DateString); + Instant parsedInstantFromRfc1123arser = DateUtils.parseRfc1123Date(rfc1123DateString); + assertThat(parsedInstantFromRfc822Parser).isEqualTo(parsedInstantFromRfc1123arser); + } + + @Test + public void resolverStyleOfRfc822FormatterMatchesRfc1123Formatter() { + assertThat(RFC_822_DATE_TIME.getResolverStyle()).isSameAs(RFC_1123_DATE_TIME.getResolverStyle()); + } + + @Test + public void chronologyOfRfc822FormatterMatchesRfc1123Formatter() { + assertThat(RFC_822_DATE_TIME.getChronology()).isSameAs(RFC_1123_DATE_TIME.getChronology()); + } + @Test public void formatRfc1123Date() throws ParseException { String string = DateUtils.formatRfc1123Date(INSTANT);