Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #11648 - Introducing HttpDateTime class. #11672

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@

/**
* ThreadLocal data parsers for HTTP style dates
* @deprecated use {@link HttpDateTime} instead
*/
@Deprecated(since = "12.0.9", forRemoval = true)
public class DateParser
{
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
Expand All @@ -47,6 +49,10 @@ public class DateParser
"EEE dd-MMM-yy HH:mm:ss zzz", "EEE dd-MMM-yy HH:mm:ss"
};

/**
* @deprecated use {@link HttpDateTime#parseToEpoch(String)} instead
*/
@Deprecated(since = "12.0.9", forRemoval = true)
public static long parseDate(String date)
{
return DATE_PARSER.apply(DateParser::new, DateParser::parse, date);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@
package org.eclipse.jetty.http;

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
Expand Down Expand Up @@ -921,22 +918,19 @@ static String toString(HttpCookie httpCookie)
*/
static String formatExpires(Instant expires)
{
return DateTimeFormatter.RFC_1123_DATE_TIME
.withZone(ZoneOffset.UTC)
.format(expires);
return HttpDateTime.format(expires);
}

/**
* <p>Parses the {@code Expires} attribute value
* (in RFC 1123 format) into an {@link Instant}.</p>
* <p>Parses the {@code Expires} Date/Time attribute value
* into an {@link Instant}.</p>
*
* @param expires an instant in the RFC 1123 string format
* @param expires a date/time in one of the RFC6265 supported formats
* @return an {@link Instant} parsed from the given string
*/
static Instant parseExpires(String expires)
{
// TODO: RFC 1123 format only for now, see https://www.rfc-editor.org/rfc/rfc2616#section-3.3.1.
return ZonedDateTime.parse(expires, DateTimeFormatter.RFC_1123_DATE_TIME).toInstant();
return HttpDateTime.parse(expires);
}

private static Map<String, String> lazyAttributePut(Map<String, String> attributes, String key, String value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http;

import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Objects;
import java.util.StringTokenizer;

import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* HTTP Date/Time parsing and formatting.
*
* <p>
* Also covers RFC6265 Cookie Date parsing and formatting.
* </p>
gregw marked this conversation as resolved.
Show resolved Hide resolved
*/
public class HttpDateTime
{
private static final Logger LOG = LoggerFactory.getLogger(HttpDateTime.class);

private static final Index<Integer> MONTH_CACHE = new Index.Builder<Integer>()
.caseSensitive(false)
// Note: Calendar.Month fields are zero based.
.with("Jan", Calendar.JANUARY + 1)
.with("Feb", Calendar.FEBRUARY + 1)
.with("Mar", Calendar.MARCH + 1)
.with("Apr", Calendar.APRIL + 1)
.with("May", Calendar.MAY + 1)
.with("Jun", Calendar.JUNE + 1)
.with("Jul", Calendar.JULY + 1)
.with("Aug", Calendar.AUGUST + 1)
.with("Sep", Calendar.SEPTEMBER + 1)
.with("Oct", Calendar.OCTOBER + 1)
.with("Nov", Calendar.NOVEMBER + 1)
.with("Dec", Calendar.DECEMBER + 1)
.build();

private HttpDateTime()
{
}

/**
* Similar to {@link #parse(String)} but returns unix epoch
*
* @param datetime the Date/Time to parse.
* @return unix epoch in milliseconds, or -1 if unable to parse the input date/time
*/
public static long parseToEpoch(String datetime)
{
try
{
Instant instant = parse(datetime);
return instant.toEpochMilli();
gregw marked this conversation as resolved.
Show resolved Hide resolved
}
catch (IllegalArgumentException e)
{
if (LOG.isDebugEnabled())
LOG.debug("Unable to parse Date/Time: {}", datetime, e);
return -1;
}
}

/**
* <p>Parses a Date/Time value</p>
*
* <p>Supports the following Date/Time formats found in both
* <a href="https://datatracker.ietf.org/doc/html/rfc9110#name-date-time-formats">RFC 9110 (HTTP Semantics)</a> and
* <a href="https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1">RFC 6265 (HTTP State Management Mechanism)</a>
* </p>
gregw marked this conversation as resolved.
Show resolved Hide resolved
*
* <ul>
* <li>{@code Sun, 06 Nov 1994 08:49:37 GMT} - RFC 1123 (preferred)</li>
* <li>{@code Sunday, 06-Nov-94 08:49:37 GMT} - RFC 850 (obsolete)</li>
* <li>{@code Sun Nov 6 08:49:37 1994} - ANSI C's {@code asctime()} format</li>
* </ul>
*
* <p>
* Parsing is done according to the algorithm specified in
* <a href="https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.1">RFC6265: Section 5.1.1: Date</a>
* </p>
*
* @param datetime a Date/Time string in a supported format
* @return an {@link Instant} parsed from the given string
* @throws IllegalArgumentException if unable to parse date/time
*/
public static Instant parse(String datetime)
gregw marked this conversation as resolved.
Show resolved Hide resolved
{
Objects.requireNonNull(datetime, "Date/Time string cannot be null");

int year = -1;
int month = -1;
int day = -1;
int hour = -1;
int minute = -1;
int second = -1;

try
{
int tokenCount = 0;
StringTokenizer tokenizer = new StringTokenizer(datetime, "\t" + // %x09
" !\"#$%&'()*+,-./" + " + " + // %x20-2F
";<=>?@" + // %x3B-40
"[\\]^_`" + // %x5B-60
"{|}~" // %x7B-7E
);
gregw marked this conversation as resolved.
Show resolved Hide resolved
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
// ensure we don't exceed the number of expected tokens.
if (++tokenCount > 6)
{
// This is a horribly bad syntax / format
throw new IllegalStateException("Too many delimiters for a Date/Time format");
}

if (token.isBlank())
continue; // skip blank tokens

// RFC 6265 - Section 5.1.1 - Step 2.1 - time (00:00:00)
if (hour == (-1) && token.length() == 8 && token.charAt(2) == ':' && token.charAt(5) == ':')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please get rid of parens around -1 everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

{
second = StringUtil.toInt(token, 6);
minute = StringUtil.toInt(token, 3);
hour = StringUtil.toInt(token, 0);
continue;
}

// RFC 6265 - Section 5.1.1 - Step 2.2
if (day == (-1) && token.length() <= 2)
{
day = StringUtil.toInt(token, 0);
continue;
}

// RFC 6265 - Section 5.1.1 - Step 2.3
if (month == (-1) && token.length() == 3)
{
Integer m = MONTH_CACHE.getBest(token);
if (m != null)
{
month = m;
continue;
}
}

// RFC 6265 - Section 5.1.1 - Step 2.4
if (year == (-1))
{
if (token.length() <= 2)
{
year = StringUtil.toInt(token, 0);
}
else if (token.length() == 4)
{
year = StringUtil.toInt(token, 0);
}
continue;
}
}
}
catch (Throwable x)
{
if (LOG.isDebugEnabled())
LOG.debug("Ignore: Unable to parse Date/Time", x);
}

// RFC 6265 - Section 5.1.1 - Step 3
if ((year > 70) && (year <= 99))
year += 1900;
// RFC 6265 - Section 5.1.1 - Step 4
if ((year >= 0) && (year <= 69))
year += 2000;

// RFC 6265 - Section 5.1.1 - Step 5
if (day == (-1))
throw new IllegalArgumentException("Missing [day]: " + datetime);
if (month == (-1))
throw new IllegalArgumentException("Missing [month]: " + datetime);
if (year == (-1))
throw new IllegalArgumentException("Missing [year]: " + datetime);
if (hour == (-1))
throw new IllegalArgumentException("Missing [time]: " + datetime);
if (day < 1 || day > 31)
throw new IllegalArgumentException("Invalid [day]: " + datetime);
if (month < 1 || month > 31)
gregw marked this conversation as resolved.
Show resolved Hide resolved
throw new IllegalArgumentException("Invalid [month]: " + datetime);
if (hour > 23)
throw new IllegalArgumentException("Invalid [hour]: " + datetime);
if (minute > 59)
throw new IllegalArgumentException("Invalid [minute]: " + datetime);
if (second > 59)
throw new IllegalArgumentException("Invalid [second]: " + datetime);

// RFC 6265 - Section 5.1.1 - Step 6
ZonedDateTime dateTime = ZonedDateTime.of(year,
month, day, hour, minute, second, 0,
ZoneId.of("GMT"));
gregw marked this conversation as resolved.
Show resolved Hide resolved

// RFC 6265 - Section 5.1.1 - Step 7
return dateTime.toInstant();
}

public static String format(Instant instant)
gregw marked this conversation as resolved.
Show resolved Hide resolved
{
return DateTimeFormatter.RFC_1123_DATE_TIME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interesting! IT does the hard coded "GMT" for us, even if we pass in "UTC". Nice! Well not nice... ugly history in evidence... but at least it is consistent. The time is in UTC, but just reported as "GMT"!

.withZone(ZoneOffset.UTC)
.format(instant);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -517,10 +517,7 @@ private static long parseDateField(HttpField field)
if (val == null)
return -1;

final long date = DateParser.parseDate(val);
if (date == -1)
throw new IllegalArgumentException("Cannot convert date: " + val);
return date;
return HttpDateTime.parse(val).toEpochMilli();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package org.eclipse.jetty.http;

import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.List;

import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -90,7 +89,7 @@ public void testParseCookies(String setCookieValue, HttpCookie expectedCookie)
public static List<Arguments> invalidAttributes()
{
return List.of(
Arguments.of("Expires", "blah", DateTimeParseException.class),
Arguments.of("Expires", "blah", IllegalArgumentException.class),
Arguments.of("HttpOnly", "blah", IllegalArgumentException.class),
Arguments.of("Max-Age", "blah", NumberFormatException.class),
Arguments.of("SameSite", "blah", IllegalArgumentException.class),
Expand All @@ -105,4 +104,5 @@ public void testParseInvalidAttributes(String name, String value, Class<? extend
assertThrows(failure, () -> HttpCookie.build("A", "1")
.attribute(name, value));
}

}
Loading
Loading