cookies with sameSite
This commit is contained in:
parent
509b8073eb
commit
f322697702
35 changed files with 1628 additions and 167 deletions
|
@ -13,7 +13,6 @@ import io.netty.handler.codec.http.HttpUtil;
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||||
import io.netty.handler.codec.http.QueryStringEncoder;
|
import io.netty.handler.codec.http.QueryStringEncoder;
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
|
||||||
|
|
||||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||||
import io.netty.util.AsciiString;
|
import io.netty.util.AsciiString;
|
||||||
|
@ -26,6 +25,7 @@ import org.xbib.netty.http.client.listener.StatusListener;
|
||||||
import org.xbib.netty.http.client.retry.BackOff;
|
import org.xbib.netty.http.client.retry.BackOff;
|
||||||
import org.xbib.netty.http.common.HttpAddress;
|
import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.common.HttpParameters;
|
import org.xbib.netty.http.common.HttpParameters;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
|
||||||
import java.nio.charset.MalformedInputException;
|
import java.nio.charset.MalformedInputException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -80,9 +80,9 @@ public class Request {
|
||||||
private StatusListener statusListener;
|
private StatusListener statusListener;
|
||||||
|
|
||||||
private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
|
private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
|
||||||
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
|
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
|
||||||
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
|
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
|
||||||
boolean isBackOff, BackOff backOff) {
|
boolean isBackOff, BackOff backOff) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.uri = uri;
|
this.uri = uri;
|
||||||
this.httpVersion = httpVersion;
|
this.httpVersion = httpVersion;
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
package org.xbib.netty.http.client.cookie;
|
||||||
|
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieDecoder;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieHeaderNames;
|
||||||
|
import org.xbib.netty.http.common.cookie.DefaultCookie;
|
||||||
|
import org.xbib.netty.http.common.util.TimeUtils;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie decoder to be used client side.
|
||||||
|
*
|
||||||
|
* It will store the way the raw value was wrapped in {@link Cookie#setWrap(boolean)} so it can be
|
||||||
|
* eventually sent back to the Origin server as is.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public final class ClientCookieDecoder extends CookieDecoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict encoder that validates that name and value chars are in the valid scope
|
||||||
|
* defined in RFC6265
|
||||||
|
*/
|
||||||
|
public static final ClientCookieDecoder STRICT = new ClientCookieDecoder(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lax instance that doesn't validate name and value
|
||||||
|
*/
|
||||||
|
public static final ClientCookieDecoder LAX = new ClientCookieDecoder(false);
|
||||||
|
|
||||||
|
private ClientCookieDecoder(boolean strict) {
|
||||||
|
super(strict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @param header header
|
||||||
|
* @return the decoded {@link Cookie}
|
||||||
|
*/
|
||||||
|
public Cookie decode(String header) {
|
||||||
|
final int headerLen = Objects.requireNonNull(header, "header").length();
|
||||||
|
if (headerLen == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CookieBuilder cookieBuilder = null;
|
||||||
|
int i = 0;
|
||||||
|
while (i < headerLen) {
|
||||||
|
while (i < headerLen) {
|
||||||
|
// Skip spaces and separators.
|
||||||
|
char c = header.charAt(i);
|
||||||
|
if (c == ',') {
|
||||||
|
// Having multiple cookies in a single Set-Cookie header is
|
||||||
|
// deprecated, modern browsers only parse the first one
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
switch (c) {
|
||||||
|
case '\t':
|
||||||
|
case '\n':
|
||||||
|
case 0x0b:
|
||||||
|
case '\f':
|
||||||
|
case '\r':
|
||||||
|
case ' ':
|
||||||
|
case ';':
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int nameBegin = i;
|
||||||
|
int nameEnd = 0;
|
||||||
|
int valueBegin = 0;
|
||||||
|
int valueEnd = 0;
|
||||||
|
while (i < headerLen) {
|
||||||
|
char curChar = header.charAt(i);
|
||||||
|
if (curChar == ';') {
|
||||||
|
nameEnd = i;
|
||||||
|
valueBegin = valueEnd = -1;
|
||||||
|
break;
|
||||||
|
} else if (curChar == '=') {
|
||||||
|
nameEnd = i;
|
||||||
|
i++;
|
||||||
|
if (i == headerLen) {
|
||||||
|
valueBegin = valueEnd = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
valueBegin = i;
|
||||||
|
int semiPos = header.indexOf(';', i);
|
||||||
|
valueEnd = i = semiPos > 0 ? semiPos : headerLen;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i == headerLen) {
|
||||||
|
nameEnd = headerLen;
|
||||||
|
valueBegin = valueEnd = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') {
|
||||||
|
valueEnd--;
|
||||||
|
}
|
||||||
|
if (nameEnd >= nameBegin && valueEnd >= valueBegin) {
|
||||||
|
if (cookieBuilder == null) {
|
||||||
|
DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
|
||||||
|
if (cookie == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
cookieBuilder = new CookieBuilder(cookie, header);
|
||||||
|
} else {
|
||||||
|
cookieBuilder.appendAttribute(nameBegin, nameEnd, valueBegin, valueEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieBuilder != null ? cookieBuilder.cookie() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CookieBuilder {
|
||||||
|
|
||||||
|
private final String header;
|
||||||
|
|
||||||
|
private final DefaultCookie cookie;
|
||||||
|
|
||||||
|
private String domain;
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
private long maxAge = Long.MIN_VALUE;
|
||||||
|
|
||||||
|
private int expiresStart;
|
||||||
|
|
||||||
|
private int expiresEnd;
|
||||||
|
|
||||||
|
private boolean secure;
|
||||||
|
|
||||||
|
private boolean httpOnly;
|
||||||
|
|
||||||
|
private String sameSite;
|
||||||
|
|
||||||
|
CookieBuilder(DefaultCookie cookie, String header) {
|
||||||
|
this.cookie = cookie;
|
||||||
|
this.header = header;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long mergeMaxAgeAndExpires() {
|
||||||
|
if (maxAge != Long.MIN_VALUE) {
|
||||||
|
return maxAge;
|
||||||
|
} else if (isValueDefined(expiresStart, expiresEnd)) {
|
||||||
|
Instant expiresDate = TimeUtils.parseDate(header); //DateFormatter.parseHttpDate(header, expiresStart, expiresEnd)
|
||||||
|
if (expiresDate != null) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
long maxAgeMillis = expiresDate.toEpochMilli() - now.toEpochMilli();
|
||||||
|
return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Long.MIN_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cookie cookie() {
|
||||||
|
cookie.setDomain(domain);
|
||||||
|
cookie.setPath(path);
|
||||||
|
cookie.setMaxAge(mergeMaxAgeAndExpires());
|
||||||
|
cookie.setSecure(secure);
|
||||||
|
cookie.setHttpOnly(httpOnly);
|
||||||
|
cookie.setSameSite(sameSite);
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse and store a key-value pair. First one is considered to be the
|
||||||
|
* cookie name/value. Unknown attribute names are silently discarded.
|
||||||
|
*
|
||||||
|
* @param keyStart
|
||||||
|
* where the key starts in the header
|
||||||
|
* @param keyEnd
|
||||||
|
* where the key ends in the header
|
||||||
|
* @param valueStart
|
||||||
|
* where the value starts in the header
|
||||||
|
* @param valueEnd
|
||||||
|
* where the value ends in the header
|
||||||
|
*/
|
||||||
|
void appendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd) {
|
||||||
|
int length = keyEnd - keyStart;
|
||||||
|
if (length == 4) {
|
||||||
|
parse4(keyStart, valueStart, valueEnd);
|
||||||
|
} else if (length == 6) {
|
||||||
|
parse6(keyStart, valueStart, valueEnd);
|
||||||
|
} else if (length == 7) {
|
||||||
|
parse7(keyStart, valueStart, valueEnd);
|
||||||
|
} else if (length == 8) {
|
||||||
|
parse8(keyStart, valueStart, valueEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse4(int nameStart, int valueStart, int valueEnd) {
|
||||||
|
if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) {
|
||||||
|
path = computeValue(valueStart, valueEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse6(int nameStart, int valueStart, int valueEnd) {
|
||||||
|
if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) {
|
||||||
|
domain = computeValue(valueStart, valueEnd);
|
||||||
|
} else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) {
|
||||||
|
secure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMaxAge(String value) {
|
||||||
|
try {
|
||||||
|
maxAge = Math.max(Long.parseLong(value), 0L);
|
||||||
|
} catch (NumberFormatException e1) {
|
||||||
|
// ignore failure to parse -> treat as session cookie
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse7(int nameStart, int valueStart, int valueEnd) {
|
||||||
|
if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) {
|
||||||
|
expiresStart = valueStart;
|
||||||
|
expiresEnd = valueEnd;
|
||||||
|
} else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) {
|
||||||
|
setMaxAge(computeValue(valueStart, valueEnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse8(int nameStart, int valueStart, int valueEnd) {
|
||||||
|
if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) {
|
||||||
|
httpOnly = true;
|
||||||
|
} else if (header.regionMatches(true, nameStart, CookieHeaderNames.SAMESITE, 0, 8)) {
|
||||||
|
setSameSite(computeValue(valueStart, valueEnd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValueDefined(int valueStart, int valueEnd) {
|
||||||
|
return valueStart != -1 && valueStart != valueEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String computeValue(int valueStart, int valueEnd) {
|
||||||
|
return isValueDefined(valueStart, valueEnd) ? header.substring(valueStart, valueEnd) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSameSite(String value) {
|
||||||
|
sameSite = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package org.xbib.netty.http.client.cookie;
|
||||||
|
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieEncoder;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieUtil;
|
||||||
|
import org.xbib.netty.http.common.cookie.DefaultCookie;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used client side, so
|
||||||
|
* only name=value pairs are sent.
|
||||||
|
*
|
||||||
|
* Note that multiple cookies are supposed to be sent at once in a single "Cookie" header.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* // Example
|
||||||
|
* HttpRequest req = ...
|
||||||
|
* res.setHeader("Cookie", {@link ClientCookieEncoder}.encode("JSESSIONID", "1234"))
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public final class ClientCookieEncoder extends CookieEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict encoder that validates that name and value chars are in the valid scope and (for methods that accept
|
||||||
|
* multiple cookies) sorts cookies into order of decreasing path length, as specified in RFC6265.
|
||||||
|
*/
|
||||||
|
public static final ClientCookieEncoder STRICT = new ClientCookieEncoder(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lax instance that doesn't validate name and value, and (for methods that accept multiple cookies) keeps
|
||||||
|
* cookies in the order in which they were given.
|
||||||
|
*/
|
||||||
|
public static final ClientCookieEncoder LAX = new ClientCookieEncoder(false);
|
||||||
|
|
||||||
|
private ClientCookieEncoder(boolean strict) {
|
||||||
|
super(strict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookie into a Cookie header value.
|
||||||
|
*
|
||||||
|
* @param name
|
||||||
|
* the cookie name
|
||||||
|
* @param value
|
||||||
|
* the cookie value
|
||||||
|
* @return a Rfc6265 style Cookie header value
|
||||||
|
*/
|
||||||
|
public String encode(String name, String value) {
|
||||||
|
return encode(new DefaultCookie(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookie into a Cookie header value.
|
||||||
|
*
|
||||||
|
* @param cookie the specified cookie
|
||||||
|
* @return a Rfc6265 style Cookie header value
|
||||||
|
*/
|
||||||
|
public String encode(Cookie cookie) {
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
encode(buf, Objects.requireNonNull(cookie, "cookie"));
|
||||||
|
return CookieUtil.stripTrailingSeparator(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort cookies into decreasing order of path length, breaking ties by sorting into increasing chronological
|
||||||
|
* order of creation time, as recommended by RFC 6265.
|
||||||
|
*/
|
||||||
|
private static final Comparator<Cookie> COOKIE_COMPARATOR = (c1, c2) -> {
|
||||||
|
String path1 = c1.path();
|
||||||
|
String path2 = c2.path();
|
||||||
|
// Cookies with unspecified path default to the path of the request. We don't
|
||||||
|
// know the request path here, but we assume that the length of an unspecified
|
||||||
|
// path is longer than any specified path (i.e. pathless cookies come first),
|
||||||
|
// because setting cookies with a path longer than the request path is of
|
||||||
|
// limited use.
|
||||||
|
int len1 = path1 == null ? Integer.MAX_VALUE : path1.length();
|
||||||
|
int len2 = path2 == null ? Integer.MAX_VALUE : path2.length();
|
||||||
|
int diff = len2 - len1;
|
||||||
|
if (diff != 0) {
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
// Rely on Java's sort stability to retain creation order in cases where
|
||||||
|
// cookies have same path length.
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookies into a single Cookie header value.
|
||||||
|
*
|
||||||
|
* @param cookies
|
||||||
|
* some cookies
|
||||||
|
* @return a Rfc6265 style Cookie header value, null if no cookies are passed.
|
||||||
|
*/
|
||||||
|
public String encode(Cookie[] cookies) {
|
||||||
|
if (Objects.requireNonNull(cookies, "cookies").length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
if (strict) {
|
||||||
|
if (cookies.length == 1) {
|
||||||
|
encode(buf, cookies[0]);
|
||||||
|
} else {
|
||||||
|
Cookie[] cookiesSorted = Arrays.copyOf(cookies, cookies.length);
|
||||||
|
Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
|
||||||
|
for (Cookie c : cookiesSorted) {
|
||||||
|
encode(buf, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Cookie c : cookies) {
|
||||||
|
encode(buf, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CookieUtil.stripTrailingSeparatorOrNull(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookies into a single Cookie header value.
|
||||||
|
*
|
||||||
|
* @param cookies
|
||||||
|
* some cookies
|
||||||
|
* @return a Rfc6265 style Cookie header value, null if no cookies are passed.
|
||||||
|
*/
|
||||||
|
public String encode(Collection<? extends Cookie> cookies) {
|
||||||
|
if (Objects.requireNonNull(cookies, "cookies").isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
if (strict) {
|
||||||
|
if (cookies.size() == 1) {
|
||||||
|
encode(buf, cookies.iterator().next());
|
||||||
|
} else {
|
||||||
|
Cookie[] cookiesSorted = cookies.toArray(new Cookie[cookies.size()]);
|
||||||
|
Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
|
||||||
|
for (Cookie c : cookiesSorted) {
|
||||||
|
encode(buf, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (Cookie c : cookies) {
|
||||||
|
encode(buf, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CookieUtil.stripTrailingSeparatorOrNull(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookies into a single Cookie header value.
|
||||||
|
*
|
||||||
|
* @param cookies some cookies
|
||||||
|
* @return a Rfc6265 style Cookie header value, null if no cookies are passed.
|
||||||
|
*/
|
||||||
|
public String encode(Iterable<? extends Cookie> cookies) {
|
||||||
|
Iterator<? extends Cookie> cookiesIt = Objects.requireNonNull(cookies, "cookies").iterator();
|
||||||
|
if (!cookiesIt.hasNext()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
if (strict) {
|
||||||
|
Cookie firstCookie = cookiesIt.next();
|
||||||
|
if (!cookiesIt.hasNext()) {
|
||||||
|
encode(buf, firstCookie);
|
||||||
|
} else {
|
||||||
|
List<Cookie> cookiesList = new ArrayList<>();
|
||||||
|
cookiesList.add(firstCookie);
|
||||||
|
while (cookiesIt.hasNext()) {
|
||||||
|
cookiesList.add(cookiesIt.next());
|
||||||
|
}
|
||||||
|
Cookie[] cookiesSorted = cookiesList.toArray(new Cookie[0]);
|
||||||
|
Arrays.sort(cookiesSorted, COOKIE_COMPARATOR);
|
||||||
|
for (Cookie c : cookiesSorted) {
|
||||||
|
encode(buf, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
while (cookiesIt.hasNext()) {
|
||||||
|
encode(buf, cookiesIt.next());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CookieUtil.stripTrailingSeparatorOrNull(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void encode(StringBuilder buf, Cookie c) {
|
||||||
|
final String name = c.name();
|
||||||
|
final String value = c.value() != null ? c.value() : "";
|
||||||
|
validateCookie(name, value);
|
||||||
|
if (c.wrap()) {
|
||||||
|
CookieUtil.addQuoted(buf, name, value);
|
||||||
|
} else {
|
||||||
|
CookieUtil.add(buf, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package org.xbib.netty.http.client.listener;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface CookieListener {
|
public interface CookieListener {
|
||||||
|
|
|
@ -4,7 +4,6 @@ import io.netty.channel.Channel;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
|
||||||
import io.netty.handler.ssl.SslHandler;
|
import io.netty.handler.ssl.SslHandler;
|
||||||
import org.xbib.net.PercentDecoder;
|
import org.xbib.net.PercentDecoder;
|
||||||
import org.xbib.net.URL;
|
import org.xbib.net.URL;
|
||||||
|
@ -13,6 +12,8 @@ import org.xbib.netty.http.client.Client;
|
||||||
import org.xbib.netty.http.common.HttpAddress;
|
import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.client.Request;
|
import org.xbib.netty.http.client.Request;
|
||||||
import org.xbib.netty.http.client.retry.BackOff;
|
import org.xbib.netty.http.client.retry.BackOff;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieBox;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -21,7 +22,6 @@ import java.nio.charset.MalformedInputException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.charset.UnmappableCharacterException;
|
import java.nio.charset.UnmappableCharacterException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -55,7 +55,7 @@ abstract class BaseTransport implements Transport {
|
||||||
|
|
||||||
final SortedMap<String, Request> requests;
|
final SortedMap<String, Request> requests;
|
||||||
|
|
||||||
private Map<Cookie, Boolean> cookieBox;
|
private CookieBox cookieBox;
|
||||||
|
|
||||||
BaseTransport(Client client, HttpAddress httpAddress) {
|
BaseTransport(Client client, HttpAddress httpAddress) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
@ -336,17 +336,19 @@ abstract class BaseTransport implements Transport {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCookieBox(Map<Cookie, Boolean> cookieBox) {
|
@Override
|
||||||
|
public void setCookieBox(CookieBox cookieBox) {
|
||||||
this.cookieBox = cookieBox;
|
this.cookieBox = cookieBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Cookie, Boolean> getCookieBox() {
|
@Override
|
||||||
|
public CookieBox getCookieBox() {
|
||||||
return cookieBox;
|
return cookieBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addCookie(Cookie cookie) {
|
void addCookie(Cookie cookie) {
|
||||||
if (cookieBox == null) {
|
if (cookieBox == null) {
|
||||||
this.cookieBox = Collections.synchronizedMap(new LRUCache<Cookie, Boolean>(32));
|
this.cookieBox = new CookieBox(32);
|
||||||
}
|
}
|
||||||
cookieBox.put(cookie, true);
|
cookieBox.put(cookie, true);
|
||||||
}
|
}
|
||||||
|
@ -376,18 +378,4 @@ abstract class BaseTransport implements Transport {
|
||||||
return (secureScheme && cookie.isSecure()) || (!secureScheme && !cookie.isSecure());
|
return (secureScheme && cookie.isSecure()) || (!secureScheme && !cookie.isSecure());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
|
||||||
static class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
|
||||||
|
|
||||||
private final int cacheSize;
|
|
||||||
|
|
||||||
LRUCache(int cacheSize) {
|
|
||||||
super(16, 0.75f, true);
|
|
||||||
this.cacheSize = cacheSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
|
||||||
return size() >= cacheSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,6 @@ import io.netty.channel.ChannelPipeline;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.HttpObjectAggregator;
|
import io.netty.handler.codec.http.HttpObjectAggregator;
|
||||||
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
|
|
||||||
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
|
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
|
||||||
import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
|
import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
|
||||||
import io.netty.handler.codec.http2.DefaultHttp2Headers;
|
import io.netty.handler.codec.http2.DefaultHttp2Headers;
|
||||||
import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
|
import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
|
||||||
|
@ -20,6 +17,8 @@ import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||||
import io.netty.util.AsciiString;
|
import io.netty.util.AsciiString;
|
||||||
import org.xbib.net.URLSyntaxException;
|
import org.xbib.net.URLSyntaxException;
|
||||||
import org.xbib.netty.http.client.Client;
|
import org.xbib.netty.http.client.Client;
|
||||||
|
import org.xbib.netty.http.client.cookie.ClientCookieDecoder;
|
||||||
|
import org.xbib.netty.http.client.cookie.ClientCookieEncoder;
|
||||||
import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler;
|
import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler;
|
||||||
import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec;
|
import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec;
|
||||||
import org.xbib.netty.http.client.listener.CookieListener;
|
import org.xbib.netty.http.client.listener.CookieListener;
|
||||||
|
@ -27,6 +26,7 @@ import org.xbib.netty.http.client.listener.StatusListener;
|
||||||
import org.xbib.netty.http.common.HttpAddress;
|
import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.client.Request;
|
import org.xbib.netty.http.client.Request;
|
||||||
import org.xbib.netty.http.client.listener.ResponseListener;
|
import org.xbib.netty.http.client.listener.ResponseListener;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
|
@ -5,19 +5,19 @@ import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
|
|
||||||
import io.netty.handler.codec.http.cookie.ClientCookieEncoder;
|
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
|
||||||
import io.netty.handler.codec.http2.Http2Headers;
|
import io.netty.handler.codec.http2.Http2Headers;
|
||||||
import io.netty.handler.codec.http2.Http2Settings;
|
import io.netty.handler.codec.http2.Http2Settings;
|
||||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||||
import org.xbib.net.URLSyntaxException;
|
import org.xbib.net.URLSyntaxException;
|
||||||
import org.xbib.netty.http.client.Client;
|
import org.xbib.netty.http.client.Client;
|
||||||
|
import org.xbib.netty.http.client.cookie.ClientCookieDecoder;
|
||||||
|
import org.xbib.netty.http.client.cookie.ClientCookieEncoder;
|
||||||
import org.xbib.netty.http.client.listener.CookieListener;
|
import org.xbib.netty.http.client.listener.CookieListener;
|
||||||
import org.xbib.netty.http.client.listener.StatusListener;
|
import org.xbib.netty.http.client.listener.StatusListener;
|
||||||
import org.xbib.netty.http.common.HttpAddress;
|
import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.client.Request;
|
import org.xbib.netty.http.client.Request;
|
||||||
import org.xbib.netty.http.client.listener.ResponseListener;
|
import org.xbib.netty.http.client.listener.ResponseListener;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
|
@ -2,15 +2,14 @@ package org.xbib.netty.http.client.transport;
|
||||||
|
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http.cookie.Cookie;
|
|
||||||
import io.netty.handler.codec.http2.Http2Headers;
|
import io.netty.handler.codec.http2.Http2Headers;
|
||||||
import io.netty.handler.codec.http2.Http2Settings;
|
import io.netty.handler.codec.http2.Http2Settings;
|
||||||
import io.netty.util.AttributeKey;
|
import io.netty.util.AttributeKey;
|
||||||
import org.xbib.netty.http.client.Request;
|
import org.xbib.netty.http.client.Request;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieBox;
|
||||||
|
|
||||||
import javax.net.ssl.SSLSession;
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
@ -31,9 +30,9 @@ public interface Transport {
|
||||||
|
|
||||||
void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers);
|
void pushPromiseReceived(Channel channel, Integer streamId, Integer promisedStreamId, Http2Headers headers);
|
||||||
|
|
||||||
void setCookieBox(Map<Cookie, Boolean> cookieBox);
|
void setCookieBox(CookieBox cookieBox);
|
||||||
|
|
||||||
Map<Cookie, Boolean> getCookieBox();
|
CookieBox getCookieBox();
|
||||||
|
|
||||||
Transport get();
|
Transport get();
|
||||||
|
|
||||||
|
|
|
@ -108,10 +108,7 @@ class PoolTest {
|
||||||
long avgConnCountPerNode = connCountSum / 2;
|
long avgConnCountPerNode = connCountSum / 2;
|
||||||
for (HttpAddress nodeAddr: nodeFreq.keySet()) {
|
for (HttpAddress nodeAddr: nodeFreq.keySet()) {
|
||||||
assertTrue(nodeFreq.get(nodeAddr).sum() > 0);
|
assertTrue(nodeFreq.get(nodeAddr).sum() > 0);
|
||||||
assertEquals(/*"Node count: " + nodeCount + ", node: " + nodeAddr
|
assertEquals(avgConnCountPerNode, nodeFreq.get(nodeAddr).sum(), 1.5 * avgConnCountPerNode);
|
||||||
+ ", expected connection count: " + avgConnCountPerNode + ", actual: "
|
|
||||||
+ nodeFreq.get(nodeAddr).sum(),*/
|
|
||||||
avgConnCountPerNode, nodeFreq.get(nodeAddr).sum(), 1.5 * avgConnCountPerNode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface defining an
|
||||||
|
* <a href="http://en.wikipedia.org/wiki/HTTP_cookie">HTTP cookie</a>.
|
||||||
|
*/
|
||||||
|
public interface Cookie extends Comparable<Cookie> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @return The name of this {@link Cookie}
|
||||||
|
*/
|
||||||
|
String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @return The value of this {@link Cookie}
|
||||||
|
*/
|
||||||
|
String value();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @param value The value to set
|
||||||
|
*/
|
||||||
|
void setValue(String value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the raw value of this {@link Cookie},
|
||||||
|
* was wrapped with double quotes in original Set-Cookie header.
|
||||||
|
*
|
||||||
|
* @return If the value of this {@link Cookie} is to be wrapped
|
||||||
|
*/
|
||||||
|
boolean wrap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets true if the value of this {@link Cookie}
|
||||||
|
* is to be wrapped with double quotes.
|
||||||
|
*
|
||||||
|
* @param wrap true if wrap
|
||||||
|
*/
|
||||||
|
void setWrap(boolean wrap);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the domain of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @return The domain of this {@link Cookie}
|
||||||
|
*/
|
||||||
|
String domain();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the domain of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @param domain The domain to use
|
||||||
|
*/
|
||||||
|
void setDomain(String domain);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the path of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @return The {@link Cookie}'s path
|
||||||
|
*/
|
||||||
|
String path();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the path of this {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @param path The path to use for this {@link Cookie}
|
||||||
|
*/
|
||||||
|
void setPath(String path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum age of this {@link Cookie} in seconds or {@link Long#MIN_VALUE} if unspecified
|
||||||
|
*
|
||||||
|
* @return The maximum age of this {@link Cookie}
|
||||||
|
*/
|
||||||
|
long maxAge();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the maximum age of this {@link Cookie} in seconds.
|
||||||
|
* If an age of {@code 0} is specified, this {@link Cookie} will be
|
||||||
|
* automatically removed by browser because it will expire immediately.
|
||||||
|
* If {@link Long#MIN_VALUE} is specified, this {@link Cookie} will be removed when the
|
||||||
|
* browser is closed.
|
||||||
|
*
|
||||||
|
* @param maxAge The maximum age of this {@link Cookie} in seconds
|
||||||
|
*/
|
||||||
|
void setMaxAge(long maxAge);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if this {@link Cookie} is secure
|
||||||
|
*
|
||||||
|
* @return True if this {@link Cookie} is secure, otherwise false
|
||||||
|
*/
|
||||||
|
boolean isSecure();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the security getStatus of this {@link Cookie}
|
||||||
|
*
|
||||||
|
* @param secure True if this {@link Cookie} is to be secure, otherwise false
|
||||||
|
*/
|
||||||
|
void setSecure(boolean secure);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks to see if this {@link Cookie} can only be accessed via HTTP.
|
||||||
|
* If this returns true, the {@link Cookie} cannot be accessed through
|
||||||
|
* client side script - But only if the browser supports it.
|
||||||
|
* For more information, please look <a href="http://www.owasp.org/index.php/HTTPOnly">here</a>
|
||||||
|
*
|
||||||
|
* @return True if this {@link Cookie} is HTTP-only or false if it isn't
|
||||||
|
*/
|
||||||
|
boolean isHttpOnly();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if this {@link Cookie} is HTTP only.
|
||||||
|
* If set to true, this {@link Cookie} cannot be accessed by a client
|
||||||
|
* side script. However, this works only if the browser supports it.
|
||||||
|
* For for information, please look
|
||||||
|
* <a href="http://www.owasp.org/index.php/HTTPOnly">here</a>.
|
||||||
|
*
|
||||||
|
* @param httpOnly True if the {@link Cookie} is HTTP only, otherwise false.
|
||||||
|
*/
|
||||||
|
void setHttpOnly(boolean httpOnly);
|
||||||
|
|
||||||
|
String sameSite();
|
||||||
|
|
||||||
|
void setSameSite(String sameSite);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
import org.xbib.netty.http.common.util.LRUCache;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class CookieBox extends LRUCache<Cookie, Boolean> {
|
||||||
|
|
||||||
|
public CookieBox(int cacheSize) {
|
||||||
|
super(cacheSize);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent of Client and Server side cookie decoders.
|
||||||
|
*/
|
||||||
|
public abstract class CookieDecoder {
|
||||||
|
|
||||||
|
protected final boolean strict;
|
||||||
|
|
||||||
|
protected CookieDecoder(boolean strict) {
|
||||||
|
this.strict = strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected DefaultCookie initCookie(String header, int nameBegin, int nameEnd, int valueBegin, int valueEnd) {
|
||||||
|
if (nameBegin == -1 || nameBegin == nameEnd) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (valueBegin == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
CharSequence wrappedValue = CharBuffer.wrap(header, valueBegin, valueEnd);
|
||||||
|
CharSequence unwrappedValue = CookieUtil.unwrapValue(wrappedValue);
|
||||||
|
if (unwrappedValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String name = header.substring(nameBegin, nameEnd);
|
||||||
|
if (strict && CookieUtil.firstInvalidCookieNameOctet(name) >= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final boolean wrap = unwrappedValue.length() != valueEnd - valueBegin;
|
||||||
|
if (strict && CookieUtil.firstInvalidCookieValueOctet(unwrappedValue) >= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
DefaultCookie cookie = new DefaultCookie(name, unwrappedValue.toString());
|
||||||
|
cookie.setWrap(wrap);
|
||||||
|
return cookie;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent of Client and Server side cookie encoders
|
||||||
|
*/
|
||||||
|
public abstract class CookieEncoder {
|
||||||
|
|
||||||
|
protected final boolean strict;
|
||||||
|
|
||||||
|
protected CookieEncoder(boolean strict) {
|
||||||
|
this.strict = strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void validateCookie(String name, String value) {
|
||||||
|
if (strict) {
|
||||||
|
int pos;
|
||||||
|
if ((pos = CookieUtil.firstInvalidCookieNameOctet(name)) >= 0) {
|
||||||
|
throw new IllegalArgumentException("Cookie name contains an invalid char: " + (name.charAt(pos)));
|
||||||
|
}
|
||||||
|
CharSequence unwrappedValue = CookieUtil.unwrapValue(value);
|
||||||
|
if (unwrappedValue == null) {
|
||||||
|
throw new IllegalArgumentException("Cookie value wrapping quotes are not balanced: " + value);
|
||||||
|
}
|
||||||
|
if ((pos = CookieUtil.firstInvalidCookieValueOctet(unwrappedValue)) >= 0) {
|
||||||
|
throw new IllegalArgumentException("Cookie value contains an invalid char: " + (value.charAt(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
public interface CookieHeaderNames {
|
||||||
|
|
||||||
|
String PATH = "Path";
|
||||||
|
|
||||||
|
String EXPIRES = "Expires";
|
||||||
|
|
||||||
|
String MAX_AGE = "Max-Age";
|
||||||
|
|
||||||
|
String DOMAIN = "Domain";
|
||||||
|
|
||||||
|
String SECURE = "Secure";
|
||||||
|
|
||||||
|
String HTTPONLY = "HTTPOnly";
|
||||||
|
|
||||||
|
String SAMESITE = "SameSite";
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
import java.util.BitSet;
|
||||||
|
|
||||||
|
public final class CookieUtil {
|
||||||
|
|
||||||
|
private static final BitSet VALID_COOKIE_NAME_OCTETS;
|
||||||
|
|
||||||
|
private static final BitSet VALID_COOKIE_VALUE_OCTETS;
|
||||||
|
|
||||||
|
private static final BitSet VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
int[] separators = new int[] {
|
||||||
|
'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' ', '\t'
|
||||||
|
};
|
||||||
|
VALID_COOKIE_NAME_OCTETS = validCookieNameOctets(separators);
|
||||||
|
VALID_COOKIE_VALUE_OCTETS = validCookieValueOctets();
|
||||||
|
VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS = validCookieAttributeValueOctets();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal space
|
||||||
|
*/
|
||||||
|
public static final char SP = 32;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Equals '='
|
||||||
|
*/
|
||||||
|
public static final char EQUALS = 61;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semicolon ';'
|
||||||
|
*/
|
||||||
|
public static final char SEMICOLON = 59;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Double quote '"'
|
||||||
|
*/
|
||||||
|
public static final char DOUBLE_QUOTE = 34;
|
||||||
|
|
||||||
|
private CookieUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BitSet validCookieNameOctets(int[] separators) {
|
||||||
|
BitSet bits = new BitSet();
|
||||||
|
for (int i = 32; i < 127; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
for (int separator : separators) {
|
||||||
|
bits.set(separator, false);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
|
||||||
|
// US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon, and backslash
|
||||||
|
private static BitSet validCookieValueOctets() {
|
||||||
|
BitSet bits = new BitSet();
|
||||||
|
bits.set(0x21);
|
||||||
|
for (int i = 0x23; i <= 0x2B; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
for (int i = 0x2D; i <= 0x3A; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
for (int i = 0x3C; i <= 0x5B; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
for (int i = 0x5D; i <= 0x7E; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// path-value = <any CHAR except CTLs or ";">
|
||||||
|
private static BitSet validCookieAttributeValueOctets() {
|
||||||
|
BitSet bits = new BitSet();
|
||||||
|
for (int i = 32; i < 127; i++) {
|
||||||
|
bits.set(i);
|
||||||
|
}
|
||||||
|
// ';' = 59
|
||||||
|
bits.set(59, false);
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param buf a buffer where some cookies were maybe encoded
|
||||||
|
* @return the buffer String without the trailing separator, or null if no cookie was appended.
|
||||||
|
*/
|
||||||
|
public static String stripTrailingSeparatorOrNull(StringBuilder buf) {
|
||||||
|
return buf.length() == 0 ? null : stripTrailingSeparator(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String stripTrailingSeparator(StringBuilder buf) {
|
||||||
|
if (buf.length() > 0) {
|
||||||
|
buf.setLength(buf.length() - 2);
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void add(StringBuilder sb, String name, long val) {
|
||||||
|
sb.append(name)
|
||||||
|
.append(EQUALS)
|
||||||
|
.append(val)
|
||||||
|
.append(SEMICOLON)
|
||||||
|
.append(SP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void add(StringBuilder sb, String name, String val) {
|
||||||
|
sb.append(name)
|
||||||
|
.append(EQUALS)
|
||||||
|
.append(val)
|
||||||
|
.append(SEMICOLON)
|
||||||
|
.append(SP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void add(StringBuilder sb, String name) {
|
||||||
|
sb.append(name)
|
||||||
|
.append(SEMICOLON)
|
||||||
|
.append(SP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void addQuoted(StringBuilder sb, String name, String val) {
|
||||||
|
if (val == null) {
|
||||||
|
val = "";
|
||||||
|
}
|
||||||
|
sb.append(name)
|
||||||
|
.append(EQUALS)
|
||||||
|
.append(DOUBLE_QUOTE)
|
||||||
|
.append(val)
|
||||||
|
.append(DOUBLE_QUOTE)
|
||||||
|
.append(SEMICOLON)
|
||||||
|
.append(SP);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int firstInvalidCookieNameOctet(CharSequence cs) {
|
||||||
|
return firstInvalidOctet(cs, VALID_COOKIE_NAME_OCTETS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int firstInvalidCookieValueOctet(CharSequence cs) {
|
||||||
|
return firstInvalidOctet(cs, VALID_COOKIE_VALUE_OCTETS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int firstInvalidOctet(CharSequence cs, BitSet bits) {
|
||||||
|
for (int i = 0; i < cs.length(); i++) {
|
||||||
|
char c = cs.charAt(i);
|
||||||
|
if (!bits.get(c)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CharSequence unwrapValue(CharSequence cs) {
|
||||||
|
final int len = cs.length();
|
||||||
|
if (len > 0 && cs.charAt(0) == ('"')) {
|
||||||
|
if (len >= 2 && cs.charAt(len - 1) == ('"')) {
|
||||||
|
return len == 2 ? "" : cs.subSequence(1, len - 1);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String validateAttributeValue(String name, String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
value = value.trim();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int i = firstInvalidOctet(value, VALID_COOKIE_ATTRIBUTE_VALUE_OCTETS);
|
||||||
|
if (i != -1) {
|
||||||
|
throw new IllegalArgumentException(name + " contains the prohibited characters: " + (value.charAt(i)));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package org.xbib.netty.http.common.cookie;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default {@link Cookie} implementation.
|
||||||
|
*/
|
||||||
|
public class DefaultCookie implements Cookie {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
private boolean wrap;
|
||||||
|
|
||||||
|
private String domain;
|
||||||
|
|
||||||
|
private String path;
|
||||||
|
|
||||||
|
private long maxAge = Long.MIN_VALUE;
|
||||||
|
|
||||||
|
private boolean secure;
|
||||||
|
|
||||||
|
private boolean httpOnly;
|
||||||
|
|
||||||
|
private String sameSite;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new cookie with the specified name and value.
|
||||||
|
* @param name name
|
||||||
|
* @param value value
|
||||||
|
*/
|
||||||
|
public DefaultCookie(String name, String value) {
|
||||||
|
this.name = Objects.requireNonNull(name, "name").trim();
|
||||||
|
if (this.name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("empty name");
|
||||||
|
}
|
||||||
|
setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String name() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = Objects.requireNonNull(value, "value");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean wrap() {
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setWrap(boolean wrap) {
|
||||||
|
this.wrap = wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String domain() {
|
||||||
|
return domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDomain(String domain) {
|
||||||
|
this.domain = CookieUtil.validateAttributeValue("domain", domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String path() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPath(String path) {
|
||||||
|
this.path = CookieUtil.validateAttributeValue("path", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long maxAge() {
|
||||||
|
return maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setMaxAge(long maxAge) {
|
||||||
|
this.maxAge = maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSecure() {
|
||||||
|
return secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSecure(boolean secure) {
|
||||||
|
this.secure = secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isHttpOnly() {
|
||||||
|
return httpOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHttpOnly(boolean httpOnly) {
|
||||||
|
this.httpOnly = httpOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSameSite(String sameSite) {
|
||||||
|
this.sameSite = sameSite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String sameSite() {
|
||||||
|
return sameSite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return name().hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof Cookie)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Cookie that = (Cookie) o;
|
||||||
|
if (!name().equals(that.name())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (path() == null) {
|
||||||
|
if (that.path() != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (that.path() == null) {
|
||||||
|
return false;
|
||||||
|
} else if (!path().equals(that.path())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (domain() == null) {
|
||||||
|
if (that.domain() != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return domain().equalsIgnoreCase(that.domain());
|
||||||
|
}
|
||||||
|
if (sameSite() == null) {
|
||||||
|
return that.sameSite() == null;
|
||||||
|
} else if (that.sameSite() == null) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return sameSite().equalsIgnoreCase(that.sameSite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Cookie c) {
|
||||||
|
int v = name().compareTo(c.name());
|
||||||
|
if (v != 0) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
if (path() == null) {
|
||||||
|
if (c.path() != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (c.path() == null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
v = path().compareTo(c.path());
|
||||||
|
if (v != 0) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (domain() == null) {
|
||||||
|
if (c.domain() != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else if (c.domain() == null) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
v = domain().compareToIgnoreCase(c.domain());
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder buf = new StringBuilder()
|
||||||
|
.append(name()).append('=').append(value());
|
||||||
|
if (domain() != null) {
|
||||||
|
buf.append(", domain=").append(domain());
|
||||||
|
}
|
||||||
|
if (path() != null) {
|
||||||
|
buf.append(", path=").append(path());
|
||||||
|
}
|
||||||
|
if (maxAge() >= 0) {
|
||||||
|
buf.append(", maxAge=").append(maxAge()).append('s');
|
||||||
|
}
|
||||||
|
if (isSecure()) {
|
||||||
|
buf.append(", secure");
|
||||||
|
}
|
||||||
|
if (isHttpOnly()) {
|
||||||
|
buf.append(", HTTPOnly");
|
||||||
|
}
|
||||||
|
if (sameSite() != null) {
|
||||||
|
buf.append(", SameSite=").append(sameSite());
|
||||||
|
}
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.xbib.netty.http.common.util;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||||||
|
|
||||||
|
private final int cacheSize;
|
||||||
|
|
||||||
|
public LRUCache(int cacheSize) {
|
||||||
|
super(16, 0.75f, true);
|
||||||
|
this.cacheSize = cacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||||||
|
return size() >= cacheSize;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.xbib.netty.http.common.util;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
|
|
||||||
|
public class TimeUtils {
|
||||||
|
|
||||||
|
public static String formatInstant(Instant instant) {
|
||||||
|
return DateTimeFormatter.RFC_1123_DATE_TIME
|
||||||
|
.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatMillis(long millis) {
|
||||||
|
return formatInstant(Instant.ofEpochMilli(millis));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatSeconds(long seconds) {
|
||||||
|
return formatInstant(Instant.now().plusSeconds(seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz";
|
||||||
|
|
||||||
|
private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy";
|
||||||
|
|
||||||
|
private static final DateTimeFormatter[] dateTimeFormatters = {
|
||||||
|
DateTimeFormatter.RFC_1123_DATE_TIME,
|
||||||
|
DateTimeFormatter.ofPattern(RFC1036_PATTERN),
|
||||||
|
DateTimeFormatter.ofPattern(ASCIITIME_PATTERN)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Instant parseDate(String date) {
|
||||||
|
if (date == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int semicolonIndex = date.indexOf(';');
|
||||||
|
String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date;
|
||||||
|
// RFC 2616 allows RFC 1123, RFC 1036, ASCII time
|
||||||
|
for (DateTimeFormatter formatter : dateTimeFormatters) {
|
||||||
|
try {
|
||||||
|
return Instant.from(formatter.withZone(ZoneId.of("UTC")).parse(trimmedDate));
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
// skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,9 +24,9 @@ import org.xbib.netty.http.server.endpoint.NamedServer;
|
||||||
import org.xbib.netty.http.server.handler.http.HttpChannelInitializer;
|
import org.xbib.netty.http.server.handler.http.HttpChannelInitializer;
|
||||||
import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer;
|
import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer;
|
||||||
import org.xbib.netty.http.common.SecurityUtil;
|
import org.xbib.netty.http.common.SecurityUtil;
|
||||||
import org.xbib.netty.http.server.transport.HttpServerTransport;
|
import org.xbib.netty.http.server.transport.HttpTransport;
|
||||||
import org.xbib.netty.http.server.transport.Http2ServerTransport;
|
import org.xbib.netty.http.server.transport.Http2Transport;
|
||||||
import org.xbib.netty.http.server.transport.ServerTransport;
|
import org.xbib.netty.http.server.transport.Transport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -181,8 +181,8 @@ public final class Server {
|
||||||
logger.log(level, NetworkUtils::displayNetworkInterfaces);
|
logger.log(level, NetworkUtils::displayNetworkInterfaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerTransport newTransport(HttpVersion httpVersion) {
|
public Transport newTransport(HttpVersion httpVersion) {
|
||||||
return httpVersion.majorVersion() == 1 ? new HttpServerTransport(this) : new Http2ServerTransport(this);
|
return httpVersion.majorVersion() == 1 ? new HttpTransport(this) : new Http2Transport(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void shutdownGracefully() throws IOException {
|
public synchronized void shutdownGracefully() throws IOException {
|
||||||
|
|
|
@ -2,9 +2,11 @@ package org.xbib.netty.http.server;
|
||||||
|
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
import org.xbib.net.URL;
|
import org.xbib.net.URL;
|
||||||
import org.xbib.netty.http.common.HttpParameters;
|
import org.xbib.netty.http.common.HttpParameters;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -43,6 +45,8 @@ public interface ServerRequest {
|
||||||
|
|
||||||
Integer requestId();
|
Integer requestId();
|
||||||
|
|
||||||
|
SSLSession getSession();
|
||||||
|
|
||||||
class EndpointInfo implements Comparable<EndpointInfo> {
|
class EndpointInfo implements Comparable<EndpointInfo> {
|
||||||
|
|
||||||
private final String path;
|
private final String path;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import io.netty.buffer.ByteBufUtil;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.stream.ChunkedInput;
|
import io.netty.handler.stream.ChunkedInput;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
|
||||||
import java.nio.CharBuffer;
|
import java.nio.CharBuffer;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
@ -15,20 +16,20 @@ import java.nio.charset.StandardCharsets;
|
||||||
*/
|
*/
|
||||||
public interface ServerResponse {
|
public interface ServerResponse {
|
||||||
|
|
||||||
void setHeader(CharSequence name, String value);
|
|
||||||
|
|
||||||
CharSequence getHeader(CharSequence name);
|
|
||||||
|
|
||||||
ChannelHandlerContext getChannelHandlerContext();
|
ChannelHandlerContext getChannelHandlerContext();
|
||||||
|
|
||||||
HttpResponseStatus getStatus();
|
HttpResponseStatus getStatus();
|
||||||
|
|
||||||
ServerResponse withStatus(HttpResponseStatus httpResponseStatus);
|
ServerResponse withStatus(HttpResponseStatus httpResponseStatus);
|
||||||
|
|
||||||
|
ServerResponse withHeader(CharSequence name, String value);
|
||||||
|
|
||||||
ServerResponse withContentType(String contentType);
|
ServerResponse withContentType(String contentType);
|
||||||
|
|
||||||
ServerResponse withCharset(Charset charset);
|
ServerResponse withCharset(Charset charset);
|
||||||
|
|
||||||
|
ServerResponse withCookie(Cookie cookie);
|
||||||
|
|
||||||
void write(ByteBuf byteBuf);
|
void write(ByteBuf byteBuf);
|
||||||
|
|
||||||
void write(ChunkedInput<ByteBuf> chunkedInput);
|
void write(ChunkedInput<ByteBuf> chunkedInput);
|
||||||
|
@ -48,14 +49,14 @@ public interface ServerResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void write(ServerResponse serverResponse, String text) {
|
static void write(ServerResponse serverResponse, String text) {
|
||||||
write(serverResponse, HttpResponseStatus.OK, "text/plain; charset=utf-8", text);
|
write(serverResponse, HttpResponseStatus.OK, "text/plain", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType, String text) {
|
static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType, String text) {
|
||||||
serverResponse.withStatus(status)
|
serverResponse.withStatus(status)
|
||||||
.withContentType(contentType)
|
.withContentType(contentType)
|
||||||
.withCharset(StandardCharsets.UTF_8).
|
.withCharset(StandardCharsets.UTF_8)
|
||||||
write(ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text));
|
.write(ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void write(ServerResponse serverResponse,
|
static void write(ServerResponse serverResponse,
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
package org.xbib.netty.http.server.cookie;
|
||||||
|
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieDecoder;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieHeaderNames;
|
||||||
|
import org.xbib.netty.http.common.cookie.DefaultCookie;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie decoder to be used server side.
|
||||||
|
*
|
||||||
|
* Only name and value fields are expected, so old fields are not populated (path, domain, etc).
|
||||||
|
*
|
||||||
|
* Old <a href="http://tools.ietf.org/html/rfc2965">RFC2965</a> cookies are still supported,
|
||||||
|
* old fields will simply be ignored.
|
||||||
|
*/
|
||||||
|
public final class ServerCookieDecoder extends CookieDecoder {
|
||||||
|
|
||||||
|
private static final String RFC2965_VERSION = "\\$Version";
|
||||||
|
|
||||||
|
private static final String RFC2965_PATH = "\\$" + CookieHeaderNames.PATH;
|
||||||
|
|
||||||
|
private static final String RFC2965_DOMAIN = "\\$" + CookieHeaderNames.DOMAIN;
|
||||||
|
|
||||||
|
private static final String RFC2965_PORT = "\\$Port";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict encoder that validates that name and value chars are in the valid scope
|
||||||
|
* defined in RFC6265
|
||||||
|
*/
|
||||||
|
public static final ServerCookieDecoder STRICT = new ServerCookieDecoder(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lax instance that doesn't validate name and value
|
||||||
|
*/
|
||||||
|
public static final ServerCookieDecoder LAX = new ServerCookieDecoder(false);
|
||||||
|
|
||||||
|
private ServerCookieDecoder(boolean strict) {
|
||||||
|
super(strict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}.
|
||||||
|
*
|
||||||
|
* @param header header
|
||||||
|
* @return the decoded {@link Cookie}
|
||||||
|
*/
|
||||||
|
public Set<Cookie> decode(String header) {
|
||||||
|
final int headerLen = Objects.requireNonNull(header, "header").length();
|
||||||
|
if (headerLen == 0) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
Set<Cookie> cookies = new TreeSet<Cookie>();
|
||||||
|
int i = 0;
|
||||||
|
boolean rfc2965Style = false;
|
||||||
|
if (header.regionMatches(true, 0, RFC2965_VERSION, 0, RFC2965_VERSION.length())) {
|
||||||
|
// RFC 2965 style cookie, move to after version value
|
||||||
|
i = header.indexOf(';') + 1;
|
||||||
|
rfc2965Style = true;
|
||||||
|
}
|
||||||
|
while (i < headerLen) {
|
||||||
|
// Skip spaces and separators.
|
||||||
|
while (i < headerLen) {
|
||||||
|
char c = header.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '\t':
|
||||||
|
case '\n':
|
||||||
|
case 0x0b:
|
||||||
|
case '\f':
|
||||||
|
case '\r':
|
||||||
|
case ' ':
|
||||||
|
case ',':
|
||||||
|
case ';':
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
int nameBegin = i;
|
||||||
|
int nameEnd = 0;
|
||||||
|
int valueBegin = 0;
|
||||||
|
int valueEnd = 0;
|
||||||
|
while (i < headerLen) {
|
||||||
|
char curChar = header.charAt(i);
|
||||||
|
if (curChar == ';') {
|
||||||
|
// NAME; (no value till ';')
|
||||||
|
nameEnd = i;
|
||||||
|
valueBegin = valueEnd = -1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
} else if (curChar == '=') {
|
||||||
|
// NAME=VALUE
|
||||||
|
nameEnd = i;
|
||||||
|
i++;
|
||||||
|
if (i == headerLen) {
|
||||||
|
// NAME= (empty value, i.e. nothing after '=')
|
||||||
|
valueBegin = valueEnd = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
valueBegin = i;
|
||||||
|
int semiPos = header.indexOf(';', i);
|
||||||
|
valueEnd = i = semiPos > 0 ? semiPos : headerLen;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (i == headerLen) {
|
||||||
|
// NAME (no value till the end of string)
|
||||||
|
nameEnd = headerLen;
|
||||||
|
valueBegin = valueEnd = -1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rfc2965Style && (header.regionMatches(nameBegin, RFC2965_PATH, 0, RFC2965_PATH.length()) ||
|
||||||
|
header.regionMatches(nameBegin, RFC2965_DOMAIN, 0, RFC2965_DOMAIN.length()) ||
|
||||||
|
header.regionMatches(nameBegin, RFC2965_PORT, 0, RFC2965_PORT.length()))) {
|
||||||
|
// skip obsolete RFC2965 fields
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (nameEnd >= nameBegin && valueEnd >= valueBegin) {
|
||||||
|
DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
|
||||||
|
if (cookie != null) {
|
||||||
|
cookies.add(cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookies;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
package org.xbib.netty.http.server.cookie;
|
||||||
|
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieEncoder;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieHeaderNames;
|
||||||
|
import org.xbib.netty.http.common.cookie.CookieUtil;
|
||||||
|
import org.xbib.netty.http.common.cookie.DefaultCookie;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <a href="http://tools.ietf.org/html/rfc6265">RFC6265</a> compliant cookie encoder to be used server side,
|
||||||
|
* so some fields are sent (Version is typically ignored).
|
||||||
|
*
|
||||||
|
* As Cookie merges Expires and MaxAge into one single field, only Max-Age field is sent.
|
||||||
|
*
|
||||||
|
* Note that multiple cookies are supposed to be sent at once in a single "Set-Cookie" header.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* // Example
|
||||||
|
* HttpRequest req = ...
|
||||||
|
* res.withHeader("Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"))
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @see ServerCookieDecoder
|
||||||
|
*/
|
||||||
|
public final class ServerCookieEncoder extends CookieEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict encoder that validates that name and value chars are in the valid scope
|
||||||
|
* defined in RFC6265, and (for methods that accept multiple cookies) that only
|
||||||
|
* one cookie is encoded with any given name. (If multiple cookies have the same
|
||||||
|
* name, the last one is the one that is encoded.)
|
||||||
|
*/
|
||||||
|
public static final ServerCookieEncoder STRICT = new ServerCookieEncoder(true);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lax instance that doesn't validate name and value, and that allows multiple
|
||||||
|
* cookies with the same name.
|
||||||
|
*/
|
||||||
|
public static final ServerCookieEncoder LAX = new ServerCookieEncoder(false);
|
||||||
|
|
||||||
|
private ServerCookieEncoder(boolean strict) {
|
||||||
|
super(strict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookie name-value pair into a Set-Cookie header value.
|
||||||
|
*
|
||||||
|
* @param name the cookie name
|
||||||
|
* @param value the cookie value
|
||||||
|
* @return a single Set-Cookie header value
|
||||||
|
*/
|
||||||
|
public String encode(String name, String value) {
|
||||||
|
return encode(new DefaultCookie(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the specified cookie into a Set-Cookie header value.
|
||||||
|
*
|
||||||
|
* @param cookie the cookie
|
||||||
|
* @return a single Set-Cookie header value
|
||||||
|
*/
|
||||||
|
public String encode(Cookie cookie) {
|
||||||
|
final String name = Objects.requireNonNull(cookie, "cookie").name();
|
||||||
|
final String value = cookie.value() != null ? cookie.value() : "";
|
||||||
|
validateCookie(name, value);
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
if (cookie.wrap()) {
|
||||||
|
CookieUtil.addQuoted(buf, name, value);
|
||||||
|
} else {
|
||||||
|
CookieUtil.add(buf, name, value);
|
||||||
|
}
|
||||||
|
if (cookie.maxAge() != Long.MIN_VALUE) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge());
|
||||||
|
//Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis())
|
||||||
|
buf.append(CookieHeaderNames.EXPIRES);
|
||||||
|
buf.append(CookieUtil.EQUALS);
|
||||||
|
//DateFormatter.append(expires, buf)
|
||||||
|
buf.append(CookieUtil.SEMICOLON);
|
||||||
|
buf.append(CookieUtil.SP);
|
||||||
|
}
|
||||||
|
if (cookie.path() != null) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.PATH, cookie.path());
|
||||||
|
}
|
||||||
|
if (cookie.domain() != null) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.DOMAIN, cookie.domain());
|
||||||
|
}
|
||||||
|
if (cookie.isSecure()) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.SECURE);
|
||||||
|
}
|
||||||
|
if (cookie.isHttpOnly()) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.HTTPONLY);
|
||||||
|
}
|
||||||
|
if (cookie.sameSite() != null) {
|
||||||
|
CookieUtil.add(buf, CookieHeaderNames.SAMESITE, cookie.sameSite());
|
||||||
|
}
|
||||||
|
return CookieUtil.stripTrailingSeparator(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplicate a list of encoded cookies by keeping only the last instance with a given name.
|
||||||
|
*
|
||||||
|
* @param encoded The list of encoded cookies.
|
||||||
|
* @param nameToLastIndex A map from cookie name to index of last cookie instance.
|
||||||
|
* @return The encoded list with all but the last instance of a named cookie.
|
||||||
|
*/
|
||||||
|
private static List<String> dedup(List<String> encoded, Map<String, Integer> nameToLastIndex) {
|
||||||
|
boolean[] isLastInstance = new boolean[encoded.size()];
|
||||||
|
for (int idx : nameToLastIndex.values()) {
|
||||||
|
isLastInstance[idx] = true;
|
||||||
|
}
|
||||||
|
List<String> dedupd = new ArrayList<>(nameToLastIndex.size());
|
||||||
|
int n = encoded.size();
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
if (isLastInstance[i]) {
|
||||||
|
dedupd.add(encoded.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dedupd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch encodes cookies into Set-Cookie header values.
|
||||||
|
*
|
||||||
|
* @param cookies a bunch of cookies
|
||||||
|
* @return the corresponding bunch of Set-Cookie headers
|
||||||
|
*/
|
||||||
|
public List<String> encode(Cookie... cookies) {
|
||||||
|
if (Objects.requireNonNull(cookies, "cookies").length == 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> encoded = new ArrayList<>(cookies.length);
|
||||||
|
Map<String, Integer> nameToIndex = strict && cookies.length > 1 ? new LinkedHashMap<>() : null;
|
||||||
|
boolean hasDupdName = false;
|
||||||
|
for (int i = 0; i < cookies.length; i++) {
|
||||||
|
Cookie c = cookies[i];
|
||||||
|
encoded.add(encode(c));
|
||||||
|
if (nameToIndex != null) {
|
||||||
|
hasDupdName |= nameToIndex.put(c.name(), i) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch encodes cookies into Set-Cookie header values.
|
||||||
|
*
|
||||||
|
* @param cookies a bunch of cookies
|
||||||
|
* @return the corresponding bunch of Set-Cookie headers
|
||||||
|
*/
|
||||||
|
public List<String> encode(Collection<? extends Cookie> cookies) {
|
||||||
|
if (Objects.requireNonNull(cookies, "cookies").isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> encoded = new ArrayList<>(cookies.size());
|
||||||
|
Map<String, Integer> nameToIndex = strict && cookies.size() > 1 ? new LinkedHashMap<>() : null;
|
||||||
|
int i = 0;
|
||||||
|
boolean hasDupdName = false;
|
||||||
|
for (Cookie c : cookies) {
|
||||||
|
encoded.add(encode(c));
|
||||||
|
if (nameToIndex != null) {
|
||||||
|
hasDupdName |= nameToIndex.put(c.name(), i++) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batch encodes cookies into Set-Cookie header values.
|
||||||
|
*
|
||||||
|
* @param cookies a bunch of cookies
|
||||||
|
* @return the corresponding bunch of Set-Cookie headers
|
||||||
|
*/
|
||||||
|
public List<String> encode(Iterable<? extends Cookie> cookies) {
|
||||||
|
Iterator<? extends Cookie> cookiesIt = Objects.requireNonNull(cookies, "cookies").iterator();
|
||||||
|
if (!cookiesIt.hasNext()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<String> encoded = new ArrayList<>();
|
||||||
|
Cookie firstCookie = cookiesIt.next();
|
||||||
|
Map<String, Integer> nameToIndex = strict && cookiesIt.hasNext() ? new LinkedHashMap<>() : null;
|
||||||
|
int i = 0;
|
||||||
|
encoded.add(encode(firstCookie));
|
||||||
|
boolean hasDupdName = nameToIndex != null && (nameToIndex.put(firstCookie.name(), i++) != null);
|
||||||
|
while (cookiesIt.hasNext()) {
|
||||||
|
Cookie c = cookiesIt.next();
|
||||||
|
encoded.add(encode(c));
|
||||||
|
if (nameToIndex != null) {
|
||||||
|
hasDupdName |= nameToIndex.put(c.name(), i++) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasDupdName ? dedup(encoded, nameToIndex) : encoded;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import io.netty.handler.codec.http.HttpHeaders;
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
import io.netty.handler.codec.http.HttpMethod;
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.stream.ChunkedNioStream;
|
import io.netty.handler.stream.ChunkedNioStream;
|
||||||
|
import org.xbib.netty.http.common.util.TimeUtils;
|
||||||
import org.xbib.netty.http.server.ServerRequest;
|
import org.xbib.netty.http.server.ServerRequest;
|
||||||
import org.xbib.netty.http.server.ServerResponse;
|
import org.xbib.netty.http.server.ServerResponse;
|
||||||
import org.xbib.netty.http.server.util.MimeTypeUtils;
|
import org.xbib.netty.http.server.util.MimeTypeUtils;
|
||||||
|
@ -25,11 +26,6 @@ import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.time.format.DateTimeParseException;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -59,14 +55,14 @@ public abstract class ResourceService implements Service {
|
||||||
long maxAgeSeconds = 24 * 3600;
|
long maxAgeSeconds = 24 * 3600;
|
||||||
long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds;
|
long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds;
|
||||||
if (isCacheResponseEnabled()) {
|
if (isCacheResponseEnabled()) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
serverResponse.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis))
|
||||||
serverResponse.setHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
|
.withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
|
||||||
}
|
}
|
||||||
boolean sent = false;
|
boolean sent = false;
|
||||||
if (isETagResponseEnabled()) {
|
if (isETagResponseEnabled()) {
|
||||||
Instant lastModifiedInstant = resource.getLastModified();
|
Instant lastModifiedInstant = resource.getLastModified();
|
||||||
String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength();
|
String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength();
|
||||||
Instant ifUnmodifiedSinceInstant = parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE));
|
Instant ifUnmodifiedSinceInstant = TimeUtils.parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE));
|
||||||
if (ifUnmodifiedSinceInstant != null &&
|
if (ifUnmodifiedSinceInstant != null &&
|
||||||
ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
||||||
ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED);
|
ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED);
|
||||||
|
@ -79,28 +75,28 @@ public abstract class ResourceService implements Service {
|
||||||
}
|
}
|
||||||
String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH);
|
String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH);
|
||||||
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
|
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis));
|
||||||
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Instant ifModifiedSinceInstant = parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
|
Instant ifModifiedSinceInstant = TimeUtils.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
|
||||||
if (ifModifiedSinceInstant != null &&
|
if (ifModifiedSinceInstant != null &&
|
||||||
ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis));
|
||||||
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||||
serverResponse.setHeader(HttpHeaderNames.LAST_MODIFIED, formatInstant(lastModifiedInstant));
|
.withHeader(HttpHeaderNames.LAST_MODIFIED, TimeUtils.formatInstant(lastModifiedInstant));
|
||||||
if (isRangeResponseEnabled()) {
|
if (isRangeResponseEnabled()) {
|
||||||
performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers);
|
performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers);
|
||||||
sent = true;
|
sent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!sent) {
|
if (!sent) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(resource.getLength()));
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(resource.getLength()));
|
||||||
send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse);
|
send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,20 +106,20 @@ public abstract class ResourceService implements Service {
|
||||||
String contentType, String eTag,
|
String contentType, String eTag,
|
||||||
HttpHeaders headers) {
|
HttpHeaders headers) {
|
||||||
long length = resource.getLength();
|
long length = resource.getLength();
|
||||||
serverResponse.setHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
serverResponse.withHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
||||||
Range full = new Range(0, length - 1, length);
|
Range full = new Range(0, length - 1, length);
|
||||||
List<Range> ranges = new ArrayList<>();
|
List<Range> ranges = new ArrayList<>();
|
||||||
String range = headers.get(HttpHeaderNames.RANGE);
|
String range = headers.get(HttpHeaderNames.RANGE);
|
||||||
if (range != null) {
|
if (range != null) {
|
||||||
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length);
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length);
|
||||||
ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String ifRange = headers.get(HttpHeaderNames.IF_RANGE);
|
String ifRange = headers.get(HttpHeaderNames.IF_RANGE);
|
||||||
if (ifRange != null && !ifRange.equals(eTag)) {
|
if (ifRange != null && !ifRange.equals(eTag)) {
|
||||||
try {
|
try {
|
||||||
Instant ifRangeTime = parseDate(ifRange);
|
Instant ifRangeTime = TimeUtils.parseDate(ifRange);
|
||||||
if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) {
|
if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) {
|
||||||
ranges.add(full);
|
ranges.add(full);
|
||||||
}
|
}
|
||||||
|
@ -142,7 +138,7 @@ public abstract class ResourceService implements Service {
|
||||||
end = length - 1;
|
end = length - 1;
|
||||||
}
|
}
|
||||||
if (start > end) {
|
if (start > end) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length);
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length);
|
||||||
ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -151,16 +147,16 @@ public abstract class ResourceService implements Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ranges.isEmpty() || ranges.get(0) == full) {
|
if (ranges.isEmpty() || ranges.get(0) == full) {
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total);
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total)
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
||||||
send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse, full.start, full.length);
|
send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse, full.start, full.length);
|
||||||
} else if (ranges.size() == 1) {
|
} else if (ranges.size() == 1) {
|
||||||
Range r = ranges.get(0);
|
Range r = ranges.get(0);
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total);
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total)
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
||||||
send(resource.getURL(), HttpResponseStatus.PARTIAL_CONTENT, contentType, serverRequest, serverResponse, r.start, r.length);
|
send(resource.getURL(), HttpResponseStatus.PARTIAL_CONTENT, contentType, serverRequest, serverResponse, r.start, r.length);
|
||||||
} else {
|
} else {
|
||||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY");
|
serverResponse.withHeader(HttpHeaderNames.CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY");
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (Range r : ranges) {
|
for (Range r : ranges) {
|
||||||
try {
|
try {
|
||||||
|
@ -184,45 +180,6 @@ public abstract class ResourceService implements Service {
|
||||||
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String formatInstant(Instant instant) {
|
|
||||||
return DateTimeFormatter.RFC_1123_DATE_TIME
|
|
||||||
.format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatMillis(long millis) {
|
|
||||||
return formatInstant(Instant.ofEpochMilli(millis));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String formatSeconds(long seconds) {
|
|
||||||
return formatInstant(Instant.now().plusSeconds(seconds));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz";
|
|
||||||
|
|
||||||
private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy";
|
|
||||||
|
|
||||||
private static final DateTimeFormatter[] dateTimeFormatters = {
|
|
||||||
DateTimeFormatter.RFC_1123_DATE_TIME,
|
|
||||||
DateTimeFormatter.ofPattern(RFC1036_PATTERN),
|
|
||||||
DateTimeFormatter.ofPattern(ASCIITIME_PATTERN)
|
|
||||||
};
|
|
||||||
|
|
||||||
private static Instant parseDate(String date) {
|
|
||||||
if (date == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
int semicolonIndex = date.indexOf(';');
|
|
||||||
String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date;
|
|
||||||
// RFC 2616 allows RFC 1123, RFC 1036, ASCII time
|
|
||||||
for (DateTimeFormatter formatter : dateTimeFormatters) {
|
|
||||||
try {
|
|
||||||
return Instant.from(formatter.withZone(ZoneId.of("UTC")).parse(trimmedDate));
|
|
||||||
} catch (DateTimeParseException e) {
|
|
||||||
logger.log(Level.FINEST, e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long sublong(String value, int beginIndex, int endIndex) {
|
private static long sublong(String value, int beginIndex, int endIndex) {
|
||||||
String substring = value.substring(beginIndex, endIndex);
|
String substring = value.substring(beginIndex, endIndex);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.server.Server;
|
import org.xbib.netty.http.server.Server;
|
||||||
import org.xbib.netty.http.server.ServerConfig;
|
import org.xbib.netty.http.server.ServerConfig;
|
||||||
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
||||||
import org.xbib.netty.http.server.transport.ServerTransport;
|
import org.xbib.netty.http.server.transport.Transport;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -41,7 +41,7 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
|
|
||||||
private final HttpHandler httpHandler;
|
private final HttpHandler httpHandler;
|
||||||
|
|
||||||
private final SniHandler sniHandler;
|
private final DomainNameMapping<SslContext> domainNameMapping;
|
||||||
|
|
||||||
public HttpChannelInitializer(Server server,
|
public HttpChannelInitializer(Server server,
|
||||||
HttpAddress httpAddress,
|
HttpAddress httpAddress,
|
||||||
|
@ -50,13 +50,13 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
this.serverConfig = server.getServerConfig();
|
this.serverConfig = server.getServerConfig();
|
||||||
this.httpAddress = httpAddress;
|
this.httpAddress = httpAddress;
|
||||||
this.httpHandler = new HttpHandler(server);
|
this.httpHandler = new HttpHandler(server);
|
||||||
this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null;
|
this.domainNameMapping = domainNameMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initChannel(SocketChannel channel) {
|
public void initChannel(SocketChannel channel) {
|
||||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||||
if (serverConfig.isDebug()) {
|
if (serverConfig.isDebug()) {
|
||||||
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,7 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureEncrypted(SocketChannel channel) {
|
private void configureEncrypted(SocketChannel channel) {
|
||||||
if (sniHandler != null) {
|
channel.pipeline().addLast("sni-handker", new SniHandler(domainNameMapping));
|
||||||
channel.pipeline().addLast("sni-handker", sniHandler);
|
|
||||||
}
|
|
||||||
configureCleartext(channel);
|
configureCleartext(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,8 +112,8 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
||||||
HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg;
|
HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg;
|
||||||
if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) {
|
if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) {
|
||||||
FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest();
|
FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest();
|
||||||
ServerTransport serverTransport = server.newTransport(fullHttpRequest.protocolVersion());
|
Transport transport = server.newTransport(fullHttpRequest.protocolVersion());
|
||||||
serverTransport.requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId());
|
transport.requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
super.channelRead(ctx, msg);
|
super.channelRead(ctx, msg);
|
||||||
|
|
|
@ -30,7 +30,7 @@ import org.xbib.netty.http.common.HttpAddress;
|
||||||
import org.xbib.netty.http.server.Server;
|
import org.xbib.netty.http.server.Server;
|
||||||
import org.xbib.netty.http.server.ServerConfig;
|
import org.xbib.netty.http.server.ServerConfig;
|
||||||
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
import org.xbib.netty.http.server.handler.TrafficLoggingHandler;
|
||||||
import org.xbib.netty.http.server.transport.ServerTransport;
|
import org.xbib.netty.http.server.transport.Transport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -46,7 +46,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
|
|
||||||
private final HttpAddress httpAddress;
|
private final HttpAddress httpAddress;
|
||||||
|
|
||||||
private final SniHandler sniHandler;
|
private final DomainNameMapping<SslContext> domainNameMapping;
|
||||||
|
|
||||||
public Http2ChannelInitializer(Server server,
|
public Http2ChannelInitializer(Server server,
|
||||||
HttpAddress httpAddress,
|
HttpAddress httpAddress,
|
||||||
|
@ -54,13 +54,13 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.serverConfig = server.getServerConfig();
|
this.serverConfig = server.getServerConfig();
|
||||||
this.httpAddress = httpAddress;
|
this.httpAddress = httpAddress;
|
||||||
this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null;
|
this.domainNameMapping = domainNameMapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initChannel(Channel channel) {
|
public void initChannel(Channel channel) {
|
||||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||||
if (serverConfig.isDebug()) {
|
if (serverConfig.isDebug()) {
|
||||||
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
||||||
}
|
}
|
||||||
|
@ -75,9 +75,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureEncrypted(Channel channel) {
|
private void configureEncrypted(Channel channel) {
|
||||||
if (sniHandler != null) {
|
channel.pipeline().addLast("sni-handler", new SniHandler(domainNameMapping));
|
||||||
channel.pipeline().addLast("sni-handler", sniHandler);
|
|
||||||
}
|
|
||||||
configureCleartext(channel);
|
configureCleartext(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +84,8 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
|
Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
|
||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel channel) {
|
protected void initChannel(Channel channel) {
|
||||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||||
ChannelPipeline pipeline = channel.pipeline();
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
pipeline.addLast("multiplex-server-frame-converter",
|
pipeline.addLast("multiplex-server-frame-converter",
|
||||||
new Http2StreamFrameToHttpObjectCodec(true));
|
new Http2StreamFrameToHttpObjectCodec(true));
|
||||||
|
@ -125,19 +123,12 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
p.addLast("server-messages", new ServerMessages());
|
p.addLast("server-messages", new ServerMessages());
|
||||||
}
|
}
|
||||||
|
|
||||||
public SslContext getSessionContext() {
|
|
||||||
if (httpAddress.isSecure()) {
|
|
||||||
return sniHandler.sslContext();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
|
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
|
||||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||||
serverTransport.requestReceived(ctx, fullHttpRequest);
|
transport.requestReceived(ctx, fullHttpRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,23 +136,23 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
|
||||||
if (msg instanceof DefaultHttp2SettingsFrame) {
|
if (msg instanceof DefaultHttp2SettingsFrame) {
|
||||||
DefaultHttp2SettingsFrame http2SettingsFrame = (DefaultHttp2SettingsFrame) msg;
|
DefaultHttp2SettingsFrame http2SettingsFrame = (DefaultHttp2SettingsFrame) msg;
|
||||||
serverTransport.settingsReceived(ctx, http2SettingsFrame.settings());
|
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||||
|
transport.settingsReceived(ctx, http2SettingsFrame.settings());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
|
||||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||||
ctx.fireUserEventTriggered(evt);
|
ctx.fireUserEventTriggered(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
|
||||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||||
serverTransport.exceptionReceived(ctx, cause);
|
transport.exceptionReceived(ctx, cause);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ public class SeekableChunkedNioStream extends ChunkedNioStream {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance that fetches data from the specified channel.
|
* Creates a new instance that fetches data from the specified channel.
|
||||||
|
* @param in input
|
||||||
*/
|
*/
|
||||||
public SeekableChunkedNioStream(SeekableByteChannel in) {
|
public SeekableChunkedNioStream(SeekableByteChannel in) {
|
||||||
super(in);
|
super(in);
|
||||||
|
@ -23,6 +24,7 @@ public class SeekableChunkedNioStream extends ChunkedNioStream {
|
||||||
/**
|
/**
|
||||||
* Creates a new instance that fetches data from the specified channel.
|
* Creates a new instance that fetches data from the specified channel.
|
||||||
*
|
*
|
||||||
|
* @param in channel
|
||||||
* @param chunkSize the number of bytes to fetch on each call
|
* @param chunkSize the number of bytes to fetch on each call
|
||||||
*/
|
*/
|
||||||
public SeekableChunkedNioStream(SeekableByteChannel in, int chunkSize) {
|
public SeekableChunkedNioStream(SeekableByteChannel in, int chunkSize) {
|
||||||
|
@ -32,8 +34,10 @@ public class SeekableChunkedNioStream extends ChunkedNioStream {
|
||||||
/**
|
/**
|
||||||
* Creates a new instance that fetches data from the specified channel.
|
* Creates a new instance that fetches data from the specified channel.
|
||||||
*
|
*
|
||||||
|
* @param in channel
|
||||||
* @param position the position in the byte channel
|
* @param position the position in the byte channel
|
||||||
* @param chunkSize the number of bytes to fetch on each call
|
* @param chunkSize the number of bytes to fetch on each call
|
||||||
|
* @throws IOException if creation fails
|
||||||
*/
|
*/
|
||||||
public SeekableChunkedNioStream(SeekableByteChannel in, long position, int chunkSize) throws IOException {
|
public SeekableChunkedNioStream(SeekableByteChannel in, long position, int chunkSize) throws IOException {
|
||||||
super(in, chunkSize);
|
super(in, chunkSize);
|
||||||
|
|
|
@ -57,6 +57,7 @@ abstract class HttpStreamsHandler<In extends HttpMessage, Out extends HttpMessag
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the given incoming message has a body.
|
* Whether the given incoming message has a body.
|
||||||
|
* @param in input
|
||||||
*/
|
*/
|
||||||
protected abstract boolean hasBody(In in);
|
protected abstract boolean hasBody(In in);
|
||||||
|
|
||||||
|
@ -116,6 +117,8 @@ abstract class HttpStreamsHandler<In extends HttpMessage, Out extends HttpMessag
|
||||||
*
|
*
|
||||||
* Provided so that the client subclass can intercept this to hold off sending the body of an expect 100 continue
|
* Provided so that the client subclass can intercept this to hold off sending the body of an expect 100 continue
|
||||||
* request.
|
* request.
|
||||||
|
* @param msg msg
|
||||||
|
* @param subscriber subscriber
|
||||||
*/
|
*/
|
||||||
protected void subscribeSubscriberToStream(StreamedHttpMessage msg, Subscriber<HttpContent> subscriber) {
|
protected void subscribeSubscriberToStream(StreamedHttpMessage msg, Subscriber<HttpContent> subscriber) {
|
||||||
msg.subscribe(subscriber);
|
msg.subscribe(subscriber);
|
||||||
|
|
|
@ -15,15 +15,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
abstract class BaseServerTransport implements ServerTransport {
|
abstract class BaseTransport implements Transport {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(BaseServerTransport.class.getName());
|
private static final Logger logger = Logger.getLogger(BaseTransport.class.getName());
|
||||||
|
|
||||||
static final AtomicInteger requestCounter = new AtomicInteger();
|
static final AtomicInteger requestCounter = new AtomicInteger();
|
||||||
|
|
||||||
protected final Server server;
|
protected final Server server;
|
||||||
|
|
||||||
BaseServerTransport(Server server) {
|
BaseTransport(Server server) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,11 @@ import io.netty.handler.codec.http2.Http2Headers;
|
||||||
import io.netty.handler.codec.http2.Http2HeadersFrame;
|
import io.netty.handler.codec.http2.Http2HeadersFrame;
|
||||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||||
import io.netty.handler.stream.ChunkedInput;
|
import io.netty.handler.stream.ChunkedInput;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
import org.xbib.netty.http.server.ServerName;
|
import org.xbib.netty.http.server.ServerName;
|
||||||
import org.xbib.netty.http.server.ServerRequest;
|
import org.xbib.netty.http.server.ServerRequest;
|
||||||
import org.xbib.netty.http.server.ServerResponse;
|
import org.xbib.netty.http.server.ServerResponse;
|
||||||
|
import org.xbib.netty.http.server.cookie.ServerCookieEncoder;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
|
@ -49,13 +51,16 @@ public class Http2ServerResponse implements ServerResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setHeader(CharSequence name, String value) {
|
public ServerResponse withHeader(CharSequence name, String value) {
|
||||||
headers.set(name, value);
|
headers.set(name, value);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CharSequence getHeader(CharSequence name) {
|
public ServerResponse withCookie(Cookie cookie) {
|
||||||
return headers.get(name);
|
Objects.requireNonNull(cookie);
|
||||||
|
headers.add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -12,9 +12,9 @@ import org.xbib.netty.http.server.endpoint.NamedServer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class Http2ServerTransport extends BaseServerTransport {
|
public class Http2Transport extends BaseTransport {
|
||||||
|
|
||||||
public Http2ServerTransport(Server server) {
|
public Http2Transport(Server server) {
|
||||||
super(server);
|
super(server);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,13 @@ import io.netty.buffer.ByteBuf;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.http.FullHttpRequest;
|
import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
import io.netty.handler.codec.http.HttpUtil;
|
import io.netty.handler.codec.http.HttpUtil;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
import org.xbib.net.QueryParameters;
|
import org.xbib.net.QueryParameters;
|
||||||
import org.xbib.net.URL;
|
import org.xbib.net.URL;
|
||||||
import org.xbib.netty.http.common.HttpParameters;
|
import org.xbib.netty.http.common.HttpParameters;
|
||||||
import org.xbib.netty.http.server.ServerRequest;
|
import org.xbib.netty.http.server.ServerRequest;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.MalformedInputException;
|
import java.nio.charset.MalformedInputException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
@ -52,6 +54,8 @@ public class HttpServerRequest implements ServerRequest {
|
||||||
|
|
||||||
private Integer requestId;
|
private Integer requestId;
|
||||||
|
|
||||||
|
private SSLSession sslSession;
|
||||||
|
|
||||||
public void setChannelHandlerContext(ChannelHandlerContext ctx) {
|
public void setChannelHandlerContext(ChannelHandlerContext ctx) {
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
}
|
}
|
||||||
|
@ -169,6 +173,15 @@ public class HttpServerRequest implements ServerRequest {
|
||||||
return requestId;
|
return requestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSession(SSLSession sslSession) {
|
||||||
|
this.sslSession = sslSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLSession getSession() {
|
||||||
|
return sslSession;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ServerRequest[request=" + httpRequest + "]";
|
return "ServerRequest[request=" + httpRequest + "]";
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,11 @@ import io.netty.handler.codec.http.HttpResponse;
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.codec.http.HttpVersion;
|
import io.netty.handler.codec.http.HttpVersion;
|
||||||
import io.netty.handler.stream.ChunkedInput;
|
import io.netty.handler.stream.ChunkedInput;
|
||||||
|
import org.xbib.netty.http.common.cookie.Cookie;
|
||||||
import org.xbib.netty.http.server.ServerName;
|
import org.xbib.netty.http.server.ServerName;
|
||||||
import org.xbib.netty.http.server.ServerRequest;
|
import org.xbib.netty.http.server.ServerRequest;
|
||||||
import org.xbib.netty.http.server.ServerResponse;
|
import org.xbib.netty.http.server.ServerResponse;
|
||||||
|
import org.xbib.netty.http.server.cookie.ServerCookieEncoder;
|
||||||
import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse;
|
import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse;
|
||||||
|
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
@ -53,13 +55,9 @@ public class HttpServerResponse implements ServerResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setHeader(CharSequence name, String value) {
|
public ServerResponse withHeader(CharSequence name, String value) {
|
||||||
headers.set(name, value);
|
headers.set(name, value);
|
||||||
}
|
return this;
|
||||||
|
|
||||||
@Override
|
|
||||||
public CharSequence getHeader(CharSequence name) {
|
|
||||||
return headers.get(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -95,6 +93,13 @@ public class HttpServerResponse implements ServerResponse {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServerResponse withCookie(Cookie cookie) {
|
||||||
|
Objects.requireNonNull(cookie);
|
||||||
|
headers.add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(ByteBuf byteBuf) {
|
public void write(ByteBuf byteBuf) {
|
||||||
Objects.requireNonNull(byteBuf);
|
Objects.requireNonNull(byteBuf);
|
||||||
|
|
|
@ -5,15 +5,16 @@ import io.netty.handler.codec.http.FullHttpRequest;
|
||||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||||
import io.netty.handler.codec.http2.Http2Settings;
|
import io.netty.handler.codec.http2.Http2Settings;
|
||||||
|
import io.netty.handler.ssl.SslHandler;
|
||||||
import org.xbib.netty.http.server.Server;
|
import org.xbib.netty.http.server.Server;
|
||||||
import org.xbib.netty.http.server.ServerResponse;
|
import org.xbib.netty.http.server.ServerResponse;
|
||||||
import org.xbib.netty.http.server.endpoint.NamedServer;
|
import org.xbib.netty.http.server.endpoint.NamedServer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class HttpServerTransport extends BaseServerTransport {
|
public class HttpTransport extends BaseTransport {
|
||||||
|
|
||||||
public HttpServerTransport(Server server) {
|
public HttpTransport(Server server) {
|
||||||
super(server);
|
super(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +36,10 @@ public class HttpServerTransport extends BaseServerTransport {
|
||||||
serverRequest.setRequest(fullHttpRequest);
|
serverRequest.setRequest(fullHttpRequest);
|
||||||
serverRequest.setSequenceId(sequenceId);
|
serverRequest.setSequenceId(sequenceId);
|
||||||
serverRequest.setRequestId(requestId);
|
serverRequest.setRequestId(requestId);
|
||||||
|
SslHandler sslHandler = ctx.channel().pipeline().get(SslHandler.class);
|
||||||
|
if (sslHandler != null) {
|
||||||
|
serverRequest.setSession(sslHandler.engine().getSession());
|
||||||
|
}
|
||||||
HttpServerResponse serverResponse = new HttpServerResponse(serverRequest);
|
HttpServerResponse serverResponse = new HttpServerResponse(serverRequest);
|
||||||
if (acceptRequest(namedServer, serverRequest, serverResponse)) {
|
if (acceptRequest(namedServer, serverRequest, serverResponse)) {
|
||||||
handle(namedServer, serverRequest, serverResponse);
|
handle(namedServer, serverRequest, serverResponse);
|
|
@ -7,9 +7,9 @@ import io.netty.util.AttributeKey;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public interface ServerTransport {
|
public interface Transport {
|
||||||
|
|
||||||
AttributeKey<ServerTransport> TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport");
|
AttributeKey<Transport> TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport");
|
||||||
|
|
||||||
void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException;
|
void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException;
|
||||||
|
|
Loading…
Reference in a new issue