cookies with sameSite

This commit is contained in:
Jörg Prante 2019-06-27 23:20:19 +02:00
parent 509b8073eb
commit f322697702
35 changed files with 1628 additions and 167 deletions

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View file

@ -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 {

View file

@ -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;
}
}
} }

View file

@ -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;

View file

@ -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;

View file

@ -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();

View file

@ -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);
} }
} }
} }

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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)));
}
}
}
}

View file

@ -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";
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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);
} }
} }
} }

View file

@ -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);

View file

@ -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);

View file

@ -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;
} }

View file

@ -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

View file

@ -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);
} }

View file

@ -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 + "]";
} }

View file

@ -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);

View file

@ -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);

View file

@ -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;