2008-11-19 08:22:15 +01:00
|
|
|
/*
|
2012-06-04 22:31:44 +02:00
|
|
|
* Copyright 2012 The Netty Project
|
2009-06-19 19:48:17 +02:00
|
|
|
*
|
2011-12-09 06:18:34 +01:00
|
|
|
* 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:
|
2008-11-19 08:22:15 +01:00
|
|
|
*
|
2012-06-04 22:31:44 +02:00
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
2008-11-19 08:22:15 +01:00
|
|
|
*
|
2009-08-28 09:15:49 +02:00
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
2011-12-09 06:18:34 +01:00
|
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
2009-08-28 09:15:49 +02:00
|
|
|
* License for the specific language governing permissions and limitations
|
|
|
|
* under the License.
|
2008-11-19 08:22:15 +01:00
|
|
|
*/
|
2011-12-09 04:38:59 +01:00
|
|
|
package io.netty.handler.codec.http;
|
2008-11-19 08:22:15 +01:00
|
|
|
|
2012-05-31 20:32:42 +02:00
|
|
|
import io.netty.util.CharsetUtil;
|
|
|
|
|
2008-11-19 08:22:15 +01:00
|
|
|
import java.net.URI;
|
2011-01-12 11:11:32 +01:00
|
|
|
import java.net.URLDecoder;
|
2017-05-31 07:04:55 +02:00
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
import java.nio.CharBuffer;
|
|
|
|
import java.nio.charset.CharacterCodingException;
|
2009-12-29 06:52:00 +01:00
|
|
|
import java.nio.charset.Charset;
|
2017-05-31 07:04:55 +02:00
|
|
|
import java.nio.charset.CharsetDecoder;
|
|
|
|
import java.nio.charset.CoderResult;
|
2008-11-19 08:22:15 +01:00
|
|
|
import java.util.ArrayList;
|
2010-06-14 12:57:48 +02:00
|
|
|
import java.util.Collections;
|
|
|
|
import java.util.LinkedHashMap;
|
2008-11-19 08:22:15 +01:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.Map;
|
|
|
|
|
2017-05-31 07:04:55 +02:00
|
|
|
import static io.netty.util.internal.ObjectUtil.*;
|
|
|
|
import static io.netty.util.internal.StringUtil.*;
|
2017-02-27 10:49:40 +01:00
|
|
|
|
2008-11-19 08:22:15 +01:00
|
|
|
/**
|
2009-06-19 16:49:26 +02:00
|
|
|
* Splits an HTTP query string into a path string and key-value parameter pairs.
|
2009-06-19 17:05:47 +02:00
|
|
|
* This decoder is for one time use only. Create a new instance for each URI:
|
|
|
|
* <pre>
|
2011-11-22 20:14:10 +01:00
|
|
|
* {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world&x=1;y=2");
|
2015-05-21 06:04:46 +02:00
|
|
|
* assert decoder.path().equals("/hello");
|
|
|
|
* assert decoder.parameters().get("recipient").get(0).equals("world");
|
|
|
|
* assert decoder.parameters().get("x").get(0).equals("1");
|
|
|
|
* assert decoder.parameters().get("y").get(0).equals("2");
|
2009-06-19 17:05:47 +02:00
|
|
|
* </pre>
|
2011-12-30 09:58:51 +01:00
|
|
|
*
|
|
|
|
* This decoder can also decode the content of an HTTP POST request whose
|
|
|
|
* content type is <tt>application/x-www-form-urlencoded</tt>:
|
|
|
|
* <pre>
|
|
|
|
* {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false);
|
|
|
|
* ...
|
|
|
|
* </pre>
|
|
|
|
*
|
|
|
|
* <h3>HashDOS vulnerability fix</h3>
|
|
|
|
*
|
2014-08-01 02:05:54 +02:00
|
|
|
* As a workaround to the <a href="http://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder
|
2012-06-08 12:28:12 +02:00
|
|
|
* limits the maximum number of decoded key-value parameter pairs, up to {@literal 1024} by
|
|
|
|
* default, and you can configure it when you construct the decoder by passing an additional
|
|
|
|
* integer parameter.
|
2011-12-30 09:58:51 +01:00
|
|
|
*
|
2009-06-19 17:05:47 +02:00
|
|
|
* @see QueryStringEncoder
|
2008-11-19 08:22:15 +01:00
|
|
|
*/
|
|
|
|
public class QueryStringDecoder {
|
|
|
|
|
2011-12-30 09:58:51 +01:00
|
|
|
private static final int DEFAULT_MAX_PARAMS = 1024;
|
|
|
|
|
2009-12-29 06:52:00 +01:00
|
|
|
private final Charset charset;
|
2008-11-19 08:22:15 +01:00
|
|
|
private final String uri;
|
2011-12-30 09:58:51 +01:00
|
|
|
private final int maxParams;
|
2017-05-31 07:04:55 +02:00
|
|
|
private int pathEndIdx;
|
2008-11-19 08:22:15 +01:00
|
|
|
private String path;
|
2010-06-14 12:57:48 +02:00
|
|
|
private Map<String, List<String>> params;
|
2008-11-19 08:22:15 +01:00
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI. The decoder will
|
|
|
|
* assume that the query string is encoded in UTF-8.
|
|
|
|
*/
|
2008-11-19 08:22:15 +01:00
|
|
|
public QueryStringDecoder(String uri) {
|
2012-05-31 20:32:42 +02:00
|
|
|
this(uri, HttpConstants.DEFAULT_CHARSET);
|
2009-02-26 07:34:07 +01:00
|
|
|
}
|
|
|
|
|
2011-12-30 09:58:51 +01:00
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
|
|
|
public QueryStringDecoder(String uri, boolean hasPath) {
|
2012-05-31 20:32:42 +02:00
|
|
|
this(uri, HttpConstants.DEFAULT_CHARSET, hasPath);
|
2011-12-30 09:58:51 +01:00
|
|
|
}
|
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
2009-12-29 06:52:00 +01:00
|
|
|
public QueryStringDecoder(String uri, Charset charset) {
|
2011-12-30 09:58:51 +01:00
|
|
|
this(uri, charset, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
|
|
|
public QueryStringDecoder(String uri, Charset charset, boolean hasPath) {
|
|
|
|
this(uri, charset, hasPath, DEFAULT_MAX_PARAMS);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
|
|
|
public QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) {
|
2017-05-31 07:04:55 +02:00
|
|
|
this.uri = checkNotNull(uri, "uri");
|
|
|
|
this.charset = checkNotNull(charset, "charset");
|
|
|
|
this.maxParams = checkPositive(maxParams, "maxParams");
|
|
|
|
|
|
|
|
// `-1` means that path end index will be initialized lazily
|
|
|
|
pathEndIdx = hasPath ? -1 : 0;
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI. The decoder will
|
|
|
|
* assume that the query string is encoded in UTF-8.
|
|
|
|
*/
|
2009-02-26 07:34:07 +01:00
|
|
|
public QueryStringDecoder(URI uri) {
|
2012-05-31 20:32:42 +02:00
|
|
|
this(uri, HttpConstants.DEFAULT_CHARSET);
|
2009-02-26 07:34:07 +01:00
|
|
|
}
|
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
2012-01-11 12:16:14 +01:00
|
|
|
public QueryStringDecoder(URI uri, Charset charset) {
|
2011-12-30 09:58:51 +01:00
|
|
|
this(uri, charset, DEFAULT_MAX_PARAMS);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a new decoder that decodes the specified URI encoded in the
|
|
|
|
* specified charset.
|
|
|
|
*/
|
|
|
|
public QueryStringDecoder(URI uri, Charset charset, int maxParams) {
|
2012-02-15 08:30:22 +01:00
|
|
|
String rawPath = uri.getRawPath();
|
2017-05-31 07:04:55 +02:00
|
|
|
if (rawPath == null) {
|
2017-02-27 10:49:40 +01:00
|
|
|
rawPath = EMPTY_STRING;
|
2012-02-15 08:30:22 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
String rawQuery = uri.getRawQuery();
|
2012-05-31 20:32:42 +02:00
|
|
|
// Also take care of cut of things like "http://localhost"
|
2017-05-31 07:04:55 +02:00
|
|
|
this.uri = rawQuery == null? rawPath : rawPath + '?' + rawQuery;
|
|
|
|
this.charset = checkNotNull(charset, "charset");
|
|
|
|
this.maxParams = checkPositive(maxParams, "maxParams");
|
|
|
|
pathEndIdx = rawPath.length();
|
|
|
|
}
|
2009-02-26 07:34:07 +01:00
|
|
|
|
2017-05-31 07:04:55 +02:00
|
|
|
@Override
|
|
|
|
public String toString() {
|
|
|
|
return uri();
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
|
2014-09-19 20:06:22 +02:00
|
|
|
/**
|
|
|
|
* Returns the uri used to initialize this {@link QueryStringDecoder}.
|
|
|
|
*/
|
|
|
|
public String uri() {
|
|
|
|
return uri;
|
|
|
|
}
|
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Returns the decoded path string of the URI.
|
|
|
|
*/
|
2013-01-17 06:48:03 +01:00
|
|
|
public String path() {
|
2010-06-14 12:57:48 +02:00
|
|
|
if (path == null) {
|
2017-05-31 07:04:55 +02:00
|
|
|
path = decodeComponent(uri, 0, pathEndIdx(), charset, true);
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2009-06-19 16:49:26 +02:00
|
|
|
/**
|
|
|
|
* Returns the decoded key-value parameter pairs of the URI.
|
|
|
|
*/
|
2013-01-17 06:48:03 +01:00
|
|
|
public Map<String, List<String>> parameters() {
|
2010-06-14 12:57:48 +02:00
|
|
|
if (params == null) {
|
2017-05-31 07:04:55 +02:00
|
|
|
params = decodeParams(uri, pathEndIdx(), charset, maxParams);
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
2017-10-23 07:18:24 +02:00
|
|
|
/**
|
|
|
|
* Returns the raw path string of the URI.
|
|
|
|
*/
|
|
|
|
public String rawPath() {
|
|
|
|
return uri.substring(0, pathEndIdx());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns raw query string of the URI.
|
|
|
|
*/
|
|
|
|
public String rawQuery() {
|
|
|
|
int start = pathEndIdx() + 1;
|
|
|
|
return start < uri.length() ? uri.substring(start) : EMPTY_STRING;
|
|
|
|
}
|
|
|
|
|
2017-05-31 07:04:55 +02:00
|
|
|
private int pathEndIdx() {
|
|
|
|
if (pathEndIdx == -1) {
|
|
|
|
pathEndIdx = findPathEndIndex(uri);
|
|
|
|
}
|
|
|
|
return pathEndIdx;
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit) {
|
|
|
|
int len = s.length();
|
|
|
|
if (from >= len) {
|
|
|
|
return Collections.emptyMap();
|
|
|
|
}
|
|
|
|
if (s.charAt(from) == '?') {
|
|
|
|
from++;
|
|
|
|
}
|
|
|
|
Map<String, List<String>> params = new LinkedHashMap<String, List<String>>();
|
|
|
|
int nameStart = from;
|
|
|
|
int valueStart = -1;
|
|
|
|
int i;
|
|
|
|
loop:
|
|
|
|
for (i = from; i < len; i++) {
|
|
|
|
switch (s.charAt(i)) {
|
|
|
|
case '=':
|
|
|
|
if (nameStart == i) {
|
|
|
|
nameStart = i + 1;
|
|
|
|
} else if (valueStart < nameStart) {
|
|
|
|
valueStart = i + 1;
|
2010-06-14 12:57:48 +02:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
break;
|
|
|
|
case '&':
|
|
|
|
case ';':
|
|
|
|
if (addParam(s, nameStart, valueStart, i, params, charset)) {
|
|
|
|
paramsLimit--;
|
|
|
|
if (paramsLimit == 0) {
|
|
|
|
return params;
|
2011-12-30 09:58:51 +01:00
|
|
|
}
|
2010-06-14 12:57:48 +02:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
nameStart = i + 1;
|
|
|
|
break;
|
|
|
|
case '#':
|
|
|
|
break loop;
|
|
|
|
default:
|
|
|
|
// continue
|
2009-02-12 05:39:17 +01:00
|
|
|
}
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
addParam(s, nameStart, valueStart, i, params, charset);
|
|
|
|
return params;
|
2011-12-30 09:58:51 +01:00
|
|
|
}
|
2010-06-14 12:57:48 +02:00
|
|
|
|
2017-05-31 07:04:55 +02:00
|
|
|
private static boolean addParam(String s, int nameStart, int valueStart, int valueEnd,
|
|
|
|
Map<String, List<String>> params, Charset charset) {
|
|
|
|
if (nameStart >= valueEnd) {
|
2011-12-30 09:58:51 +01:00
|
|
|
return false;
|
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
if (valueStart <= nameStart) {
|
|
|
|
valueStart = valueEnd + 1;
|
|
|
|
}
|
|
|
|
String name = decodeComponent(s, nameStart, valueStart - 1, charset, false);
|
|
|
|
String value = decodeComponent(s, valueStart, valueEnd, charset, false);
|
2011-12-30 09:58:51 +01:00
|
|
|
List<String> values = params.get(name);
|
|
|
|
if (values == null) {
|
|
|
|
values = new ArrayList<String>(1); // Often there's only 1 value.
|
|
|
|
params.put(name, values);
|
|
|
|
}
|
|
|
|
values.add(value);
|
|
|
|
return true;
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
|
2010-12-04 08:12:03 +01:00
|
|
|
/**
|
|
|
|
* Decodes a bit of an URL encoded by a browser.
|
|
|
|
* <p>
|
2011-01-12 11:11:32 +01:00
|
|
|
* This is equivalent to calling {@link #decodeComponent(String, Charset)}
|
2010-12-04 08:12:03 +01:00
|
|
|
* 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) {
|
2012-05-31 20:32:42 +02:00
|
|
|
return decodeComponent(s, HttpConstants.DEFAULT_CHARSET);
|
2010-12-04 08:12:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Decodes a bit of an URL encoded by a browser.
|
|
|
|
* <p>
|
|
|
|
* 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}.
|
|
|
|
* <p>
|
|
|
|
* This is essentially equivalent to calling
|
2017-05-31 07:04:55 +02:00
|
|
|
* {@link URLDecoder#decode(String, String)}
|
2010-12-04 08:12:03 +01:00
|
|
|
* 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
|
2011-01-12 11:11:32 +01:00
|
|
|
* be {@link CharsetUtil#UTF_8}.
|
2010-12-04 08:12:03 +01:00
|
|
|
* @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.
|
|
|
|
*/
|
2014-06-24 10:39:46 +02:00
|
|
|
public static String decodeComponent(final String s, final Charset charset) {
|
2009-03-14 14:48:01 +01:00
|
|
|
if (s == null) {
|
2017-02-27 10:49:40 +01:00
|
|
|
return EMPTY_STRING;
|
2009-03-14 14:48:01 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
return decodeComponent(s, 0, s.length(), charset, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
private static String decodeComponent(String s, int from, int toExcluded, Charset charset, boolean isPath) {
|
|
|
|
int len = toExcluded - from;
|
|
|
|
if (len <= 0) {
|
|
|
|
return EMPTY_STRING;
|
|
|
|
}
|
|
|
|
int firstEscaped = -1;
|
|
|
|
for (int i = from; i < toExcluded; i++) {
|
|
|
|
char c = s.charAt(i);
|
|
|
|
if (c == '%' || c == '+' && !isPath) {
|
|
|
|
firstEscaped = i;
|
2014-02-14 22:25:57 +01:00
|
|
|
break;
|
2010-12-04 08:12:03 +01:00
|
|
|
}
|
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
if (firstEscaped == -1) {
|
|
|
|
return s.substring(from, toExcluded);
|
2010-12-04 08:12:03 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
|
|
|
|
CharsetDecoder decoder = CharsetUtil.decoder(charset);
|
|
|
|
|
|
|
|
// Each encoded byte takes 3 characters (e.g. "%20")
|
|
|
|
int decodedCapacity = (toExcluded - firstEscaped) / 3;
|
|
|
|
ByteBuffer byteBuf = ByteBuffer.allocate(decodedCapacity);
|
|
|
|
CharBuffer charBuf = CharBuffer.allocate(decodedCapacity);
|
|
|
|
|
|
|
|
StringBuilder strBuf = new StringBuilder(len);
|
|
|
|
strBuf.append(s, from, firstEscaped);
|
|
|
|
|
|
|
|
for (int i = firstEscaped; i < toExcluded; i++) {
|
2010-12-04 08:12:03 +01:00
|
|
|
char c = s.charAt(i);
|
2017-05-31 07:04:55 +02:00
|
|
|
if (c != '%') {
|
|
|
|
strBuf.append(c != '+' || isPath? c : SPACE);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
byteBuf.clear();
|
|
|
|
do {
|
2017-06-03 02:27:44 +02:00
|
|
|
if (i + 3 > toExcluded) {
|
|
|
|
throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
|
|
|
|
}
|
|
|
|
byteBuf.put(decodeHexByte(s, i + 1));
|
2017-05-31 07:04:55 +02:00
|
|
|
i += 3;
|
|
|
|
} while (i < toExcluded && s.charAt(i) == '%');
|
|
|
|
i--;
|
|
|
|
|
|
|
|
byteBuf.flip();
|
|
|
|
charBuf.clear();
|
|
|
|
CoderResult result = decoder.reset().decode(byteBuf, charBuf, true);
|
|
|
|
try {
|
|
|
|
if (!result.isUnderflow()) {
|
|
|
|
result.throwException();
|
|
|
|
}
|
|
|
|
result = decoder.flush(charBuf);
|
|
|
|
if (!result.isUnderflow()) {
|
|
|
|
result.throwException();
|
|
|
|
}
|
|
|
|
} catch (CharacterCodingException ex) {
|
|
|
|
throw new IllegalStateException(ex);
|
2010-12-04 08:12:03 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
strBuf.append(charBuf.flip());
|
|
|
|
}
|
|
|
|
return strBuf.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
private static int findPathEndIndex(String uri) {
|
|
|
|
int len = uri.length();
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
|
char c = uri.charAt(i);
|
|
|
|
if (c == '?' || c == '#') {
|
|
|
|
return i;
|
|
|
|
}
|
2009-02-26 07:34:07 +01:00
|
|
|
}
|
2017-05-31 07:04:55 +02:00
|
|
|
return len;
|
2008-11-19 08:22:15 +01:00
|
|
|
}
|
|
|
|
}
|