From f322697702162837a8b0ec98bb6587b76ed46c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Thu, 27 Jun 2019 23:20:19 +0200 Subject: [PATCH] cookies with sameSite --- .../org/xbib/netty/http/client/Request.java | 8 +- .../client/cookie/ClientCookieDecoder.java | 250 ++++++++++++++++++ .../client/cookie/ClientCookieEncoder.java | 201 ++++++++++++++ .../http/client/listener/CookieListener.java | 2 +- .../http/client/transport/BaseTransport.java | 28 +- .../http/client/transport/Http2Transport.java | 6 +- .../http/client/transport/HttpTransport.java | 6 +- .../http/client/transport/Transport.java | 7 +- .../netty/http/client/test/pool/PoolTest.java | 5 +- .../xbib/netty/http/common/cookie/Cookie.java | 130 +++++++++ .../netty/http/common/cookie/CookieBox.java | 11 + .../http/common/cookie/CookieDecoder.java | 40 +++ .../http/common/cookie/CookieEncoder.java | 29 ++ .../http/common/cookie/CookieHeaderNames.java | 18 ++ .../netty/http/common/cookie/CookieUtil.java | 181 +++++++++++++ .../http/common/cookie/DefaultCookie.java | 223 ++++++++++++++++ .../xbib/netty/http/common/util/LRUCache.java | 19 ++ .../netty/http/common/util/TimeUtils.java | 52 ++++ .../org/xbib/netty/http/server/Server.java | 10 +- .../xbib/netty/http/server/ServerRequest.java | 4 + .../netty/http/server/ServerResponse.java | 15 +- .../server/cookie/ServerCookieDecoder.java | 135 ++++++++++ .../server/cookie/ServerCookieEncoder.java | 202 ++++++++++++++ .../endpoint/service/ResourceService.java | 85 ++---- .../handler/http/HttpChannelInitializer.java | 18 +- .../http2/Http2ChannelInitializer.java | 39 ++- .../stream/SeekableChunkedNioStream.java | 4 + .../server/reactive/HttpStreamsHandler.java | 3 + ...erverTransport.java => BaseTransport.java} | 6 +- .../server/transport/Http2ServerResponse.java | 11 +- ...rverTransport.java => Http2Transport.java} | 4 +- .../server/transport/HttpServerRequest.java | 13 + .../server/transport/HttpServerResponse.java | 17 +- ...erverTransport.java => HttpTransport.java} | 9 +- .../{ServerTransport.java => Transport.java} | 4 +- 35 files changed, 1628 insertions(+), 167 deletions(-) create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieBox.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieDecoder.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieEncoder.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieHeaderNames.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieUtil.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/transport/{BaseServerTransport.java => BaseTransport.java} (94%) rename netty-http-server/src/main/java/org/xbib/netty/http/server/transport/{Http2ServerTransport.java => Http2Transport.java} (94%) rename netty-http-server/src/main/java/org/xbib/netty/http/server/transport/{HttpServerTransport.java => HttpTransport.java} (85%) rename netty-http-server/src/main/java/org/xbib/netty/http/server/transport/{ServerTransport.java => Transport.java} (84%) diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java index f5f3724..5d45e5b 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java @@ -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 cookies, ByteBuf content, - long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, - boolean isBackOff, BackOff backOff) { + HttpHeaders headers, Collection cookies, ByteBuf content, + long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, + boolean isBackOff, BackOff backOff) { this.url = url; this.uri = uri; this.httpVersion = httpVersion; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java new file mode 100644 index 0000000..acaf343 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieDecoder.java @@ -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 RFC6265 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; + } + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java new file mode 100644 index 0000000..b0ae1c1 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/cookie/ClientCookieEncoder.java @@ -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 RFC6265 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. + * + *
+ * // Example
+ * HttpRequest req = ...
+ * res.setHeader("Cookie", {@link ClientCookieEncoder}.encode("JSESSIONID", "1234"))
+ * 
+ * + */ +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_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 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 cookies) { + Iterator 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 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); + } + } +} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java index 1552176..0049a33 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java @@ -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 { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index 5de7434..5bdfab8 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -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 requests; - private Map 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 cookieBox) { + @Override + public void setCookieBox(CookieBox cookieBox) { this.cookieBox = cookieBox; } - public Map getCookieBox() { + @Override + public CookieBox getCookieBox() { return cookieBox; } void addCookie(Cookie cookie) { if (cookieBox == null) { - this.cookieBox = Collections.synchronizedMap(new LRUCache(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 extends LinkedHashMap { - - private final int cacheSize; - - LRUCache(int cacheSize) { - super(16, 0.75f, true); - this.cacheSize = cacheSize; - } - - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() >= cacheSize; - } - } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java index eed0ed5..5643337 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java @@ -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; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java index c535a54..35688c9 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java @@ -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; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java index 41186ab..05cfe12 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java @@ -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 cookieBox); + void setCookieBox(CookieBox cookieBox); - Map getCookieBox(); + CookieBox getCookieBox(); Transport get(); diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java index 78f01fc..fb8444d 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java @@ -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); } } } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java new file mode 100644 index 0000000..b3206d4 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/Cookie.java @@ -0,0 +1,130 @@ +package org.xbib.netty.http.common.cookie; + +/** + * An interface defining an + * HTTP cookie. + */ +public interface Cookie extends Comparable { + + /** + * 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 here + * + * @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 + * here. + * + * @param httpOnly True if the {@link Cookie} is HTTP only, otherwise false. + */ + void setHttpOnly(boolean httpOnly); + + String sameSite(); + + void setSameSite(String sameSite); +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieBox.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieBox.java new file mode 100644 index 0000000..cfe5b9d --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieBox.java @@ -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 { + + public CookieBox(int cacheSize) { + super(cacheSize); + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieDecoder.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieDecoder.java new file mode 100644 index 0000000..2f1f919 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieDecoder.java @@ -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; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieEncoder.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieEncoder.java new file mode 100644 index 0000000..0692fd5 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieEncoder.java @@ -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))); + } + } + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieHeaderNames.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieHeaderNames.java new file mode 100644 index 0000000..e7a8667 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieHeaderNames.java @@ -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"; +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieUtil.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieUtil.java new file mode 100644 index 0000000..611b80a --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/CookieUtil.java @@ -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 = + 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; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java new file mode 100644 index 0000000..0f65941 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/cookie/DefaultCookie.java @@ -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(); + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java new file mode 100644 index 0000000..b648332 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LRUCache.java @@ -0,0 +1,19 @@ +package org.xbib.netty.http.common.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +@SuppressWarnings("serial") +public class LRUCache extends LinkedHashMap { + + private final int cacheSize; + + public LRUCache(int cacheSize) { + super(16, 0.75f, true); + this.cacheSize = cacheSize; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java new file mode 100644 index 0000000..7a9be59 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/TimeUtils.java @@ -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; + } + +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java index 40d7f7e..9bf4e2e 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java @@ -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 { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java index 6586dab..76fea3d 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java @@ -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 { private final String path; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java index 61a6063..c0590b9 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java @@ -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 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, diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java new file mode 100644 index 0000000..85759c7 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieDecoder.java @@ -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 RFC6265 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 RFC2965 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 decode(String header) { + final int headerLen = Objects.requireNonNull(header, "header").length(); + if (headerLen == 0) { + return Collections.emptySet(); + } + Set cookies = new TreeSet(); + 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; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java new file mode 100644 index 0000000..4d14ab7 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/cookie/ServerCookieEncoder.java @@ -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 RFC6265 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. + * + *
+ * // Example
+ * HttpRequest req = ...
+ * res.withHeader("Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"))
+ * 
+ * + * @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 dedup(List encoded, Map nameToLastIndex) { + boolean[] isLastInstance = new boolean[encoded.size()]; + for (int idx : nameToLastIndex.values()) { + isLastInstance[idx] = true; + } + List 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 encode(Cookie... cookies) { + if (Objects.requireNonNull(cookies, "cookies").length == 0) { + return Collections.emptyList(); + } + List encoded = new ArrayList<>(cookies.length); + Map 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 encode(Collection cookies) { + if (Objects.requireNonNull(cookies, "cookies").isEmpty()) { + return Collections.emptyList(); + } + List encoded = new ArrayList<>(cookies.size()); + Map 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 encode(Iterable cookies) { + Iterator cookiesIt = Objects.requireNonNull(cookies, "cookies").iterator(); + if (!cookiesIt.hasNext()) { + return Collections.emptyList(); + } + List encoded = new ArrayList<>(); + Cookie firstCookie = cookiesIt.next(); + Map 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; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java index d3da4f9..63798b7 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java @@ -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 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); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java index 5679aa3..caef37c 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java @@ -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 { private final HttpHandler httpHandler; - private final SniHandler sniHandler; + private final DomainNameMapping domainNameMapping; public HttpChannelInitializer(Server server, HttpAddress httpAddress, @@ -50,13 +50,13 @@ public class HttpChannelInitializer extends ChannelInitializer { 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 { } 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 { 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); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java index b8a5f07..05d5c94 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -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 { private final HttpAddress httpAddress; - private final SniHandler sniHandler; + private final DomainNameMapping domainNameMapping; public Http2ChannelInitializer(Server server, HttpAddress httpAddress, @@ -54,13 +54,13 @@ public class Http2ChannelInitializer extends ChannelInitializer { 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 { } 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 { Http2MultiplexCodecBuilder serverMultiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { @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 { p.addLast("server-messages", new ServerMessages()); } - public SslContext getSessionContext() { - if (httpAddress.isSecure()) { - return sniHandler.sslContext(); - } - return null; - } - class ServerRequestHandler extends SimpleChannelInboundHandler { @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 { @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); } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java index d93c908..f6bb390 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java @@ -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); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java index 4700e6b..82857c0 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/reactive/HttpStreamsHandler.java @@ -57,6 +57,7 @@ abstract class HttpStreamsHandler subscriber) { msg.subscribe(subscriber); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java similarity index 94% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java index 5b97ca1..894b5d6 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseTransport.java @@ -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; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java index 80eafc6..5fdf4dd 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java @@ -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 diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java similarity index 94% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java index 265573b..321904b 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2Transport.java @@ -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); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java index 8dfa8b9..7e49bfc 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java @@ -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 + "]"; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java index 2a6fc17..4bcf267 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java @@ -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); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java similarity index 85% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java index 7e2f7b3..950e8fa 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpTransport.java @@ -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); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java similarity index 84% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java index 14ef451..8f88546 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Transport.java @@ -7,9 +7,9 @@ import io.netty.util.AttributeKey; import java.io.IOException; -public interface ServerTransport { +public interface Transport { - AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException;