From c298230128a72c791d101bb6fd5d0451e274e90f Mon Sep 17 00:00:00 2001 From: Stephane Landelle Date: Mon, 19 Jan 2015 15:06:35 +0100 Subject: [PATCH] RFC6265 cookies support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: Currently Netty supports a weird implementation of RFC 2965. First, this RFC has been deprecated by RFC 6265 and nobody on the internet use this format. Then, there's a confusion between client side and server side encoding and decoding. Typically, clients should only send name=value pairs. This PR introduces RFC 6265 support, but keeps on supporting RFC 2965 in the sense that old unused fields are simply ignored, and Cookie fields won't be populated. Deprecated fields are comment, commentUrl, version, discard and ports. It also provides a mechanism for safe server-client-server roundtrip, as User-Agents are not supposed to interpret cookie values but return them as-is (e.g. if Set-Cookie contained a quoted value, it should be sent back in the Cookie header in quoted form too). Also, there are performance gains to be obtained by not allocating the attribute name Strings, as we only want to match them to find which POJO field to populate. Modifications: - New RFC6265ClientCookieEncoder/Decoder and RFC6265ServerCookieEncoder/Decoder pairs that live alongside old CookieEncoder/Decoder pair to not break backward compatibility. - New Cookie.rawValue field, used for lossless server-client-server roundtrip. Result: RFC 6265 support. Clean separation of client and server side. Decoder performance gain: Benchmark Mode Samples Score Error Units parseOldClientDecoder thrpt 20 2070169,228 ± 105044,970 ops/s parseRFC6265ClientDecoder thrpt 20 2954015,476 ± 126670,633 ops/s This commit closes #3221 and #1406. --- .../io/netty/handler/codec/http/Cookie.java | 15 + .../handler/codec/http/CookieEncoderUtil.java | 8 + .../handler/codec/http/DefaultCookie.java | 14 + .../codec/http/HttpHeaderDateFormat.java | 4 +- .../http/Rfc6265ClientCookieDecoder.java | 313 ++++++++++++++++++ .../http/Rfc6265ClientCookieEncoder.java | 128 +++++++ .../http/Rfc6265ServerCookieDecoder.java | 182 ++++++++++ .../http/Rfc6265ServerCookieEncoder.java | 172 ++++++++++ .../http/Rfc6265ClientCookieDecoderTest.java | 310 +++++++++++++++++ .../http/Rfc6265ClientCookieEncoderTest.java | 52 +++ .../http/Rfc6265ServerCookieDecoderTest.java | 228 +++++++++++++ .../http/Rfc6265ServerCookieEncoderTest.java | 46 +++ 12 files changed, 1470 insertions(+), 2 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieEncoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieDecoder.java create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoder.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ClientCookieEncoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ServerCookieDecoderTest.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoderTest.java 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 45e47d6791..bc36aa8cb3 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 @@ -56,6 +56,21 @@ public interface Cookie extends Comparable { */ 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 value The raw value to set + */ + void setRawValue(String rawValue); + /** * @deprecated Use {@link #domain()} instead. */ 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 index bb45518db2..2155a96b41 100644 --- 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 @@ -24,6 +24,14 @@ final class CookieEncoderUtil { 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); 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 b38553fc45..61cc73a230 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 @@ -28,6 +28,7 @@ public class DefaultCookie implements Cookie { private final String name; private String value; + private String rawValue; private String domain; private String path; private String comment; @@ -107,6 +108,19 @@ public class DefaultCookie implements Cookie { 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() { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpHeaderDateFormat.java index 5c89e7c5f1..24d884bc86 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 @@ -29,8 +29,8 @@ import java.util.TimeZone; * */ final class HttpHeaderDateFormat extends SimpleDateFormat { diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoder.java new file mode 100644 index 0000000000..ffc31da746 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoder.java @@ -0,0 +1,313 @@ +/* + * 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 static io.netty.handler.codec.http.CookieEncoderUtil.stringBuilder; + +import java.text.ParsePosition; +import java.util.Date; + +/** + * 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 + * eventually sent back to the Origin server as is. + * + * @see Rfc6265ClientCookieEncoder + */ +public final class Rfc6265ClientCookieDecoder { + + /** + * 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(); + + if (headerLen == 0) { + return null; + } + + CookieBuilder cookieBuilder = null; + + loop: for (int i = 0;;) { + + // Skip spaces and separators. + for (;;) { + if (i == headerLen) { + break loop; + } + char c = header.charAt(i); + if (c == ',') { + // Having multiple cookies in a single Set-Cookie header is + // deprecated, modern browsers only parse the first one + break loop; + + } else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f' + || c == '\r' || c == ' ' || c == ';') { + i++; + continue; + } + break; + } + + int newNameStart = i; + int newNameEnd = i; + String value, rawValue; + boolean first = true; + + if (i == headerLen) { + value = rawValue = null; + } else { + keyValLoop: for (;;) { + + char curChar = header.charAt(i); + if (curChar == ';') { + // NAME; (no value till ';') + newNameEnd = i; + value = rawValue = null; + first = false; + break keyValLoop; + } else if (curChar == '=') { + // NAME=VALUE + newNameEnd = i; + i++; + if (i == headerLen) { + // NAME= (empty value, i.e. nothing after '=') + value = rawValue = ""; + first = false; + 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 = first ? header.substring(rawValueStart, rawValueEnd) : null; + first = false; + 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 = first ? header.substring(rawValueStart, rawValueEnd) : null; + first = false; + 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; + } + } + break keyValLoop; + } else { + i++; + } + + if (i == headerLen) { + // NAME (no value till the end of string) + newNameEnd = i; + first = false; + value = rawValue = null; + break; + } + } + } + + if (cookieBuilder == null) { + cookieBuilder = new CookieBuilder(header, newNameStart, newNameEnd, value, rawValue); + } else { + cookieBuilder.appendAttribute(header, newNameStart, newNameEnd, value); + } + } + return cookieBuilder.cookie(); + } + + private static class CookieBuilder { + + private final String name; + private final String value; + private final String rawValue; + private String domain; + private String path; + private long maxAge = Long.MIN_VALUE; + private String expires; + 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; + } + + private long mergeMaxAgeAndExpire(long maxAge, String expires) { + // max age has precedence over expires + if (maxAge != Long.MIN_VALUE) { + return maxAge; + } else if (expires != null) { + Date expiresDate = HttpHeaderDateFormat.get().parse(expires, new ParsePosition(0)); + if (expiresDate != null) { + long maxAgeMillis = expiresDate.getTime() - System.currentTimeMillis(); + return maxAgeMillis / 1000 + (maxAgeMillis % 1000 != 0 ? 1 : 0); + } + } + return Long.MIN_VALUE; + } + + public Cookie cookie() { + 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)); + cookie.setSecure(secure); + cookie.setHttpOnly(httpOnly); + return cookie; + } + + /** + * Parse and store a key-value pair. First one is considered to be the + * cookie name/value. Unknown attribute names are silently discarded. + * + * @param header + * the HTTP header + * @param keyStart + * where the key starts in the header + * @param keyEnd + * where the key ends in the header + * @param value + * the decoded value + */ + public void appendAttribute(String header, int keyStart, int keyEnd, + String value) { + setCookieAttribute(header, keyStart, keyEnd, value); + } + + private void setCookieAttribute(String header, int keyStart, + int keyEnd, String value) { + + int length = keyEnd - keyStart; + + if (length == 4) { + parse4(header, keyStart, value); + } else if (length == 6) { + parse6(header, keyStart, value); + } else if (length == 7) { + parse7(header, keyStart, value); + } else if (length == 8) { + parse8(header, keyStart, value); + } + } + + private void parse4(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, "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)) { + secure = true; + } + } + + private void setExpire(String value) { + expires = value; + } + + private void setMaxAge(String value) { + try { + maxAge = Math.max(Long.valueOf(value), 0L); + } catch (NumberFormatException e1) { + // ignore failure to parse -> treat as session cookie + } + } + + private void parse7(String header, int nameStart, String value) { + if (header.regionMatches(true, nameStart, "Expires", 0, 7)) { + setExpire(value); + } else if (header.regionMatches(true, nameStart, "Max-Age", 0, 7)) { + setMaxAge(value); + } + } + + private void parse8(String header, int nameStart, String value) { + + if (header.regionMatches(true, nameStart, "HttpOnly", 0, 8)) { + httpOnly = true; + } + } + } + + private Rfc6265ClientCookieDecoder() { + // unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieEncoder.java new file mode 100644 index 0000000000..0834a5405b --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ClientCookieEncoder.java @@ -0,0 +1,128 @@ +/* + * 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 static io.netty.handler.codec.http.CookieEncoderUtil.*; + +/** + * 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 Rfc6265ClientCookieEncoder}.encode("JSESSIONID", "1234"));
+ * 
+ * + * @see Rfc6265ClientCookieDecoder + */ +public final class Rfc6265ClientCookieEncoder { + + /** + * 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 static 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 static String encode(Cookie cookie) { + if (cookie == null) { + throw new NullPointerException("cookie"); + } + + StringBuilder buf = stringBuilder(); + encode(buf, 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 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); + } + + /** + * 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 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); + } + + private Rfc6265ClientCookieEncoder() { + // unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieDecoder.java new file mode 100644 index 0000000000..ab53b46798 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieDecoder.java @@ -0,0 +1,182 @@ +/* + * 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 static io.netty.handler.codec.http.CookieEncoderUtil.*; + +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 Rfc6265ServerCookieEncoder + */ +public final class Rfc6265ServerCookieDecoder { + + /** + * 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 Rfc6265ServerCookieDecoder() { + // unused + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoder.java new file mode 100644 index 0000000000..20dad0fba0 --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoder.java @@ -0,0 +1,172 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static io.netty.handler.codec.http.CookieEncoderUtil.*; + +/** + * 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 Rfc6265ServerCookieEncoder}.encode("JSESSIONID", "1234"));
+ * 
+ * + * @see Rfc6265ServerCookieDecoder + */ +public final class Rfc6265ServerCookieEncoder { + + /** + * 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 static 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 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()); + } + + 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); + } + + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + 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; + } + + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + 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; + } + + /** + * Batch encodes cookies into Set-Cookie header values. + * + * @param cookies a bunch of cookies + * @return the corresponding bunch of Set-Cookie headers + */ + 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; + } + + private Rfc6265ServerCookieEncoder() { + // Unused + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoderTest.java new file mode 100644 index 0000000000..5f7d3f1287 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ClientCookieDecoderTest.java @@ -0,0 +1,310 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.TimeZone; + +import org.junit.Test; + +public class Rfc6265ClientCookieDecoderTest { + @Test + public void testDecodingSingleCookieV0() { + String cookieString = "myCookie=myValue;expires=XXX;path=/apathsomewhere;domain=.adomainsomewhere;secure;"; + cookieString = cookieString.replace("XXX", HttpHeaderDateFormat.get() + .format(new Date(System.currentTimeMillis() + 50000))); + + Cookie cookie = Rfc6265ClientCookieDecoder.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + + boolean fail = true; + for (int i = 40; i <= 60; i++) { + if (cookie.maxAge() == i) { + fail = false; + break; + } + } + if (fail) { + fail("expected: 50, actual: " + cookie.maxAge()); + } + + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV0ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=0;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV1() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;domain=.adomainsomewhere" + + ";secure;comment=this is a comment;version=1;"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(cookieString); + assertEquals("myValue", cookie.value()); + assertNotNull(cookie); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV1ExtraParamsIgnored() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=1;" + + "commentURL=http://aurl.com;port='80,8080';discard;"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingSingleCookieV2() { + String cookieString = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=http://aurl.com;port=\"80,8080\";discard;"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(cookieString); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingComplexCookie() { + String c1 = "myCookie=myValue;max-age=50;path=/apathsomewhere;" + + "domain=.adomainsomewhere;secure;comment=this is a comment;version=2;" + + "commentURL=\"http://aurl.com\";port='80,8080';discard;"; + + Cookie cookie = Rfc6265ClientCookieDecoder.decode(c1); + assertNotNull(cookie); + assertEquals("myValue", cookie.value()); + assertEquals(".adomainsomewhere", cookie.domain()); + assertEquals(50, cookie.maxAge()); + assertEquals("/apathsomewhere", cookie.path()); + assertTrue(cookie.isSecure()); + } + + @Test + public void testDecodingQuotedCookie() { + Collection sources = new ArrayList(); + sources.add("a=\"\","); + sources.add("b=\"1\","); + 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(Rfc6265ClientCookieDecoder.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"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(source); + + assertEquals("ARPT", cookie.name()); + assertEquals("LWUKQPSWRTUN04CKKJI", cookie.value()); + } + + @Test + public void testDecodingLongDates() { + Calendar cookieDate = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cookieDate.set(9999, Calendar.DECEMBER, 31, 23, 59, 59); + long expectedMaxAge = (cookieDate.getTimeInMillis() - System + .currentTimeMillis()) / 1000; + + String source = "Format=EU; expires=Fri, 31-Dec-9999 23:59:59 GMT; path=/"; + + Cookie cookie = Rfc6265ClientCookieDecoder.decode(source); + + assertTrue(Math.abs(expectedMaxAge - cookie.maxAge()) < 2); + } + + @Test + public void testDecodingValueWithComma() { + String source = "UserCookie=timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE;" + + " expires=Sat, 01-Dec-2012 10:53:31 GMT; path=/"; + + Cookie cookie = Rfc6265ClientCookieDecoder.decode(source); + + assertEquals( + "timeZoneName=(GMT+04:00) Moscow, St. Petersburg, Volgograd&promocode=®ion=BE", + cookie.value()); + } + + @Test + public void testDecodingWeirdNames1() { + String src = "path=; expires=Mon, 01-Jan-1990 00:00:00 GMT; path=/; domain=.www.google.com"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(src); + assertEquals("path", cookie.name()); + assertEquals("", cookie.value()); + assertEquals("/", cookie.path()); + } + + @Test + public void testDecodingWeirdNames2() { + String src = "HTTPOnly="; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(src); + assertEquals("HTTPOnly", cookie.name()); + assertEquals("", cookie.value()); + } + + @Test + public void testDecodingValuesWithCommasAndEquals() { + String src = "A=v=1&lg=en-US,it-IT,it&intl=it&np=1;T=z=E"; + Cookie cookie = Rfc6265ClientCookieDecoder.decode(src); + assertEquals("A", cookie.name()); + assertEquals("v=1&lg=en-US,it-IT,it&intl=it&np=1", cookie.value()); + } + + @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 = Rfc6265ServerCookieDecoder.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 = Rfc6265ServerCookieDecoder.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 = Rfc6265ServerCookieDecoder.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 = Rfc6265ServerCookieDecoder.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 = Rfc6265ServerCookieDecoder.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 = Rfc6265ServerCookieDecoder.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/Rfc6265ServerCookieEncoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoderTest.java new file mode 100644 index 0000000000..659f4708a2 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/Rfc6265ServerCookieEncoderTest.java @@ -0,0 +1,46 @@ +/* + * 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 static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +public class Rfc6265ServerCookieEncoderTest { + @Test + public void testEncodingSingleCookieV0() { + String result = "myCookie=myValue; Max-Age=50; Path=/apathsomewhere; Domain=.adomainsomewhere; Secure"; + Cookie cookie = new DefaultCookie("myCookie", "myValue"); + cookie.setDomain(".adomainsomewhere"); + cookie.setMaxAge(50); + cookie.setPath("/apathsomewhere"); + cookie.setSecure(true); + + String encodedCookie = Rfc6265ServerCookieEncoder.encode(cookie); + assertEquals(result, encodedCookie); + } + + @Test + public void testEncodingWithNoCookies() { + String encodedCookie1 = Rfc6265ClientCookieEncoder.encode(); + List encodedCookie2 = Rfc6265ServerCookieEncoder.encode(); + assertNull(encodedCookie1); + assertNotNull(encodedCookie2); + assertTrue(encodedCookie2.isEmpty()); + } +}