cookies = CookieDecoder.decode(value);
+ localCookies.addAll(cookies);
}
}
}
diff --git a/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java b/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java
index ba2024d1c8..60b9379664 100644
--- a/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java
+++ b/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Locale;
+import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpProvider;
import com.ning.http.client.ByteArrayPart;
@@ -459,61 +460,12 @@ public static String parseCharset(String contentType) {
return null;
}
+ @Deprecated
public static Cookie parseCookie(String value) {
- String[] fields = value.split(";\\s*");
- String[] cookie = fields[0].split("=", 2);
- String cookieName = cookie[0];
- String cookieValue = (cookie.length == 1) ? null : cookie[1];
-
- int maxAge = -1;
- String path = null;
- String domain = null;
- boolean secure = false;
-
- boolean maxAgeSet = false;
- boolean expiresSet = false;
-
- for (int j = 1; j < fields.length; j++) {
- if ("secure".equalsIgnoreCase(fields[j])) {
- secure = true;
- } else if (fields[j].indexOf('=') > 0) {
- String[] f = fields[j].split("=");
- if (f.length == 1) continue; // Add protection against null field values
-
- // favor 'max-age' field over 'expires'
- if (!maxAgeSet && "max-age".equalsIgnoreCase(f[0])) {
- try {
- maxAge = Math.max(Integer.valueOf(removeQuote(f[1])), 0);
- } catch (NumberFormatException e1) {
- // ignore failure to parse -> treat as session cookie
- // invalidate a previously parsed expires-field
- maxAge = -1;
- }
- maxAgeSet = true;
- } else if (!maxAgeSet && !expiresSet && "expires".equalsIgnoreCase(f[0])) {
- try {
- maxAge = Math.max(convertExpireField(f[1]), 0);
- } catch (Exception e) {
- // original behavior, is this correct at all (expires field with max-age semantics)?
- try {
- maxAge = Math.max(Integer.valueOf(f[1]), 0);
- } catch (NumberFormatException e1) {
- // ignore failure to parse -> treat as session cookie
- }
- }
- expiresSet = true;
- } else if ("domain".equalsIgnoreCase(f[0])) {
- domain = f[1];
- } else if ("path".equalsIgnoreCase(f[0])) {
- path = f[1];
- }
- }
- }
-
- return new Cookie(domain, cookieName, cookieValue, path, maxAge, secure);
+ return CookieDecoder.decode(value).iterator().next();
}
- public static int convertExpireField(String timestring) throws Exception {
+ public static int convertExpireField(String timestring) {
String trimmedTimeString = removeQuote(timestring.trim());
long now = System.currentTimeMillis();
Date date = null;
@@ -525,8 +477,8 @@ public static int convertExpireField(String timestring) throws Exception {
}
if (date != null) {
- long expire = date.getTime();
- return (int) ((expire - now) / 1000);
+ long maxAgeMillis = date.getTime() - now;
+ return (int) (maxAgeMillis / 1000) + (maxAgeMillis % 1000 != 0? 1 : 0);
} else
throw new IllegalArgumentException("Not a valid expire field " + trimmedTimeString);
}
diff --git a/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoder.java b/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoder.java
new file mode 100644
index 0000000000..15cbfe22e5
--- /dev/null
+++ b/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoder.java
@@ -0,0 +1,304 @@
+/*
+ * 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.
+ */
+/*
+ * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package com.ning.org.jboss.netty.handler.codec.http;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.ning.org.jboss.netty.util.internal.StringUtil;
+import com.ning.http.client.Cookie;
+import com.ning.http.util.AsyncHttpProviderUtils;
+
+/**
+ * Decodes an HTTP header value into {@link Cookie}s. This decoder can decode the HTTP cookie version 0, 1, and 2.
+ *
+ *
+ * {@link HttpRequest} req = ...;
+ * String value = req.getHeader("Cookie");
+ * Set<{@link Cookie}> cookies = new {@link CookieDecoder}().decode(value);
+ *
+ *
+ * @see CookieEncoder
+ *
+ * @apiviz.stereotype utility
+ * @apiviz.has org.jboss.netty.handler.codec.http.Cookie oneway - - decodes
+ */
+public class CookieDecoder {
+
+ private static final char COMMA = ',';
+
+ /**
+ * Creates a new decoder.
+ */
+ private CookieDecoder() {
+ }
+
+ /**
+ * Decodes the specified HTTP header value into {@link Cookie}s.
+ *
+ * @return the decoded {@link Cookie}s
+ */
+ public static Set decode(String header) {
+ List names = new ArrayList(8);
+ List values = new ArrayList(8);
+ extractKeyValuePairs(header, names, values);
+
+ if (names.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ int i;
+ int version = 0;
+
+ // $Version is the only attribute that can appear before the actual
+ // cookie name-value pair.
+ if (names.get(0).equalsIgnoreCase(CookieHeaderNames.VERSION)) {
+ try {
+ version = Integer.parseInt(values.get(0));
+ } catch (NumberFormatException e) {
+ // Ignore.
+ }
+ i = 1;
+ } else {
+ i = 0;
+ }
+
+ if (names.size() <= i) {
+ // There's a version attribute, but nothing more.
+ return Collections.emptySet();
+ }
+
+ Set cookies = new TreeSet();
+ for (; i < names.size(); i++) {
+ String name = names.get(i);
+ String value = values.get(i);
+ if (value == null) {
+ value = "";
+ }
+
+ String cookieName = name;
+ String cookieValue = value;
+ boolean discard = false;
+ boolean secure = false;
+ boolean httpOnly = false;
+ String comment = null;
+ String commentURL = null;
+ String domain = null;
+ String path = null;
+ int maxAge = Integer.MIN_VALUE;
+ List ports = Collections.emptyList();
+
+ for (int j = i + 1; j < names.size(); j++, i++) {
+ name = names.get(j);
+ value = values.get(j);
+
+ if (CookieHeaderNames.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)) {
+ comment = value;
+ } else if (CookieHeaderNames.COMMENTURL.equalsIgnoreCase(name)) {
+ commentURL = value;
+ } else if (CookieHeaderNames.DOMAIN.equalsIgnoreCase(name)) {
+ domain = value;
+ } else if (CookieHeaderNames.PATH.equalsIgnoreCase(name)) {
+ path = value;
+ } else if (CookieHeaderNames.EXPIRES.equalsIgnoreCase(name)) {
+ try {
+ maxAge = AsyncHttpProviderUtils.convertExpireField(value);
+ } catch (Exception e) {
+ // original behavior, is this correct at all (expires field with max-age semantics)?
+ try {
+ maxAge = Math.max(Integer.valueOf(value), 0);
+ } catch (NumberFormatException e1) {
+ // ignore failure to parse -> treat as session cookie
+ }
+ }
+ } else if (CookieHeaderNames.MAX_AGE.equalsIgnoreCase(name)) {
+ maxAge = Integer.parseInt(value);
+ } else if (CookieHeaderNames.VERSION.equalsIgnoreCase(name)) {
+ version = Integer.parseInt(value);
+ } else if (CookieHeaderNames.PORT.equalsIgnoreCase(name)) {
+ String[] portList = StringUtil.split(value, COMMA);
+ ports = new ArrayList(2);
+ for (String s1 : portList) {
+ try {
+ ports.add(Integer.valueOf(s1));
+ } catch (NumberFormatException e) {
+ // Ignore.
+ }
+ }
+ } else {
+ break;
+ }
+ }
+
+ Cookie c = new Cookie(domain, cookieName, cookieValue, path, maxAge, secure, version, httpOnly, discard, comment, commentURL, ports);
+ cookies.add(c);
+ }
+
+ return cookies;
+ }
+
+ private static void extractKeyValuePairs(final String header, final List names, final List values) {
+
+ final int headerLen = header.length();
+ loop: for (int i = 0;;) {
+
+ // Skip spaces and separators.
+ for (;;) {
+ if (i == headerLen) {
+ break loop;
+ }
+ switch (header.charAt(i)) {
+ case '\t':
+ case '\n':
+ case 0x0b:
+ case '\f':
+ case '\r':
+ case ' ':
+ case ',':
+ case ';':
+ i++;
+ continue;
+ }
+ break;
+ }
+
+ // Skip '$'.
+ for (;;) {
+ if (i == headerLen) {
+ break loop;
+ }
+ if (header.charAt(i) == '$') {
+ i++;
+ continue;
+ }
+ break;
+ }
+
+ String name;
+ String value;
+
+ if (i == headerLen) {
+ name = null;
+ value = null;
+ } else {
+ int newNameStart = i;
+ keyValLoop: for (;;) {
+ switch (header.charAt(i)) {
+ case ';':
+ // NAME; (no value till ';')
+ name = header.substring(newNameStart, i);
+ value = null;
+ break keyValLoop;
+ case '=':
+ // NAME=VALUE
+ name = header.substring(newNameStart, 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 == '"' || c == '\'') {
+ // NAME="VALUE" or NAME='VALUE'
+ StringBuilder newValueBuf = new StringBuilder(header.length() - i);
+ 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++);
+ switch (c) {
+ case '\\':
+ case '"':
+ case '\'':
+ // Escape last backslash.
+ newValueBuf.setCharAt(newValueBuf.length() - 1, c);
+ break;
+ default:
+ // 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;
+ default:
+ i++;
+ }
+
+ if (i == headerLen) {
+ // NAME (no value till the end of string)
+ name = header.substring(newNameStart);
+ value = null;
+ break;
+ }
+ }
+ }
+
+ names.add(name);
+ values.add(value);
+ }
+ }
+}
diff --git a/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieHeaderNames.java b/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieHeaderNames.java
new file mode 100644
index 0000000000..5d3e6c9249
--- /dev/null
+++ b/src/main/java/com/ning/org/jboss/netty/handler/codec/http/CookieHeaderNames.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+/*
+ * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package com.ning.org.jboss.netty.handler.codec.http;
+
+final class CookieHeaderNames {
+ static final String PATH = "Path";
+
+ static final String EXPIRES = "Expires";
+
+ static final String MAX_AGE = "Max-Age";
+
+ static final String DOMAIN = "Domain";
+
+ 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";
+
+ private CookieHeaderNames() {
+ // Unused.
+ }
+}
+
diff --git a/src/main/java/com/ning/org/jboss/netty/util/internal/StringUtil.java b/src/main/java/com/ning/org/jboss/netty/util/internal/StringUtil.java
new file mode 100644
index 0000000000..b7e55976eb
--- /dev/null
+++ b/src/main/java/com/ning/org/jboss/netty/util/internal/StringUtil.java
@@ -0,0 +1,73 @@
+/*
+ * 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 com.ning.org.jboss.netty.util.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * String utility class.
+ */
+public final class StringUtil {
+
+ private StringUtil() {
+ // Unused.
+ }
+
+ private static final String EMPTY_STRING = "";
+
+ /**
+ * Splits the specified {@link String} with the specified delimiter. This operation is a simplified and optimized
+ * version of {@link String#split(String)}.
+ */
+ public static String[] split(String value, char delim) {
+ final int end = value.length();
+ final List res = new ArrayList();
+
+ int start = 0;
+ for (int i = 0; i < end; i ++) {
+ if (value.charAt(i) == delim) {
+ if (start == i) {
+ res.add(EMPTY_STRING);
+ } else {
+ res.add(value.substring(start, i));
+ }
+ start = i + 1;
+ }
+ }
+
+ if (start == 0) { // If no delimiter was found in the value
+ res.add(value);
+ } else {
+ if (start != end) {
+ // Add the last element if it's not empty.
+ res.add(value.substring(start, end));
+ } else {
+ // Truncate trailing empty elements.
+ for (int i = res.size() - 1; i >= 0; i --) {
+ if (res.get(i).length() == 0) {
+ res.remove(i);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ return res.toArray(new String[res.size()]);
+ }
+}
+
diff --git a/src/test/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoderTest.java b/src/test/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoderTest.java
new file mode 100644
index 0000000000..5d2f9cb09b
--- /dev/null
+++ b/src/test/java/com/ning/org/jboss/netty/handler/codec/http/CookieDecoderTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved.
+ *
+ * This program is licensed to you under the Apache License Version 2.0,
+ * and you may not use this file except in compliance with the Apache License Version 2.0.
+ * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the Apache License Version 2.0 is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
+ */
+package com.ning.org.jboss.netty.handler.codec.http;
+
+import java.util.Set;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import com.ning.http.client.Cookie;
+
+public class CookieDecoderTest {
+
+ @Test(groups = "fast")
+ public void testDecodeUnquoted() {
+ Set cookies = CookieDecoder.decode("foo=value; domain=/; path=/");
+ Assert.assertEquals(cookies.size(), 1);
+
+ Cookie first = cookies.iterator().next();
+ Assert.assertEquals(first.getValue(), "value");
+ Assert.assertEquals(first.getDomain(), "/");
+ Assert.assertEquals(first.getPath(), "/");
+ }
+
+ @Test(groups = "fast")
+ public void testDecodeQuoted() {
+ Set cookies = CookieDecoder.decode("ALPHA=\"VALUE1\"; Domain=docs.foo.com; Path=/accounts; Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; HttpOnly");
+ Assert.assertEquals(cookies.size(), 1);
+
+ Cookie first = cookies.iterator().next();
+ Assert.assertEquals(first.getValue(), "VALUE1");
+ }
+
+ @Test(groups = "fast")
+ public void testDecodeQuotedContainingEscapedQuote() {
+ Set cookies = CookieDecoder.decode("ALPHA=\"VALUE1\\\"\"; Domain=docs.foo.com; Path=/accounts; Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; HttpOnly");
+ Assert.assertEquals(cookies.size(), 1);
+
+ Cookie first = cookies.iterator().next();
+ Assert.assertEquals(first.getValue(), "VALUE1\"");
+ }
+}