diff --git a/src/main/java/org/jboss/netty/handler/codec/http/QueryStringDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/QueryStringDecoder.java index 8d21543e32..80805b05f0 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/QueryStringDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/QueryStringDecoder.java @@ -15,27 +15,42 @@ */ package org.jboss.netty.handler.codec.http; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.nio.charset.Charset; -import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.jboss.netty.util.CharsetUtil; + /** * Splits an HTTP query string into a path string and key-value parameter pairs. * This decoder is for one time use only. Create a new instance for each URI: *
* {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world&x=1;y=2"); * assert decoder.getPath().equals("/hello"); - * assert decoder.getParameters().get("recipient").equals("world"); - * assert decoder.getParameters().get("x").equals("1"); - * assert decoder.getParameters().get("y").equals("2"); + * assert decoder.getParameters().get("recipient").get(0).equals("world"); + * assert decoder.getParameters().get("x").get(0).equals("1"); + * assert decoder.getParameters().get("y").get(0).equals("2"); *+ * + * This decoder can also decode the content of an HTTP POST request whose + * content type is application/x-www-form-urlencoded: + *
+ * {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false); + * ... + *+ * + *
+ * This is equivalent to calling {@link #decodeComponent(String, Charset)} + * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2). + * @param s The string to decode (can be empty). + * @return The decoded string, or {@code s} if there's nothing to decode. + * If the string to decode is {@code null}, returns an empty string. + * @throws IllegalArgumentException if the string contains a malformed + * escape sequence. + */ + public static String decodeComponent(final String s) { + return decodeComponent(s, HttpCodecUtil.DEFAULT_CHARSET); + } + + /** + * Decodes a bit of an URL encoded by a browser. + *
+ * The string is expected to be encoded as per RFC 3986, Section 2. + * This is the encoding used by JavaScript functions {@code encodeURI} + * and {@code encodeURIComponent}, but not {@code escape}. For example + * in this encoding, é (in Unicode {@code U+00E9} or in UTF-8 + * {@code 0xC3 0xA9}) is encoded as {@code %C3%A9} or {@code %c3%a9}. + *
+ * This is essentially equivalent to calling
+ * {@link URLDecoder#decode(String, String) URLDecoder.decode}(s, charset.name())
+ * except that it's over 2x faster and generates less garbage for the GC.
+ * Actually this function doesn't allocate any memory if there's nothing
+ * to decode, the argument itself is returned.
+ * @param s The string to decode (can be empty).
+ * @param charset The charset to use to decode the string (should really
+ * be {@link CharsetUtil#UTF_8}.
+ * @return The decoded string, or {@code s} if there's nothing to decode.
+ * If the string to decode is {@code null}, returns an empty string.
+ * @throws IllegalArgumentException if the string contains a malformed
+ * escape sequence.
+ */
+ @SuppressWarnings("fallthrough")
+ public static String decodeComponent(final String s,
+ final Charset charset) {
+ if (s == null) {
+ return "";
+ }
+ final int size = s.length();
+ boolean modified = false;
+ for (int i = 0; i < size; i++) {
+ final char c = s.charAt(i);
+ switch (c) {
+ case '%':
+ i++; // We can skip at least one char, e.g. `%%'.
+ // Fall through.
+ case '+':
+ modified = true;
+ break;
+ }
+ }
+ if (!modified) {
+ return s;
+ }
+ final byte[] buf = new byte[size];
+ int pos = 0; // position in `buf'.
+ for (int i = 0; i < size; i++) {
+ char c = s.charAt(i);
+ switch (c) {
+ case '+':
+ buf[pos++] = ' '; // "+" -> " "
+ break;
+ case '%':
+ if (i == size - 1) {
+ throw new IllegalArgumentException("unterminated escape"
+ + " sequence at end of string: " + s);
+ }
+ c = s.charAt(++i);
+ if (c == '%') {
+ buf[pos++] = '%'; // "%%" -> "%"
+ break;
+ } else if (i == size - 1) {
+ throw new IllegalArgumentException("partial escape"
+ + " sequence at end of string: " + s);
+ }
+ c = decodeHexNibble(c);
+ final char c2 = decodeHexNibble(s.charAt(++i));
+ if (c == Character.MAX_VALUE || c2 == Character.MAX_VALUE) {
+ throw new IllegalArgumentException(
+ "invalid escape sequence `%" + s.charAt(i - 1)
+ + s.charAt(i) + "' at index " + (i - 2)
+ + " of: " + s);
+ }
+ c = (char) (c * 16 + c2);
+ // Fall through.
+ default:
+ buf[pos++] = (byte) c;
+ break;
+ }
+ }
+ return new String(buf, 0, pos, charset);
+ }
+
+ /**
+ * Helper to decode half of a hexadecimal number from a string.
+ * @param c The ASCII character of the hexadecimal number to decode.
+ * Must be in the range {@code [0-9a-fA-F]}.
+ * @return The hexadecimal value represented in the ASCII character
+ * given, or {@link Character#MAX_VALUE} if the character is invalid.
+ */
+ private static char decodeHexNibble(final char c) {
+ if ('0' <= c && c <= '9') {
+ return (char) (c - '0');
+ } else if ('a' <= c && c <= 'f') {
+ return (char) (c - 'a' + 10);
+ } else if ('A' <= c && c <= 'F') {
+ return (char) (c - 'A' + 10);
+ } else {
+ return Character.MAX_VALUE;
+ }
}
}
diff --git a/src/test/java/org/jboss/netty/handler/codec/http/QueryStringDecoderTest.java b/src/test/java/org/jboss/netty/handler/codec/http/QueryStringDecoderTest.java
index 44bae4932a..77536a4d2f 100644
--- a/src/test/java/org/jboss/netty/handler/codec/http/QueryStringDecoderTest.java
+++ b/src/test/java/org/jboss/netty/handler/codec/http/QueryStringDecoderTest.java
@@ -15,6 +15,9 @@
*/
package org.jboss.netty.handler.codec.http;
+import java.util.List;
+import java.util.Map;
+
import org.jboss.netty.util.CharsetUtil;
import org.junit.Assert;
import org.junit.Test;
@@ -93,6 +96,70 @@ public class QueryStringDecoderTest {
assertQueryString("/foo?a=1&a=&a=", "/foo?a=1&a&a=");
}
+ @Test
+ public void testHashDos() throws Exception {
+ StringBuilder buf = new StringBuilder();
+ buf.append('?');
+ for (int i = 0; i < 65536; i ++) {
+ buf.append('k');
+ buf.append(i);
+ buf.append("=v");
+ buf.append(i);
+ buf.append('&');
+ }
+ Assert.assertEquals(1024, new QueryStringDecoder(buf.toString()).getParameters().size());
+ }
+
+ @Test
+ public void testHasPath() throws Exception {
+ QueryStringDecoder decoder = new QueryStringDecoder("1=2", false);
+ Assert.assertEquals("", decoder.getPath());
+ Map