package org.lobobrowser.request;
import java.net.URI;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.DateTimeException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
final class CookieParsing {
private static final Logger logger = Logger.getLogger(CookieParsing.class.getName());
private static final DateFormat EXPIRES_FORMAT;
private static final DateFormat EXPIRES_FORMAT_BAK1;
private static final DateFormat EXPIRES_FORMAT_BAK2;
static {
// Note: Using yy in case years are given as two digits.
// Note: Must use US locale for cookie dates.
final Locale locale = Locale.US;
final SimpleDateFormat ef1 = new SimpleDateFormat("EEE, dd MMM yy HH:mm:ss 'GMT'", locale);
final SimpleDateFormat ef2 = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss 'GMT'", locale);
final SimpleDateFormat ef3 = new SimpleDateFormat("EEE MMM dd HH:mm:ss yy 'GMT'", locale);
final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
ef1.setTimeZone(gmtTimeZone);
ef2.setTimeZone(gmtTimeZone);
ef3.setTimeZone(gmtTimeZone);
EXPIRES_FORMAT = ef1;
EXPIRES_FORMAT_BAK1 = ef2;
EXPIRES_FORMAT_BAK2 = ef3;
}
/** @deprecated Use parseExpiresRFC6265 instead */
@Deprecated
private static Optional<java.util.Date> parseExpires(final String expiresStr) {
Optional<java.util.Date> expiresDate = Optional.empty();
synchronized (EXPIRES_FORMAT) {
try {
expiresDate = Optional.of(EXPIRES_FORMAT.parse(expiresStr));
} catch (final Exception pe) {
if (logger.isLoggable(Level.INFO)) {
logger.log(Level.INFO, "parseExpires(): Bad date format: " + expiresStr + ". Will try again.", pe);
}
try {
expiresDate = Optional.of(EXPIRES_FORMAT_BAK1.parse(expiresStr));
} catch (final Exception pe2) {
try {
expiresDate = Optional.of(EXPIRES_FORMAT_BAK2.parse(expiresStr));
} catch (final ParseException pe3) {
logger.log(Level.SEVERE, "parseExpires(): Giving up on cookie date format: " + expiresStr, pe3);
}
}
}
}
return expiresDate;
}
static Optional<CookieDetails> parseCookieSpec(final URI requestURL, final String cookieSpec) {
final StringTokenizer tok = new StringTokenizer(cookieSpec, ";");
String cookieName = null;
String cookieValue = null;
String domain = null;
String path = null;
Optional<Date> expires = Optional.empty();
Long maxAge = null;
boolean secure = false;
boolean httpOnly = false;
boolean hasCookieName = false;
while (tok.hasMoreTokens()) {
final String token = tok.nextToken();
if (!hasCookieName) {
final Matcher matcher = COOKIE_PAIR_STRICT_MEDIUM.matcher(token);
// if ((idx == -1) || (name.length() == 0)) {
if (!matcher.lookingAt()) {
return Optional.empty();
} else {
cookieName = matcher.group(1); // the regexp should trim() already
// cookieValue = matcher.group(3); // [2] is quotes or empty-string
cookieValue = matcher.group(2);
hasCookieName = true;
}
} else {
final int idx = token.indexOf('=');
final String name = idx == -1 ? token.trim() : token.substring(0, idx).trim();
final String value = idx == -1 ? "" : token.substring(idx + 1).trim();
if ("max-age".equalsIgnoreCase(name)) {
try {
maxAge = Long.parseLong(value);
} catch (final NumberFormatException e) {
// Ignore this attribute
logger.log(Level.WARNING, "parseCookieSpec(): Max-age is not formatted correctly: " + value + ".");
}
} else if ("path".equalsIgnoreCase(name)) {
path = value;
} else if ("domain".equalsIgnoreCase(name)) {
if (value.length() == 0) {
// Ignore this attribute
logger.log(Level.WARNING, "parseCookieSpec(): domain is empty, hence attribute is ignored");
} else {
if (value.charAt(0) == '.') {
domain = value.substring(1);
} else {
domain = value;
}
}
} else if ("expires".equalsIgnoreCase(name)) {
final Optional<Date> parsedExpires = parseExpiresRFC6265(value);
if (parsedExpires.isPresent()) {
expires = parsedExpires;
}
} else if ("secure".equalsIgnoreCase(name)) {
secure = true;
} else if ("httponly".equalsIgnoreCase(name)) {
httpOnly = true;
}
}
}
return Optional.of(new CookieDetails(requestURL, cookieName, cookieValue, domain, path, expires, maxAge, secure, httpOnly));
}
private static final Map<String, Integer> monthToNum = new HashMap<>();
static {
monthToNum.put("jan", 1);
monthToNum.put("feb", 2);
monthToNum.put("mar", 3);
monthToNum.put("apr", 4);
monthToNum.put("may", 5);
monthToNum.put("jun", 6);
monthToNum.put("jul", 7);
monthToNum.put("aug", 8);
monthToNum.put("sep", 9);
monthToNum.put("oct", 10);
monthToNum.put("nov", 11);
monthToNum.put("dec", 12);
}
private static final Pattern DATE_DELIM = Pattern.compile("[\\x09\\x20-\\x2F\\x3B-\\x40\\x5B-\\x60\\x7B-\\x7E]");
//From RFC2616 S2.2:
private static final Pattern TOKEN = Pattern.compile("[\\x21\\x23-\\x26\\x2A\\x2B\\x2D\\x2E\\x30-\\x39\\x41-\\x5A\\x5E-\\x7A\\x7C\\x7E]");
//From RFC6265 S4.1.1
//note that it excludes \x3B ";"
private static final Pattern COOKIE_OCTET = Pattern.compile("[\\x21\\x23-\\x2B\\x2D-\\x3A\\x3C-\\x5B\\x5D-\\x7E]");
// private static final Pattern COOKIE_OCTETS = Pattern.compile('^' + COOKIE_OCTET.pattern() + '$');
//The name/key cannot be empty but the value can (S5.2):
// private static final Pattern COOKIE_PAIR_STRICT = Pattern.compile("^(" + TOKEN.pattern() + "+)=(\"?)(" + COOKIE_OCTET.pattern() + "*)\\2$");
private static final Pattern COOKIE_PAIR_STRICT_MEDIUM = Pattern.compile("^(" + TOKEN.pattern() + "+)=((\"?)(" + COOKIE_OCTET.pattern()
+ "*)\\3).*$");
// private static final Pattern COOKIE_PAIR = Pattern.compile("^([^=\\s]+)\\s*=\\s*(\"?)\\s*(.*)\\s*\\2\\s*$");
//RFC6265 S4.1.1 defines extension-av as 'any CHAR except CTLs or ";"'
//Note ';' is \x3B
// private static final Pattern NON_CTL_SEMICOLON = Pattern.compile("[\\x20-\\x3A\\x3C-\\x7E]+");
// private static final Pattern EXTENSION_AV = NON_CTL_SEMICOLON;
// private static final Pattern PATH_VALUE = NON_CTL_SEMICOLON;
//Used for checking whether or not there is a trailing semi-colon
// private static final Pattern TRAILING_SEMICOLON = Pattern.compile(";+$");
/* RFC6265 S5.1.1.5:
* [fail if] the day-of-month-value is less than 1 or greater than 31
*/
private static final Pattern DAY_OF_MONTH = Pattern.compile("^(0?[1-9]|[12][0-9]|3[01])$");
/* RFC6265 S5.1.1.5:
* [fail if]
* * the hour-value is greater than 23,
* * the minute-value is greater than 59, or
* * the second-value is greater than 59.
*/
// private static final Pattern TIME = Pattern.compile("(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])");
// private static final Pattern STRICT_TIME = Pattern.compile("^(0?[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$");
private static final Pattern LENIENT_TIME = Pattern.compile("([0-9][0-9]?):([0-9][0-9]?):([0-9][0-9]?)");
private static final Pattern MONTH = Pattern.compile("^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec).*$", Pattern.CASE_INSENSITIVE);
private static final Pattern YEAR = Pattern.compile("^([0-9][0-9]{1,3})$");
static Optional<java.util.Date> parseExpiresRFC6265(final String expiresStr) {
final Optional<java.util.Date> expiresDate = Optional.empty();
final String[] tokens = DATE_DELIM.split(expiresStr);
if (tokens.length == 0) {
return expiresDate;
}
boolean found_time = false, found_dom = false, found_month = false, found_year = false;
// LocalDateTime date = LocalDateTime.MIN;
// date = date.withNano(0);
// LocalDate date = LocalDate.MIN;
int dayOfMonth = 0;
int month = 0;
int year = 0;
int hour = 0;
int minute = 0;
int second = 0;
// time = time.withNano(0);
for (final String token2 : tokens) {
final String token = token2.trim();
if (token.length() == 0) {
continue;
}
// var result;
/* 2.1. If the found-time flag is not set and the token matches the time
* production, set the found-time flag and set the hour- value,
* minute-value, and second-value to the numbers denoted by the digits in
* the date-token, respectively. Skip the remaining sub-steps and continue
* to the next date-token.
*/
if (!found_time) {
// final Matcher matcher = (strict ? STRICT_TIME : LENIENT_TIME).matcher(token);
final Matcher matcher = LENIENT_TIME.matcher(token);
if (matcher.matches()) {
found_time = true;
hour = Integer.parseInt(matcher.group(1));
minute = Integer.parseInt(matcher.group(2));
second = Integer.parseInt(matcher.group(3));
continue;
}
}
/* 2.2. If the found-day-of-month flag is not set and the date-token matches
* the day-of-month production, set the found-day-of- month flag and set
* the day-of-month-value to the number denoted by the date-token. Skip
* the remaining sub-steps and continue to the next date-token.
*/
if (!found_dom) {
final Matcher matcher = DAY_OF_MONTH.matcher(token);
if (matcher.matches()) {
found_dom = true;
dayOfMonth = Integer.parseInt(matcher.group(1));
continue;
}
}
/* 2.3. If the found-month flag is not set and the date-token matches the
* month production, set the found-month flag and set the month-value to
* the month denoted by the date-token. Skip the remaining sub-steps and
* continue to the next date-token.
*/
if (!found_month) {
final Matcher matcher = MONTH.matcher(token);
if (matcher.matches()) {
found_month = true;
month = monthToNum.get(matcher.group(1).toLowerCase());
continue;
}
}
/* 2.4. If the found-year flag is not set and the date-token matches the year
* production, set the found-year flag and set the year-value to the number
* denoted by the date-token. Skip the remaining sub-steps and continue to
* the next date-token.
*/
if (!found_year) {
final Matcher matcher = YEAR.matcher(token);
if (matcher.matches()) {
year = Integer.parseInt(matcher.group(1));
/* From S5.1.1:
* 3. If the year-value is greater than or equal to 70 and less
* than or equal to 99, increment the year-value by 1900.
* 4. If the year-value is greater than or equal to 0 and less
* than or equal to 69, increment the year-value by 2000.
*/
if ((70 <= year) && (year <= 99)) {
year += 1900;
} else if ((0 <= year) && (year <= 69)) {
year += 2000;
}
if (year < 1601) {
return expiresDate; // 5. ... the year-value is less than 1601
}
found_year = true;
continue;
}
}
}
if (!(found_time && found_dom && found_month && found_year)) {
// 5. ... at least one of the found-day-of-month, found-month, found-year, or found-time flags is not set,
return expiresDate;
} else {
try {
final OffsetDateTime dateTime = OffsetDateTime.of(year, month, dayOfMonth, hour, minute, second, 0, ZoneOffset.UTC);
final long millis = dateTime.toInstant().toEpochMilli();
return Optional.of(new Date(millis));
} catch (final DateTimeException e) {
logger.log(Level.WARNING, "parseExpires(): Bad date-time.", e);
return Optional.empty();
}
}
}
}