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.QueryStringDecoder;
|
||||
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.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.common.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpParameters;
|
||||
import org.xbib.netty.http.common.cookie.Cookie;
|
||||
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
@ -80,9 +80,9 @@ public class Request {
|
|||
private StatusListener statusListener;
|
||||
|
||||
private Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod,
|
||||
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
|
||||
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
|
||||
boolean isBackOff, BackOff backOff) {
|
||||
HttpHeaders headers, Collection<Cookie> cookies, ByteBuf content,
|
||||
long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount,
|
||||
boolean isBackOff, BackOff backOff) {
|
||||
this.url = url;
|
||||
this.uri = uri;
|
||||
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;
|
||||
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
import org.xbib.netty.http.common.cookie.Cookie;
|
||||
|
||||
@FunctionalInterface
|
||||
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.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import org.xbib.net.PercentDecoder;
|
||||
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.client.Request;
|
||||
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 java.io.IOException;
|
||||
|
@ -21,7 +22,6 @@ import java.nio.charset.MalformedInputException;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.charset.UnmappableCharacterException;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
@ -55,7 +55,7 @@ abstract class BaseTransport implements Transport {
|
|||
|
||||
final SortedMap<String, Request> requests;
|
||||
|
||||
private Map<Cookie, Boolean> cookieBox;
|
||||
private CookieBox cookieBox;
|
||||
|
||||
BaseTransport(Client client, HttpAddress httpAddress) {
|
||||
this.client = client;
|
||||
|
@ -336,17 +336,19 @@ abstract class BaseTransport implements Transport {
|
|||
return null;
|
||||
}
|
||||
|
||||
public void setCookieBox(Map<Cookie, Boolean> cookieBox) {
|
||||
@Override
|
||||
public void setCookieBox(CookieBox cookieBox) {
|
||||
this.cookieBox = cookieBox;
|
||||
}
|
||||
|
||||
public Map<Cookie, Boolean> getCookieBox() {
|
||||
@Override
|
||||
public CookieBox getCookieBox() {
|
||||
return cookieBox;
|
||||
}
|
||||
|
||||
void addCookie(Cookie cookie) {
|
||||
if (cookieBox == null) {
|
||||
this.cookieBox = Collections.synchronizedMap(new LRUCache<Cookie, Boolean>(32));
|
||||
this.cookieBox = new CookieBox(32);
|
||||
}
|
||||
cookieBox.put(cookie, true);
|
||||
}
|
||||
|
@ -376,18 +378,4 @@ abstract class BaseTransport implements Transport {
|
|||
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.HttpHeaderNames;
|
||||
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.DefaultHttp2Headers;
|
||||
import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
|
||||
|
@ -20,6 +17,8 @@ import io.netty.handler.codec.http2.HttpConversionUtil;
|
|||
import io.netty.util.AsciiString;
|
||||
import org.xbib.net.URLSyntaxException;
|
||||
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.Http2StreamFrameToHttpObjectCodec;
|
||||
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.client.Request;
|
||||
import org.xbib.netty.http.client.listener.ResponseListener;
|
||||
import org.xbib.netty.http.common.cookie.Cookie;
|
||||
|
||||
import java.io.IOException;
|
||||
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.FullHttpResponse;
|
||||
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.Http2Settings;
|
||||
import io.netty.handler.codec.http2.HttpConversionUtil;
|
||||
import org.xbib.net.URLSyntaxException;
|
||||
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.StatusListener;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
import org.xbib.netty.http.client.listener.ResponseListener;
|
||||
import org.xbib.netty.http.common.cookie.Cookie;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
|
|
@ -2,15 +2,14 @@ package org.xbib.netty.http.client.transport;
|
|||
|
||||
import io.netty.channel.Channel;
|
||||
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.Http2Settings;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
import org.xbib.netty.http.common.cookie.CookieBox;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
@ -31,9 +30,9 @@ public interface Transport {
|
|||
|
||||
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();
|
||||
|
||||
|
|
|
@ -108,10 +108,7 @@ class PoolTest {
|
|||
long avgConnCountPerNode = connCountSum / 2;
|
||||
for (HttpAddress nodeAddr: nodeFreq.keySet()) {
|
||||
assertTrue(nodeFreq.get(nodeAddr).sum() > 0);
|
||||
assertEquals(/*"Node count: " + nodeCount + ", node: " + nodeAddr
|
||||
+ ", expected connection count: " + avgConnCountPerNode + ", actual: "
|
||||
+ nodeFreq.get(nodeAddr).sum(),*/
|
||||
avgConnCountPerNode, nodeFreq.get(nodeAddr).sum(), 1.5 * avgConnCountPerNode);
|
||||
assertEquals(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.http2.Http2ChannelInitializer;
|
||||
import org.xbib.netty.http.common.SecurityUtil;
|
||||
import org.xbib.netty.http.server.transport.HttpServerTransport;
|
||||
import org.xbib.netty.http.server.transport.Http2ServerTransport;
|
||||
import org.xbib.netty.http.server.transport.ServerTransport;
|
||||
import org.xbib.netty.http.server.transport.HttpTransport;
|
||||
import org.xbib.netty.http.server.transport.Http2Transport;
|
||||
import org.xbib.netty.http.server.transport.Transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
|
@ -181,8 +181,8 @@ public final class Server {
|
|||
logger.log(level, NetworkUtils::displayNetworkInterfaces);
|
||||
}
|
||||
|
||||
public ServerTransport newTransport(HttpVersion httpVersion) {
|
||||
return httpVersion.majorVersion() == 1 ? new HttpServerTransport(this) : new Http2ServerTransport(this);
|
||||
public Transport newTransport(HttpVersion httpVersion) {
|
||||
return httpVersion.majorVersion() == 1 ? new HttpTransport(this) : new Http2Transport(this);
|
||||
}
|
||||
|
||||
public synchronized void shutdownGracefully() throws IOException {
|
||||
|
|
|
@ -2,9 +2,11 @@ package org.xbib.netty.http.server;
|
|||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import org.xbib.net.URL;
|
||||
import org.xbib.netty.http.common.HttpParameters;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -43,6 +45,8 @@ public interface ServerRequest {
|
|||
|
||||
Integer requestId();
|
||||
|
||||
SSLSession getSession();
|
||||
|
||||
class EndpointInfo implements Comparable<EndpointInfo> {
|
||||
|
||||
private final String path;
|
||||
|
|
|
@ -5,6 +5,7 @@ import io.netty.buffer.ByteBufUtil;
|
|||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.stream.ChunkedInput;
|
||||
import org.xbib.netty.http.common.cookie.Cookie;
|
||||
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
|
@ -15,20 +16,20 @@ import java.nio.charset.StandardCharsets;
|
|||
*/
|
||||
public interface ServerResponse {
|
||||
|
||||
void setHeader(CharSequence name, String value);
|
||||
|
||||
CharSequence getHeader(CharSequence name);
|
||||
|
||||
ChannelHandlerContext getChannelHandlerContext();
|
||||
|
||||
HttpResponseStatus getStatus();
|
||||
|
||||
ServerResponse withStatus(HttpResponseStatus httpResponseStatus);
|
||||
|
||||
ServerResponse withHeader(CharSequence name, String value);
|
||||
|
||||
ServerResponse withContentType(String contentType);
|
||||
|
||||
ServerResponse withCharset(Charset charset);
|
||||
|
||||
ServerResponse withCookie(Cookie cookie);
|
||||
|
||||
void write(ByteBuf byteBuf);
|
||||
|
||||
void write(ChunkedInput<ByteBuf> chunkedInput);
|
||||
|
@ -48,14 +49,14 @@ public interface ServerResponse {
|
|||
}
|
||||
|
||||
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) {
|
||||
serverResponse.withStatus(status)
|
||||
.withContentType(contentType)
|
||||
.withCharset(StandardCharsets.UTF_8).
|
||||
write(ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text));
|
||||
.withCharset(StandardCharsets.UTF_8)
|
||||
.write(ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text));
|
||||
}
|
||||
|
||||
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.HttpResponseStatus;
|
||||
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.ServerResponse;
|
||||
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.Paths;
|
||||
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.Arrays;
|
||||
import java.util.List;
|
||||
|
@ -59,14 +55,14 @@ public abstract class ResourceService implements Service {
|
|||
long maxAgeSeconds = 24 * 3600;
|
||||
long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds;
|
||||
if (isCacheResponseEnabled()) {
|
||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
||||
serverResponse.setHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
|
||||
serverResponse.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis))
|
||||
.withHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds);
|
||||
}
|
||||
boolean sent = false;
|
||||
if (isETagResponseEnabled()) {
|
||||
Instant lastModifiedInstant = resource.getLastModified();
|
||||
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 &&
|
||||
ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
||||
ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED);
|
||||
|
@ -79,28 +75,28 @@ public abstract class ResourceService implements Service {
|
|||
}
|
||||
String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH);
|
||||
if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) {
|
||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
||||
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||
.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis));
|
||||
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
||||
return;
|
||||
}
|
||||
Instant ifModifiedSinceInstant = parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
|
||||
Instant ifModifiedSinceInstant = TimeUtils.parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE));
|
||||
if (ifModifiedSinceInstant != null &&
|
||||
ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) {
|
||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
||||
serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis));
|
||||
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||
.withHeader(HttpHeaderNames.EXPIRES, TimeUtils.formatMillis(expirationMillis));
|
||||
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED);
|
||||
return;
|
||||
}
|
||||
serverResponse.setHeader(HttpHeaderNames.ETAG, eTag);
|
||||
serverResponse.setHeader(HttpHeaderNames.LAST_MODIFIED, formatInstant(lastModifiedInstant));
|
||||
serverResponse.withHeader(HttpHeaderNames.ETAG, eTag)
|
||||
.withHeader(HttpHeaderNames.LAST_MODIFIED, TimeUtils.formatInstant(lastModifiedInstant));
|
||||
if (isRangeResponseEnabled()) {
|
||||
performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers);
|
||||
sent = true;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -110,20 +106,20 @@ public abstract class ResourceService implements Service {
|
|||
String contentType, String eTag,
|
||||
HttpHeaders headers) {
|
||||
long length = resource.getLength();
|
||||
serverResponse.setHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
||||
serverResponse.withHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes");
|
||||
Range full = new Range(0, length - 1, length);
|
||||
List<Range> ranges = new ArrayList<>();
|
||||
String range = headers.get(HttpHeaderNames.RANGE);
|
||||
if (range != null) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
String ifRange = headers.get(HttpHeaderNames.IF_RANGE);
|
||||
if (ifRange != null && !ifRange.equals(eTag)) {
|
||||
try {
|
||||
Instant ifRangeTime = parseDate(ifRange);
|
||||
Instant ifRangeTime = TimeUtils.parseDate(ifRange);
|
||||
if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) {
|
||||
ranges.add(full);
|
||||
}
|
||||
|
@ -142,7 +138,7 @@ public abstract class ResourceService implements Service {
|
|||
end = length - 1;
|
||||
}
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
@ -151,16 +147,16 @@ public abstract class ResourceService implements Service {
|
|||
}
|
||||
}
|
||||
if (ranges.isEmpty() || ranges.get(0) == full) {
|
||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total);
|
||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
||||
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total)
|
||||
.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length));
|
||||
send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse, full.start, full.length);
|
||||
} else if (ranges.size() == 1) {
|
||||
Range r = ranges.get(0);
|
||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total);
|
||||
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
||||
serverResponse.withHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total)
|
||||
.withHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length));
|
||||
send(resource.getURL(), HttpResponseStatus.PARTIAL_CONTENT, contentType, serverRequest, serverResponse, r.start, r.length);
|
||||
} 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();
|
||||
for (Range r : ranges) {
|
||||
try {
|
||||
|
@ -184,45 +180,6 @@ public abstract class ResourceService implements Service {
|
|||
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) {
|
||||
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.ServerConfig;
|
||||
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.util.logging.Level;
|
||||
|
@ -41,7 +41,7 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
|||
|
||||
private final HttpHandler httpHandler;
|
||||
|
||||
private final SniHandler sniHandler;
|
||||
private final DomainNameMapping<SslContext> domainNameMapping;
|
||||
|
||||
public HttpChannelInitializer(Server server,
|
||||
HttpAddress httpAddress,
|
||||
|
@ -50,13 +50,13 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
|||
this.serverConfig = server.getServerConfig();
|
||||
this.httpAddress = httpAddress;
|
||||
this.httpHandler = new HttpHandler(server);
|
||||
this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null;
|
||||
this.domainNameMapping = domainNameMapping;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initChannel(SocketChannel channel) {
|
||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
||||
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||
if (serverConfig.isDebug()) {
|
||||
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
||||
}
|
||||
|
@ -71,9 +71,7 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
|||
}
|
||||
|
||||
private void configureEncrypted(SocketChannel channel) {
|
||||
if (sniHandler != null) {
|
||||
channel.pipeline().addLast("sni-handker", sniHandler);
|
||||
}
|
||||
channel.pipeline().addLast("sni-handker", new SniHandler(domainNameMapping));
|
||||
configureCleartext(channel);
|
||||
}
|
||||
|
||||
|
@ -114,8 +112,8 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
|
|||
HttpPipelinedRequest httpPipelinedRequest = (HttpPipelinedRequest) msg;
|
||||
if (httpPipelinedRequest.getRequest() instanceof FullHttpRequest) {
|
||||
FullHttpRequest fullHttpRequest = (FullHttpRequest) httpPipelinedRequest.getRequest();
|
||||
ServerTransport serverTransport = server.newTransport(fullHttpRequest.protocolVersion());
|
||||
serverTransport.requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId());
|
||||
Transport transport = server.newTransport(fullHttpRequest.protocolVersion());
|
||||
transport.requestReceived(ctx, fullHttpRequest, httpPipelinedRequest.getSequenceId());
|
||||
}
|
||||
} else {
|
||||
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.ServerConfig;
|
||||
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.util.logging.Level;
|
||||
|
@ -46,7 +46,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
|
||||
private final HttpAddress httpAddress;
|
||||
|
||||
private final SniHandler sniHandler;
|
||||
private final DomainNameMapping<SslContext> domainNameMapping;
|
||||
|
||||
public Http2ChannelInitializer(Server server,
|
||||
HttpAddress httpAddress,
|
||||
|
@ -54,13 +54,13 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
this.server = server;
|
||||
this.serverConfig = server.getServerConfig();
|
||||
this.httpAddress = httpAddress;
|
||||
this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null;
|
||||
this.domainNameMapping = domainNameMapping;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initChannel(Channel channel) {
|
||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
||||
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||
if (serverConfig.isDebug()) {
|
||||
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
||||
}
|
||||
|
@ -75,9 +75,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
}
|
||||
|
||||
private void configureEncrypted(Channel channel) {
|
||||
if (sniHandler != null) {
|
||||
channel.pipeline().addLast("sni-handler", sniHandler);
|
||||
}
|
||||
channel.pipeline().addLast("sni-handler", new SniHandler(domainNameMapping));
|
||||
configureCleartext(channel);
|
||||
}
|
||||
|
||||
|
@ -86,8 +84,8 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel channel) {
|
||||
ServerTransport serverTransport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).set(serverTransport);
|
||||
Transport transport = server.newTransport(httpAddress.getVersion());
|
||||
channel.attr(Transport.TRANSPORT_ATTRIBUTE_KEY).set(transport);
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addLast("multiplex-server-frame-converter",
|
||||
new Http2StreamFrameToHttpObjectCodec(true));
|
||||
|
@ -125,19 +123,12 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
p.addLast("server-messages", new ServerMessages());
|
||||
}
|
||||
|
||||
public SslContext getSessionContext() {
|
||||
if (httpAddress.isSecure()) {
|
||||
return sniHandler.sslContext();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException {
|
||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||
serverTransport.requestReceived(ctx, fullHttpRequest);
|
||||
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||
transport.requestReceived(ctx, fullHttpRequest);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,23 +136,23 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
|
|||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||
if (msg instanceof DefaultHttp2SettingsFrame) {
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws IOException {
|
||||
ServerTransport serverTransport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||
serverTransport.exceptionReceived(ctx, cause);
|
||||
Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get();
|
||||
transport.exceptionReceived(ctx, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ public class SeekableChunkedNioStream extends ChunkedNioStream {
|
|||
|
||||
/**
|
||||
* Creates a new instance that fetches data from the specified channel.
|
||||
* @param in input
|
||||
*/
|
||||
public SeekableChunkedNioStream(SeekableByteChannel in) {
|
||||
super(in);
|
||||
|
@ -23,6 +24,7 @@ public class SeekableChunkedNioStream extends ChunkedNioStream {
|
|||
/**
|
||||
* 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
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param in channel
|
||||
* @param position the position in the byte channel
|
||||
* @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 {
|
||||
super(in, chunkSize);
|
||||
|
|
|
@ -57,6 +57,7 @@ abstract class HttpStreamsHandler<In extends HttpMessage, Out extends HttpMessag
|
|||
|
||||
/**
|
||||
* Whether the given incoming message has a body.
|
||||
* @param in input
|
||||
*/
|
||||
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
|
||||
* request.
|
||||
* @param msg msg
|
||||
* @param subscriber subscriber
|
||||
*/
|
||||
protected void subscribeSubscriberToStream(StreamedHttpMessage msg, Subscriber<HttpContent> subscriber) {
|
||||
msg.subscribe(subscriber);
|
||||
|
|
|
@ -15,15 +15,15 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import java.util.logging.Level;
|
||||
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();
|
||||
|
||||
protected final Server server;
|
||||
|
||||
BaseServerTransport(Server server) {
|
||||
BaseTransport(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.HttpConversionUtil;
|
||||
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.ServerRequest;
|
||||
import org.xbib.netty.http.server.ServerResponse;
|
||||
import org.xbib.netty.http.server.cookie.ServerCookieEncoder;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.time.ZoneOffset;
|
||||
|
@ -49,13 +51,16 @@ public class Http2ServerResponse implements ServerResponse {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setHeader(CharSequence name, String value) {
|
||||
public ServerResponse withHeader(CharSequence name, String value) {
|
||||
headers.set(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getHeader(CharSequence name) {
|
||||
return headers.get(name);
|
||||
public ServerResponse withCookie(Cookie cookie) {
|
||||
Objects.requireNonNull(cookie);
|
||||
headers.add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -12,9 +12,9 @@ import org.xbib.netty.http.server.endpoint.NamedServer;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
public class Http2ServerTransport extends BaseServerTransport {
|
||||
public class Http2Transport extends BaseTransport {
|
||||
|
||||
public Http2ServerTransport(Server server) {
|
||||
public Http2Transport(Server server) {
|
||||
super(server);
|
||||
}
|
||||
|
|
@ -4,11 +4,13 @@ import io.netty.buffer.ByteBuf;
|
|||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpUtil;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import org.xbib.net.QueryParameters;
|
||||
import org.xbib.net.URL;
|
||||
import org.xbib.netty.http.common.HttpParameters;
|
||||
import org.xbib.netty.http.server.ServerRequest;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.MalformedInputException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
@ -52,6 +54,8 @@ public class HttpServerRequest implements ServerRequest {
|
|||
|
||||
private Integer requestId;
|
||||
|
||||
private SSLSession sslSession;
|
||||
|
||||
public void setChannelHandlerContext(ChannelHandlerContext ctx) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
@ -169,6 +173,15 @@ public class HttpServerRequest implements ServerRequest {
|
|||
return requestId;
|
||||
}
|
||||
|
||||
public void setSession(SSLSession sslSession) {
|
||||
this.sslSession = sslSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLSession getSession() {
|
||||
return sslSession;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
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.HttpVersion;
|
||||
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.ServerRequest;
|
||||
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 java.nio.charset.Charset;
|
||||
|
@ -53,13 +55,9 @@ public class HttpServerResponse implements ServerResponse {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setHeader(CharSequence name, String value) {
|
||||
public ServerResponse withHeader(CharSequence name, String value) {
|
||||
headers.set(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getHeader(CharSequence name) {
|
||||
return headers.get(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -95,6 +93,13 @@ public class HttpServerResponse implements ServerResponse {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServerResponse withCookie(Cookie cookie) {
|
||||
Objects.requireNonNull(cookie);
|
||||
headers.add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ByteBuf 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.HttpResponseStatus;
|
||||
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.ServerResponse;
|
||||
import org.xbib.netty.http.server.endpoint.NamedServer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class HttpServerTransport extends BaseServerTransport {
|
||||
public class HttpTransport extends BaseTransport {
|
||||
|
||||
public HttpServerTransport(Server server) {
|
||||
public HttpTransport(Server server) {
|
||||
super(server);
|
||||
}
|
||||
|
||||
|
@ -35,6 +36,10 @@ public class HttpServerTransport extends BaseServerTransport {
|
|||
serverRequest.setRequest(fullHttpRequest);
|
||||
serverRequest.setSequenceId(sequenceId);
|
||||
serverRequest.setRequestId(requestId);
|
||||
SslHandler sslHandler = ctx.channel().pipeline().get(SslHandler.class);
|
||||
if (sslHandler != null) {
|
||||
serverRequest.setSession(sslHandler.engine().getSession());
|
||||
}
|
||||
HttpServerResponse serverResponse = new HttpServerResponse(serverRequest);
|
||||
if (acceptRequest(namedServer, serverRequest, serverResponse)) {
|
||||
handle(namedServer, serverRequest, serverResponse);
|
|
@ -7,9 +7,9 @@ import io.netty.util.AttributeKey;
|
|||
|
||||
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;
|
||||
|
Loading…
Reference in a new issue