447 lines
14 KiB
Java
447 lines
14 KiB
Java
/*
|
|
* Copyright 2016 The Netty Project
|
|
*
|
|
* The Netty Project licenses this file to you under the Apache License,
|
|
* version 2.0 (the "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at:
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
package io.netty.handler.codec;
|
|
|
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
|
|
|
import io.netty.util.AsciiString;
|
|
import io.netty.util.concurrent.FastThreadLocal;
|
|
|
|
import java.util.BitSet;
|
|
import java.util.Calendar;
|
|
import java.util.Date;
|
|
import java.util.GregorianCalendar;
|
|
import java.util.TimeZone;
|
|
|
|
/**
|
|
* A formatter for HTTP header dates, such as "Expires" and "Date" headers, or "expires" field in "Set-Cookie".
|
|
*
|
|
* On the parsing side, it honors RFC6265 (so it supports RFC1123).
|
|
* Note that:
|
|
* <ul>
|
|
* <li>Day of week is ignored and not validated</li>
|
|
* <li>Timezone is ignored, as RFC6265 assumes UTC</li>
|
|
* </ul>
|
|
* If you're looking for a date format that validates day of week, or supports other timezones, consider using
|
|
* java.util.DateTimeFormatter.RFC_1123_DATE_TIME.
|
|
*
|
|
* On the formatting side, it uses RFC1123 format.
|
|
*
|
|
* @see <a href="https://tools.ietf.org/html/rfc6265#section-5.1.1">RFC6265</a> for the parsing side
|
|
* @see <a href="https://tools.ietf.org/html/rfc1123#page-55">RFC1123</a> for the encoding side.
|
|
*/
|
|
public final class DateFormatter {
|
|
|
|
private static final BitSet DELIMITERS = new BitSet();
|
|
static {
|
|
DELIMITERS.set(0x09);
|
|
for (char c = 0x20; c <= 0x2F; c++) {
|
|
DELIMITERS.set(c);
|
|
}
|
|
for (char c = 0x3B; c <= 0x40; c++) {
|
|
DELIMITERS.set(c);
|
|
}
|
|
for (char c = 0x5B; c <= 0x60; c++) {
|
|
DELIMITERS.set(c);
|
|
}
|
|
for (char c = 0x7B; c <= 0x7E; c++) {
|
|
DELIMITERS.set(c);
|
|
}
|
|
}
|
|
|
|
private static final String[] DAY_OF_WEEK_TO_SHORT_NAME =
|
|
new String[]{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
|
|
|
private static final String[] CALENDAR_MONTH_TO_SHORT_NAME =
|
|
new String[]{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
|
|
|
private static final FastThreadLocal<DateFormatter> INSTANCES =
|
|
new FastThreadLocal<DateFormatter>() {
|
|
@Override
|
|
protected DateFormatter initialValue() {
|
|
return new DateFormatter();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Parse some text into a {@link Date}, according to RFC6265
|
|
* @param txt text to parse
|
|
* @return a {@link Date}, or null if text couldn't be parsed
|
|
*/
|
|
public static Date parseHttpDate(CharSequence txt) {
|
|
return parseHttpDate(txt, 0, txt.length());
|
|
}
|
|
|
|
/**
|
|
* Parse some text into a {@link Date}, according to RFC6265
|
|
* @param txt text to parse
|
|
* @param start the start index inside <code>txt</code>
|
|
* @param end the end index inside <code>txt</code>
|
|
* @return a {@link Date}, or null if text couldn't be parsed
|
|
*/
|
|
public static Date parseHttpDate(CharSequence txt, int start, int end) {
|
|
int length = end - start;
|
|
if (length == 0) {
|
|
return null;
|
|
} else if (length < 0) {
|
|
throw new IllegalArgumentException("Can't have end < start");
|
|
} else if (length > 64) {
|
|
throw new IllegalArgumentException("Can't parse more than 64 chars," +
|
|
"looks like a user error or a malformed header");
|
|
}
|
|
return formatter().parse0(checkNotNull(txt, "txt"), start, end);
|
|
}
|
|
|
|
/**
|
|
* Format a {@link Date} into RFC1123 format
|
|
* @param date the date to format
|
|
* @return a RFC1123 string
|
|
*/
|
|
public static String format(Date date) {
|
|
return formatter().format0(checkNotNull(date, "date"));
|
|
}
|
|
|
|
/**
|
|
* Append a {@link Date} to a {@link StringBuilder} into RFC1123 format
|
|
* @param date the date to format
|
|
* @param sb the StringBuilder
|
|
* @return the same StringBuilder
|
|
*/
|
|
public static StringBuilder append(Date date, StringBuilder sb) {
|
|
return formatter().append0(checkNotNull(date, "date"), checkNotNull(sb, "sb"));
|
|
}
|
|
|
|
private static DateFormatter formatter() {
|
|
DateFormatter formatter = INSTANCES.get();
|
|
formatter.reset();
|
|
return formatter;
|
|
}
|
|
|
|
// delimiter = %x09 / %x20-2F / %x3B-40 / %x5B-60 / %x7B-7E
|
|
private static boolean isDelim(char c) {
|
|
return DELIMITERS.get(c);
|
|
}
|
|
|
|
private static boolean isDigit(char c) {
|
|
return c >= 48 && c <= 57;
|
|
}
|
|
|
|
private static int getNumericalValue(char c) {
|
|
return c - 48;
|
|
}
|
|
|
|
private final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
|
private final StringBuilder sb = new StringBuilder(29); // Sun, 27 Nov 2016 19:37:15 GMT
|
|
private boolean timeFound;
|
|
private int hours;
|
|
private int minutes;
|
|
private int seconds;
|
|
private boolean dayOfMonthFound;
|
|
private int dayOfMonth;
|
|
private boolean monthFound;
|
|
private int month;
|
|
private boolean yearFound;
|
|
private int year;
|
|
|
|
private DateFormatter() {
|
|
reset();
|
|
}
|
|
|
|
public void reset() {
|
|
timeFound = false;
|
|
hours = -1;
|
|
minutes = -1;
|
|
seconds = -1;
|
|
dayOfMonthFound = false;
|
|
dayOfMonth = -1;
|
|
monthFound = false;
|
|
month = -1;
|
|
yearFound = false;
|
|
year = -1;
|
|
cal.clear();
|
|
sb.setLength(0);
|
|
}
|
|
|
|
private boolean tryParseTime(CharSequence txt, int tokenStart, int tokenEnd) {
|
|
int len = tokenEnd - tokenStart;
|
|
|
|
// h:m:s to hh:mm:ss
|
|
if (len < 5 || len > 8) {
|
|
return false;
|
|
}
|
|
|
|
int localHours = -1;
|
|
int localMinutes = -1;
|
|
int localSeconds = -1;
|
|
int currentPartNumber = 0;
|
|
int currentPartValue = 0;
|
|
int numDigits = 0;
|
|
|
|
for (int i = tokenStart; i < tokenEnd; i++) {
|
|
char c = txt.charAt(i);
|
|
if (isDigit(c)) {
|
|
currentPartValue = currentPartValue * 10 + getNumericalValue(c);
|
|
if (++numDigits > 2) {
|
|
return false; // too many digits in this part
|
|
}
|
|
} else if (c == ':') {
|
|
if (numDigits == 0) {
|
|
// no digits between separators
|
|
return false;
|
|
}
|
|
switch (currentPartNumber) {
|
|
case 0:
|
|
// flushing hours
|
|
localHours = currentPartValue;
|
|
break;
|
|
case 1:
|
|
// flushing minutes
|
|
localMinutes = currentPartValue;
|
|
break;
|
|
default:
|
|
// invalid, too many :
|
|
return false;
|
|
}
|
|
currentPartValue = 0;
|
|
currentPartNumber++;
|
|
numDigits = 0;
|
|
} else {
|
|
// invalid char
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (numDigits > 0) {
|
|
// pending seconds
|
|
localSeconds = currentPartValue;
|
|
}
|
|
|
|
if (localHours >= 0 && localMinutes >= 0 && localSeconds >= 0) {
|
|
hours = localHours;
|
|
minutes = localMinutes;
|
|
seconds = localSeconds;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean tryParseDayOfMonth(CharSequence txt, int tokenStart, int tokenEnd) {
|
|
int len = tokenEnd - tokenStart;
|
|
|
|
if (len == 1) {
|
|
char c0 = txt.charAt(tokenStart);
|
|
if (isDigit(c0)) {
|
|
dayOfMonth = getNumericalValue(c0);
|
|
return true;
|
|
}
|
|
|
|
} else if (len == 2) {
|
|
char c0 = txt.charAt(tokenStart);
|
|
char c1 = txt.charAt(tokenStart + 1);
|
|
if (isDigit(c0) && isDigit(c1)) {
|
|
dayOfMonth = getNumericalValue(c0) * 10 + getNumericalValue(c1);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static boolean matchMonth(String month, CharSequence txt, int tokenStart) {
|
|
return AsciiString.regionMatchesAscii(month, true, 0, txt, tokenStart, 3);
|
|
}
|
|
|
|
private boolean tryParseMonth(CharSequence txt, int tokenStart, int tokenEnd) {
|
|
int len = tokenEnd - tokenStart;
|
|
|
|
if (len != 3) {
|
|
return false;
|
|
}
|
|
|
|
if (matchMonth("Jan", txt, tokenStart)) {
|
|
month = Calendar.JANUARY;
|
|
} else if (matchMonth("Feb", txt, tokenStart)) {
|
|
month = Calendar.FEBRUARY;
|
|
} else if (matchMonth("Mar", txt, tokenStart)) {
|
|
month = Calendar.MARCH;
|
|
} else if (matchMonth("Apr", txt, tokenStart)) {
|
|
month = Calendar.APRIL;
|
|
} else if (matchMonth("May", txt, tokenStart)) {
|
|
month = Calendar.MAY;
|
|
} else if (matchMonth("Jun", txt, tokenStart)) {
|
|
month = Calendar.JUNE;
|
|
} else if (matchMonth("Jul", txt, tokenStart)) {
|
|
month = Calendar.JULY;
|
|
} else if (matchMonth("Aug", txt, tokenStart)) {
|
|
month = Calendar.AUGUST;
|
|
} else if (matchMonth("Sep", txt, tokenStart)) {
|
|
month = Calendar.SEPTEMBER;
|
|
} else if (matchMonth("Oct", txt, tokenStart)) {
|
|
month = Calendar.OCTOBER;
|
|
} else if (matchMonth("Nov", txt, tokenStart)) {
|
|
month = Calendar.NOVEMBER;
|
|
} else if (matchMonth("Dec", txt, tokenStart)) {
|
|
month = Calendar.DECEMBER;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private boolean tryParseYear(CharSequence txt, int tokenStart, int tokenEnd) {
|
|
int len = tokenEnd - tokenStart;
|
|
|
|
if (len == 2) {
|
|
char c0 = txt.charAt(tokenStart);
|
|
char c1 = txt.charAt(tokenStart + 1);
|
|
if (isDigit(c0) && isDigit(c1)) {
|
|
year = getNumericalValue(c0) * 10 + getNumericalValue(c1);
|
|
return true;
|
|
}
|
|
|
|
} else if (len == 4) {
|
|
char c0 = txt.charAt(tokenStart);
|
|
char c1 = txt.charAt(tokenStart + 1);
|
|
char c2 = txt.charAt(tokenStart + 2);
|
|
char c3 = txt.charAt(tokenStart + 3);
|
|
if (isDigit(c0) && isDigit(c1) && isDigit(c2) && isDigit(c3)) {
|
|
year = getNumericalValue(c0) * 1000 +
|
|
getNumericalValue(c1) * 100 +
|
|
getNumericalValue(c2) * 10 +
|
|
getNumericalValue(c3);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private boolean parseToken(CharSequence txt, int tokenStart, int tokenEnd) {
|
|
// return true if all parts are found
|
|
if (!timeFound) {
|
|
timeFound = tryParseTime(txt, tokenStart, tokenEnd);
|
|
if (timeFound) {
|
|
return dayOfMonthFound && monthFound && yearFound;
|
|
}
|
|
}
|
|
|
|
if (!dayOfMonthFound) {
|
|
dayOfMonthFound = tryParseDayOfMonth(txt, tokenStart, tokenEnd);
|
|
if (dayOfMonthFound) {
|
|
return timeFound && monthFound && yearFound;
|
|
}
|
|
}
|
|
|
|
if (!monthFound) {
|
|
monthFound = tryParseMonth(txt, tokenStart, tokenEnd);
|
|
if (monthFound) {
|
|
return timeFound && dayOfMonthFound && yearFound;
|
|
}
|
|
}
|
|
|
|
if (!yearFound) {
|
|
yearFound = tryParseYear(txt, tokenStart, tokenEnd);
|
|
}
|
|
return timeFound && dayOfMonthFound && monthFound && yearFound;
|
|
}
|
|
|
|
private Date parse0(CharSequence txt, int start, int end) {
|
|
boolean allPartsFound = parse1(txt, start, end);
|
|
return allPartsFound && normalizeAndValidate() ? computeDate() : null;
|
|
}
|
|
|
|
private boolean parse1(CharSequence txt, int start, int end) {
|
|
// return true if all parts are found
|
|
int tokenStart = -1;
|
|
|
|
for (int i = start; i < end; i++) {
|
|
char c = txt.charAt(i);
|
|
|
|
if (isDelim(c)) {
|
|
if (tokenStart != -1) {
|
|
// terminate token
|
|
if (parseToken(txt, tokenStart, i)) {
|
|
return true;
|
|
}
|
|
tokenStart = -1;
|
|
}
|
|
} else if (tokenStart == -1) {
|
|
// start new token
|
|
tokenStart = i;
|
|
}
|
|
}
|
|
|
|
// terminate trailing token
|
|
return tokenStart != -1 && parseToken(txt, tokenStart, txt.length());
|
|
}
|
|
|
|
private boolean normalizeAndValidate() {
|
|
if (dayOfMonth < 1
|
|
|| dayOfMonth > 31
|
|
|| hours > 23
|
|
|| minutes > 59
|
|
|| seconds > 59) {
|
|
return false;
|
|
}
|
|
|
|
if (year >= 70 && year <= 99) {
|
|
year += 1900;
|
|
} else if (year >= 0 && year < 70) {
|
|
year += 2000;
|
|
} else if (year < 1601) {
|
|
// invalid value
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private Date computeDate() {
|
|
cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);
|
|
cal.set(Calendar.MONTH, month);
|
|
cal.set(Calendar.YEAR, year);
|
|
cal.set(Calendar.HOUR_OF_DAY, hours);
|
|
cal.set(Calendar.MINUTE, minutes);
|
|
cal.set(Calendar.SECOND, seconds);
|
|
return cal.getTime();
|
|
}
|
|
|
|
private String format0(Date date) {
|
|
append0(date, sb);
|
|
return sb.toString();
|
|
}
|
|
|
|
private StringBuilder append0(Date date, StringBuilder sb) {
|
|
cal.setTime(date);
|
|
|
|
sb.append(DAY_OF_WEEK_TO_SHORT_NAME[cal.get(Calendar.DAY_OF_WEEK) - 1]).append(", ");
|
|
sb.append(cal.get(Calendar.DAY_OF_MONTH)).append(' ');
|
|
sb.append(CALENDAR_MONTH_TO_SHORT_NAME[cal.get(Calendar.MONTH)]).append(' ');
|
|
sb.append(cal.get(Calendar.YEAR)).append(' ');
|
|
appendZeroLeftPadded(cal.get(Calendar.HOUR_OF_DAY), sb).append(':');
|
|
appendZeroLeftPadded(cal.get(Calendar.MINUTE), sb).append(':');
|
|
return appendZeroLeftPadded(cal.get(Calendar.SECOND), sb).append(" GMT");
|
|
}
|
|
|
|
private static StringBuilder appendZeroLeftPadded(int value, StringBuilder sb) {
|
|
if (value < 10) {
|
|
sb.append('0');
|
|
}
|
|
return sb.append(value);
|
|
}
|
|
}
|