diff --git a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java index cf68e85247..922080cbb6 100644 --- a/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java +++ b/http-netty/src/main/java/io/micronaut/http/netty/AbstractNettyHttpRequest.java @@ -26,6 +26,7 @@ import io.micronaut.http.netty.stream.StreamedHttpRequest; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpConstants; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.util.DefaultAttributeMap; @@ -191,6 +192,16 @@ public String getPath() { */ protected abstract Charset initCharset(Charset characterEncoding); + /** + * @return the maximum number of parameters. + */ + protected abstract int getMaxParams(); + + /** + * @return {@code true} if yes, {@code false} otherwise. + */ + protected abstract boolean isSemicolonIsNormalChar(); + /** * @param uri The URI * @return The query string decoder @@ -198,7 +209,11 @@ public String getPath() { @SuppressWarnings("ConstantConditions") protected final QueryStringDecoder createDecoder(URI uri) { Charset cs = getCharacterEncoding(); - return cs != null ? new QueryStringDecoder(uri, cs) : new QueryStringDecoder(uri); + boolean semicolonIsNormalChar = isSemicolonIsNormalChar(); + int maxParams = getMaxParams(); + return cs != null ? + new QueryStringDecoder(uri, cs, maxParams, semicolonIsNormalChar) : + new QueryStringDecoder(uri, HttpConstants.DEFAULT_CHARSET, maxParams, semicolonIsNormalChar); } private NettyHttpParameters decodeParameters() { diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java index 08652fb2b5..85b05c49cb 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyHttpRequest.java @@ -608,6 +608,16 @@ protected Charset initCharset(Charset characterEncoding) { return characterEncoding == null ? serverConfiguration.getDefaultCharset() : characterEncoding; } + @Override + protected int getMaxParams() { + return serverConfiguration.getMaxParams(); + } + + @Override + protected boolean isSemicolonIsNormalChar() { + return serverConfiguration.isSemicolonIsNormalChar(); + } + /** * @return Return true if the request is form data. */ diff --git a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java index 0c8fb4a099..f8458facc0 100644 --- a/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java +++ b/http-server/src/main/java/io/micronaut/http/server/HttpServerConfiguration.java @@ -127,6 +127,12 @@ public class HttpServerConfiguration implements ServerContextPathProvider { */ @SuppressWarnings("WeakerAccess") public static final boolean DEFAULT_DISPATCH_OPTIONS_REQUESTS = false; + + @SuppressWarnings("WeakerAccess") + public static final boolean DEFAULT_SEMICOLON_IS_NORMAL_CHAR = false; + + @SuppressWarnings("WeakerAccess") + public static final int DEFAULT_MAX_PARAMS = 1024; private Integer port; private String host; private Integer readTimeout; @@ -155,6 +161,8 @@ public class HttpServerConfiguration implements ServerContextPathProvider { private ThreadSelection threadSelection = ThreadSelection.MANUAL; private boolean validateUrl = true; private boolean notFoundOnMissingBody = true; + private boolean semicolonIsNormalChar = DEFAULT_SEMICOLON_IS_NORMAL_CHAR; + private int maxParams = DEFAULT_MAX_PARAMS; /** * Default constructor. @@ -598,6 +606,45 @@ public void setNotFoundOnMissingBody(boolean notFoundOnMissingBody) { this.notFoundOnMissingBody = notFoundOnMissingBody; } + /** + * Returns whether the semicolon is considered a normal character in the query. + * A "normal" semicolon is one that is not used as a parameter separator. + * + * @return {@code true} if the semicolon is a normal character, {@code false} otherwise. + * @since 4.8 + */ + public boolean isSemicolonIsNormalChar() { + return semicolonIsNormalChar; + } + + /** + * Sets whether the semicolon should be considered a normal character in the query. + * A "normal" semicolon is one that is not used as a parameter separator. + * + * @param semicolonIsNormalChar {@code true} if the semicolon should be a normal character, {@code false} otherwise. + * @since 4.8 + */ + public void setSemicolonIsNormalChar(boolean semicolonIsNormalChar) { + this.semicolonIsNormalChar = semicolonIsNormalChar; + } + + /** + * @return the maximum parameter count. + * @since 4.8 + */ + public int getMaxParams() { + return maxParams; + } + + /** + * @param maxParams the maximum parameter count. + * @since 4.8 + */ + public void setMaxParams(int maxParams) { + this.maxParams = maxParams; + } + + /** * Configuration for multipart handling. */ diff --git a/http/src/main/java/io/micronaut/http/form/FormConfiguration.java b/http/src/main/java/io/micronaut/http/form/FormConfiguration.java index 40edb6253f..0538af5c02 100644 --- a/http/src/main/java/io/micronaut/http/form/FormConfiguration.java +++ b/http/src/main/java/io/micronaut/http/form/FormConfiguration.java @@ -26,4 +26,12 @@ public interface FormConfiguration { * @return default maximum of decoded key value parameters. It defaults to 1024. */ int getMaxDecodedKeyValueParameters(); + + /** + * @return true if the semicolon handle as a normal character, false otherwise. + */ + default boolean isSemicolonIsNormalChar() { + return false; + } + } diff --git a/http/src/main/java/io/micronaut/http/form/FormConfigurationProperties.java b/http/src/main/java/io/micronaut/http/form/FormConfigurationProperties.java index e612f3df5d..8a56c7b8f6 100644 --- a/http/src/main/java/io/micronaut/http/form/FormConfigurationProperties.java +++ b/http/src/main/java/io/micronaut/http/form/FormConfigurationProperties.java @@ -34,7 +34,15 @@ final class FormConfigurationProperties implements FormConfiguration { @SuppressWarnings("WeakerAccess") private static final int DEFAULT_MAX_DECODED_KEY_VALUE_PARAMETERS = 1024; + /** + * Default value indicating whether the semicolon is treated as a normal character + * used in {@link io.micronaut.http.form.FormUrlEncodedDecoder}. + */ + @SuppressWarnings("WeakerAccess") + private static final boolean DEFAULT_SEMICOLON_IS_NORMAL_CHAR = false; + private int maxDecodedKeyValueParameters = DEFAULT_MAX_DECODED_KEY_VALUE_PARAMETERS; + private boolean semicolonIsNormalChar = DEFAULT_SEMICOLON_IS_NORMAL_CHAR; /** * @@ -45,6 +53,15 @@ public int getMaxDecodedKeyValueParameters() { return maxDecodedKeyValueParameters; } + + /** + * @return true if the semicolon is treated as a normal character, false otherwise + */ + @Override + public boolean isSemicolonIsNormalChar() { + return semicolonIsNormalChar; + } + /** * default maximum of decoded key value parameters. Default value {@link #DEFAULT_MAX_DECODED_KEY_VALUE_PARAMETERS}. * @param maxDecodedKeyValueParameters default maximum of decoded key value parameters @@ -52,4 +69,11 @@ public int getMaxDecodedKeyValueParameters() { public void setMaxDecodedKeyValueParameters(int maxDecodedKeyValueParameters) { this.maxDecodedKeyValueParameters = maxDecodedKeyValueParameters; } + + /** + * @param semicolonIsNormalChar true if the semicolon should be treated as a normal character, false otherwise + */ + public void setSemicolonIsNormalChar(boolean semicolonIsNormalChar) { + this.semicolonIsNormalChar = semicolonIsNormalChar; + } } diff --git a/http/src/main/java/io/micronaut/http/uri/DefaultFormUrlEncodedDecoder.java b/http/src/main/java/io/micronaut/http/uri/DefaultFormUrlEncodedDecoder.java index 876de83ec0..2875ab3e7b 100644 --- a/http/src/main/java/io/micronaut/http/uri/DefaultFormUrlEncodedDecoder.java +++ b/http/src/main/java/io/micronaut/http/uri/DefaultFormUrlEncodedDecoder.java @@ -36,7 +36,9 @@ final class DefaultFormUrlEncodedDecoder implements FormUrlEncodedDecoder { @NonNull public Map decode(@NonNull String formUrlEncodedString, @NonNull Charset charset) { - QueryStringDecoder decoder = new QueryStringDecoder(formUrlEncodedString, charset, false, formConfiguration.getMaxDecodedKeyValueParameters()); + QueryStringDecoder decoder = new QueryStringDecoder(formUrlEncodedString, charset, false, + formConfiguration.getMaxDecodedKeyValueParameters(), + formConfiguration.isSemicolonIsNormalChar()); return flatten(decoder.parameters()); } } diff --git a/http/src/main/java/io/micronaut/http/uri/QueryStringDecoder.java b/http/src/main/java/io/micronaut/http/uri/QueryStringDecoder.java index 3c11e641eb..a847d16a9b 100644 --- a/http/src/main/java/io/micronaut/http/uri/QueryStringDecoder.java +++ b/http/src/main/java/io/micronaut/http/uri/QueryStringDecoder.java @@ -63,6 +63,7 @@ final class QueryStringDecoder { private final Charset charset; private final String uri; private final int maxParams; + private final boolean semicolonIsNormalChar; private int pathEndIdx; private String path; private Map> params; @@ -121,9 +122,14 @@ final class QueryStringDecoder { * @param maxParams The maximum number of params */ QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams) { + this(uri, charset, hasPath, maxParams, false); + } + + QueryStringDecoder(String uri, Charset charset, boolean hasPath, int maxParams, boolean semicolonIsNormalChar) { this.uri = Objects.requireNonNull(uri, "uri"); this.charset = Objects.requireNonNull(charset, "charset"); this.maxParams = maxParams; + this.semicolonIsNormalChar = semicolonIsNormalChar; // `-1` means that path end index will be initialized lazily pathEndIdx = hasPath ? -1 : 0; @@ -159,6 +165,10 @@ final class QueryStringDecoder { * @param maxParams The maximum number of params */ QueryStringDecoder(URI uri, Charset charset, int maxParams) { + this(uri, charset, maxParams, false); + } + + QueryStringDecoder(URI uri, Charset charset, int maxParams, boolean semicolonIsNormalChar) { String rawPath = uri.getRawPath(); if (rawPath == null) { rawPath = EMPTY_STRING; @@ -166,6 +176,7 @@ final class QueryStringDecoder { this.uri = uriToString(uri); this.charset = Objects.requireNonNull(charset, "charset"); this.maxParams = ArgumentUtils.requirePositive("maxParams", maxParams); + this.semicolonIsNormalChar = semicolonIsNormalChar; pathEndIdx = rawPath.length(); } @@ -196,7 +207,7 @@ public String path() { */ public Map> parameters() { if (params == null) { - params = decodeParams(uri, pathEndIdx(), charset, maxParams); + params = decodeParams(uri, pathEndIdx(), charset, maxParams, semicolonIsNormalChar); } return params; } @@ -260,6 +271,10 @@ private static String uriToString(URI uri) { } private static Map> decodeParams(String s, int from, Charset charset, int paramsLimit) { + return decodeParams(s, from, charset, paramsLimit, false); + } + + private static Map> decodeParams(String s, int from, Charset charset, int paramsLimit, boolean semicolonIsNormalChar) { int len = s.length(); if (from >= len) { return Collections.emptyMap(); @@ -283,6 +298,9 @@ private static Map> decodeParams(String s, int from, Charse break; case '&': case ';': + if (semicolonIsNormalChar) { + continue; + } if (addParam(s, nameStart, valueStart, i, params, charset)) { paramsLimit--; if (paramsLimit == 0) { diff --git a/http/src/test/java/io/micronaut/http/form/FormConfigurationTest.java b/http/src/test/java/io/micronaut/http/form/FormConfigurationTest.java index 4a96cbbe33..ac3be606fa 100644 --- a/http/src/test/java/io/micronaut/http/form/FormConfigurationTest.java +++ b/http/src/test/java/io/micronaut/http/form/FormConfigurationTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; @MicronautTest(startApplication = false) class FormConfigurationTest { @@ -17,4 +18,9 @@ class FormConfigurationTest { void defaultMaxParams() { assertEquals(1024, formConfiguration.getMaxDecodedKeyValueParameters()); } + + @Test + void defaultSemicolonIsNormalChar() { + assertFalse(formConfiguration.isSemicolonIsNormalChar()); + } } diff --git a/http/src/test/java/io/micronaut/http/form/FormConfigurationViaPropertyTest.java b/http/src/test/java/io/micronaut/http/form/FormConfigurationViaPropertyTest.java index 00b97f061f..706ee15253 100644 --- a/http/src/test/java/io/micronaut/http/form/FormConfigurationViaPropertyTest.java +++ b/http/src/test/java/io/micronaut/http/form/FormConfigurationViaPropertyTest.java @@ -7,8 +7,10 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; @Property(name = "micronaut.http.forms.max-decoded-key-value-parameters", value = "512") +@Property(name = "micronaut.http.forms.semicolon-is-normal-char", value = "true") @MicronautTest(startApplication = false) class FormConfigurationViaPropertyTest { @@ -19,4 +21,9 @@ class FormConfigurationViaPropertyTest { void maxParamCanBeSetViaProperty() { assertEquals(512, formConfiguration.getMaxDecodedKeyValueParameters()); } + + @Test + void semicolonIsNormalCharCanBeSetViaProperty() { + assertTrue(formConfiguration.isSemicolonIsNormalChar()); + } }