From 97d871a7553a01384b43df855dccdda5205ae77a Mon Sep 17 00:00:00 2001 From: Stephane Landelle Date: Tue, 14 Apr 2015 13:57:32 +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 | 60 +--- .../io/netty/handler/codec/http/Cookie.java | 142 +++------- .../handler/codec/http/CookieDecoder.java | 106 ++++++- .../handler/codec/http/CookieEncoderUtil.java | 96 ------- .../netty/handler/codec/http/CookieUtil.java | 104 +++++++ .../handler/codec/http/DefaultCookie.java | 265 ++--------------- .../codec/http/HttpHeaderDateFormat.java | 4 +- .../netty/handler/codec/http/HttpRequest.java | 19 +- .../handler/codec/http/HttpResponse.java | 16 +- .../codec/http/ServerCookieDecoder.java | 182 ------------ .../codec/http/ServerCookieEncoder.java | 100 +------ .../{ => cookie}/ClientCookieDecoder.java | 180 +++++------- .../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 ++ .../codec/http/ServerCookieDecoderTest.java | 228 --------------- .../{ => cookie}/ClientCookieDecoderTest.java | 168 +++++------ .../{ => cookie}/ClientCookieEncoderTest.java | 24 +- .../http/cookie/ServerCookieDecoderTest.java | 185 ++++++++++++ .../{ => cookie}/ServerCookieEncoderTest.java | 10 +- .../example/http/snoop/HttpSnoopClient.java | 6 +- .../http/snoop/HttpSnoopServerHandler.java | 14 +- .../example/http/upload/HttpUploadClient.java | 6 +- .../http/upload/HttpUploadServerHandler.java | 12 +- 31 files changed, 1863 insertions(+), 1290 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 delete mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieDecoder.java rename codec-http/src/main/java/io/netty/handler/codec/http/{ => cookie}/ClientCookieDecoder.java (53%) 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/ServerCookieDecoderTest.java rename codec-http/src/test/java/io/netty/handler/codec/http/{ => cookie}/ClientCookieDecoderTest.java (54%) rename codec-http/src/test/java/io/netty/handler/codec/http/{ => cookie}/ClientCookieEncoderTest.java (73%) create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieDecoderTest.java rename codec-http/src/test/java/io/netty/handler/codec/http/{ => cookie}/ServerCookieEncoderTest.java (85%) 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 6cefdb674c..0df3078705 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.*; - /** * A RFC6265 compliant cookie encoder to be used client side, * so only name=value pairs are sent. @@ -34,6 +32,7 @@ import static io.netty.handler.codec.http.CookieEncoderUtil.*; * * @see ClientCookieDecoder */ +@Deprecated public final class ClientCookieEncoder { /** @@ -43,8 +42,9 @@ public final class ClientCookieEncoder { * @param value the cookie value * @return a Rfc6265 style 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.ClientCookieEncoder.LAX.encode(name, value); } /** @@ -53,14 +53,9 @@ public final class ClientCookieEncoder { * @param specified the cookie * @return a Rfc6265 style Cookie header 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); } /** @@ -69,24 +64,9 @@ public final class ClientCookieEncoder { * @param cookies some cookies * @return a Rfc6265 style Cookie header value, null if no cookies are passed. */ + @Deprecated public static String encode(Cookie... cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - if (cookies.length == 0) { - return null; - } - - StringBuilder buf = stringBuilder(); - for (Cookie c : cookies) { - if (c == null) { - break; - } - - encode(buf, c); - } - return stripTrailingSeparatorOrNull(buf); + return io.netty.handler.codec.http.cookie.ClientCookieEncoder.LAX.encode(cookies); } /** @@ -95,31 +75,9 @@ public final class ClientCookieEncoder { * @param cookies some cookies * @return a Rfc6265 style Cookie header value, null if no cookies are passed. */ + @Deprecated public static String encode(Iterable cookies) { - if (cookies == null) { - throw new NullPointerException("cookies"); - } - - if (!cookies.iterator().hasNext()) { - return null; - } - - StringBuilder buf = stringBuilder(); - for (Cookie c : cookies) { - if (c == null) { - break; - } - - encode(buf, c); - } - return stripTrailingSeparatorOrNull(buf); - } - - private static void encode(StringBuilder buf, Cookie c) { - // rawValue > value > "" - String value = c.rawValue() != null ? c.rawValue() - : c.value() != null ? c.value() : ""; - addUnquoted(buf, c.name(), value); + 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 48cbea4df2..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,8 +20,10 @@ 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 { /** * @deprecated Use {@link #name()} instead. @@ -29,88 +31,24 @@ public interface Cookie extends Comparable { @Deprecated String getName(); - /** - * Returns the name of this {@link Cookie}. - * - * @return The name of this {@link Cookie} - */ - String name(); - /** * @deprecated Use {@link #value()} instead. */ @Deprecated String getValue(); - /** - * 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 the raw value of this {@link Cookie}, - * as it was set in original Set-Cookie header. - * - * @return The raw value of this {@link Cookie} - */ - String rawValue(); - - /** - * Sets the raw value of this {@link Cookie}. - * - * @param rawValue The raw value to set - */ - void setRawValue(String rawValue); - /** * @deprecated Use {@link #domain()} instead. */ @Deprecated String getDomain(); - /** - * 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); - /** * @deprecated Use {@link #path()} instead. */ @Deprecated String getPath(); - /** - * 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); - /** * @deprecated Use {@link #comment()} instead. */ @@ -121,14 +59,20 @@ public interface Cookie extends Comparable { * Returns the comment of this {@link Cookie}. * * @return The comment of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ + @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); /** @@ -141,7 +85,10 @@ public interface Cookie extends Comparable { * 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 */ + @Deprecated long maxAge(); /** @@ -152,7 +99,10 @@ 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); /** @@ -165,51 +115,22 @@ public interface Cookie extends Comparable { * Returns the version of this {@link Cookie}. * * @return The version of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ + @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 - */ - 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 Use {@link #commentUrl()} instead. */ @@ -220,14 +141,20 @@ public interface Cookie extends Comparable { * Returns the comment URL of this {@link Cookie}. * * @return The comment URL of this {@link Cookie} + * + * @deprecated Not part of RFC6265 */ + @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); /** @@ -235,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(); /** @@ -244,7 +174,10 @@ 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); /** @@ -257,14 +190,20 @@ public interface Cookie extends Comparable { * 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 */ + @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); /** @@ -272,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 a9573dc0fe..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,7 +31,8 @@ import java.util.Set; import java.util.TreeSet; /** - * @deprecated Use {@link ClientCookieDecoder} or {@link ServerCookieDecoder} instead. + * @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. @@ -36,20 +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); @@ -63,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) { @@ -87,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; @@ -103,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; @@ -129,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 { @@ -168,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;;) { @@ -290,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 2155a96b41..0000000000 --- a/codec-http/src/main/java/io/netty/handler/codec/http/CookieEncoderUtil.java +++ /dev/null @@ -1,96 +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(); - } - - /** - * @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, 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 61cc73a230..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 @@ -19,63 +19,26 @@ import java.util.Collections; import java.util.Set; 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 rawValue; - 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 @@ -84,75 +47,24 @@ public class DefaultCookie implements Cookie { return name(); } - @Override - public String name() { - return name; - } - @Override @Deprecated public String getValue() { return value(); } - @Override - public String value() { - return value; - } - - @Override - public void setValue(String value) { - if (value == null) { - throw new NullPointerException("value"); - } - this.value = value; - } - - @Override - public String rawValue() { - return rawValue; - } - - @Override - public void setRawValue(String rawValue) { - if (value == null) { - throw new NullPointerException("rawValue"); - } - this.rawValue = rawValue; - } - @Override @Deprecated public String getDomain() { return domain(); } - @Override - public String domain() { - return domain; - } - - @Override - public void setDomain(String domain) { - this.domain = validateValue("domain", domain); - } - @Override @Deprecated public String getPath() { return path(); } - @Override - public String path() { - return path; - } - - @Override - public void setPath(String path) { - this.path = validateValue("path", path); - } - @Override @Deprecated public String getComment() { @@ -160,11 +72,13 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public String comment() { return comment; } @Override + @Deprecated public void setComment(String comment) { this.comment = validateValue("comment", comment); } @@ -176,21 +90,25 @@ public class DefaultCookie implements Cookie { } @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; } @@ -202,6 +120,7 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public Set ports() { if (unmodifiablePorts == null) { unmodifiablePorts = Collections.unmodifiableSet(ports); @@ -210,6 +129,7 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public void setPorts(int... ports) { if (ports == null) { throw new NullPointerException("ports"); @@ -232,6 +152,7 @@ public class DefaultCookie implements Cookie { } @Override + @Deprecated public void setPorts(Iterable ports) { Set newPorts = new TreeSet(); for (int p: ports) { @@ -254,16 +175,6 @@ public class DefaultCookie implements Cookie { return maxAge(); } - @Override - public long maxAge() { - return maxAge; - } - - @Override - public void setMaxAge(long maxAge) { - this.maxAge = maxAge; - } - @Override @Deprecated public int getVersion() { @@ -271,158 +182,14 @@ public class DefaultCookie implements Cookie { } @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 name().hashCode(); - } - - @Override - public boolean equals(Object o) { - 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; - 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 = new StringBuilder() - .append(name()) - .append('=') - .append(value()); - if (domain() != null) { - buf.append(", domain=") - .append(domain()); - } - if (path() != null) { - buf.append(", path=") - .append(path()); - } - if (comment() != null) { - buf.append(", comment=") - .append(comment()); - } - if (maxAge() >= 0) { - buf.append(", maxAge=") - .append(maxAge()) - .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 24d884bc86..a089a1856a 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 6db9a2e7e9..775926bd92 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,22 +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 ClientCookieDecoder}, {@link ServerCookieDecoder}, - * {@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 ClientCookieDecoder - * @see ServerCookieDecoder + * @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 4b78ec728b..bcfd2f0309 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 @@ -15,20 +15,22 @@ */ package io.netty.handler.codec.http; - /** * An HTTP response. * *

    Accessing Cookies

    *

    - * Unlike the Servlet API, {@link Cookie} support is provided separately via {@link ClientCookieDecoder}, - * {@link ServerCookieDecoder}, {@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 ClientCookieEncoder - * @see ServerCookieEncoder - * @see ClientCookieDecoder - * @see ServerCookieDecoder + * @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/ServerCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieDecoder.java deleted file mode 100644 index d5361da4c4..0000000000 --- a/codec-http/src/main/java/io/netty/handler/codec/http/ServerCookieDecoder.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * 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; - -import java.util.Collections; -import java.util.Set; -import java.util.TreeSet; - -import static io.netty.handler.codec.http.CookieEncoderUtil.*; - -/** - * 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 { - - /** - * Decodes the specified Set-Cookie HTTP header value into a {@link Cookie}. - * - * @return the decoded {@link Cookie} - */ - public static Set decode(String header) { - - if (header == null) { - throw new NullPointerException("header"); - } - - final int headerLen = header.length(); - - if (headerLen == 0) { - return Collections.emptySet(); - } - - Set cookies = new TreeSet(); - - int i = 0; - - boolean rfc2965Style = false; - if (header.regionMatches(true, 0, "$Version", 0, 8)) { - // 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 newNameStart = i; - int newNameEnd = i; - String value; - - if (i == headerLen) { - value = null; - } else { - keyValLoop: for (;;) { - - char curChar = header.charAt(i); - if (curChar == ';') { - // NAME; (no value till ';') - newNameEnd = i; - value = null; - break keyValLoop; - } else if (curChar == '=') { - // NAME=VALUE - newNameEnd = i; - i++; - if (i == headerLen) { - // NAME= (empty value, i.e. nothing after '=') - value = ""; - break keyValLoop; - } - - int newValueStart = i; - char c = header.charAt(i); - if (c == '"') { - // NAME="VALUE" - StringBuilder newValueBuf = stringBuilder(); - - final char q = c; - boolean hadBackslash = false; - i++; - for (;;) { - if (i == headerLen) { - value = newValueBuf.toString(); - break keyValLoop; - } - if (hadBackslash) { - hadBackslash = false; - c = header.charAt(i++); - if (c == '\\' || c == '"') { - // Escape last backslash. - newValueBuf.setCharAt(newValueBuf.length() - 1, c); - } else { - // Do not escape last backslash. - newValueBuf.append(c); - } - } else { - c = header.charAt(i++); - if (c == q) { - value = newValueBuf.toString(); - break keyValLoop; - } - newValueBuf.append(c); - if (c == '\\') { - hadBackslash = true; - } - } - } - } else { - // NAME=VALUE; - int semiPos = header.indexOf(';', i); - if (semiPos > 0) { - value = header.substring(newValueStart, semiPos); - i = semiPos; - } else { - value = header.substring(newValueStart); - i = headerLen; - } - } - break keyValLoop; - } else { - i++; - } - - if (i == headerLen) { - // NAME (no value till the end of string) - newNameEnd = headerLen; - value = null; - break; - } - } - } - - if (!rfc2965Style || (!header.regionMatches(newNameStart, "$Path", 0, "$Path".length()) && - !header.regionMatches(newNameStart, "$Domain", 0, "$Domain".length()) && - !header.regionMatches(newNameStart, "$Port", 0, "$Port".length()))) { - - // skip obsolete RFC2965 fields - String name = header.substring(newNameStart, newNameEnd); - cookies.add(new DefaultCookie(name, value)); - } - } - - return cookies; - } - - private ServerCookieDecoder() { - // unused - } -} 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 887333f204..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 @@ -15,12 +15,7 @@ */ package io.netty.handler.codec.http; -import static io.netty.handler.codec.http.CookieEncoderUtil.*; - -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.Date; import java.util.List; /** @@ -38,7 +33,10 @@ import java.util.List; * * * @see ServerCookieDecoder + * + * @deprecated Use {@link io.netty.handler.codec.http.cookie.ServerCookieEncoder} instead */ +@Deprecated public final class ServerCookieEncoder { /** @@ -48,8 +46,9 @@ public final class ServerCookieEncoder { * @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); } /** @@ -58,40 +57,9 @@ public final class ServerCookieEncoder { * @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(); - - addUnquoted(buf, cookie.name(), cookie.value()); - - if (cookie.maxAge() != Long.MIN_VALUE) { - add(buf, CookieHeaderNames.MAX_AGE, cookie.maxAge()); - Date expires = new Date(cookie.maxAge() * 1000 + System.currentTimeMillis()); - addUnquoted(buf, CookieHeaderNames.EXPIRES, HttpHeaderDateFormat.get().format(expires)); - } - - if (cookie.path() != null) { - addUnquoted(buf, CookieHeaderNames.PATH, cookie.path()); - } - - if (cookie.domain() != null) { - addUnquoted(buf, CookieHeaderNames.DOMAIN, cookie.domain()); - } - 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); - } - - return stripTrailingSeparator(buf); + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookie); } /** @@ -100,23 +68,9 @@ public final class ServerCookieEncoder { * @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"); - } - - if (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; + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookies); } /** @@ -125,23 +79,9 @@ public final class ServerCookieEncoder { * @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"); - } - - if (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; + return io.netty.handler.codec.http.cookie.ServerCookieEncoder.LAX.encode(cookies); } /** @@ -150,23 +90,9 @@ public final class ServerCookieEncoder { * @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"); - } - - if (!cookies.iterator().hasNext()) { - return Collections.emptyList(); - } - - 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/ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java similarity index 53% rename from codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieDecoder.java rename to codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java index d2e57dbed5..168b4a4aee 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/ClientCookieDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/cookie/ClientCookieDecoder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 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,35 +13,47 @@ * 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; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.handler.codec.http.HttpHeaderDateFormat; import java.text.ParsePosition; import java.util.Date; -import static io.netty.handler.codec.http.CookieEncoderUtil.*; - /** * A RFC6265 compliant cookie decoder to be used client side. * - * It will store the raw value in {@link Cookie#setRawValue(String)} so it can be + * 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 { +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 static Cookie decode(String header) { - - if (header == null) { - throw new NullPointerException("header"); - } - - final int headerLen = header.length(); + public Cookie decode(String header) { + final int headerLen = checkNotNull(header, "header").length(); if (headerLen == 0) { return null; @@ -70,89 +82,35 @@ public final class ClientCookieDecoder { break; } - int newNameStart = i; - int newNameEnd = i; - String value, rawValue; + int nameBegin = i; + int nameEnd = i; + int valueBegin = -1; + int valueEnd = -1; - if (i == headerLen) { - value = rawValue = null; - } else { + if (i != headerLen) { keyValLoop: for (;;) { char curChar = header.charAt(i); if (curChar == ';') { // NAME; (no value till ';') - newNameEnd = i; - value = rawValue = null; + nameEnd = i; + valueBegin = valueEnd = -1; break keyValLoop; + } else if (curChar == '=') { // NAME=VALUE - newNameEnd = i; + nameEnd = i; i++; if (i == headerLen) { // NAME= (empty value, i.e. nothing after '=') - value = rawValue = ""; + valueBegin = valueEnd = 0; break keyValLoop; } - int newValueStart = i; - char c = header.charAt(i); - if (c == '"') { - // NAME="VALUE" - StringBuilder newValueBuf = stringBuilder(); - - int rawValueStart = i; - int rawValueEnd = i; - - final char q = c; - boolean hadBackslash = false; - i++; - for (;;) { - if (i == headerLen) { - value = newValueBuf.toString(); - // only need to compute raw value for cookie - // value which is in first position - rawValue = header.substring(rawValueStart, rawValueEnd); - break keyValLoop; - } - if (hadBackslash) { - hadBackslash = false; - c = header.charAt(i++); - rawValueEnd = i; - if (c == '\\' || c == '"') { - newValueBuf.setCharAt(newValueBuf.length() - 1, c); - } else { - // Do not escape last backslash. - newValueBuf.append(c); - } - } else { - c = header.charAt(i++); - rawValueEnd = i; - if (c == q) { - value = newValueBuf.toString(); - // only need to compute raw value for - // cookie value which is in first - // position - rawValue = header.substring(rawValueStart, rawValueEnd); - break keyValLoop; - } - newValueBuf.append(c); - if (c == '\\') { - hadBackslash = true; - } - } - } - } else { - // NAME=VALUE; - int semiPos = header.indexOf(';', i); - if (semiPos > 0) { - value = rawValue = header.substring(newValueStart, semiPos); - i = semiPos; - } else { - value = rawValue = header.substring(newValueStart); - i = headerLen; - } - } + valueBegin = i; + // NAME=VALUE; + int semiPos = header.indexOf(';', i); + valueEnd = i = semiPos > 0 ? semiPos : headerLen; break keyValLoop; } else { i++; @@ -160,17 +118,31 @@ public final class ClientCookieDecoder { if (i == headerLen) { // NAME (no value till the end of string) - newNameEnd = i; - value = rawValue = null; + nameEnd = headerLen; + valueBegin = valueEnd = -1; break; } } } + if (valueEnd > 0 && header.charAt(valueEnd - 1) == ',') { + // old multiple cookies separator, skipping it + valueEnd--; + } + if (cookieBuilder == null) { - cookieBuilder = new CookieBuilder(header, newNameStart, newNameEnd, value, rawValue); + // cookie name-value pair + DefaultCookie cookie = initCookie(header, nameBegin, nameEnd, valueBegin, valueEnd); + + if (cookie == null) { + return null; + } + + cookieBuilder = new CookieBuilder(cookie); } else { - cookieBuilder.appendAttribute(header, newNameStart, newNameEnd, value); + // cookie attribute + String attrValue = valueBegin == -1 ? null : header.substring(valueBegin, valueEnd); + cookieBuilder.appendAttribute(header, nameBegin, nameEnd, attrValue); } } return cookieBuilder.cookie(); @@ -178,9 +150,7 @@ public final class ClientCookieDecoder { private static class CookieBuilder { - private final String name; - private final String value; - private final String rawValue; + private final DefaultCookie cookie; private String domain; private String path; private long maxAge = Long.MIN_VALUE; @@ -188,11 +158,8 @@ public final class ClientCookieDecoder { private boolean secure; private boolean httpOnly; - public CookieBuilder(String header, int keyStart, int keyEnd, - String value, String rawValue) { - name = header.substring(keyStart, keyEnd); - this.value = value; - this.rawValue = rawValue; + public CookieBuilder(DefaultCookie cookie) { + this.cookie = cookie; } private long mergeMaxAgeAndExpire(long maxAge, String expires) { @@ -210,13 +177,6 @@ public final class ClientCookieDecoder { } public Cookie cookie() { - if (name == null) { - return null; - } - - DefaultCookie cookie = new DefaultCookie(name, value); - cookie.setValue(value); - cookie.setRawValue(rawValue); cookie.setDomain(domain); cookie.setPath(path); cookie.setMaxAge(mergeMaxAgeAndExpire(maxAge, expires)); @@ -245,7 +205,6 @@ public final class ClientCookieDecoder { private void setCookieAttribute(String header, int keyStart, int keyEnd, String value) { - int length = keyEnd - keyStart; if (length == 4) { @@ -260,15 +219,15 @@ public final class ClientCookieDecoder { } private void parse4(String header, int nameStart, String value) { - if (header.regionMatches(true, nameStart, "Path", 0, 4)) { + 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, "Domain", 0, 5)) { - domain = value.isEmpty() ? null : value; - } else if (header.regionMatches(true, nameStart, "Secure", 0, 5)) { + 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; } } @@ -286,22 +245,17 @@ public final class ClientCookieDecoder { } private void parse7(String header, int nameStart, String value) { - if (header.regionMatches(true, nameStart, "Expires", 0, 7)) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.EXPIRES, 0, 7)) { setExpire(value); - } else if (header.regionMatches(true, nameStart, "Max-Age", 0, 7)) { + } 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, "HttpOnly", 0, 8)) { + if (header.regionMatches(true, nameStart, CookieHeaderNames.HTTPONLY, 0, 8)) { httpOnly = true; } } } - - private ClientCookieDecoder() { - // unused - } } 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/ServerCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/ServerCookieDecoderTest.java deleted file mode 100644 index 2ebd1b87c7..0000000000 --- a/codec-http/src/test/java/io/netty/handler/codec/http/ServerCookieDecoderTest.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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; - -import org.junit.Test; - -import java.util.Date; -import java.util.Iterator; -import java.util.Set; - -import static org.junit.Assert.*; - -public class ServerCookieDecoderTest { - @Test - public void testDecodingSingleCookie() { - String cookieString = "myCookie=myValue"; - cookieString = cookieString.replace("XXX", - HttpHeaderDateFormat.get().format(new Date(System.currentTimeMillis() + 50000))); - - Set cookies = ServerCookieDecoder.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.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 testDecodingQuotedCookie() { - String source = - "a=\"\";" + - "b=\"1\";" + - "c=\"\\\"1\\\"2\\\"\";" + - "d=\"1\\\"2\\\"3\";" + - "e=\"\\\"\\\"\";" + - "f=\"1\\\"\\\"2\";" + - "g=\"\\\\\";" + - "h=\"';,\\x\""; - - Set cookies = ServerCookieDecoder.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()); - - c = it.next(); - assertEquals("c", c.name()); - assertEquals("\"1\"2\"", c.value()); - - c = it.next(); - assertEquals("d", c.name()); - assertEquals("1\"2\"3", c.value()); - - c = it.next(); - assertEquals("e", c.name()); - assertEquals("\"\"", c.value()); - - c = it.next(); - assertEquals("f", c.name()); - assertEquals("1\"\"2", c.value()); - - c = it.next(); - assertEquals("g", c.name()); - assertEquals("\\", c.value()); - - c = it.next(); - assertEquals("h", c.name()); - assertEquals("';,\\x", 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"; - Set cookies = ServerCookieDecoder.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.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.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()); - } -} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java similarity index 54% rename from codec-http/src/test/java/io/netty/handler/codec/http/ClientCookieDecoderTest.java rename to codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java index c254d8447c..bf596e4f7a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/ClientCookieDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ClientCookieDecoderTest.java @@ -13,7 +13,7 @@ * 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; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -22,6 +22,10 @@ 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; @@ -29,8 +33,6 @@ import java.util.Date; import java.util.Iterator; import java.util.TimeZone; -import org.junit.Test; - public class ClientCookieDecoderTest { @Test public void testDecodingSingleCookieV0() { @@ -38,7 +40,7 @@ public class ClientCookieDecoderTest { cookieString = cookieString.replace("XXX", HttpHeaderDateFormat.get() .format(new Date(System.currentTimeMillis() + 50000))); - Cookie cookie = ClientCookieDecoder.decode(cookieString); + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); assertEquals("myValue", cookie.value()); assertEquals(".adomainsomewhere", cookie.domain()); @@ -63,7 +65,7 @@ public class ClientCookieDecoderTest { 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.decode(cookieString); + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); assertEquals("myValue", cookie.value()); assertEquals(".adomainsomewhere", cookie.domain()); @@ -76,7 +78,7 @@ public class ClientCookieDecoderTest { 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.decode(cookieString); + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertEquals("myValue", cookie.value()); assertNotNull(cookie); assertEquals(".adomainsomewhere", cookie.domain()); @@ -90,7 +92,7 @@ public class ClientCookieDecoderTest { 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.decode(cookieString); + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); assertEquals("myValue", cookie.value()); assertEquals(".adomainsomewhere", cookie.domain()); @@ -104,7 +106,7 @@ public class ClientCookieDecoderTest { 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.decode(cookieString); + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); assertNotNull(cookie); assertEquals("myValue", cookie.value()); assertEquals(".adomainsomewhere", cookie.domain()); @@ -119,7 +121,7 @@ public class ClientCookieDecoderTest { + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + "commentURL=\"http://aurl.com\";port='80,8080';discard;"; - Cookie cookie = ClientCookieDecoder.decode(c1); + Cookie cookie = ClientCookieDecoder.STRICT.decode(c1); assertNotNull(cookie); assertEquals("myValue", cookie.value()); assertEquals(".adomainsomewhere", cookie.domain()); @@ -133,16 +135,10 @@ public class ClientCookieDecoderTest { Collection sources = new ArrayList(); sources.add("a=\"\","); sources.add("b=\"1\","); - sources.add("c=\"\\\"1\\\"2\\\"\","); - sources.add("d=\"1\\\"2\\\"3\","); - sources.add("e=\"\\\"\\\"\","); - sources.add("f=\"1\\\"\\\"2\","); - sources.add("g=\"\\\\\","); - sources.add("h=\"';,\\x\""); Collection cookies = new ArrayList(); for (String source : sources) { - cookies.add(ClientCookieDecoder.decode(source)); + cookies.add(ClientCookieDecoder.STRICT.decode(source)); } Iterator it = cookies.iterator(); @@ -156,30 +152,6 @@ public class ClientCookieDecoderTest { assertEquals("b", c.name()); assertEquals("1", c.value()); - c = it.next(); - assertEquals("c", c.name()); - assertEquals("\"1\"2\"", c.value()); - - c = it.next(); - assertEquals("d", c.name()); - assertEquals("1\"2\"3", c.value()); - - c = it.next(); - assertEquals("e", c.name()); - assertEquals("\"\"", c.value()); - - c = it.next(); - assertEquals("f", c.name()); - assertEquals("1\"\"2", c.value()); - - c = it.next(); - assertEquals("g", c.name()); - assertEquals("\\", c.value()); - - c = it.next(); - assertEquals("h", c.name()); - assertEquals("';,\\x", c.value()); - assertFalse(it.hasNext()); } @@ -191,7 +163,7 @@ public class ClientCookieDecoderTest { + "__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.decode(source); + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); assertEquals("ARPT", cookie.name()); assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value()); @@ -206,27 +178,25 @@ public class ClientCookieDecoderTest { String source = "Format=EU; expires=Fri, 31-Dec-9999 23:59:59 GMT; path=/"; - Cookie cookie = ClientCookieDecoder.decode(source); + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2); } @Test - public void testDecodingValueWithComma() { + 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.decode(source); + Cookie cookie = ClientCookieDecoder.STRICT.decode(source); - assertEquals( - "timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE", - cookie.value()); + 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.decode(src); + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); assertEquals("path", cookie.name()); assertEquals("", cookie.value()); assertEquals("/", cookie.path()); @@ -235,67 +205,67 @@ public class ClientCookieDecoderTest { @Test public void testDecodingWeirdNames2() { String src = "HTTPOnly="; - Cookie cookie = ClientCookieDecoder.decode(src); + Cookie cookie = ClientCookieDecoder.STRICT.decode(src); assertEquals("HTTPOnly", cookie.name()); assertEquals("", cookie.value()); } @Test - public void testDecodingValuesWithCommasAndEquals() { + public void testDecodingValuesWithCommasAndEqualsFails() { String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"; - Cookie cookie = ClientCookieDecoder.decode(src); - assertEquals("A", cookie.name()); - assertEquals("v=1&lg=en-US,it-IT,it&intl=it&np=1", cookie.value()); + 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____" + + "%=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/ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java similarity index 85% rename from codec-http/src/test/java/io/netty/handler/codec/http/ServerCookieEncoderTest.java rename to codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java index bcdee4e1b8..350499046a 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/ServerCookieEncoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/cookie/ServerCookieEncoderTest.java @@ -13,10 +13,12 @@ * 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; import org.junit.Test; +import io.netty.handler.codec.http.HttpHeaderDateFormat; + import java.text.ParseException; import java.util.Date; import java.util.List; @@ -40,7 +42,7 @@ public class ServerCookieEncoderTest { cookie.setPath("/apathsomewhere"); cookie.setSecure(true); - String encodedCookie = ServerCookieEncoder.encode(cookie); + String encodedCookie = ServerCookieEncoder.STRICT.encode(cookie); Matcher matcher = Pattern.compile(result).matcher(encodedCookie); assertTrue(matcher.find()); @@ -52,8 +54,8 @@ public class ServerCookieEncoderTest { @Test public void testEncodingWithNoCookies() { - String encodedCookie1 = ClientCookieEncoder.encode(); - List encodedCookie2 = ServerCookieEncoder.encode(); + String encodedCookie1 = ClientCookieEncoder.STRICT.encode(); + List encodedCookie2 = ServerCookieEncoder.STRICT.encode(); assertNull(encodedCookie1); assertNotNull(encodedCookie2); assertTrue(encodedCookie2.isEmpty()); diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java index 8737c82782..28055597b3 100644 --- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java +++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopClient.java @@ -20,14 +20,14 @@ import io.netty.channel.Channel; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.http.ClientCookieEncoder; -import io.netty.handler.codec.http.DefaultCookie; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @@ -91,7 +91,7 @@ public final class HttpSnoopClient { // Set some example cookies. request.headers().set( HttpHeaderNames.COOKIE, - ClientCookieEncoder.encode( + ClientCookieEncoder.STRICT.encode( new DefaultCookie("my-cookie", "foo"), new DefaultCookie("another-cookie", "bar"))); diff --git a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java index 57e8dc96f7..69d0a4abc3 100644 --- a/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java +++ b/example/src/main/java/io/netty/example/http/snoop/HttpSnoopServerHandler.java @@ -21,7 +21,6 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.DecoderResult; -import io.netty.handler.codec.http.Cookie; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpContent; @@ -33,8 +32,9 @@ import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http.ServerCookieDecoder; -import io.netty.handler.codec.http.ServerCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import io.netty.util.CharsetUtil; import java.util.List; @@ -165,17 +165,17 @@ public class HttpSnoopServerHandler extends SimpleChannelInboundHandler // Encode the cookie. String cookieString = request.headers().get(HttpHeaderNames.COOKIE); if (cookieString != null) { - Set cookies = ServerCookieDecoder.decode(cookieString); + Set cookies = ServerCookieDecoder.STRICT.decode(cookieString); if (!cookies.isEmpty()) { // Reset the cookies if necessary. for (Cookie cookie: cookies) { - response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode(cookie)); + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); } } } else { // Browser sent no cookie. Add some. - response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode("key1", "value1")); - response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.encode("key2", "value2")); + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key1", "value1")); + response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key2", "value2")); } // Write the response. diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java index 2c2c51e65e..ff31b96ceb 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadClient.java @@ -21,8 +21,6 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.http.ClientCookieEncoder; -import io.netty.handler.codec.http.DefaultCookie; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; @@ -31,6 +29,8 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.QueryStringEncoder; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.DiskAttribute; import io.netty.handler.codec.http.multipart.DiskFileUpload; @@ -181,7 +181,7 @@ public final class HttpUploadClient { // headers.set("Keep-Alive","300"); headers.set( - HttpHeaderNames.COOKIE, ClientCookieEncoder.encode( + HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode( new DefaultCookie("my-cookie", "foo"), new DefaultCookie("another-cookie", "bar")) ); diff --git a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java index a5f10ab455..ed41134c4c 100644 --- a/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java +++ b/example/src/main/java/io/netty/example/http/upload/HttpUploadServerHandler.java @@ -21,7 +21,6 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.handler.codec.http.Cookie; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpContent; @@ -35,8 +34,9 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http.ServerCookieDecoder; -import io.netty.handler.codec.http.ServerCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.DiskAttribute; @@ -125,7 +125,7 @@ public class HttpUploadServerHandler extends SimpleChannelInboundHandler