Skip to content

Commit

Permalink
HttpServerRequest supports query parameters.
Browse files Browse the repository at this point in the history
  • Loading branch information
jchenga committed Oct 16, 2023
1 parent 0cfc731 commit ac78a0b
Show file tree
Hide file tree
Showing 7 changed files with 565 additions and 0 deletions.
1 change: 1 addition & 0 deletions reactor-netty-http/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ task japicmp(type: JapicmpTask) {

compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ]
methodExcludes = [
'reactor.netty.http.server.HttpServerRequest#queryParams()'
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
Expand Down Expand Up @@ -251,6 +253,10 @@ protected HttpMessageLogFactory httpMessageLogFactory() {

protected abstract void beforeMarkSentHeaders();

protected Map<String, List<String>> parseQueryParams(String uri) {
return QueryStringDecoder.decodeParams(uri);
}

protected abstract void afterMarkSentHeaders();

protected abstract boolean isContentAlwaysEmpty();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* Copyright (c) 2012-2023 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed 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
*
* https://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 reactor.netty.http;

import static io.netty.util.internal.StringUtil.EMPTY_STRING;
import static io.netty.util.internal.StringUtil.SPACE;
import static io.netty.util.internal.StringUtil.decodeHexByte;

import io.netty.handler.codec.http.HttpConstants;
import io.netty.util.internal.PlatformDependent;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* Provides utility methods to split an HTTP query string into key-value parameter pairs.
* <pre>
* {@link Map} parameters = {@link QueryStringDecoder}.decodeParams("/hello?recipient=world&x=1;y=2");
* assert parameters.get("recipient").get(0).equals("world");
* assert parameters.get.get("x").get(0).equals("1");
* assert parameters.get.get("y").get(0).equals("2");
* </pre>
*
*
* <h3>HashDOS vulnerability fix</h3>
*
* As a workaround to the <a href="https://netty.io/s/hashdos">HashDOS</a> vulnerability, the decoder
* limits the maximum number of decoded key-value parameter pairs, up to {@literal 1024} by
* default. you can configure it when you construct the decoder by passing an additional
* integer parameter.
*
*/
public class QueryStringDecoder {

private static final int DEFAULT_MAX_PARAMS = 1024;

public static Map<String, List<String>> decodeParams(final String uri) {

return decodeParams(uri, HttpConstants.DEFAULT_CHARSET,
DEFAULT_MAX_PARAMS, true);
}

public static Map<String, List<String>> decodeParams(final String uri, final boolean semiColonIsNormalChar) {

return decodeParams(uri, HttpConstants.DEFAULT_CHARSET,
DEFAULT_MAX_PARAMS, semiColonIsNormalChar);
}

public static Map<String, List<String>> decodeParams(final String uri, final Charset charset, final int maxParams, final boolean semicolonIsNormalChar) {
Objects.requireNonNull(uri, "uri");
Objects.requireNonNull(charset, "charset");
if (maxParams < 1) {
throw new IllegalArgumentException("maxParams must be positive");
}

int from = findPathEndIndex(uri);
return decodeParams(uri, from, charset,
maxParams, semicolonIsNormalChar);
}

private static Map<String, List<String>> decodeParams(String s, int from, Charset charset, int paramsLimit,
boolean semicolonIsNormalChar) {
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;
}
break;
case ';':
if (semicolonIsNormalChar) {
continue;
}
// fall-through
case '&':
if (addParam(s, nameStart, valueStart, i, params, charset)) {
paramsLimit--;
if (paramsLimit == 0) {
return params;
}
}
nameStart = i + 1;
break;
case '#':
break loop;
default:
// continue
}
}
addParam(s, nameStart, valueStart, i, params, charset);
return params;
}

private static boolean addParam(String s, int nameStart, int valueStart, int valueEnd,
Map<String, List<String>> params, Charset charset) {
if (nameStart >= valueEnd) {
return false;
}
if (valueStart <= nameStart) {
valueStart = valueEnd + 1;
}
String name = decodeComponent(s, nameStart, valueStart - 1, charset, false);
String value = decodeComponent(s, valueStart, valueEnd, charset, false);
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;
}

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;
break;
}
}
if (firstEscaped == -1) {
return s.substring(from, toExcluded);
}

// Each encoded byte takes 3 characters (e.g. "%20")
int decodedCapacity = (toExcluded - firstEscaped) / 3;
byte[] buf = PlatformDependent.allocateUninitializedArray(decodedCapacity);
int bufIdx;

StringBuilder strBuf = new StringBuilder(len);
strBuf.append(s, from, firstEscaped);

for (int i = firstEscaped; i < toExcluded; i++) {
char c = s.charAt(i);
if (c != '%') {
strBuf.append(c != '+' || isPath ? c : SPACE);
continue;
}

bufIdx = 0;
do {
if (i + 3 > toExcluded) {
throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s);
}
buf[bufIdx++] = decodeHexByte(s, i + 1);
i += 3;
} while (i < toExcluded && s.charAt(i) == '%');
i--;

strBuf.append(new String(buf, 0, bufIdx, charset));
}
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;
}
}
return len;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
Expand Down Expand Up @@ -124,6 +125,7 @@ class HttpServerOperations extends HttpOperations<HttpServerRequest, HttpServerR
final String scheme;
final ZonedDateTime timestamp;

Map<String, List<String>> queryParamsMap;
BiPredicate<HttpServerRequest, HttpServerResponse> compressionPredicate;
Function<? super String, Map<String, String>> paramsResolver;
String path;
Expand Down Expand Up @@ -466,6 +468,14 @@ public HttpHeaders requestHeaders() {
throw new IllegalStateException("request not parsed");
}

@Override
public Map<String, List<String>> queryParams() {
if (queryParamsMap == null) {
queryParamsMap = Collections.unmodifiableMap(parseQueryParams(this.nettyRequest.uri()));
}
return queryParamsMap;
}

@Override
public String scheme() {
if (connectionInfo != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.net.InetSocketAddress;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down Expand Up @@ -143,6 +144,14 @@ default Flux<HttpContent> receiveContent() {
*/
HttpHeaders requestHeaders();

/**
* return parsed and decoded query parameter name value pairs
*
* @return query parameters {@link Map}
* @since 1.1.6
*/
Map<String, List<String>> queryParams();

/**
* Returns the inbound protocol and version.
*
Expand Down

0 comments on commit ac78a0b

Please sign in to comment.