From d98b21be045a315ced88ada84000e4757cfb9892 Mon Sep 17 00:00:00 2001 From: Stephane Landelle Date: Wed, 6 May 2015 20:52:48 +0200 Subject: [PATCH] Validate cookie name and value characters Motivation: RFC6265 specifies which characters are allowed in a cookie name and value. Netty is currently too lax, which can used for HttpOnly escaping. Modification: In ServerCookieDecoder: discard cookie key-value pairs that contain invalid characters. In ClientCookieEncoder: throw an exception when trying to encode cookies with invalid characters. Result: The problem described in the motivation section is fixed. --- .../codec/http/ClientCookieEncoder.java | 78 +-- .../io/netty/handler/codec/http/Cookie.java | 147 +++--- .../handler/codec/http/CookieDecoder.java | 107 +++- .../handler/codec/http/CookieEncoderUtil.java | 88 ---- .../netty/handler/codec/http/CookieUtil.java | 104 ++++ .../handler/codec/http/DefaultCookie.java | 263 ++-------- .../codec/http/HttpHeaderDateFormat.java | 4 +- .../netty/handler/codec/http/HttpRequest.java | 18 +- .../handler/codec/http/HttpResponse.java | 14 +- .../codec/http/ServerCookieEncoder.java | 173 ++----- .../http/cookie/ClientCookieDecoder.java | 261 ++++++++++ .../http/cookie/ClientCookieEncoder.java | 140 ++++++ .../handler/codec/http/cookie/Cookie.java | 141 ++++++ .../codec/http/cookie/CookieDecoder.java | 84 ++++ .../codec/http/cookie/CookieEncoder.java | 51 ++ .../http/{ => cookie}/CookieHeaderNames.java | 28 +- .../handler/codec/http/cookie/CookieUtil.java | 158 ++++++ .../codec/http/cookie/DefaultCookie.java | 268 ++++++++++ .../http/cookie/ServerCookieDecoder.java | 157 ++++++ .../http/cookie/ServerCookieEncoder.java | 179 +++++++ .../codec/http/cookie/package-info.java | 20 + .../handler/codec/http/CookieDecoderTest.java | 474 ------------------ .../handler/codec/http/CookieEncoderTest.java | 148 ------ .../http/cookie/ClientCookieDecoderTest.java | 280 +++++++++++ .../http/cookie/ClientCookieEncoderTest.java | 52 ++ .../http/cookie/ServerCookieDecoderTest.java | 185 +++++++ .../http/cookie/ServerCookieEncoderTest.java | 63 +++ 27 files changed, 2459 insertions(+), 1226 deletions(-) delete mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/CookieEncoderUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/CookieUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/Cookie.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieEncoder.java rename codec-http/src/main/java/io/netty/handler/codec/http/{ => cookie}/CookieHeaderNames.java (52%) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/cookie/package-info.java delete mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/CookieDecoderTest.java delete mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/CookieEncoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieEncoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieEncoder.java index 5a5b5bae74..19382612a1 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieEncoder.java @@ -15,8 +15,6 @@ */ package io.netty.handler.codec.http; -import static io.netty.handler.codec.http.CookieEncoderUtil.*; - /** * Encodes client-side {@link Cookie}s into an HTTP header value. This encoder can encode * the HTTP cookie version 0, 1, and 2. @@ -27,88 +25,32 @@ import static io.netty.handler.codec.http.CookieEncoderUtil.*; * * * @see CookieDecoder + * @deprecated Use {@link io.netty.handler.codec.http.cookie.ClientCookieEncoder} instead. */ +@Deprecated public final class ClientCookieEncoder { /** * Encodes the specified cookie into an HTTP header value. */ + @Deprecated public static String encode(String name, String value) { - return encode(new DefaultCookie(name, value)); + return io.netty.handler.codec.http.cookie.ClientCookieEncoder.LAX.encode(name, value); } + @Deprecated public static String encode(Cookie cookie) { - if (cookie == null) { - throw new NullPointerException("cookie"); - } - - StringBuilder buf = stringBuilder(); - encode(buf, cookie); - return stripTrailingSeparator(buf); + return io.netty.handler.codec.http.cookie.ClientCookieEncoder.LAX.encode(cookie); } + @Deprecated public static String encode(Cookie... cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - StringBuilder buf = stringBuilder(); - for (Cookie c: cookies) { - if (c == null) { - break; - } - - encode(buf, c); - } - return stripTrailingSeparator(buf); + return io.netty.handler.codec.http.cookie.ClientCookieEncoder.LAX.encode(cookies); } + @Deprecated public static String encode(Iterable cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - StringBuilder buf = stringBuilder(); - for (Cookie c: cookies) { - if (c == null) { - break; - } - - encode(buf, c); - } - return stripTrailingSeparator(buf); - } - - private static void encode(StringBuilder buf, Cookie c) { - if (c.getVersion() >= 1) { - add(buf, '$' + CookieHeaderNames.VERSION, 1); - } - - add(buf, c.getName(), c.getValue()); - - if (c.getPath() != null) { - add(buf, '$' + CookieHeaderNames.PATH, c.getPath()); - } - - if (c.getDomain() != null) { - add(buf, '$' + CookieHeaderNames.DOMAIN, c.getDomain()); - } - - if (c.getVersion() >= 1) { - if (!c.getPorts().isEmpty()) { - buf.append('$'); - buf.append(CookieHeaderNames.PORT); - buf.append((char) HttpConstants.EQUALS); - buf.append((char) HttpConstants.DOUBLE_QUOTE); - for (int port: c.getPorts()) { - buf.append(port); - buf.append((char) HttpConstants.COMMA); - } - buf.setCharAt(buf.length() - 1, (char) HttpConstants.DOUBLE_QUOTE); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); - } - } + return io.netty.handler.codec.http.cookie.ClientCookieEncoder.LAX.encode(cookies); } private ClientCookieEncoder() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/Cookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/Cookie.java index c154d2de26..b1c73a2edf 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/Cookie.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/Cookie.java @@ -20,78 +20,76 @@ import java.util.Set; /** * An interface defining an * HTTP cookie. + * @deprecated Use {@link io.netty.handler.codec.http.cookie.Cookie} instead. */ -public interface Cookie extends Comparable { +@Deprecated +public interface Cookie extends io.netty.handler.codec.http.cookie.Cookie { /** - * Returns the name of this {@link Cookie}. - * - * @return The name of this {@link Cookie} + * @deprecated Use {@link #name()} instead. */ + @Deprecated String getName(); /** - * Returns the value of this {@link Cookie}. - * - * @return The value of this {@link Cookie} + * @deprecated Use {@link #value()} instead. */ + @Deprecated String getValue(); /** - * Sets the value of this {@link Cookie}. - * - * @param value The value to set - */ - void setValue(String value); - - /** - * Returns the domain of this {@link Cookie}. - * - * @return The domain of this {@link Cookie} + * @deprecated Use {@link #domain()} instead. */ + @Deprecated String getDomain(); /** - * 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 + * @deprecated Use {@link #path()} instead. */ + @Deprecated String getPath(); /** - * Sets the path of this {@link Cookie}. - * - * @param path The path to use for this {@link Cookie} + * @deprecated Use {@link #comment()} instead. */ - void setPath(String path); + @Deprecated + String getComment(); /** * Returns the comment of this {@link Cookie}. * * @return The comment of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ - String getComment(); + @Deprecated + String comment(); /** * Sets the comment of this {@link Cookie}. * * @param comment The comment to use + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setComment(String comment); + /** + * @deprecated Use {@link #maxAge()} instead. + */ + @Deprecated + long getMaxAge(); + /** * 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} + * + * @deprecated Not part of RFC6265 */ - long getMaxAge(); + @Deprecated + long maxAge(); /** * Sets the maximum age of this {@link Cookie} in seconds. @@ -101,70 +99,62 @@ public interface Cookie extends Comparable { * browser is closed. * * @param maxAge The maximum age of this {@link Cookie} in seconds + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setMaxAge(long maxAge); + /** + * @deprecated Use {@link #version()} instead. + */ + @Deprecated + int getVersion(); + /** * Returns the version of this {@link Cookie}. * * @return The version of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ - int getVersion(); + @Deprecated + int version(); /** * Sets the version of this {@link Cookie}. * * @param version The new version to use + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setVersion(int version); /** - * Checks to see if this {@link Cookie} is secure - * - * @return True if this {@link Cookie} is secure, otherwise false + * @deprecated Use {@link #commentUrl()} instead. */ - 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); + @Deprecated + String getCommentUrl(); /** * Returns the comment URL of this {@link Cookie}. * * @return The comment URL of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ - String getCommentUrl(); + @Deprecated + String commentUrl(); /** * Sets the comment URL of this {@link Cookie}. * * @param commentUrl The comment URL to use + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setCommentUrl(String commentUrl); /** @@ -172,7 +162,10 @@ public interface Cookie extends Comparable { * at the end of the current session. * * @return True if this {@link Cookie} is to be discarded, otherwise false + * + * @deprecated Not part of RFC6265 */ + @Deprecated boolean isDiscard(); /** @@ -181,21 +174,36 @@ public interface Cookie extends Comparable { * at the end of the current session * * @param discard True if the {@link Cookie} is to be discarded + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setDiscard(boolean discard); + /** + * @deprecated Use {@link #ports()} instead. + */ + @Deprecated + Set getPorts(); + /** * Returns the ports that this {@link Cookie} can be accessed on. * * @return The {@link Set} of ports that this {@link Cookie} can use + * + * @deprecated Not part of RFC6265 */ - Set getPorts(); + @Deprecated + Set ports(); /** * Sets the ports that this {@link Cookie} can be accessed on. * * @param ports The ports that this {@link Cookie} can be accessed on + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setPorts(int... ports); /** @@ -203,6 +211,9 @@ public interface Cookie extends Comparable { * * @param ports The {@link Iterable} collection of ports that this * {@link Cookie} can be accessed on. + * + * @deprecated Not part of RFC6265 */ + @Deprecated void setPorts(Iterable ports); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/CookieDecoder.java index 2f79ec3534..b71b7ad1de 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CookieDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/CookieDecoder.java @@ -15,7 +15,13 @@ */ package io.netty.handler.codec.http; +import static io.netty.handler.codec.http.CookieUtil.firstInvalidCookieNameOctet; +import static io.netty.handler.codec.http.CookieUtil.firstInvalidCookieValueOctet; +import static io.netty.handler.codec.http.CookieUtil.unwrapValue; +import io.netty.handler.codec.http.cookie.CookieHeaderNames; import io.netty.util.internal.StringUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; import java.text.ParseException; import java.util.ArrayList; @@ -25,6 +31,9 @@ import java.util.Set; import java.util.TreeSet; /** + * @deprecated Use {@link io.netty.handler.codec.http.cookie.ClientCookieDecoder} + * or {@link io.netty.handler.codec.http.cookie.ServerCookieDecoder} instead. + * * Decodes an HTTP header value into {@link Cookie}s. This decoder can decode * the HTTP cookie version 0, 1, and 2. * @@ -34,19 +43,46 @@ import java.util.TreeSet; * Set<{@link Cookie}> cookies = {@link CookieDecoder}.decode(value); * * - * @see ClientCookieEncoder - * @see ServerCookieEncoder + * @see io.netty.handler.codec.http.cookie.ClientCookieDecoder + * @see io.netty.handler.codec.http.cookie.ServerCookieDecoder */ +@Deprecated public final class CookieDecoder { + private final InternalLogger logger = InternalLoggerFactory.getInstance(getClass()); + + private static final CookieDecoder STRICT = new CookieDecoder(true); + + private static final CookieDecoder LAX = new CookieDecoder(false); + + private static final String COMMENT = "Comment"; + + private static final String COMMENTURL = "CommentURL"; + + private static final String DISCARD = "Discard"; + + private static final String PORT = "Port"; + + private static final String VERSION = "Version"; + private static final char COMMA = ','; + private final boolean strict; + + public static Set decode(String header) { + return decode(header, true); + } + + public static Set decode(String header, boolean strict) { + return (strict ? STRICT : LAX).doDecode(header); + } + /** * Decodes the specified HTTP header value into {@link Cookie}s. * * @return the decoded {@link Cookie}s */ - public static Set decode(String header) { + private Set doDecode(String header) { List names = new ArrayList(8); List values = new ArrayList(8); extractKeyValuePairs(header, names, values); @@ -60,7 +96,7 @@ public final class CookieDecoder { // $Version is the only attribute that can appear before the actual // cookie name-value pair. - if (names.get(0).equalsIgnoreCase(CookieHeaderNames.VERSION)) { + if (names.get(0).equalsIgnoreCase(VERSION)) { try { version = Integer.parseInt(values.get(0)); } catch (NumberFormatException e) { @@ -84,7 +120,11 @@ public final class CookieDecoder { value = ""; } - Cookie c = new DefaultCookie(name, value); + Cookie c = initCookie(name, value); + + if (c == null) { + break; + } boolean discard = false; boolean secure = false; @@ -100,15 +140,15 @@ public final class CookieDecoder { name = names.get(j); value = values.get(j); - if (CookieHeaderNames.DISCARD.equalsIgnoreCase(name)) { + if (DISCARD.equalsIgnoreCase(name)) { discard = true; } else if (CookieHeaderNames.SECURE.equalsIgnoreCase(name)) { secure = true; } else if (CookieHeaderNames.HTTPONLY.equalsIgnoreCase(name)) { httpOnly = true; - } else if (CookieHeaderNames.COMMENT.equalsIgnoreCase(name)) { + } else if (COMMENT.equalsIgnoreCase(name)) { comment = value; - } else if (CookieHeaderNames.COMMENTURL.equalsIgnoreCase(name)) { + } else if (COMMENTURL.equalsIgnoreCase(name)) { commentURL = value; } else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) { domain = value; @@ -126,9 +166,9 @@ public final class CookieDecoder { } } else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) { maxAge = Integer.parseInt(value); - } else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) { + } else if (VERSION.equalsIgnoreCase(name)) { version = Integer.parseInt(value); - } else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) { + } else if (PORT.equalsIgnoreCase(name)) { String[] portList = StringUtil.split(value, COMMA); for (String s1: portList) { try { @@ -165,7 +205,6 @@ public final class CookieDecoder { private static void extractKeyValuePairs( final String header, final List names, final List values) { - final int headerLen = header.length(); loop: for (int i = 0;;) { @@ -287,7 +326,49 @@ public final class CookieDecoder { } } - private CookieDecoder() { - // Unused + private CookieDecoder(boolean strict) { + this.strict = strict; + } + + private DefaultCookie initCookie(String name, String value) { + if (name == null || name.length() == 0) { + logger.debug("Skipping cookie with null name"); + return null; + } + + if (value == null) { + logger.debug("Skipping cookie with null value"); + return null; + } + + CharSequence unwrappedValue = unwrapValue(value); + if (unwrappedValue == null) { + logger.debug("Skipping cookie because starting quotes are not properly balanced in '{}'", + unwrappedValue); + return null; + } + + int invalidOctetPos; + if (strict && (invalidOctetPos = firstInvalidCookieNameOctet(name)) >= 0) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping cookie because name '{}' contains invalid char '{}'", + name, name.charAt(invalidOctetPos)); + } + return null; + } + + final boolean wrap = unwrappedValue.length() != value.length(); + + if (strict && (invalidOctetPos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping cookie because value '{}' contains invalid char '{}'", + unwrappedValue, unwrappedValue.charAt(invalidOctetPos)); + } + return null; + } + + DefaultCookie cookie = new DefaultCookie(name, unwrappedValue.toString()); + cookie.setWrap(wrap); + return cookie; } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CookieEncoderUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/CookieEncoderUtil.java deleted file mode 100644 index bb45518db2..0000000000 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CookieEncoderUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package io.netty.handler.codec.http; - - -import io.netty.util.internal.InternalThreadLocalMap; - -final class CookieEncoderUtil { - - static StringBuilder stringBuilder() { - return InternalThreadLocalMap.get().stringBuilder(); - } - - static String stripTrailingSeparator(StringBuilder buf) { - if (buf.length() > 0) { - buf.setLength(buf.length() - 2); - } - return buf.toString(); - } - - static void add(StringBuilder sb, String name, String val) { - if (val == null) { - addQuoted(sb, name, ""); - return; - } - - for (int i = 0; i < val.length(); i ++) { - char c = val.charAt(i); - switch (c) { - case '\t': case ' ': case '"': case '(': case ')': case ',': - case '/': case ':': case ';': case '<': case '=': case '>': - case '?': case '@': case '[': case '\\': case ']': - case '{': case '}': - addQuoted(sb, name, val); - return; - } - } - - addUnquoted(sb, name, val); - } - - static void addUnquoted(StringBuilder sb, String name, String val) { - sb.append(name); - sb.append((char) HttpConstants.EQUALS); - sb.append(val); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); - } - - static void addQuoted(StringBuilder sb, String name, String val) { - if (val == null) { - val = ""; - } - - sb.append(name); - sb.append((char) HttpConstants.EQUALS); - sb.append((char) HttpConstants.DOUBLE_QUOTE); - sb.append(val.replace("\\", "\\\\").replace("\"", "\\\"")); - sb.append((char) HttpConstants.DOUBLE_QUOTE); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); - } - - static void add(StringBuilder sb, String name, long val) { - sb.append(name); - sb.append((char) HttpConstants.EQUALS); - sb.append(val); - sb.append((char) HttpConstants.SEMICOLON); - sb.append((char) HttpConstants.SP); - } - - private CookieEncoderUtil() { - // Unused - } -} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CookieUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/CookieUtil.java new file mode 100644 index 0000000000..fdaae035fd --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/CookieUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http; + +import java.util.BitSet; + +/** + * @deprecated Duplicate of package private ${@link io.netty.handler.codec.http.cookie.CookieUtil} + */ +@Deprecated +final class CookieUtil { + + private static final BitSet VALID_COOKIE_VALUE_OCTETS = validCookieValueOctets(); + + private static final BitSet VALID_COOKIE_NAME_OCTETS = validCookieNameOctets(VALID_COOKIE_VALUE_OCTETS); + + // US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon, and backslash + private static BitSet validCookieValueOctets() { + BitSet bits = new BitSet(8); + for (int i = 35; i < 127; i++) { + // US-ASCII characters excluding CTLs (%x00-1F / %x7F) + bits.set(i); + } + bits.set('"', false); // exclude DQUOTE = %x22 + bits.set(',', false); // exclude comma = %x2C + bits.set(';', false); // exclude semicolon = %x3B + bits.set('\\', false); // exclude backslash = %x5C + return bits; + } + + // token = 1* + // separators = "(" | ")" | "<" | ">" | "@" + // | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" + // | "{" | "}" | SP | HT + private static BitSet validCookieNameOctets(BitSet validCookieValueOctets) { + BitSet bits = new BitSet(8); + bits.or(validCookieValueOctets); + bits.set('(', false); + bits.set(')', false); + bits.set('<', false); + bits.set('>', false); + bits.set('@', false); + bits.set(':', false); + bits.set('/', false); + bits.set('[', false); + bits.set(']', false); + bits.set('?', false); + bits.set('=', false); + bits.set('{', false); + bits.set('}', false); + bits.set(' ', false); + bits.set('\t', false); + return bits; + } + + static int firstInvalidCookieNameOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_NAME_OCTETS); + } + + static int firstInvalidCookieValueOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_VALUE_OCTETS); + } + + 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; + } + + static CharSequence unwrapValue(CharSequence cs) { + final int len = cs.length(); + if (len > 0 && cs.charAt(0) == '"') { + if (len >= 2 && cs.charAt(len - 1) == '"') { + // properly balanced + return len == 2 ? "" : cs.subSequence(1, len - 1); + } else { + return null; + } + } + return cs; + } + + private CookieUtil() { + // Unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java index 59add577e0..902ba16a2d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/DefaultCookie.java @@ -21,130 +21,107 @@ import java.util.TreeSet; /** * The default {@link Cookie} implementation. + * + * @deprecated Use {@link io.netty.handler.codec.http.cookie.DefaultCookie} instead. */ -public class DefaultCookie implements Cookie { +@Deprecated +public class DefaultCookie extends io.netty.handler.codec.http.cookie.DefaultCookie implements Cookie { - private final String name; - private String value; - private String domain; - private String path; private String comment; private String commentUrl; private boolean discard; private Set ports = Collections.emptySet(); private Set unmodifiablePorts = ports; - private long maxAge = Long.MIN_VALUE; private int version; - private boolean secure; - private boolean httpOnly; /** * Creates a new cookie with the specified name and value. */ public DefaultCookie(String name, String value) { - if (name == null) { - throw new NullPointerException("name"); - } - name = name.trim(); - if (name.isEmpty()) { - throw new IllegalArgumentException("empty name"); - } - - for (int i = 0; i < name.length(); i ++) { - char c = name.charAt(i); - if (c > 127) { - throw new IllegalArgumentException( - "name contains non-ascii character: " + name); - } - - // Check prohibited characters. - switch (c) { - case '\t': case '\n': case 0x0b: case '\f': case '\r': - case ' ': case ',': case ';': case '=': - throw new IllegalArgumentException( - "name contains one of the following prohibited characters: " + - "=,; \\t\\r\\n\\v\\f: " + name); - } - } - - if (name.charAt(0) == '$') { - throw new IllegalArgumentException("name starting with '$' not allowed: " + name); - } - - this.name = name; - setValue(value); + super(name, value); } @Override + @Deprecated public String getName() { - return name; + return name(); } @Override + @Deprecated public String getValue() { - return value; - } - - @Override - public void setValue(String value) { - if (value == null) { - throw new NullPointerException("value"); - } - this.value = value; + return value(); } @Override + @Deprecated public String getDomain() { - return domain; - } - - @Override - public void setDomain(String domain) { - this.domain = validateValue("domain", domain); + return domain(); } @Override + @Deprecated public String getPath() { - return path; - } - - @Override - public void setPath(String path) { - this.path = validateValue("path", path); + return path(); } @Override + @Deprecated public String getComment() { + return comment(); + } + + @Override + @Deprecated + public String comment() { return comment; } @Override + @Deprecated public void setComment(String comment) { this.comment = validateValue("comment", comment); } @Override + @Deprecated public String getCommentUrl() { + return commentUrl(); + } + + @Override + @Deprecated + public String commentUrl() { return commentUrl; } @Override + @Deprecated public void setCommentUrl(String commentUrl) { this.commentUrl = validateValue("commentUrl", commentUrl); } @Override + @Deprecated public boolean isDiscard() { return discard; } @Override + @Deprecated public void setDiscard(boolean discard) { this.discard = discard; } @Override + @Deprecated public Set getPorts() { + return ports(); + } + + @Override + @Deprecated + public Set ports() { if (unmodifiablePorts == null) { unmodifiablePorts = Collections.unmodifiableSet(ports); } @@ -152,6 +129,7 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public void setPorts(int... ports) { if (ports == null) { throw new NullPointerException("ports"); @@ -174,6 +152,7 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public void setPorts(Iterable ports) { Set newPorts = new TreeSet(); for (int p: ports) { @@ -191,168 +170,26 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public long getMaxAge() { - return maxAge; - } - - @Override - public void setMaxAge(long maxAge) { - this.maxAge = maxAge; + return maxAge(); } @Override + @Deprecated public int getVersion() { + return version(); + } + + @Override + @Deprecated + public int version() { return version; } @Override + @Deprecated public void setVersion(int version) { this.version = version; } - - @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 int hashCode() { - return getName().hashCode(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Cookie)) { - return false; - } - - Cookie that = (Cookie) o; - if (!getName().equalsIgnoreCase(that.getName())) { - return false; - } - - if (getPath() == null) { - if (that.getPath() != null) { - return false; - } - } else if (that.getPath() == null) { - return false; - } else if (!getPath().equals(that.getPath())) { - return false; - } - - if (getDomain() == null) { - if (that.getDomain() != null) { - return false; - } - } else if (that.getDomain() == null) { - return false; - } else { - return getDomain().equalsIgnoreCase(that.getDomain()); - } - - return true; - } - - @Override - public int compareTo(Cookie c) { - int v; - v = getName().compareToIgnoreCase(c.getName()); - if (v != 0) { - return v; - } - - if (getPath() == null) { - if (c.getPath() != null) { - return -1; - } - } else if (c.getPath() == null) { - return 1; - } else { - v = getPath().compareTo(c.getPath()); - if (v != 0) { - return v; - } - } - - if (getDomain() == null) { - if (c.getDomain() != null) { - return -1; - } - } else if (c.getDomain() == null) { - return 1; - } else { - v = getDomain().compareToIgnoreCase(c.getDomain()); - return v; - } - - return 0; - } - - @Override - public String toString() { - StringBuilder buf = new StringBuilder() - .append(getName()) - .append('=') - .append(getValue()); - if (getDomain() != null) { - buf.append(", domain=") - .append(getDomain()); - } - if (getPath() != null) { - buf.append(", path=") - .append(getPath()); - } - if (getComment() != null) { - buf.append(", comment=") - .append(getComment()); - } - if (getMaxAge() >= 0) { - buf.append(", maxAge=") - .append(getMaxAge()) - .append('s'); - } - if (isSecure()) { - buf.append(", secure"); - } - if (isHttpOnly()) { - buf.append(", HTTPOnly"); - } - return buf.toString(); - } - - private static String validateValue(String name, String value) { - if (value == null) { - return null; - } - value = value.trim(); - if (value.isEmpty()) { - return null; - } - for (int i = 0; i < value.length(); i ++) { - char c = value.charAt(i); - switch (c) { - case '\r': case '\n': case '\f': case 0x0b: case ';': - throw new IllegalArgumentException( - name + " contains one of the following prohibited characters: " + - ";\\r\\n\\f\\v (" + value + ')'); - } - } - return value; - } } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java index 5c89e7c5f1..ab6349f057 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java @@ -33,7 +33,7 @@ import java.util.TimeZone; *
  • Sun Nov 6 08:49:37 1994: obsolete specification
  • * */ -final class HttpHeaderDateFormat extends SimpleDateFormat { +public final class HttpHeaderDateFormat extends SimpleDateFormat { private static final long serialVersionUID = -925286159755905325L; private final SimpleDateFormat format1 = new HttpHeaderDateFormatObsolete1(); @@ -47,7 +47,7 @@ final class HttpHeaderDateFormat extends SimpleDateFormat { } }; - static HttpHeaderDateFormat get() { + public static HttpHeaderDateFormat get() { return dateFormatThreadLocal.get(); } diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequest.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequest.java index c17a634b11..1d949df89f 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequest.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequest.java @@ -15,21 +15,25 @@ */ package io.netty.handler.codec.http; - /** * An HTTP request. * *

    Accessing Query Parameters and Cookie

    *

    * Unlike the Servlet API, a query string is constructed and decomposed by - * {@link QueryStringEncoder} and {@link QueryStringDecoder}. {@link Cookie} - * support is also provided separately via {@link CookieDecoder}, {@link ClientCookieEncoder}, - * and {@link @ServerCookieEncoder}. + * {@link QueryStringEncoder} and {@link QueryStringDecoder}. + * + * {@link io.netty.handler.codec.http.cookie.Cookie} support is also provided + * separately via {@link io.netty.handler.codec.http.cookie.ServerCookieDecoder}, + * {@link io.netty.handler.codec.http.cookie.ClientCookieDecoder}, + * {@link io.netty.handler.codec.http.cookie.ServerCookieEncoder}, + * and {@link @io.netty.handler.codec.http.cookie.ClientCookieEncoder}. * * @see HttpResponse - * @see ClientCookieEncoder - * @see ServerCookieEncoder - * @see CookieDecoder + * @see io.netty.handler.codec.http.cookie.ServerCookieDecoder + * @see io.netty.handler.codec.http.cookie.ClientCookieDecoder + * @see io.netty.handler.codec.http.cookie.ServerCookieEncoder + * @see io.netty.handler.codec.http.cookie.ClientCookieEncoder */ public interface HttpRequest extends HttpMessage { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponse.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponse.java index 7c7ebe2427..e5cf83eab9 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponse.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponse.java @@ -21,13 +21,17 @@ package io.netty.handler.codec.http; * *

    Accessing Cookies

    *

    - * Unlike the Servlet API, {@link Cookie} support is provided separately via {@link CookieDecoder}, - * {@link ClientCookieEncoder}, and {@link ServerCookieEncoder}. + * Unlike the Servlet API, {@link io.netty.handler.codec.http.cookie.Cookie} support is provided + * separately via {@link io.netty.handler.codec.http.cookie.ServerCookieDecoder}, + * {@link io.netty.handler.codec.http.cookie.ClientCookieDecoder}, + * {@link io.netty.handler.codec.http.cookie.ServerCookieEncoder}, + * and {@link @io.netty.handler.codec.http.cookie.ClientCookieEncoder}. * * @see HttpRequest - * @see CookieDecoder - * @see ClientCookieEncoder - * @see ServerCookieEncoder + * @see io.netty.handler.codec.http.cookie.ServerCookieDecoder + * @see io.netty.handler.codec.http.cookie.ClientCookieDecoder + * @see io.netty.handler.codec.http.cookie.ServerCookieEncoder + * @see io.netty.handler.codec.http.cookie.ClientCookieEncoder */ public interface HttpResponse extends HttpMessage { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieEncoder.java index 01e204d22d..1f58f45f12 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieEncoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieEncoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2014 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -15,153 +15,84 @@ */ package io.netty.handler.codec.http; -import java.util.ArrayList; import java.util.Collection; -import java.util.Date; import java.util.List; -import static io.netty.handler.codec.http.CookieEncoderUtil.*; - /** - * Encodes server-side {@link Cookie}s into HTTP header values. This encoder can encode - * the HTTP cookie version 0, 1, and 2. + * A RFC6265 compliant cookie encoder to be used server side, + * so some fields are sent (Version is typically ignored). + * + * As Netty's 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
      * {@link HttpRequest} req = ...;
    - * res.setHeader("Set-Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"));
    + * res.setHeader("Cookie", {@link ServerCookieEncoder}.encode("JSESSIONID", "1234"));
      * 
    * - * @see CookieDecoder + * @see ServerCookieDecoder + * + * @deprecated Use {@link io.netty.handler.codec.http.cookie.ServerCookieEncoder} instead */ +@Deprecated public final class ServerCookieEncoder { /** - * Encodes the specified cookie into an HTTP header value. + * 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 */ + @Deprecated public static String encode(String name, String value) { - return encode(new DefaultCookie(name, value)); + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(name, value); } + /** + * Encodes the specified cookie into a Set-Cookie header value. + * + * @param cookie the cookie + * @return a single Set-Cookie header value + */ + @Deprecated public static String encode(Cookie cookie) { - if (cookie == null) { - throw new NullPointerException("cookie"); - } - - StringBuilder buf = stringBuilder(); - - add(buf, cookie.getName(), cookie.getValue()); - - if (cookie.getMaxAge() != Long.MIN_VALUE) { - if (cookie.getVersion() != 0) { - add(buf, CookieHeaderNames.MAX_AGE, cookie.getMaxAge()); - } - addUnquoted(buf, CookieHeaderNames.EXPIRES, - HttpHeaderDateFormat.get().format( - new Date(System.currentTimeMillis() + - cookie.getMaxAge() * 1000L))); - } - - if (cookie.getPath() != null) { - if (cookie.getVersion() > 0) { - add(buf, CookieHeaderNames.PATH, cookie.getPath()); - } else { - addUnquoted(buf, CookieHeaderNames.PATH, cookie.getPath()); - } - } - - if (cookie.getDomain() != null) { - if (cookie.getVersion() > 0) { - add(buf, CookieHeaderNames.DOMAIN, cookie.getDomain()); - } else { - addUnquoted(buf, CookieHeaderNames.DOMAIN, cookie.getDomain()); - } - } - if (cookie.isSecure()) { - buf.append(CookieHeaderNames.SECURE); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); - } - if (cookie.isHttpOnly()) { - buf.append(CookieHeaderNames.HTTPONLY); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); - } - if (cookie.getVersion() >= 1) { - if (cookie.getComment() != null) { - add(buf, CookieHeaderNames.COMMENT, cookie.getComment()); - } - - add(buf, CookieHeaderNames.VERSION, 1); - - if (cookie.getCommentUrl() != null) { - addQuoted(buf, CookieHeaderNames.COMMENTURL, cookie.getCommentUrl()); - } - - if (!cookie.getPorts().isEmpty()) { - buf.append(CookieHeaderNames.PORT); - buf.append((char) HttpConstants.EQUALS); - buf.append((char) HttpConstants.DOUBLE_QUOTE); - for (int port: cookie.getPorts()) { - buf.append(port); - buf.append((char) HttpConstants.COMMA); - } - buf.setCharAt(buf.length() - 1, (char) HttpConstants.DOUBLE_QUOTE); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); - } - if (cookie.isDiscard()) { - buf.append(CookieHeaderNames.DISCARD); - buf.append((char) HttpConstants.SEMICOLON); - buf.append((char) HttpConstants.SP); - } - } - - return stripTrailingSeparator(buf); + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookie); } + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + @Deprecated public static List encode(Cookie... cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - List encoded = new ArrayList(cookies.length); - for (Cookie c: cookies) { - if (c == null) { - break; - } - encoded.add(encode(c)); - } - return encoded; + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookies); } + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + @Deprecated public static List encode(Collection cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - List encoded = new ArrayList(cookies.size()); - for (Cookie c: cookies) { - if (c == null) { - break; - } - encoded.add(encode(c)); - } - return encoded; + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookies); } + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + @Deprecated public static List encode(Iterable cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - List encoded = new ArrayList(); - for (Cookie c: cookies) { - if (c == null) { - break; - } - encoded.add(encode(c)); - } - return encoded; + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookies); } private ServerCookieEncoder() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java new file mode 100644 index 0000000000..168b4a4aee --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java @@ -0,0 +1,261 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.handler.codec.http.HttpHeaderDateFormat; + +import java.text.ParsePosition; +import java.util.Date; + +/** + * 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. + * + * @see ClientCookieEncoder + */ +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}. + * + * @return the decoded {@link Cookie} + */ + public Cookie decode(String header) { + final int headerLen = checkNotNull(header, "header").length(); + + if (headerLen == 0) { + return null; + } + + CookieBuilder cookieBuilder = null; + + loop: for (int i = 0;;) { + + // Skip spaces and separators. + for (;;) { + if (i == headerLen) { + break loop; + } + 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 loop; + + } else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f' + || c == '\r' || c == ' ' || c == ';') { + i++; + continue; + } + break; + } + + int nameBegin = i; + int nameEnd = i; + int valueBegin = -1; + int valueEnd = -1; + + if (i != headerLen) { + keyValLoop: for (;;) { + + char curChar = header.charAt(i); + if (curChar == ';') { + // NAME; (no value till ';') + nameEnd = i; + valueBegin = valueEnd = -1; + break keyValLoop; + + } else if (curChar == '=') { + // NAME=VALUE + nameEnd = i; + i++; + if (i == headerLen) { + // NAME= (empty value, i.e. nothing after '=') + valueBegin = valueEnd = 0; + break keyValLoop; + } + + valueBegin = i; + // NAME=VALUE; + int semiPos = header.indexOf(';', i); + valueEnd = i = semiPos > 0 ? semiPos : headerLen; + break keyValLoop; + } else { + i++; + } + + if (i == headerLen) { + // NAME (no value till the end of string) + nameEnd = headerLen; + valueBegin = valueEnd = -1; + break; + } + } + } + + if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') { + // old multiple cookies separator, skipping it + valueEnd--; + } + + if (cookieBuilder == null) { + // cookie name-value pair + DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd); + + if (cookie == null) { + return null; + } + + cookieBuilder = new CookieBuilder(cookie); + } else { + // cookie attribute + String attrValue = valueBegin == -1 ? null : header.substring(valueBegin, valueEnd); + cookieBuilder.appendAttribute(header, nameBegin, nameEnd, attrValue); + } + } + return cookieBuilder.cookie(); + } + + private static class CookieBuilder { + + private final DefaultCookie cookie; + private String domain; + private String path; + private long maxAge = Long.MIN_VALUE; + private String expires; + private boolean secure; + private boolean httpOnly; + + public CookieBuilder(DefaultCookie cookie) { + this.cookie = cookie; + } + + private long mergeMaxAgeAndExpire(long maxAge, String expires) { + // max age has precedence over expires + if (maxAge != Long.MIN_VALUE) { + return maxAge; + } else if (expires != null) { + Date expiresDate = HttpHeaderDateFormat.get().parse(expires, new ParsePosition(0)); + if (expiresDate != null) { + long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis(); + return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0); + } + } + return Long.MIN_VALUE; + } + + public Cookie cookie() { + cookie.setDomain(domain); + cookie.setPath(path); + cookie.setMaxAge(mergeMaxAgeAndExpire(maxAge, expires)); + cookie.setSecure(secure); + cookie.setHttpOnly(httpOnly); + 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 header + * the HTTP header + * @param keyStart + * where the key starts in the header + * @param keyEnd + * where the key ends in the header + * @param value + * the decoded value + */ + public void appendAttribute(String header, int keyStart, int keyEnd, + String value) { + setCookieAttribute(header, keyStart, keyEnd, value); + } + + private void setCookieAttribute(String header, int keyStart, + int keyEnd, String value) { + int length = keyEnd - keyStart; + + if (length == 4) { + parse4(header, keyStart, value); + } else if (length == 6) { + parse6(header, keyStart, value); + } else if (length == 7) { + parse7(header, keyStart, value); + } else if (length == 8) { + parse8(header, keyStart, value); + } + } + + private void parse4(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.PATH, 0, 4)) { + path = value; + } + } + + private void parse6(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.DOMAIN, 0, 5)) { + domain = value.length() > 0 ? value.toString() : null; + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.SECURE, 0, 5)) { + secure = true; + } + } + + private void setExpire(String value) { + expires = value; + } + + private void setMaxAge(String value) { + try { + maxAge = Math.max(Long.valueOf(value), 0L); + } catch (NumberFormatException e1) { + // ignore failure to parse -> treat as session cookie + } + } + + private void parse7(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) { + setExpire(value); + } else if (header.regionMatches(true, nameStart, CookieHeaderNames.MAX_AGE, 0, 7)) { + setMaxAge(value); + } + } + + private void parse8(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) { + httpOnly = true; + } + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java new file mode 100644 index 0000000000..305c738075 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieEncoder.java @@ -0,0 +1,140 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.handler.codec.http.cookie.CookieUtil.*; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.Iterator; + +import io.netty.handler.codec.http.HttpRequest; + +/** + * A RFC6265 compliant cookie encoder to be used client side, + * so only name=value pairs are sent. + * + * User-Agents are not supposed to interpret cookies, so, if present, {@link Cookie#rawValue()} will be used. + * Otherwise, {@link Cookie#value()} will be used unquoted. + * + * Note that multiple cookies are supposed to be sent at once in a single "Cookie" header. + * + *
    + * // Example
    + * {@link HttpRequest} req = ...;
    + * res.setHeader("Cookie", {@link ClientCookieEncoder}.encode("JSESSIONID", "1234"));
    + * 
    + * + * @see ClientCookieDecoder + */ +public final class ClientCookieEncoder extends CookieEncoder { + + /** + * Strict encoder that validates that name and value chars are in the valid scope + * defined in RFC6265 + */ + public static final ClientCookieEncoder STRICT = new ClientCookieEncoder(true); + + /** + * Lax instance that doesn't validate name and value + */ + 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 specified the cookie + * @return a Rfc6265 style Cookie header value + */ + public String encode(Cookie cookie) { + StringBuilder buf = stringBuilder(); + encode(buf, checkNotNull(cookie, "cookie")); + return stripTrailingSeparator(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(Cookie... cookies) { + if (checkNotNull(cookies, "cookies").length == 0) { + return null; + } + + StringBuilder buf = stringBuilder(); + for (Cookie c : cookies) { + if (c == null) { + break; + } + + encode(buf, c); + } + return 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 = checkNotNull(cookies, "cookies").iterator(); + if (!cookiesIt.hasNext()) { + return null; + } + + StringBuilder buf = stringBuilder(); + while (cookiesIt.hasNext()) { + Cookie c = cookiesIt.next(); + if (c == null) { + break; + } + + encode(buf, c); + } + return stripTrailingSeparatorOrNull(buf); + } + + private 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()) { + addQuoted(buf, name, value); + } else { + add(buf, name, value); + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/Cookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/Cookie.java new file mode 100644 index 0000000000..7633477e14 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/Cookie.java @@ -0,0 +1,141 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.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); +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieDecoder.java new file mode 100644 index 0000000000..ab3fbcf7b0 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieDecoder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.handler.codec.http.cookie.CookieUtil.firstInvalidCookieNameOctet; +import static io.netty.handler.codec.http.cookie.CookieUtil.firstInvalidCookieValueOctet; +import static io.netty.handler.codec.http.cookie.CookieUtil.unwrapValue; + +import java.nio.CharBuffer; + +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +/** + * Parent of Client and Server side cookie decoders + */ +public abstract class CookieDecoder { + + private final InternalLogger logger = InternalLoggerFactory.getInstance(getClass()); + + private 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) { + logger.debug("Skipping cookie with null name"); + return null; + } + + if (valueBegin == -1) { + logger.debug("Skipping cookie with null value"); + return null; + } + + CharSequence wrappedValue = CharBuffer.wrap(header, valueBegin, valueEnd); + CharSequence unwrappedValue = unwrapValue(wrappedValue); + if (unwrappedValue == null) { + logger.debug("Skipping cookie because starting quotes are not properly balanced in '{}'", + wrappedValue); + return null; + } + + final String name = header.substring(nameBegin, nameEnd); + + int invalidOctetPos; + if (strict && (invalidOctetPos = firstInvalidCookieNameOctet(name)) >= 0) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping cookie because name '{}' contains invalid char '{}'", + name, name.charAt(invalidOctetPos)); + } + return null; + } + + final boolean wrap = unwrappedValue.length() != valueEnd - valueBegin; + + if (strict && (invalidOctetPos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping cookie because value '{}' contains invalid char '{}'", + unwrappedValue, unwrappedValue.charAt(invalidOctetPos)); + } + return null; + } + + DefaultCookie cookie = new DefaultCookie(name, unwrappedValue.toString()); + cookie.setWrap(wrap); + return cookie; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieEncoder.java new file mode 100644 index 0000000000..1748f0a120 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieEncoder.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.handler.codec.http.cookie.CookieUtil.firstInvalidCookieNameOctet; +import static io.netty.handler.codec.http.cookie.CookieUtil.firstInvalidCookieValueOctet; +import static io.netty.handler.codec.http.cookie.CookieUtil.unwrapValue; + +/** + * Parent of Client and Server side cookie encoders + */ +public abstract class CookieEncoder { + + private final boolean strict; + + protected CookieEncoder(boolean strict) { + this.strict = strict; + } + + protected void validateCookie(String name, String value) { + if (strict) { + int pos; + + if ((pos = firstInvalidCookieNameOctet(name)) >= 0) { + throw new IllegalArgumentException("Cookie name contains an invalid char: " + name.charAt(pos)); + } + + CharSequence unwrappedValue = unwrapValue(value); + if (unwrappedValue == null) { + throw new IllegalArgumentException("Cookie value wrapping quotes are not balanced: " + value); + } + + if ((pos = firstInvalidCookieValueOctet(unwrappedValue)) >= 0) { + throw new IllegalArgumentException("Cookie value contains an invalid char: " + value.charAt(pos)); + } + } + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/CookieHeaderNames.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java similarity index 52% rename from codec-http/src/main/java/io/netty/handler/codec/http/CookieHeaderNames.java rename to codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java index 5757a83eb5..6d2e7f577c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CookieHeaderNames.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieHeaderNames.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 The Netty Project + * Copyright 2015 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance @@ -13,30 +13,20 @@ * License for the specific language governing permissions and limitations * under the License. */ -package io.netty.handler.codec.http; +package io.netty.handler.codec.http.cookie; -final class CookieHeaderNames { - static final String PATH = "Path"; +public final class CookieHeaderNames { + public static final String PATH = "Path"; - static final String EXPIRES = "Expires"; + public static final String EXPIRES = "Expires"; - static final String MAX_AGE = "Max-Age"; + public static final String MAX_AGE = "Max-Age"; - static final String DOMAIN = "Domain"; + public static final String DOMAIN = "Domain"; - static final String SECURE = "Secure"; + public static final String SECURE = "Secure"; - static final String HTTPONLY = "HTTPOnly"; - - static final String COMMENT = "Comment"; - - static final String COMMENTURL = "CommentURL"; - - static final String DISCARD = "Discard"; - - static final String PORT = "Port"; - - static final String VERSION = "Version"; + public static final String HTTPONLY = "HTTPOnly"; private CookieHeaderNames() { // Unused. diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java new file mode 100644 index 0000000000..9bc15f58a3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/CookieUtil.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import io.netty.handler.codec.http.HttpConstants; +import io.netty.util.internal.InternalThreadLocalMap; + +import java.util.BitSet; + +final class CookieUtil { + + private static final BitSet VALID_COOKIE_VALUE_OCTETS = validCookieValueOctets(); + + private static final BitSet VALID_COOKIE_NAME_OCTETS = validCookieNameOctets(VALID_COOKIE_VALUE_OCTETS); + + // US-ASCII characters excluding CTLs, whitespace, DQUOTE, comma, semicolon, and backslash + private static BitSet validCookieValueOctets() { + BitSet bits = new BitSet(8); + for (int i = 35; i < 127; i++) { + // US-ASCII characters excluding CTLs (%x00-1F / %x7F) + bits.set(i); + } + bits.set('"', false); // exclude DQUOTE = %x22 + bits.set(',', false); // exclude comma = %x2C + bits.set(';', false); // exclude semicolon = %x3B + bits.set('\\', false); // exclude backslash = %x5C + return bits; + } + + // token = 1* + // separators = "(" | ")" | "<" | ">" | "@" + // | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" + // | "{" | "}" | SP | HT + private static BitSet validCookieNameOctets(BitSet validCookieValueOctets) { + BitSet bits = new BitSet(8); + bits.or(validCookieValueOctets); + bits.set('(', false); + bits.set(')', false); + bits.set('<', false); + bits.set('>', false); + bits.set('@', false); + bits.set(':', false); + bits.set('/', false); + bits.set('[', false); + bits.set(']', false); + bits.set('?', false); + bits.set('=', false); + bits.set('{', false); + bits.set('}', false); + bits.set(' ', false); + bits.set('\t', false); + return bits; + } + + static StringBuilder stringBuilder() { + return InternalThreadLocalMap.get().stringBuilder(); + } + + /** + * @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. + */ + static String stripTrailingSeparatorOrNull(StringBuilder buf) { + return buf.length() == 0 ? null : stripTrailingSeparator(buf); + } + + static String stripTrailingSeparator(StringBuilder buf) { + if (buf.length() > 0) { + buf.setLength(buf.length() - 2); + } + return buf.toString(); + } + + static void add(StringBuilder sb, String name, long val) { + sb.append(name); + sb.append((char) HttpConstants.EQUALS); + sb.append(val); + sb.append((char) HttpConstants.SEMICOLON); + sb.append((char) HttpConstants.SP); + } + + static void add(StringBuilder sb, String name, String val) { + sb.append(name); + sb.append((char) HttpConstants.EQUALS); + sb.append(val); + sb.append((char) HttpConstants.SEMICOLON); + sb.append((char) HttpConstants.SP); + } + + static void add(StringBuilder sb, String name) { + sb.append(name); + sb.append((char) HttpConstants.SEMICOLON); + sb.append((char) HttpConstants.SP); + } + + static void addQuoted(StringBuilder sb, String name, String val) { + if (val == null) { + val = ""; + } + + sb.append(name); + sb.append((char) HttpConstants.EQUALS); + sb.append((char) HttpConstants.DOUBLE_QUOTE); + sb.append(val); + sb.append((char) HttpConstants.DOUBLE_QUOTE); + sb.append((char) HttpConstants.SEMICOLON); + sb.append((char) HttpConstants.SP); + } + + static int firstInvalidCookieNameOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_NAME_OCTETS); + } + + static int firstInvalidCookieValueOctet(CharSequence cs) { + return firstInvalidOctet(cs, VALID_COOKIE_VALUE_OCTETS); + } + + 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; + } + + static CharSequence unwrapValue(CharSequence cs) { + final int len = cs.length(); + if (len > 0 && cs.charAt(0) == '"') { + if (len >= 2 && cs.charAt(len - 1) == '"') { + // properly balanced + return len == 2 ? "" : cs.subSequence(1, len - 1); + } else { + return null; + } + } + return cs; + } + + private CookieUtil() { + // Unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java new file mode 100644 index 0000000000..c5de61c9d4 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/DefaultCookie.java @@ -0,0 +1,268 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.handler.codec.http.cookie.CookieUtil.stringBuilder; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * 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; + + /** + * Creates a new cookie with the specified name and value. + */ + public DefaultCookie(String name, String value) { + name = checkNotNull(name, "name").trim(); + if (name.isEmpty()) { + throw new IllegalArgumentException("empty name"); + } + + for (int i = 0; i < name.length(); i ++) { + char c = name.charAt(i); + if (c > 127) { + throw new IllegalArgumentException( + "name contains non-ascii character: " + name); + } + + // Check prohibited characters. + switch (c) { + case '\t': case '\n': case 0x0b: case '\f': case '\r': + case ' ': case ',': case ';': case '=': + throw new IllegalArgumentException( + "name contains one of the following prohibited characters: " + + "=,; \\t\\r\\n\\v\\f: " + name); + } + } + + if (name.charAt(0) == '$') { + throw new IllegalArgumentException("name starting with '$' not allowed: " + name); + } + + this.name = name; + setValue(value); + } + + @Override + public String name() { + return name; + } + + @Override + public String value() { + return value; + } + + @Override + public void setValue(String value) { + this.value = checkNotNull(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 = validateValue("domain", domain); + } + + @Override + public String path() { + return path; + } + + @Override + public void setPath(String path) { + this.path = validateValue("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 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().equalsIgnoreCase(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 if (that.domain() == null) { + return false; + } else { + return domain().equalsIgnoreCase(that.domain()); + } + + return true; + } + + @Override + public int compareTo(Cookie c) { + int v = name().compareToIgnoreCase(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 = 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"); + } + return buf.toString(); + } + + protected String validateValue(String name, String value) { + if (value == null) { + return null; + } + value = value.trim(); + if (value.isEmpty()) { + return null; + } + for (int i = 0; i < value.length(); i ++) { + char c = value.charAt(i); + switch (c) { + case '\r': case '\n': case '\f': case 0x0b: case ';': + throw new IllegalArgumentException( + name + " contains one of the following prohibited characters: " + + ";\\r\\n\\f\\v (" + value + ')'); + } + } + return value; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java new file mode 100644 index 0000000000..04a8e3938b --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieDecoder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import java.util.Collections; +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. + * + * @see ServerCookieEncoder + */ +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}. + * + * @return the decoded {@link Cookie} + */ + public Set decode(String header) { + final int headerLen = checkNotNull(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; + } + + loop: for (;;) { + + // Skip spaces and separators. + for (;;) { + if (i == headerLen) { + break loop; + } + char c = header.charAt(i); + if (c == '\t' || c == '\n' || c == 0x0b || c == '\f' + || c == '\r' || c == ' ' || c == ',' || c == ';') { + i++; + continue; + } + break; + } + + int nameBegin = i; + int nameEnd = i; + int valueBegin = -1; + int valueEnd = -1; + + if (i != headerLen) { + keyValLoop: for (;;) { + + char curChar = header.charAt(i); + if (curChar == ';') { + // NAME; (no value till ';') + nameEnd = i; + valueBegin = valueEnd = -1; + break keyValLoop; + + } else if (curChar == '=') { + // NAME=VALUE + nameEnd = i; + i++; + if (i == headerLen) { + // NAME= (empty value, i.e. nothing after '=') + valueBegin = valueEnd = 0; + break keyValLoop; + } + + valueBegin = i; + // NAME=VALUE; + int semiPos = header.indexOf(';', i); + valueEnd = i = semiPos > 0 ? semiPos : headerLen; + break keyValLoop; + } 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; + } + + DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd); + if (cookie != null) { + cookies.add(cookie); + } + } + + return cookies; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java new file mode 100644 index 0000000000..17376903f3 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ServerCookieEncoder.java @@ -0,0 +1,179 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static io.netty.handler.codec.http.cookie.CookieUtil.*; +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.handler.codec.http.HttpHeaderDateFormat; +import io.netty.handler.codec.http.HttpRequest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * A RFC6265 compliant cookie encoder to be used server side, + * so some fields are sent (Version is typically ignored). + * + * As Netty's 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
    + * {@link HttpRequest} req = ...;
    + * res.setHeader("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 + */ + public static final ServerCookieEncoder STRICT = new ServerCookieEncoder(true); + + /** + * Lax instance that doesn't validate name and value + */ + 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 = checkNotNull(cookie, "cookie").name(); + final String value = cookie.value() != null ? cookie.value() : ""; + + validateCookie(name, value); + + StringBuilder buf = stringBuilder(); + + if (cookie.wrap()) { + addQuoted(buf, name, value); + } else { + add(buf, name, value); + } + + if (cookie.maxAge() != Long.MIN_VALUE) { + add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge()); + Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis()); + add(buf, CookieHeaderNames.EXPIRES, HttpHeaderDateFormat.get().format(expires)); + } + + if (cookie.path() != null) { + add(buf, CookieHeaderNames.PATH, cookie.path()); + } + + if (cookie.domain() != null) { + add(buf, CookieHeaderNames.DOMAIN, cookie.domain()); + } + if (cookie.isSecure()) { + add(buf, CookieHeaderNames.SECURE); + } + if (cookie.isHttpOnly()) { + add(buf, CookieHeaderNames.HTTPONLY); + } + + return stripTrailingSeparator(buf); + } + + /** + * 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 (checkNotNull(cookies, "cookies").length == 0) { + return Collections.emptyList(); + } + + List encoded = new ArrayList(cookies.length); + for (Cookie c : cookies) { + if (c == null) { + break; + } + encoded.add(encode(c)); + } + return 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 (checkNotNull(cookies, "cookies").isEmpty()) { + return Collections.emptyList(); + } + + List encoded = new ArrayList(cookies.size()); + for (Cookie c : cookies) { + if (c == null) { + break; + } + encoded.add(encode(c)); + } + return 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) { + if (!checkNotNull(cookies, "cookies").iterator().hasNext()) { + return Collections.emptyList(); + } + + List encoded = new ArrayList(); + for (Cookie c : cookies) { + if (c == null) { + break; + } + encoded.add(encode(c)); + } + return encoded; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cookie/package-info.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/package-info.java new file mode 100644 index 0000000000..4f9ebaf0f0 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +/** + * This package contains Cookie related classes. + */ +package io.netty.handler.codec.http.cookie; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/CookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/CookieDecoderTest.java deleted file mode 100644 index bdf546c730..0000000000 --- a/codec-http/src/test/java/io/netty/handler/codec/http/CookieDecoderTest.java +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright 2012 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package io.netty.handler.codec.http; - -import org.junit.Test; - -import java.util.Calendar; -import java.util.Date; -import java.util.Iterator; -import java.util.Set; -import java.util.TimeZone; - -import static org.junit.Assert.*; - -public class CookieDecoderTest { - @Test - public void testDecodingSingleCookieV0() { - String cookieString = "myCookie=myValue;expires=XXX;path=/apathsomewhere;domain=.adomainsomewhere;secure;"; - cookieString = cookieString.replace("XXX", - HttpHeaderDateFormat.get().format(new Date(System.currentTimeMillis() + 50000))); - - Set cookies = CookieDecoder.decode(cookieString); - assertEquals(1, cookies.size()); - Cookie cookie = cookies.iterator().next(); - assertNotNull(cookie); - assertEquals("myValue", cookie.getValue()); - assertNull(cookie.getComment()); - assertNull(cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertFalse(cookie.isDiscard()); - - boolean fail = true; - for (int i = 40; i <= 60; i ++) { - if (cookie.getMaxAge() == i) { - fail = false; - break; - } - } - if (fail) { - fail("expected: 50, actual: " + cookie.getMaxAge()); - } - - assertEquals("/apathsomewhere", cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertTrue(cookie.isSecure()); - assertEquals(0, cookie.getVersion()); - } - - @Test - public void testDecodingSingleCookieV0ExtraParamsIgnored() { - String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + - "domain=.adomainsomewhere;secure;comment=this is a comment;version=0;" + - "commentURL=http://aurl.com;port=\"80,8080\";discard;"; - Set cookies = CookieDecoder.decode(cookieString); - assertEquals(1, cookies.size()); - Cookie cookie = cookies.iterator().next(); - assertNotNull(cookie); - assertEquals("myValue", cookie.getValue()); - assertNull(cookie.getComment()); - assertNull(cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertFalse(cookie.isDiscard()); - assertEquals(50, cookie.getMaxAge()); - assertEquals("/apathsomewhere", cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertTrue(cookie.isSecure()); - assertEquals(0, cookie.getVersion()); - } - @Test - public void testDecodingSingleCookieV1() { - String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + - "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;"; - Set cookies = CookieDecoder.decode(cookieString); - assertEquals(1, cookies.size()); - Cookie cookie = cookies.iterator().next(); - assertEquals("myValue", cookie.getValue()); - assertNotNull(cookie); - assertEquals("this is a comment", cookie.getComment()); - assertNull(cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertFalse(cookie.isDiscard()); - assertEquals(50, cookie.getMaxAge()); - assertEquals("/apathsomewhere", cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertTrue(cookie.isSecure()); - assertEquals(1, cookie.getVersion()); - } - - @Test - public void testDecodingSingleCookieV1ExtraParamsIgnored() { - String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + - "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;" + - "commentURL=http://aurl.com;port='80,8080';discard;"; - Set cookies = CookieDecoder.decode(cookieString); - assertEquals(1, cookies.size()); - Cookie cookie = cookies.iterator().next(); - assertNotNull(cookie); - assertEquals("myValue", cookie.getValue()); - assertEquals("this is a comment", cookie.getComment()); - assertNull(cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertFalse(cookie.isDiscard()); - assertEquals(50, cookie.getMaxAge()); - assertEquals("/apathsomewhere", cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertTrue(cookie.isSecure()); - assertEquals(1, cookie.getVersion()); - } - @Test - public void testDecodingSingleCookieV2() { - String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + - "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + - "commentURL=http://aurl.com;port=\"80,8080\";discard;"; - Set cookies = CookieDecoder.decode(cookieString); - assertEquals(1, cookies.size()); - Cookie cookie = cookies.iterator().next(); - assertNotNull(cookie); - assertEquals("myValue", cookie.getValue()); - assertEquals("this is a comment", cookie.getComment()); - assertEquals("http://aurl.com", cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertTrue(cookie.isDiscard()); - assertEquals(50, cookie.getMaxAge()); - assertEquals("/apathsomewhere", cookie.getPath()); - assertEquals(2, cookie.getPorts().size()); - assertTrue(cookie.getPorts().contains(80)); - assertTrue(cookie.getPorts().contains(8080)); - assertTrue(cookie.isSecure()); - assertEquals(2, cookie.getVersion()); - } - - @Test - public void testDecodingMultipleCookies() { - String c1 = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + - "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + - "commentURL=\"http://aurl.com\";port='80,8080';discard;"; - String c2 = "myCookie2=myValue2;max-age=0;path=/anotherpathsomewhere;" + - "domain=.anotherdomainsomewhere;comment=this is another comment;version=2;" + - "commentURL=http://anotherurl.com;"; - String c3 = "myCookie3=myValue3;max-age=0;version=2;"; - - Set cookies = CookieDecoder.decode(c1 + c2 + c3); - assertEquals(3, cookies.size()); - Iterator it = cookies.iterator(); - Cookie cookie = it.next(); - assertNotNull(cookie); - assertEquals("myValue", cookie.getValue()); - assertEquals("this is a comment", cookie.getComment()); - assertEquals("http://aurl.com", cookie.getCommentUrl()); - assertEquals(".adomainsomewhere", cookie.getDomain()); - assertTrue(cookie.isDiscard()); - assertEquals(50, cookie.getMaxAge()); - assertEquals("/apathsomewhere", cookie.getPath()); - assertEquals(2, cookie.getPorts().size()); - assertTrue(cookie.getPorts().contains(80)); - assertTrue(cookie.getPorts().contains(8080)); - assertTrue(cookie.isSecure()); - assertEquals(2, cookie.getVersion()); - cookie = it.next(); - assertNotNull(cookie); - assertEquals("myValue2", cookie.getValue()); - assertEquals("this is another comment", cookie.getComment()); - assertEquals("http://anotherurl.com", cookie.getCommentUrl()); - assertEquals(".anotherdomainsomewhere", cookie.getDomain()); - assertFalse(cookie.isDiscard()); - assertEquals(0, cookie.getMaxAge()); - assertEquals("/anotherpathsomewhere", cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertFalse(cookie.isSecure()); - assertEquals(2, cookie.getVersion()); - cookie = it.next(); - assertNotNull(cookie); - assertEquals("myValue3", cookie.getValue()); - assertNull(cookie.getComment()); - assertNull(cookie.getCommentUrl()); - assertNull(cookie.getDomain()); - assertFalse(cookie.isDiscard()); - assertEquals(0, cookie.getMaxAge()); - assertNull(cookie.getPath()); - assertTrue(cookie.getPorts().isEmpty()); - assertFalse(cookie.isSecure()); - assertEquals(2, cookie.getVersion()); - } - - @Test - public void testDecodingClientSideCookies() { - String source = "$Version=\"1\"; " + - "Part_Number=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " + - "Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\""; - - Set cookies = CookieDecoder.decode(source); - Iterator it = cookies.iterator(); - Cookie c; - - c = it.next(); - assertEquals(1, c.getVersion()); - assertEquals("Part_Number", c.getName()); - assertEquals("Rocket_Launcher_0001", c.getValue()); - assertEquals("/acme", c.getPath()); - assertNull(c.getComment()); - assertNull(c.getCommentUrl()); - assertNull(c.getDomain()); - assertTrue(c.getPorts().isEmpty()); - assertEquals(Long.MIN_VALUE, c.getMaxAge()); - - c = it.next(); - assertEquals(1, c.getVersion()); - assertEquals("Part_Number", c.getName()); - assertEquals("Riding_Rocket_0023", c.getValue()); - assertEquals("/acme/ammo", c.getPath()); - assertNull(c.getComment()); - assertNull(c.getCommentUrl()); - assertNull(c.getDomain()); - assertTrue(c.getPorts().isEmpty()); - assertEquals(Long.MIN_VALUE, c.getMaxAge()); - - assertFalse(it.hasNext()); - } - - @Test - public void testDecodingCommaSeparatedClientSideCookies() { - String source = - "$Version=\"1\"; session_id=\"1234\", " + - "$Version=\"1\"; session_id=\"1111\"; $Domain=\".cracker.edu\""; - - Set cookies = CookieDecoder.decode(source); - Iterator it = cookies.iterator(); - Cookie c; - - assertTrue(it.hasNext()); - c = it.next(); - assertEquals(1, c.getVersion()); - assertEquals("session_id", c.getName()); - assertEquals("1234", c.getValue()); - assertNull(c.getPath()); - assertNull(c.getComment()); - assertNull(c.getCommentUrl()); - assertNull(c.getDomain()); - assertTrue(c.getPorts().isEmpty()); - assertEquals(Long.MIN_VALUE, c.getMaxAge()); - - assertTrue(it.hasNext()); - c = it.next(); - assertEquals(1, c.getVersion()); - assertEquals("session_id", c.getName()); - assertEquals("1111", c.getValue()); - assertEquals(".cracker.edu", c.getDomain()); - assertNull(c.getPath()); - assertNull(c.getComment()); - assertNull(c.getCommentUrl()); - assertTrue(c.getPorts().isEmpty()); - assertEquals(Long.MIN_VALUE, c.getMaxAge()); - - assertFalse(it.hasNext()); - } - - @Test - public void testDecodingQuotedCookie() { - String source = - "a=\"\"," + - "b=\"1\"," + - "c=\"\\\"1\\\"2\\\"\"," + - "d=\"1\\\"2\\\"3\"," + - "e=\"\\\"\\\"\"," + - "f=\"1\\\"\\\"2\"," + - "g=\"\\\\\"," + - "h=\"';,\\x\""; - - Set cookies = CookieDecoder.decode(source); - Iterator it = cookies.iterator(); - Cookie c; - - c = it.next(); - assertEquals("a", c.getName()); - assertEquals("", c.getValue()); - - c = it.next(); - assertEquals("b", c.getName()); - assertEquals("1", c.getValue()); - - c = it.next(); - assertEquals("c", c.getName()); - assertEquals("\"1\"2\"", c.getValue()); - - c = it.next(); - assertEquals("d", c.getName()); - assertEquals("1\"2\"3", c.getValue()); - - c = it.next(); - assertEquals("e", c.getName()); - assertEquals("\"\"", c.getValue()); - - c = it.next(); - assertEquals("f", c.getName()); - assertEquals("1\"\"2", c.getValue()); - - c = it.next(); - assertEquals("g", c.getName()); - assertEquals("\\", c.getValue()); - - c = it.next(); - assertEquals("h", c.getName()); - assertEquals("';,\\x", c.getValue()); - - assertFalse(it.hasNext()); - } - - @Test - public void testDecodingGoogleAnalyticsCookie() { - String source = - "ARPT=LWUKQPSWRTUN04CKKJI; " + - "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished furniture; " + - "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + - "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + - "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + - "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html"; - Set cookies = CookieDecoder.decode(source); - Iterator it = cookies.iterator(); - Cookie c; - - c = it.next(); - assertEquals("__utma", c.getName()); - assertEquals("48461872.1094088325.1258140131.1258140131.1258140131.1", c.getValue()); - - c = it.next(); - assertEquals("__utmb", c.getName()); - assertEquals("48461872.13.10.1258140131", c.getValue()); - - c = it.next(); - assertEquals("__utmc", c.getName()); - assertEquals("48461872", c.getValue()); - - c = it.next(); - assertEquals("__utmz", c.getName()); - assertEquals("48461872.1258140131.1.1.utmcsr=overstock.com|" + - "utmccn=(referral)|utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html", - c.getValue()); - - c = it.next(); - assertEquals("ARPT", c.getName()); - assertEquals("LWUKQPSWRTUN04CKKJI", c.getValue()); - - c = it.next(); - assertEquals("kw-2E343B92-B097-442c-BFA5-BE371E0325A2", c.getName()); - assertEquals("unfinished furniture", c.getValue()); - - assertFalse(it.hasNext()); - } - - @Test - public void testDecodingLongDates() { - Calendar cookieDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - cookieDate.set(9999, Calendar.DECEMBER, 31, 23, 59, 59); - long expectedMaxAge = (cookieDate.getTimeInMillis() - System.currentTimeMillis()) / 1000; - - String source = "Format=EU; expires=Fri, 31-Dec-9999 23:59:59 GMT; path=/"; - - Set cookies = CookieDecoder.decode(source); - - Cookie c = cookies.iterator().next(); - assertTrue(Math.abs(expectedMaxAge - c.getMaxAge()) < 2); - } - - @Test - public void testDecodingValueWithComma() { - String source = "UserCookie=timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE;" + - " expires=Sat, 01-Dec-2012 10:53:31 GMT; path=/"; - - Set cookies = CookieDecoder.decode(source); - - Cookie c = cookies.iterator().next(); - assertEquals("timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE", c.getValue()); - } - - @Test - public void testDecodingWeirdNames1() { - String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com"; - Set cookies = CookieDecoder.decode(src); - Cookie c = cookies.iterator().next(); - assertEquals("path", c.getName()); - assertEquals("", c.getValue()); - assertEquals("/", c.getPath()); - } - - @Test - public void testDecodingWeirdNames2() { - String src = "HTTPOnly="; - Set cookies = CookieDecoder.decode(src); - Cookie c = cookies.iterator().next(); - assertEquals("HTTPOnly", c.getName()); - assertEquals("", c.getValue()); - } - - @Test - public void testDecodingValuesWithCommasAndEquals() { - String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"; - Set cookies = CookieDecoder.decode(src); - Iterator i = cookies.iterator(); - Cookie c = i.next(); - assertEquals("A", c.getName()); - assertEquals("v=1&lg=en-US,it-IT,it&intl=it&np=1", c.getValue()); - c = i.next(); - assertEquals("T", c.getName()); - assertEquals("z=E", c.getValue()); - } - - @Test - public void testDecodingLongValue() { - String longValue = - "b!!!$Q!!$ha!!!!!!" + - "%=J^wI!!3iD!!!!$=HbQW!!3iF!!!!#=J^wI!!3iH!!!!%=J^wI!!3iM!!!!%=J^wI!!3iS!!!!" + - "#=J^wI!!3iU!!!!%=J^wI!!3iZ!!!!#=J^wI!!3i]!!!!%=J^wI!!3ig!!!!%=J^wI!!3ij!!!!" + - "%=J^wI!!3ik!!!!#=J^wI!!3il!!!!$=HbQW!!3in!!!!%=J^wI!!3ip!!!!$=HbQW!!3iq!!!!" + - "$=HbQW!!3it!!!!%=J^wI!!3ix!!!!#=J^wI!!3j!!!!!$=HbQW!!3j%!!!!$=HbQW!!3j'!!!!" + - "%=J^wI!!3j(!!!!%=J^wI!!9mJ!!!!'=KqtH!!=SE!!M!!!!" + - "'=KqtH!!s1X!!!!$=MMyc!!s1_!!!!#=MN#O!!ypn!!!!'=KqtH!!ypr!!!!'=KqtH!#%h!!!!!" + - "%=KqtH!#%o!!!!!'=KqtH!#)H6!!!!!!'=KqtH!#]9R!!!!$=H/Lt!#]I6!!!!#=KqtH!#]Z#!!!!%=KqtH!#^*N!!!!" + - "#=KqtH!#^:m!!!!#=KqtH!#_*_!!!!%=J^wI!#`-7!!!!#=KqtH!#`T>!!!!'=KqtH!#`T?!!!!" + - "'=KqtH!#`TA!!!!'=KqtH!#`TB!!!!'=KqtH!#`TG!!!!'=KqtH!#`TP!!!!#=KqtH!#`U,!!!!" + - "'=KqtH!#`U/!!!!'=KqtH!#`U0!!!!#=KqtH!#`U9!!!!'=KqtH!#aEQ!!!!%=KqtH!#b<)!!!!" + - "'=KqtH!#c9-!!!!%=KqtH!#dxC!!!!%=KqtH!#dxE!!!!%=KqtH!#ev$!!!!'=KqtH!#fBi!!!!" + - "#=KqtH!#fBj!!!!'=KqtH!#fG)!!!!'=KqtH!#fG+!!!!'=KqtH!#g*B!!!!'=KqtH!$>hD!!!!+=J^x0!$?lW!!!!'=KqtH!$?ll!!!!'=KqtH!$?lm!!!!" + - "%=KqtH!$?mi!!!!'=KqtH!$?mx!!!!'=KqtH!$D7]!!!!#=J_#p!$D@T!!!!#=J_#p!$V cookies = CookieDecoder.decode("bh=\"" + longValue + "\";"); - assertEquals(1, cookies.size()); - Cookie c = cookies.iterator().next(); - assertEquals("bh", c.getName()); - assertEquals(longValue, c.getValue()); - } -} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/CookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/CookieEncoderTest.java deleted file mode 100644 index e17a2d1531..0000000000 --- a/codec-http/src/test/java/io/netty/handler/codec/http/CookieEncoderTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2012 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package io.netty.handler.codec.http; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.text.DateFormat; -import java.text.ParseException; -import java.util.Date; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.junit.Test; - -public class CookieEncoderTest { - @Test - public void testEncodingSingleCookieV0() { - String result = "myCookie=myValue; Expires=XXX; Path=/apathsomewhere; Domain=.adomainsomewhere; Secure"; - DateFormat df = HttpHeaderDateFormat.get(); - Cookie cookie = new DefaultCookie("myCookie", "myValue"); - cookie.setComment("this is a Comment"); - cookie.setCommentUrl("http://aurl.com"); - cookie.setDomain(".adomainsomewhere"); - cookie.setDiscard(true); - cookie.setMaxAge(50); - cookie.setPath("/apathsomewhere"); - cookie.setPorts(80, 8080); - cookie.setSecure(true); - - String encodedCookie = ServerCookieEncoder.encode(cookie); - - long currentTime = System.currentTimeMillis(); - boolean fail = true; - // +/- 10-second tolerance - for (int delta = 0; delta <= 20000; delta += 250) { - if (encodedCookie.equals(result.replace( - "XXX", df.format(new Date(currentTime + 40000 + delta))))) { - fail = false; - break; - } - } - - if (fail) { - fail("Expected: " + result + ", Actual: " + encodedCookie); - } - } - - private void matchCookie(String cookieValue, String pattern, int maxAge) throws ParseException { - Matcher matcher = Pattern.compile(pattern).matcher(cookieValue); - assertTrue(matcher.find()); - Date expiresDate = HttpHeaderDateFormat.get().parse(matcher.group(1)); - long diff = (expiresDate.getTime() - System.currentTimeMillis()) / 1000; - // 1 sec should be fine - assertTrue(Math.abs(diff - maxAge) <= 1); - } - - @Test - public void testEncodingSingleCookieV1() throws ParseException { - int maxAge = 50; - String result = "myCookie=myValue; Max-Age=" + maxAge + "; Expires=(.+?); Path=\"/apathsomewhere\"; " + - "Domain=.adomainsomewhere; Secure; Comment=\"this is a Comment\"; Version=1"; - Cookie cookie = new DefaultCookie("myCookie", "myValue"); - cookie.setVersion(1); - cookie.setComment("this is a Comment"); - cookie.setDomain(".adomainsomewhere"); - cookie.setMaxAge(maxAge); - cookie.setPath("/apathsomewhere"); - cookie.setSecure(true); - String encodedCookie = ServerCookieEncoder.encode(cookie); - matchCookie(encodedCookie, result, maxAge); - } - - @Test - public void testEncodingSingleCookieV2() throws ParseException { - int maxAge = 50; - String result = "myCookie=myValue; Max-Age=" + maxAge + "; Expires=(.+?); Path=\"/apathsomewhere\"; " + - "Domain=.adomainsomewhere; Secure; Comment=\"this is a Comment\"; Version=1; " + - "CommentURL=\"http://aurl.com\"; Port=\"80,8080\"; Discard"; - Cookie cookie = new DefaultCookie("myCookie", "myValue"); - cookie.setVersion(1); - cookie.setComment("this is a Comment"); - cookie.setCommentUrl("http://aurl.com"); - cookie.setDomain(".adomainsomewhere"); - cookie.setDiscard(true); - cookie.setMaxAge(maxAge); - cookie.setPath("/apathsomewhere"); - cookie.setPorts(80, 8080); - cookie.setSecure(true); - String encodedCookie = ServerCookieEncoder.encode(cookie); - matchCookie(encodedCookie, result, maxAge); - } - - @Test - public void testEncodingMultipleClientCookies() { - String c1 = "$Version=1; myCookie=myValue; $Path=\"/apathsomewhere\"; " + - "$Domain=.adomainsomewhere; $Port=\"80,8080\"; "; - String c2 = "$Version=1; myCookie2=myValue2; $Path=\"/anotherpathsomewhere\"; " + - "$Domain=.anotherdomainsomewhere; "; - String c3 = "$Version=1; myCookie3=myValue3"; - Cookie cookie = new DefaultCookie("myCookie", "myValue"); - cookie.setVersion(1); - cookie.setComment("this is a Comment"); - cookie.setCommentUrl("http://aurl.com"); - cookie.setDomain(".adomainsomewhere"); - cookie.setDiscard(true); - cookie.setMaxAge(50); - cookie.setPath("/apathsomewhere"); - cookie.setPorts(80, 8080); - cookie.setSecure(true); - Cookie cookie2 = new DefaultCookie("myCookie2", "myValue2"); - cookie2.setVersion(1); - cookie2.setComment("this is another Comment"); - cookie2.setCommentUrl("http://anotherurl.com"); - cookie2.setDomain(".anotherdomainsomewhere"); - cookie2.setDiscard(false); - cookie2.setPath("/anotherpathsomewhere"); - cookie2.setSecure(false); - Cookie cookie3 = new DefaultCookie("myCookie3", "myValue3"); - cookie3.setVersion(1); - String encodedCookie = ClientCookieEncoder.encode(cookie, cookie2, cookie3); - assertEquals(c1 + c2 + c3, encodedCookie); - } - - @Test - public void testEncodingWithNoCookies() { - String encodedCookie1 = ClientCookieEncoder.encode(); - List encodedCookie2 = ServerCookieEncoder.encode(); - assertNotNull(encodedCookie1); - assertNotNull(encodedCookie2); - } -} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java new file mode 100644 index 0000000000..bf596e4f7a --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java @@ -0,0 +1,280 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.junit.Test; + +import io.netty.handler.codec.http.HttpHeaderDateFormat; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.TimeZone; + +public class ClientCookieDecoderTest { + @Test + public void testDecodingSingleCookieV0() { + String cookieString = "myCookie=myValue;expires=XXX;path=/apathsomewhere;domain=.adomainsomewhere;secure;"; + cookieString = cookieString.replace("XXX", HttpHeaderDateFormat.get() + .format(new Date(System.currentTimeMillis() + 50000))); + + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + + boolean fail = true; + for (int i = 40; i <= 60; i++) { + if (cookie.maxAge() == i) { + fail = false; + break; + } + } + if (fail) { + fail("expected: 50, actual: " + cookie.maxAge()); + } + + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV0ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=0;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV1() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere" + + ";secure;comment=this is a comment;version=1;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertEquals("myValue", cookie.value()); + assertNotNull(cookie); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV1ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;" + + "commentURL=http://aurl.com;port='80,8080';discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV2() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingComplexCookie() { + String c1 = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=\"http://aurl.com\";port='80,8080';discard;"; + + Cookie cookie = ClientCookieDecoder.STRICT.decode(c1); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingQuotedCookie() { + Collection sources = new ArrayList(); + sources.add("a=\"\","); + sources.add("b=\"1\","); + + Collection cookies = new ArrayList(); + for (String source : sources) { + cookies.add(ClientCookieDecoder.STRICT.decode(source)); + } + + Iterator it = cookies.iterator(); + Cookie c; + + c = it.next(); + assertEquals("a", c.name()); + assertEquals("", c.value()); + + c = it.next(); + assertEquals("b", c.name()); + assertEquals("1", c.value()); + + assertFalse(it.hasNext()); + } + + @Test + public void testDecodingGoogleAnalyticsCookie() { + String source = "ARPT=LWUKQPSWRTUN04CKKJI; " + + "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished furniture; " + + "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + + "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + + "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + + "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance,/clearance,/32/dept.html"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + + assertEquals("ARPT", cookie.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value()); + } + + @Test + public void testDecodingLongDates() { + Calendar cookieDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cookieDate.set(9999, Calendar.DECEMBER, 31, 23, 59, 59); + long expectedMaxAge = (cookieDate.getTimeInMillis() - System + .currentTimeMillis()) / 1000; + + String source = "Format=EU; expires=Fri, 31-Dec-9999 23:59:59 GMT; path=/"; + + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + + assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2); + } + + @Test + public void testDecodingValueWithCommaFails() { + String source = "UserCookie=timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE;" + + " expires=Sat, 01-Dec-2012 10:53:31 GMT; path=/"; + + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); + + assertNull(cookie); + } + + @Test + public void testDecodingWeirdNames1() { + String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertEquals("path", cookie.name()); + assertEquals("", cookie.value()); + assertEquals("/", cookie.path()); + } + + @Test + public void testDecodingWeirdNames2() { + String src = "HTTPOnly="; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertEquals("HTTPOnly", cookie.name()); + assertEquals("", cookie.value()); + } + + @Test + public void testDecodingValuesWithCommasAndEqualsFails() { + String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"; + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); + assertNull(cookie); + } + + @Test + public void testDecodingLongValue() { + String longValue = + "b___$Q__$ha______" + + "%=J^wI__3iD____$=HbQW__3iF____#=J^wI__3iH____%=J^wI__3iM____%=J^wI__3iS____" + + "#=J^wI__3iU____%=J^wI__3iZ____#=J^wI__3i]____%=J^wI__3ig____%=J^wI__3ij____" + + "%=J^wI__3ik____#=J^wI__3il____$=HbQW__3in____%=J^wI__3ip____$=HbQW__3iq____" + + "$=HbQW__3it____%=J^wI__3ix____#=J^wI__3j_____$=HbQW__3j%____$=HbQW__3j'____" + + "%=J^wI__3j(____%=J^wI__9mJ____'=KqtH__=SE__M____" + + "'=KqtH__s1X____$=MMyc__s1_____#=MN#O__ypn____'=KqtH__ypr____'=KqtH_#%h_____" + + "%=KqtH_#%o_____'=KqtH_#)H6______'=KqtH_#]9R____$=H/Lt_#]I6____#=KqtH_#]Z#____%=KqtH_#^*N____" + + "#=KqtH_#^:m____#=KqtH_#_*_____%=J^wI_#`-7____#=KqtH_#`T>____'=KqtH_#`T?____" + + "'=KqtH_#`TA____'=KqtH_#`TB____'=KqtH_#`TG____'=KqtH_#`TP____#=KqtH_#`U_____" + + "'=KqtH_#`U/____'=KqtH_#`U0____#=KqtH_#`U9____'=KqtH_#aEQ____%=KqtH_#b<)____" + + "'=KqtH_#c9-____%=KqtH_#dxC____%=KqtH_#dxE____%=KqtH_#ev$____'=KqtH_#fBi____" + + "#=KqtH_#fBj____'=KqtH_#fG)____'=KqtH_#fG+____'=KqtH_#g*B____'=KqtH_$>hD____+=J^x0_$?lW____'=KqtH_$?ll____'=KqtH_$?lm____" + + "%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V cookies = ServerCookieDecoder.STRICT.decode(cookieString); + assertEquals(1, cookies.size()); + Cookie cookie = cookies.iterator().next(); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + } + + @Test + public void testDecodingMultipleCookies() { + String c1 = "myCookie=myValue;"; + String c2 = "myCookie2=myValue2;"; + String c3 = "myCookie3=myValue3;"; + + Set cookies = ServerCookieDecoder.STRICT.decode(c1 + c2 + c3); + assertEquals(3, cookies.size()); + Iterator it = cookies.iterator(); + Cookie cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue2", cookie.value()); + cookie = it.next(); + assertNotNull(cookie); + assertEquals("myValue3", cookie.value()); + } + + @Test + public void testDecodingGoogleAnalyticsCookie() { + String source = + "ARPT=LWUKQPSWRTUN04CKKJI; " + + "kw-2E343B92-B097-442c-BFA5-BE371E0325A2=unfinished_furniture; " + + "__utma=48461872.1094088325.1258140131.1258140131.1258140131.1; " + + "__utmb=48461872.13.10.1258140131; __utmc=48461872; " + + "__utmz=48461872.1258140131.1.1.utmcsr=overstock.com|utmccn=(referral)|" + + "utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance/clearance/32/dept.html"; + Set cookies = ServerCookieDecoder.STRICT.decode(source); + Iterator it = cookies.iterator(); + Cookie c; + + c = it.next(); + assertEquals("__utma", c.name()); + assertEquals("48461872.1094088325.1258140131.1258140131.1258140131.1", c.value()); + + c = it.next(); + assertEquals("__utmb", c.name()); + assertEquals("48461872.13.10.1258140131", c.value()); + + c = it.next(); + assertEquals("__utmc", c.name()); + assertEquals("48461872", c.value()); + + c = it.next(); + assertEquals("__utmz", c.name()); + assertEquals("48461872.1258140131.1.1.utmcsr=overstock.com|" + + "utmccn=(referral)|utmcmd=referral|utmcct=/Home-Garden/Furniture/Clearance/clearance/32/dept.html", + c.value()); + + c = it.next(); + assertEquals("ARPT", c.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", c.value()); + + c = it.next(); + assertEquals("kw-2E343B92-B097-442c-BFA5-BE371E0325A2", c.name()); + assertEquals("unfinished_furniture", c.value()); + + assertFalse(it.hasNext()); + } + + @Test + public void testDecodingLongValue() { + String longValue = + "b___$Q__$ha______" + + "%=J^wI__3iD____$=HbQW__3iF____#=J^wI__3iH____%=J^wI__3iM____%=J^wI__3iS____" + + "#=J^wI__3iU____%=J^wI__3iZ____#=J^wI__3i]____%=J^wI__3ig____%=J^wI__3ij____" + + "%=J^wI__3ik____#=J^wI__3il____$=HbQW__3in____%=J^wI__3ip____$=HbQW__3iq____" + + "$=HbQW__3it____%=J^wI__3ix____#=J^wI__3j_____$=HbQW__3j%____$=HbQW__3j'____" + + "%=J^wI__3j(____%=J^wI__9mJ____'=KqtH__=SE__M____" + + "'=KqtH__s1X____$=MMyc__s1_____#=MN#O__ypn____'=KqtH__ypr____'=KqtH_#%h_____" + + "%=KqtH_#%o_____'=KqtH_#)H6______'=KqtH_#]9R____$=H/Lt_#]I6____#=KqtH_#]Z#____%=KqtH_#^*N____" + + "#=KqtH_#^:m____#=KqtH_#_*_____%=J^wI_#`-7____#=KqtH_#`T>____'=KqtH_#`T?____" + + "'=KqtH_#`TA____'=KqtH_#`TB____'=KqtH_#`TG____'=KqtH_#`TP____#=KqtH_#`U_____" + + "'=KqtH_#`U/____'=KqtH_#`U0____#=KqtH_#`U9____'=KqtH_#aEQ____%=KqtH_#b<)____" + + "'=KqtH_#c9-____%=KqtH_#dxC____%=KqtH_#dxE____%=KqtH_#ev$____'=KqtH_#fBi____" + + "#=KqtH_#fBj____'=KqtH_#fG)____'=KqtH_#fG+____'=KqtH_#g*B____'=KqtH_$>hD____+=J^x0_$?lW____'=KqtH_$?ll____'=KqtH_$?lm____" + + "%=KqtH_$?mi____'=KqtH_$?mx____'=KqtH_$D7]____#=J_#p_$D@T____#=J_#p_$V cookies = ServerCookieDecoder.STRICT.decode("bh=\"" + longValue + "\";"); + assertEquals(1, cookies.size()); + Cookie c = cookies.iterator().next(); + assertEquals("bh", c.name()); + assertEquals(longValue, c.value()); + } + + @Test + public void testDecodingOldRFC2965Cookies() { + String source = "$Version=\"1\"; " + + "Part_Number1=\"Riding_Rocket_0023\"; $Path=\"/acme/ammo\"; " + + "Part_Number2=\"Rocket_Launcher_0001\"; $Path=\"/acme\""; + + Set cookies = ServerCookieDecoder.STRICT.decode(source); + Iterator it = cookies.iterator(); + Cookie c; + + c = it.next(); + assertEquals("Part_Number1", c.name()); + assertEquals("Riding_Rocket_0023", c.value()); + + c = it.next(); + assertEquals("Part_Number2", c.name()); + assertEquals("Rocket_Launcher_0001", c.value()); + + assertFalse(it.hasNext()); + } + + @Test + public void testRejectCookieValueWithSemicolon() { + Set cookies = ServerCookieDecoder.STRICT.decode("name=\"foo;bar\";"); + assertTrue(cookies.isEmpty()); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java new file mode 100644 index 0000000000..350499046a --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.codec.http.cookie; + +import org.junit.Test; + +import io.netty.handler.codec.http.HttpHeaderDateFormat; + +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.*; + +public class ServerCookieEncoderTest { + + @Test + public void testEncodingSingleCookieV0() throws ParseException { + + int maxAge = 50; + + String result = + "myCookie=myValue; Max-Age=50; Expires=(.+?); Path=/apathsomewhere; Domain=.adomainsomewhere; Secure"; + Cookie cookie = new DefaultCookie("myCookie", "myValue"); + cookie.setDomain(".adomainsomewhere"); + cookie.setMaxAge(maxAge); + cookie.setPath("/apathsomewhere"); + cookie.setSecure(true); + + String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie); + + Matcher matcher = Pattern.compile(result).matcher(encodedCookie); + assertTrue(matcher.find()); + Date expiresDate = HttpHeaderDateFormat.get().parse(matcher.group(1)); + long diff = (expiresDate.getTime() - System.currentTimeMillis()) / 1000; + // 2 secs should be fine + assertTrue(Math.abs(diff - maxAge) <= 2); + } + + @Test + public void testEncodingWithNoCookies() { + String encodedCookie1 = ClientCookieEncoder.STRICT.encode(); + List encodedCookie2 = ServerCookieEncoder.STRICT.encode(); + assertNull(encodedCookie1); + assertNotNull(encodedCookie2); + assertTrue(encodedCookie2.isEmpty()); + } +}