diff --git a/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java index ce38872686fc..9fbb5df4ab7e 100644 --- a/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java +++ b/java/org/apache/tomcat/util/http/ConcurrentDateFormat.java @@ -28,7 +28,7 @@ * A thread safe wrapper around {@link SimpleDateFormat} that does not make use of ThreadLocal and - broadly - only * creates enough SimpleDateFormat objects to satisfy the concurrency requirements. */ -public class ConcurrentDateFormat { +public class ConcurrentDateFormat implements FastHttpDateFormat.TryParseDateToTimestamp { private final String format; private final Locale locale; @@ -88,6 +88,15 @@ public Date parse(String source) throws ParseException { } } + @Override + public long tryParseDate(String dateString) { + try { + return parse(dateString).getTime(); + } catch (ParseException e) { + return -1; + } + } + private SimpleDateFormat createInstance() { SimpleDateFormat sdf = new SimpleDateFormat(format, locale); sdf.setTimeZone(timezone); diff --git a/java/org/apache/tomcat/util/http/FastHttpDateFormat.java b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java index 3abf32b67d30..f747c600887e 100644 --- a/java/org/apache/tomcat/util/http/FastHttpDateFormat.java +++ b/java/org/apache/tomcat/util/http/FastHttpDateFormat.java @@ -50,8 +50,9 @@ private FastHttpDateFormat() { private static final ConcurrentDateFormat FORMAT_RFC5322; private static final ConcurrentDateFormat FORMAT_OBSOLETE_RFC850; private static final ConcurrentDateFormat FORMAT_OBSOLETE_ASCTIME; + private static final TryParseDateToTimestamp FORMAT_RFC9651; - private static final ConcurrentDateFormat[] httpParseFormats; + private static final TryParseDateToTimestamp[] httpParseFormats; static { // All the formats that use a timezone use GMT @@ -60,9 +61,20 @@ private FastHttpDateFormat() { FORMAT_RFC5322 = new ConcurrentDateFormat(DATE_RFC5322, Locale.US, tz); FORMAT_OBSOLETE_RFC850 = new ConcurrentDateFormat(DATE_OBSOLETE_RFC850, Locale.US, tz); FORMAT_OBSOLETE_ASCTIME = new ConcurrentDateFormat(DATE_OBSOLETE_ASCTIME, Locale.US, tz); + FORMAT_RFC9651 = dateString -> { + if(dateString == null || !dateString.startsWith("@")) { + return -1; + } + try { + // An RFC 9651 timestamp is in seconds, not milliseconds. + return Long.parseLong(dateString.substring(1)) * 1_000; + } catch (NumberFormatException e) { + return -1; + } + }; httpParseFormats = - new ConcurrentDateFormat[] { FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME }; + new TryParseDateToTimestamp[] { FORMAT_RFC5322, FORMAT_OBSOLETE_RFC850, FORMAT_OBSOLETE_ASCTIME, FORMAT_RFC9651 }; } /** @@ -146,13 +158,9 @@ public static long parseDate(String value) { long date = -1; for (int i = 0; (date == -1) && (i < httpParseFormats.length); i++) { - try { - date = httpParseFormats[i].parse(value).getTime(); - updateParseCache(value, Long.valueOf(date)); - } catch (ParseException e) { - // Ignore - } + date = httpParseFormats[i].tryParseDate(value); } + updateParseCache(value, Long.valueOf(date)); return date; } @@ -185,5 +193,14 @@ private static void updateParseCache(String key, Long value) { parseCache.put(key, value); } - + @FunctionalInterface + interface TryParseDateToTimestamp { + /** + * Tries to parse the given string as a date and returns it as a timestamp. + * + * @param dateString the string representation of the date trying to be parsed + * @return the number of *milli*seconds since January 1, 1970, 00:00:00 GMT or -1 if parsing failed + */ + long tryParseDate(String dateString); + } } diff --git a/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java b/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java index 20e771ef1ea9..395405f63457 100644 --- a/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java +++ b/test/org/apache/tomcat/util/http/TestFastHttpDateFormat.java @@ -1,3 +1,4 @@ + /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with @@ -72,4 +73,25 @@ public void testGetCurrentDate() { Assert.assertTrue("Saw [" + changes + "] changes in formatted date", changes > 1 && changes < 5); } } + + @Test + public void testRfc9651Date_valid() { + Assert.assertEquals(0L, FastHttpDateFormat.parseDate("@0")); + Assert.assertEquals(1000L, FastHttpDateFormat.parseDate("@1")); + Assert.assertEquals(-1000L, FastHttpDateFormat.parseDate("@-1")); + Assert.assertEquals(1659578233000L, FastHttpDateFormat.parseDate("@1659578233")); // Example from RFC + Assert.assertEquals(-62135596800000L, FastHttpDateFormat.parseDate("@-62135596800")); // Example from RFC + Assert.assertEquals(253402214400000L, FastHttpDateFormat.parseDate("@253402214400")); // Example from RFC + } + + @Test + public void testRfc9651Date_invalid() { + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate(" @1")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@ 1")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@0000-")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@15+")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@12p")); + Assert.assertEquals(-1L, FastHttpDateFormat.parseDate("@0x15")); + } }