Fix cookie date parsing, close #6016
Motivation: * RFC6265 defines its own parser which is different from RFC1123 (it accepts RFC1123 format but also other ones). Basically, it's very lax on delimiters, ignores day of week and timezone. Currently, ClientCookieDecoder uses HttpHeaderDateFormat underneath, and can't parse valid cookies such as Github ones whose expires attribute looks like "Sun, 27 Nov 2016 19:37:15 -0000" * ServerSideCookieEncoder currently uses HttpHeaderDateFormat underneath for formatting expires field, and it's slow. Modifications: * Introduce HttpHeaderDateFormatter that correctly implement RFC6265 * Use HttpHeaderDateFormatter in ClientCookieDecoder and ServerCookieEncoder * Deprecate HttpHeaderDateFormat Result: * Proper RFC6265 dates support * Faster ServerCookieEncoder and ClientCookieDecoder * Faster tool for handling headers such as "Expires" and "Date"
This commit is contained in:
parent
0bc30a123e
commit
edc4842309
@ -32,7 +32,9 @@ import java.util.TimeZone;
|
|||||||
* <li>Sunday, 06-Nov-94 08:49:37 GMT: obsolete specification</li>
|
* <li>Sunday, 06-Nov-94 08:49:37 GMT: obsolete specification</li>
|
||||||
* <li>Sun Nov 6 08:49:37 1994: obsolete specification</li>
|
* <li>Sun Nov 6 08:49:37 1994: obsolete specification</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
|
* @deprecated Use {@link HttpHeaderDateFormatter} instead
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public final class HttpHeaderDateFormat extends SimpleDateFormat {
|
public final class HttpHeaderDateFormat extends SimpleDateFormat {
|
||||||
private static final long serialVersionUID = -925286159755905325L;
|
private static final long serialVersionUID = -925286159755905325L;
|
||||||
|
|
||||||
|
@ -0,0 +1,443 @@
|
|||||||
|
/*
|
||||||
|
* 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.http;
|
||||||
|
|
||||||
|
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 HttpHeaderDateFormatter {
|
||||||
|
|
||||||
|
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<HttpHeaderDateFormatter> INSTANCES =
|
||||||
|
new FastThreadLocal<HttpHeaderDateFormatter>() {
|
||||||
|
@Override
|
||||||
|
protected HttpHeaderDateFormatter initialValue() {
|
||||||
|
return new HttpHeaderDateFormatter();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 parse(CharSequence txt) {
|
||||||
|
return parse(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 parse(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 HttpHeaderDateFormatter formatter() {
|
||||||
|
HttpHeaderDateFormatter 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 HttpHeaderDateFormatter() {
|
||||||
|
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.set(Calendar.MILLISECOND, 0);
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (int i = tokenStart; i < tokenEnd; i++) {
|
||||||
|
char c = txt.charAt(i);
|
||||||
|
if (isDigit(c)) {
|
||||||
|
currentPartValue = currentPartValue * 10 + getNumericalValue(c);
|
||||||
|
} else if (c == ':') {
|
||||||
|
if (currentPartValue == 0) {
|
||||||
|
// invalid :: (nothing in between)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (currentPartNumber) {
|
||||||
|
case 0:
|
||||||
|
// flushing hours
|
||||||
|
localHours = currentPartValue;
|
||||||
|
currentPartValue = 0;
|
||||||
|
currentPartNumber++;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
// flushing minutes
|
||||||
|
localMinutes = currentPartValue;
|
||||||
|
currentPartValue = 0;
|
||||||
|
currentPartNumber++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// invalid, too many :
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// invalid char
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentPartValue > 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);
|
||||||
|
}
|
||||||
|
}
|
@ -15,13 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cookie;
|
package io.netty.handler.codec.http.cookie;
|
||||||
|
|
||||||
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
import io.netty.handler.codec.http.HttpHeaderDateFormatter;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.HttpHeaderDateFormat;
|
|
||||||
|
|
||||||
import java.text.ParsePosition;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie decoder to be used client side.
|
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie decoder to be used client side.
|
||||||
*
|
*
|
||||||
@ -168,14 +167,11 @@ public final class ClientCookieDecoder extends CookieDecoder {
|
|||||||
// max age has precedence over expires
|
// max age has precedence over expires
|
||||||
if (maxAge != Long.MIN_VALUE) {
|
if (maxAge != Long.MIN_VALUE) {
|
||||||
return maxAge;
|
return maxAge;
|
||||||
} else {
|
} else if (isValueDefined(expiresStart, expiresEnd)) {
|
||||||
String expires = computeValue(expiresStart, expiresEnd);
|
Date expiresDate = HttpHeaderDateFormatter.parse(header, expiresStart, expiresEnd);
|
||||||
if (expires != null) {
|
if (expiresDate != null) {
|
||||||
Date expiresDate = HttpHeaderDateFormat.get().parse(expires, new ParsePosition(0));
|
long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
|
||||||
if (expiresDate != null) {
|
return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
|
||||||
long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis();
|
|
||||||
return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Long.MIN_VALUE;
|
return Long.MIN_VALUE;
|
||||||
@ -254,8 +250,12 @@ public final class ClientCookieDecoder extends CookieDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isValueDefined(int valueStart, int valueEnd) {
|
||||||
|
return valueStart != -1 && valueStart != valueEnd;
|
||||||
|
}
|
||||||
|
|
||||||
private String computeValue(int valueStart, int valueEnd) {
|
private String computeValue(int valueStart, int valueEnd) {
|
||||||
return valueStart == -1 || valueStart == valueEnd ? null : header.substring(valueStart, valueEnd);
|
return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,9 @@ import static io.netty.handler.codec.http.cookie.CookieUtil.addQuoted;
|
|||||||
import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
|
import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder;
|
||||||
import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
|
import static io.netty.handler.codec.http.cookie.CookieUtil.stripTrailingSeparator;
|
||||||
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
import static io.netty.util.internal.ObjectUtil.checkNotNull;
|
||||||
import io.netty.handler.codec.http.HttpHeaderDateFormat;
|
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderDateFormatter;
|
||||||
|
import io.netty.handler.codec.http.HttpConstants;
|
||||||
import io.netty.handler.codec.http.HttpRequest;
|
import io.netty.handler.codec.http.HttpRequest;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -102,7 +104,11 @@ public final class ServerCookieEncoder extends CookieEncoder {
|
|||||||
if (cookie.maxAge() != Long.MIN_VALUE) {
|
if (cookie.maxAge() != Long.MIN_VALUE) {
|
||||||
add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge());
|
add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge());
|
||||||
Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis());
|
Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis());
|
||||||
add(buf, CookieHeaderNames.EXPIRES, HttpHeaderDateFormat.get().format(expires));
|
buf.append(CookieHeaderNames.EXPIRES);
|
||||||
|
buf.append((char) HttpConstants.EQUALS);
|
||||||
|
HttpHeaderDateFormatter.append(expires, buf);
|
||||||
|
buf.append((char) HttpConstants.SEMICOLON);
|
||||||
|
buf.append((char) HttpConstants.SP);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cookie.path() != null) {
|
if (cookie.path() != null) {
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* 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.http;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
import static io.netty.handler.codec.http.HttpHeaderDateFormatter.*;
|
||||||
|
|
||||||
|
public class HttpHeaderDateFormatterTest {
|
||||||
|
/**
|
||||||
|
* This date is set at "06 Nov 1994 08:49:37 GMT", from
|
||||||
|
* <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html">examples in RFC documentation</a>
|
||||||
|
*/
|
||||||
|
private static final long TIMESTAMP = 784111777000L;
|
||||||
|
private static final Date DATE = new Date(TIMESTAMP);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithSingleDigitDay() {
|
||||||
|
assertEquals(DATE, parse("Sun, 6 Nov 1994 08:49:37 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithDoubleDigitDay() {
|
||||||
|
assertEquals(DATE, parse("Sun, 06 Nov 1994 08:49:37 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithDashSeparatorSingleDigitDay() {
|
||||||
|
assertEquals(DATE, parse("Sunday, 06-Nov-94 08:49:37 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithSingleDoubleDigitDay() {
|
||||||
|
assertEquals(DATE, parse("Sunday, 6-Nov-94 08:49:37 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithoutGMT() {
|
||||||
|
assertEquals(DATE, parse("Sun Nov 6 08:49:37 1994"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithFunkyTimezone() {
|
||||||
|
assertEquals(DATE, parse("Sun Nov 6 08:49:37 1994 -0000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithSingleDigitHourMinutesAndSecond() {
|
||||||
|
assertEquals(DATE, parse("Sunday, 6-Nov-94 8:49:37 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseWithSingleDigitTime() {
|
||||||
|
assertEquals(DATE, parse("Sunday, 6 Nov 1994 8:49:37 GMT"));
|
||||||
|
|
||||||
|
Date _08_09_37 = new Date(TIMESTAMP - 40 * 60 * 1000);
|
||||||
|
assertEquals(_08_09_37, parse("Sunday, 6 Nov 1994 8:9:37 GMT"));
|
||||||
|
assertEquals(_08_09_37, parse("Sunday, 6 Nov 1994 8:09:37 GMT"));
|
||||||
|
|
||||||
|
Date _08_09_07 = new Date(TIMESTAMP - (40 * 60 + 30) * 1000);
|
||||||
|
assertEquals(_08_09_07, parse("Sunday, 6 Nov 1994 8:9:7 GMT"));
|
||||||
|
assertEquals(_08_09_07, parse("Sunday, 6 Nov 1994 8:9:07 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseInvalidInput() {
|
||||||
|
// missing field
|
||||||
|
assertNull(parse("Sun, Nov 1994 08:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 1994 08:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 08:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 :49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08::37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08:49: GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08:49 GMT"));
|
||||||
|
//invalid value
|
||||||
|
assertNull(parse("Sun, 6 FOO 1994 08:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 36 Nov 1994 08:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 28:49:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08:69:37 GMT"));
|
||||||
|
assertNull(parse("Sun, 6 Nov 1994 08:49:67 GMT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFormat() {
|
||||||
|
assertEquals("Sun, 6 Nov 1994 08:49:37 GMT", format(DATE));
|
||||||
|
}
|
||||||
|
}
|
@ -15,17 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package io.netty.handler.codec.http.cookie;
|
package io.netty.handler.codec.http.cookie;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import io.netty.handler.codec.http.HttpHeaderDateFormatter;
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.HttpHeaderDateFormat;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@ -33,12 +25,14 @@ import java.util.Date;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class ClientCookieDecoderTest {
|
public class ClientCookieDecoderTest {
|
||||||
@Test
|
@Test
|
||||||
public void testDecodingSingleCookieV0() {
|
public void testDecodingSingleCookieV0() {
|
||||||
String cookieString = "myCookie=myValue;expires=XXX;path=/apathsomewhere;domain=.adomainsomewhere;secure;";
|
String cookieString = "myCookie=myValue;expires=XXX;path=/apathsomewhere;domain=.adomainsomewhere;secure;";
|
||||||
cookieString = cookieString.replace("XXX", HttpHeaderDateFormat.get()
|
cookieString = cookieString.replace("XXX",
|
||||||
.format(new Date(System.currentTimeMillis() + 50000)));
|
HttpHeaderDateFormatter.format(new Date(System.currentTimeMillis() + 50000)));
|
||||||
|
|
||||||
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
|
Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString);
|
||||||
assertNotNull(cookie);
|
assertNotNull(cookie);
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* 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.microbench.http;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
|
||||||
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||||
|
public class ClientCookieDecoderBenchmark {
|
||||||
|
|
||||||
|
private static final String COOKIE_STRING =
|
||||||
|
"__Host-user_session_same_site=fgfMsM59vJTpZg88nxqKkIhgOt0ADF8LX8wjMMbtcb4IJMufWCnCcXORhbo9QMuyiybdtx; " +
|
||||||
|
"path=/; expires=Mon, 28 Nov 2016 13:56:01 GMT; secure; HttpOnly";
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public Cookie decodeCookieWithRfc1123ExpiresField() {
|
||||||
|
return ClientCookieDecoder.STRICT.decode(COOKIE_STRING);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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.microbench.http;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderDateFormat;
|
||||||
|
import io.netty.handler.codec.http.HttpHeaderDateFormatter;
|
||||||
|
import org.openjdk.jmh.annotations.Benchmark;
|
||||||
|
import org.openjdk.jmh.annotations.OutputTimeUnit;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@OutputTimeUnit(TimeUnit.SECONDS)
|
||||||
|
public class HttpHeaderDateFormatterBenchmark {
|
||||||
|
|
||||||
|
private static final String DATE_STRING = "Sun, 27 Nov 2016 19:18:46 GMT";
|
||||||
|
private static final Date DATE = new Date(784111777000L);
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public Date parseHttpHeaderDateFormatter() {
|
||||||
|
return HttpHeaderDateFormatter.parse(DATE_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public Date parseHttpHeaderDateFormat() throws Exception {
|
||||||
|
return HttpHeaderDateFormat.get().parse(DATE_STRING);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public String formatHttpHeaderDateFormatter() {
|
||||||
|
return HttpHeaderDateFormatter.format(DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Benchmark
|
||||||
|
public String formatHttpHeaderDateFormat() throws Exception {
|
||||||
|
return HttpHeaderDateFormat.get().format(DATE);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user