diff --git a/pom.xml b/pom.xml index 89fd0e45af..5bd153c27e 100644 --- a/pom.xml +++ b/pom.xml @@ -465,6 +465,7 @@ **/NettyAsyncHttpProvider$* **/NettyResponse **/AsyncHttpProviderUtils + **/Cookie diff --git a/src/main/java/com/ning/http/client/Cookie.java b/src/main/java/com/ning/http/client/Cookie.java index 26fd46920f..f9c6cb2362 100644 --- a/src/main/java/com/ning/http/client/Cookie.java +++ b/src/main/java/com/ning/http/client/Cookie.java @@ -20,7 +20,7 @@ import java.util.Set; import java.util.TreeSet; -public class Cookie { +public class Cookie implements Comparable{ private final String domain; private final String name; private final String value; @@ -28,27 +28,85 @@ public class Cookie { private final int maxAge; private final boolean secure; private final int version; + private final boolean httpOnly; + private final boolean discard; + private final String comment; + private final String commentUrl; + private Set ports = Collections.emptySet(); private Set unmodifiablePorts = ports; + @Deprecated public Cookie(String domain, String name, String value, String path, int maxAge, boolean secure) { - this.domain = domain; - this.name = name; - this.value = value; - this.path = path; - this.maxAge = maxAge; - this.secure = secure; - this.version = 1; + this(domain, name, value, path, maxAge, secure, 1); } + @Deprecated public Cookie(String domain, String name, String value, String path, int maxAge, boolean secure, int version) { - this.domain = domain; + this(domain, name, value, path, maxAge, secure, version, false, false, null, null, Collections. emptySet()); + } + + public Cookie(String domain, String name, String value, String path, int maxAge, boolean secure, int version, boolean httpOnly, boolean discard, String comment, String commentUrl, Iterable ports) { + + if (name == null) { + throw new NullPointerException("name"); + } + name = name.trim(); + if (name.length() == 0) { + 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); + } + + if (value == null) { + throw new NullPointerException("value"); + } + this.name = name; this.value = value; - this.path = path; + this.domain = validateValue("domain", domain); + this.path = validateValue("path", path); this.maxAge = maxAge; this.secure = secure; this.version = version; + this.httpOnly = httpOnly; + + if (version > 0) { + this.comment = validateValue("comment", comment); + } else { + this.comment = null; + } + if (version > 1) { + this.discard = discard; + this.commentUrl = validateValue("commentUrl", commentUrl); + setPorts(ports); + } else { + this.discard = false; + this.commentUrl = null; + } } public String getDomain() { @@ -79,6 +137,22 @@ public int getVersion() { return version; } + public String getComment() { + return this.comment; + } + + public String getCommentUrl() { + return this.commentUrl; + } + + public boolean isHttpOnly() { + return httpOnly; + } + + public boolean isDiscard() { + return discard; + } + public Set getPorts() { if (unmodifiablePorts == null) { unmodifiablePorts = Collections.unmodifiableSet(ports); @@ -86,28 +160,7 @@ public Set getPorts() { return unmodifiablePorts; } - public void setPorts(int... ports) { - if (ports == null) { - throw new NullPointerException("ports"); - } - - int[] portsCopy = ports.clone(); - if (portsCopy.length == 0) { - unmodifiablePorts = this.ports = Collections.emptySet(); - } else { - Set newPorts = new TreeSet(); - for (int p : portsCopy) { - if (p <= 0 || p > 65535) { - throw new IllegalArgumentException("port out of range: " + p); - } - newPorts.add(Integer.valueOf(p)); - } - this.ports = newPorts; - unmodifiablePorts = null; - } - } - - public void setPorts(Iterable ports) { + private void setPorts(Iterable ports) { Set newPorts = new TreeSet(); for (int p : ports) { if (p <= 0 || p > 65535) { @@ -125,7 +178,89 @@ public void setPorts(Iterable ports) { @Override public String toString() { - return String.format("Cookie: domain=%s, name=%s, value=%s, path=%s, maxAge=%d, secure=%s", - domain, name, value, path, maxAge, secure); + StringBuilder buf = new StringBuilder(); + buf.append(getName()); + buf.append('='); + buf.append(getValue()); + if (getDomain() != null) { + buf.append("; domain="); + buf.append(getDomain()); + } + if (getPath() != null) { + buf.append("; path="); + buf.append(getPath()); + } + if (getComment() != null) { + buf.append("; comment="); + buf.append(getComment()); + } + if (getMaxAge() >= 0) { + buf.append("; maxAge="); + buf.append(getMaxAge()); + buf.append('s'); + } + if (isSecure()) { + buf.append("; secure"); + } + if (isHttpOnly()) { + buf.append("; HTTPOnly"); + } + return buf.toString(); + } + + private String validateValue(String name, String value) { + if (value == null) { + return null; + } + value = value.trim(); + if (value.length() == 0) { + 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; + } + + public int compareTo(Cookie c) { + int v; + v = getName().compareToIgnoreCase(c.getName()); + if (v != 0) { + return v; + } + + if (getPath() == null) { + if (c.getPath() != null) { + return -1; + } + } else if (c.getPath() == null) { + return 1; + } else { + v = getPath().compareTo(c.getPath()); + if (v != 0) { + return v; + } + } + + if (getDomain() == null) { + if (c.getDomain() != null) { + return -1; + } + } else if (c.getDomain() == null) { + return 1; + } else { + v = getDomain().compareToIgnoreCase(c.getDomain()); + return v; + } + + return 0; } } diff --git a/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java b/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java index d899a40ed1..c0ad75bc47 100644 --- a/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java +++ b/src/main/java/com/ning/http/client/providers/apache/ApacheResponse.java @@ -14,6 +14,7 @@ import static com.ning.http.util.MiscUtil.isNonEmpty; +import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder; import com.ning.http.client.Cookie; import com.ning.http.client.FluentCaseInsensitiveStringsMap; import com.ning.http.client.HttpResponseBodyPart; @@ -31,7 +32,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; - +import java.util.Set; public class ApacheResponse implements Response { private final static String DEFAULT_CHARSET = "ISO-8859-1"; @@ -161,8 +162,8 @@ public List getCookies() { // TODO: ask for parsed header List v = header.getValue(); for (String value : v) { - Cookie cookie = AsyncHttpProviderUtils.parseCookie(value); - localCookies.add(cookie); + Set cookies = CookieDecoder.decode(value); + localCookies.addAll(cookies); } } } diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java index f75de1c9d0..a0632a664e 100644 --- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java +++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java @@ -15,13 +15,13 @@ import static com.ning.http.util.MiscUtil.isNonEmpty; +import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder; import com.ning.http.client.AsyncHandler; import com.ning.http.client.AsyncHttpClientConfig; import com.ning.http.client.AsyncHttpProvider; import com.ning.http.client.AsyncHttpProviderConfig; import com.ning.http.client.Body; import com.ning.http.client.BodyGenerator; -import com.ning.http.client.ConnectionPoolKeyStrategy; import com.ning.http.client.ConnectionsPool; import com.ning.http.client.Cookie; import com.ning.http.client.FluentCaseInsensitiveStringsMap; @@ -1667,8 +1667,9 @@ private static Request newRequest(final URI uri, builder.setQueryParameters(null); } for (String cookieStr : response.getHeaders().values(Header.Cookie)) { - Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); - builder.addOrReplaceCookie(c); + for (Cookie c : CookieDecoder.decode(cookieStr)) { + builder.addOrReplaceCookie(c); + } } return builder.build(); diff --git a/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java b/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java index 2fe9566e06..8dda720d8b 100644 --- a/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java +++ b/src/main/java/com/ning/http/client/providers/jdk/JDKResponse.java @@ -14,6 +14,7 @@ import static com.ning.http.util.MiscUtil.isNonEmpty; +import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder; import com.ning.http.client.Cookie; import com.ning.http.client.FluentCaseInsensitiveStringsMap; import com.ning.http.client.HttpResponseBodyPart; @@ -32,6 +33,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; @@ -175,8 +177,8 @@ public List getCookies() { // TODO: ask for parsed header List v = header.getValue(); for (String value : v) { - Cookie cookie = AsyncHttpProviderUtils.parseCookie(value); - localCookies.add(cookie); + Set cookies = CookieDecoder.decode(value); + localCookies.addAll(cookies); } } } diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java index 4f4b043b08..9cc75a946f 100644 --- a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java +++ b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java @@ -17,6 +17,7 @@ import static com.ning.http.util.MiscUtil.isNonEmpty; +import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder; import com.ning.http.client.AsyncHandler; import com.ning.http.client.AsyncHandler.STATE; import com.ning.http.client.AsyncHttpClientConfig; @@ -585,7 +586,7 @@ public void operationComplete(ChannelFuture cf) { int delay = Math.min(config.getIdleConnectionTimeoutInMs(), requestTimeout(config, future.getRequest().getPerRequestConfig())); if (delay != -1 && !future.isDone() && !future.isCancelled()) { ReaperFuture reaperFuture = new ReaperFuture(future); - Future scheduledFuture = config.reaper().scheduleAtFixedRate(reaperFuture, 0, delay, TimeUnit.MILLISECONDS); + Future scheduledFuture = config.reaper().scheduleAtFixedRate(reaperFuture, 0, delay, TimeUnit.MILLISECONDS); reaperFuture.setScheduledFuture(scheduledFuture); future.setReaperFuture(reaperFuture); } @@ -2083,13 +2084,15 @@ private boolean redirect(Request request, log.debug("Redirecting to {}", newUrl); for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE)) { - Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); - nBuilder.addOrReplaceCookie(c); + for (Cookie c : CookieDecoder.decode(cookieStr)) { + nBuilder.addOrReplaceCookie(c); + } } for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE2)) { - Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); - nBuilder.addOrReplaceCookie(c); + for (Cookie c : CookieDecoder.decode(cookieStr)) { + nBuilder.addOrReplaceCookie(c); + } } AsyncCallable ac = new AsyncCallable(future) { diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyResponse.java b/src/main/java/com/ning/http/client/providers/netty/NettyResponse.java index 65870b0ca0..3758d2b3cc 100644 --- a/src/main/java/com/ning/http/client/providers/netty/NettyResponse.java +++ b/src/main/java/com/ning/http/client/providers/netty/NettyResponse.java @@ -17,6 +17,7 @@ import static com.ning.http.util.MiscUtil.isNonEmpty; +import com.ning.org.jboss.netty.handler.codec.http.CookieDecoder; import com.ning.http.client.Cookie; import com.ning.http.client.FluentCaseInsensitiveStringsMap; import com.ning.http.client.HttpResponseBodyPart; @@ -35,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferInputStream; @@ -184,8 +186,8 @@ public List getCookies() { // TODO: ask for parsed header List v = header.getValue(); for (String value : v) { - Cookie cookie = AsyncHttpProviderUtils.parseCookie(value); - localCookies.add(cookie); + Set 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\""); + } +}