Form params with ampersand and no value fix (#2227)

* Form params with ampersand and no value fix

Signed-off-by: David Kral <david.k.kral@oracle.com>
This commit is contained in:
David Král
2020-08-04 11:01:29 +02:00
committed by GitHub
parent 8d79e66bba
commit 68ad56d260
7 changed files with 84 additions and 24 deletions

View File

@@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Provides access to any form parameters present in the request entity.
@@ -68,6 +69,7 @@ public interface FormParams extends Parameters {
@Override
public Builder add(String name, String... values) {
Objects.requireNonNull(name);
params.computeIfAbsent(name, k -> new ArrayList<>()).addAll(Arrays.asList(values));
return this;
}

View File

@@ -15,10 +15,14 @@
*/
package io.helidon.common.http;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -48,14 +52,28 @@ class FormParamsImpl extends ReadOnlyParameters implements FormParams {
}
static FormParams create(String paramAssignments, MediaType mediaType) {
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
Function<String, String> decoder = decoder(mediaType, charset);
final Map<String, List<String>> params = new HashMap<>();
Matcher m = PATTERNS.get(mediaType).matcher(paramAssignments);
while (m.find()) {
final String key = m.group(1);
final String key = decoder.apply(m.group(1));
final String value = m.group(2);
params.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
if (value == null) {
params.computeIfAbsent(key, k -> new ArrayList<>());
} else {
params.computeIfAbsent(key, k -> new ArrayList<>()).add(decoder.apply(value));
}
}
return new FormParamsImpl(params);
}
private static Function<String, String> decoder(MediaType mediaType, Charset charset) {
if (mediaType == MediaType.TEXT_PLAIN) {
return (s) -> s;
} else {
return (s) -> URLDecoder.decode(s, charset);
}
}
}

View File

@@ -15,10 +15,12 @@
*/
package io.helidon.media.common;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.Flow;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -47,7 +49,7 @@ class FormParamsBodyReader implements MessageBodyReader<FormParams> {
}
private static Pattern preparePattern(String assignmentSeparator) {
return Pattern.compile(String.format("([^=]+)=([^%1$s]+)%1$s?", assignmentSeparator));
return Pattern.compile(String.format("([^=%1$s]+)=?([^%1$s]+)?%1$s?", assignmentSeparator));
}
@Override
@@ -66,23 +68,33 @@ class FormParamsBodyReader implements MessageBodyReader<FormParams> {
MessageBodyReaderContext context) {
MediaType mediaType = context.contentType().orElseThrow();
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
Function<String, String> decoder = decoder(mediaType, charset);
Single<String> result = mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
? ContentReaders.readURLEncodedString(publisher, charset)
: ContentReaders.readString(publisher, charset);
return (Single<U>) result.map(formStr -> create(formStr, mediaType));
return (Single<U>) ContentReaders.readString(publisher, charset)
.map(formStr -> create(formStr, mediaType, decoder));
}
private FormParams create(String paramAssignments, MediaType mediaType) {
private FormParams create(String paramAssignments, MediaType mediaType, Function<String, String> decoder) {
FormParams.Builder builder = FormParams.builder();
Matcher m = PATTERNS.get(mediaType).matcher(paramAssignments);
while (m.find()) {
final String key = m.group(1);
final String value = m.group(2);
builder.add(key, value);
if (value == null) {
builder.add(decoder.apply(key));
} else {
builder.add(decoder.apply(key), decoder.apply(value));
}
}
return builder.build();
}
private Function<String, String> decoder(MediaType mediaType, Charset charset) {
if (mediaType == MediaType.TEXT_PLAIN) {
return (s) -> s;
} else {
return (s) -> URLDecoder.decode(s, charset);
}
}
}

View File

@@ -90,13 +90,21 @@ class FormParamsBodyWriter implements MessageBodyWriter<FormParams> {
Function<String, String> encoder = encoder();
StringBuilder result = new StringBuilder();
for (Map.Entry<String, List<String>> entry : formParams.toMap().entrySet()) {
for (String value : entry.getValue()) {
List<String> values = entry.getValue();
if (values.size() == 0) {
if (result.length() > 0) {
result.append(separator);
}
result.append(encoder.apply(entry.getKey()));
result.append("=");
result.append(encoder.apply(value));
} else {
for (String value : values) {
if (result.length() > 0) {
result.append(separator);
}
result.append(encoder.apply(entry.getKey()));
result.append("=");
result.append(encoder.apply(value));
}
}
}
return result.toString();

View File

@@ -80,6 +80,7 @@ public class GreetService implements Service {
.get("/redirectPath", this::redirectPath)
.get("/redirect/infinite", this::redirectInfinite)
.post("/form", this::form)
.post("/form/content", this::formContent)
.get("/secure/basic", this::basicAuth)
.get("/secure/basic/outbound", this::basicAuthOutbound)
.put("/greeting", this::updateGreetingHandler);
@@ -139,7 +140,7 @@ public class GreetService implements Service {
}
private void redirectPath(ServerRequest request,
ServerResponse response) {
ServerResponse response) {
response.headers().add(Http.Header.LOCATION, "/greet");
response.status(Http.Status.MOVED_PERMANENTLY_301).send();
}
@@ -155,6 +156,11 @@ public class GreetService implements Service {
.thenAccept(res::send);
}
private void formContent(ServerRequest req, ServerResponse res) {
req.content().as(FormParams.class)
.thenAccept(res::send);
}
/**
* Set the greeting to use in future messages.
*

View File

@@ -33,6 +33,17 @@ public class FormTest extends TestParent {
.add("name", "David Tester")
.build();
private static final String SPECIAL = "special";
private static final String MULTIPLE = "multiple";
private static final String NO_VALUE = "noValue";
private static final String SPECIAL_VALUE = "some &@#/ special value";
private static final FormParams ADVANCED_TEST_FORM = FormParams.builder()
.add(SPECIAL, SPECIAL_VALUE)
.add(MULTIPLE, "value1", "value2")
.add(NO_VALUE)
.build();
@Test
public void testHelloWorld() {
webClient.post()
@@ -62,4 +73,16 @@ public class FormTest extends TestParent {
assertThat(ex.getCause().getMessage(),
is("No writer found for type: class io.helidon.common.http.FormParamsImpl"));
}
@Test
public void testFormContent() {
FormParams received = webClient.post()
.path("/form/content")
.submit(ADVANCED_TEST_FORM, FormParams.class)
.await();
assertThat(received.all(SPECIAL), is(ADVANCED_TEST_FORM.all(SPECIAL)));
assertThat(received.all(MULTIPLE), is(ADVANCED_TEST_FORM.all(MULTIPLE)));
assertThat(received.all(NO_VALUE).size(), is(0));
}
}

View File

@@ -17,12 +17,9 @@ package io.helidon.webserver;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Flow.Publisher;
import io.helidon.common.http.DataChunk;
import io.helidon.common.http.FormParams;
import io.helidon.common.http.MediaType;
import io.helidon.common.reactive.Single;
import io.helidon.media.common.ContentReaders;
import io.helidon.media.common.DefaultMediaSupport;
@@ -61,7 +58,7 @@ public class FormParamsSupport implements Service, Handler {
Charset charset = reqMediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
req.content().registerReader(FormParams.class,
(chunks, type) -> readContent(reqMediaType, charset, chunks)
(chunks, type) -> ContentReaders.readString(chunks, charset)
.map(s -> FormParams.create(s, reqMediaType)).toStage());
req.next();
@@ -75,10 +72,4 @@ public class FormParamsSupport implements Service, Handler {
return INSTANCE;
}
private static Single<String> readContent(MediaType mediaType, Charset charset,
Publisher<DataChunk> chunks) {
return mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
? ContentReaders.readURLEncodedString(chunks, charset)
: ContentReaders.readString(chunks, charset);
}
}