diff --git a/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java b/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java index 73b6ad2909035..ace70bacee2b5 100644 --- a/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java +++ b/server/src/main/java/org/elasticsearch/common/logging/DeprecationLogger.java @@ -27,8 +27,14 @@ import org.elasticsearch.common.util.concurrent.ThreadContext; import java.nio.charset.Charset; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.SignStyle; import java.util.BitSet; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Locale; @@ -39,6 +45,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.DAY_OF_WEEK; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; + /** * A logger that logs deprecation notices. */ @@ -149,6 +163,65 @@ public void deprecatedAndMaybeLog(final String key, final String msg, final Obje Build.CURRENT.isSnapshot() ? "-SNAPSHOT" : "", Build.CURRENT.shortHash()); + /* + * RFC 7234 section 5.5 specifies that the warn-date is a quoted HTTP-date. HTTP-date is defined in RFC 7234 Appendix B as being from + * RFC 7231 section 7.1.1.1. RFC 7231 specifies an HTTP-date as an IMF-fixdate (or an obs-date referring to obsolete formats). The + * grammar for IMF-fixdate is specified as 'day-name "," SP date1 SP time-of-day SP GMT'. Here, day-name is + * (Mon|Tue|Wed|Thu|Fri|Sat|Sun). Then, date1 is 'day SP month SP year' where day is 2DIGIT, month is + * (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec), and year is 4DIGIT. Lastly, time-of-day is 'hour ":" minute ":" second' where + * hour is 2DIGIT, minute is 2DIGIT, and second is 2DIGIT. Finally, 2DIGIT and 4DIGIT have the obvious definitions. + */ + private static final DateTimeFormatter RFC_7231_DATE_TIME; + + static { + final Map dow = new HashMap<>(); + dow.put(1L, "Mon"); + dow.put(2L, "Tue"); + dow.put(3L, "Wed"); + dow.put(4L, "Thu"); + dow.put(5L, "Fri"); + dow.put(6L, "Sat"); + dow.put(7L, "Sun"); + final Map moy = new HashMap<>(); + moy.put(1L, "Jan"); + moy.put(2L, "Feb"); + moy.put(3L, "Mar"); + moy.put(4L, "Apr"); + moy.put(5L, "May"); + moy.put(6L, "Jun"); + moy.put(7L, "Jul"); + moy.put(8L, "Aug"); + moy.put(9L, "Sep"); + moy.put(10L, "Oct"); + moy.put(11L, "Nov"); + moy.put(12L, "Dec"); + RFC_7231_DATE_TIME = new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .parseLenient() + .optionalStart() + .appendText(DAY_OF_WEEK, dow) + .appendLiteral(", ") + .optionalEnd() + .appendValue(DAY_OF_MONTH, 2, 2, SignStyle.NOT_NEGATIVE) + .appendLiteral(' ') + .appendText(MONTH_OF_YEAR, moy) + .appendLiteral(' ') + .appendValue(YEAR, 4) + .appendLiteral(' ') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .optionalStart() + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .optionalEnd() + .appendLiteral(' ') + .appendOffset("+HHMM", "GMT") + .toFormatter(Locale.getDefault(Locale.Category.FORMAT)); + } + + private static final String STARTUP_TIME = RFC_7231_DATE_TIME.format(ZonedDateTime.now(ZoneId.of("GMT"))); + /** * Regular expression to test if a string matches the RFC7234 specification for warning headers. This pattern assumes that the warn code * is always 299. Further, this pattern assumes that the warn agent represents a version of Elasticsearch including the build hash. @@ -179,17 +252,18 @@ public static String extractWarningValueFromWarningHeader(final String s) { * We know the exact format of the warning header, so to extract the warning value we can skip forward from the front to the first * quote and we know the last quote is at the end of the string * - * 299 Elasticsearch-6.0.0 "warning value" - * ^ ^ - * firstQuote lastQuote + * 299 Elasticsearch-6.0.0 "warning value" "Thu, 01 Jan 1970 00:00:00 GMT" + * ^ ^ ^ + * firstQuote penultimateQuote lastQuote * * We parse this manually rather than using the capturing regular expression because the regular expression involves a lot of * backtracking and carries a performance penalty. However, when assertions are enabled, we still use the regular expression to * verify that we are maintaining the warning header format. */ final int firstQuote = s.indexOf('\"'); - final int lastQuote = s.length() - 1; - final String warningValue = s.substring(firstQuote + 1, lastQuote); + final int lastQuote = s.lastIndexOf('\"'); + final int penultimateQuote = s.lastIndexOf('\"', lastQuote - 1); + final String warningValue = s.substring(firstQuote + 1, penultimateQuote - 2); assert assertWarningValue(s, warningValue); return warningValue; } @@ -251,7 +325,11 @@ void deprecated(final Set threadContexts, final String message, f * @return a warning value formatted according to RFC 7234 */ public static String formatWarning(final String s) { - return WARNING_PREFIX + " " + "\"" + escapeAndEncode(s) + "\""; + // Assume that the common scenario won't have a string to escape and encode. + int length = WARNING_PREFIX.length() + s.length() + 35; + final StringBuilder sb = new StringBuilder(length); + sb.append(WARNING_PREFIX).append(" \"").append(escapeAndEncode(s)).append("\"").append(" \"").append(STARTUP_TIME).append("\""); + return sb.toString(); } /**