org.fakeservlet.FakeCookie Maven / Gradle / Ivy
package org.fakeservlet;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
/**
* RFC 6265 compliant http cookie parser and generator future-proof
* for future attributes
*
* Attributes
*
* Boolean attributes are added without "=..." if true and omitted if false.
* null attributes are omitted.
* Instant attributes are formatted according to RFC 1123. The following
* are standard attributes:
*
*
* - Max-Age
* - Expires
* - Secure
* - HttpOnly
* - Domain
* - Path
*
*/
public class FakeCookie {
private final String name;
private final String value;
private final Map attributes = new LinkedHashMap<>();
public FakeCookie(String name, String value) {
this.name = name;
this.value = value != null ? value : "";
if (this.value.isEmpty()) {
this.attributes.putIfAbsent("Expires", Instant.ofEpochMilli(0));
this.attributes.putIfAbsent("Max-Age", 0);
}
}
public static FakeCookie delete(String name) {
return new FakeCookie(name, null);
}
public static FakeCookie parseSetCookieHeader(String rfc6264String) {
int offset = 0;
int startKey = scanToNot(rfc6264String, offset, List.of('\t', ' '));
if (startKey == rfc6264String.length()) {
throw new IllegalArgumentException("Illegal empty set-cookie header");
}
int endKey = scanTo(rfc6264String, startKey + 1, List.of('\t', ' ', '='));
String key = rfc6264String.substring(startKey, endKey);
int equalPos = scanTo(rfc6264String, endKey - 1, List.of('='));
if (equalPos == -1) {
throw new IllegalArgumentException("No value for rfc6264String string " + rfc6264String);
}
int startValue = scanToNot(rfc6264String, equalPos + 1, List.of('\t', ' '));
String value;
int endValue;
if (rfc6264String.charAt(startValue) == '\"') {
startValue = startValue + 1;
endValue = scanTo(rfc6264String, startValue, List.of('"'));
if (endValue == -1) {
throw new IllegalArgumentException("Quoted rfc6264String value not terminated " + rfc6264String);
}
offset = scanTo(rfc6264String, endValue, List.of(';'));
} else {
endValue = scanTo(rfc6264String, startValue, List.of(';'));
if (endValue == rfc6264String.length()) {
offset = endValue;
} else if (rfc6264String.charAt(endValue + 1) != ';') {
offset = scanTo(rfc6264String, endValue, List.of(';'));
}
}
value = URLDecoder.decode(rfc6264String.substring(startValue, endValue), StandardCharsets.UTF_8);
FakeCookie cookie = new FakeCookie(key, value);
while (offset < rfc6264String.length()) {
int attributeStart = scanToNot(rfc6264String, offset + 1, List.of(' '));
int attributeEnd = scanTo(rfc6264String, attributeStart, List.of('=', ';'));
String attribute = rfc6264String.substring(attributeStart, attributeEnd);
if (attributeEnd < rfc6264String.length() && rfc6264String.charAt(attributeEnd) == '=') {
int attributeValueEnd = scanTo(rfc6264String, attributeEnd, List.of(';'));
String attributeValue = rfc6264String.substring(attributeEnd + 1, attributeValueEnd);
cookie.setAttribute(attribute, attributeValue);
offset = attributeValueEnd;
} else {
cookie.setAttribute(attribute, true);
offset = attributeEnd;
}
}
return cookie;
}
public static Map parseCookieHeader(String cookie) {
Map result = new LinkedHashMap<>();
int offset = 0;
while (offset < cookie.length()) {
if (cookie.charAt(offset) == ';') {
offset++;
}
int startKey = scanToNot(cookie, offset, List.of('\t', ' '));
int endKey = scanTo(cookie, startKey + 1, List.of('\t', ' ', '='));
String key = cookie.substring(startKey, endKey);
int equalPos = scanTo(cookie, endKey - 1, List.of('='));
if (equalPos == cookie.length()) {
throw new IllegalArgumentException("No value for cookie string " + cookie);
}
int startValue = scanToNot(cookie, equalPos + 1, List.of('\t', ' '));
String value;
int endValue;
if (cookie.charAt(startValue) == '\"') {
startValue = startValue + 1;
endValue = scanTo(cookie, startValue, List.of('"'));
if (endValue == cookie.length()) {
throw new IllegalArgumentException("Quoted cookie value not terminated " + cookie);
}
offset = scanTo(cookie, endValue, List.of(';'));
} else {
offset = endValue = scanTo(cookie, startValue, List.of(';'));
}
value = cookie.substring(startValue, endValue);
result.put(key, URLDecoder.decode(value, StandardCharsets.UTF_8));
}
return result;
}
private static int scanTo(String s, int startPos, List characters) {
for (int i = startPos; i < s.length(); i++) {
if (characters.contains(s.charAt(i))) {
return i;
}
}
return s.length();
}
private static int scanToNot(String s, int startPos, List characters) {
for (int i = startPos; i < s.length(); i++) {
if (!characters.contains(s.charAt(i))) {
return i;
}
}
return s.length();
}
public static Optional asClientCookieHeader(Collection cookies) {
if (cookies == null || cookies.isEmpty()) {
return Optional.empty();
}
return Optional.of(cookies.stream()
.map(FakeCookie::toClientCookieString)
.collect(Collectors.joining(";")));
}
public static List parseSetCookieHeaders(List setCookieHeaders) {
if (setCookieHeaders == null) {
return List.of();
}
return setCookieHeaders.stream().map(FakeCookie::parseSetCookieHeader)
.collect(Collectors.toList());
}
public static Supplier