update to groovy 4.0.11, netty 4.1.90, xbib net 3.1.0, use multimap for HTTP request

This commit is contained in:
Jörg Prante 2023-04-03 17:17:58 +02:00
parent ae1240822f
commit 845260a14e
16 changed files with 176 additions and 136 deletions

View file

@ -1,5 +1,5 @@
group = org.xbib group = org.xbib
name = netty-http name = netty-http
version = 4.1.85.0 version = 4.1.90.0
org.gradle.warning.mode = ALL org.gradle.warning.mode = ALL

View file

@ -7,7 +7,7 @@ dependencies {
test { test {
useJUnitPlatform() useJUnitPlatform()
failFast = true failFast = false
maxHeapSize '1g' maxHeapSize '1g'
systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties' systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties'
testLogging { testLogging {

View file

@ -42,7 +42,7 @@ import java.util.concurrent.CompletableFuture;
/** /**
* HTTP client request. * HTTP client request.
*/ */
public final class Request implements AutoCloseable { public final class Request {
private final URL url; private final URL url;
@ -202,7 +202,6 @@ public final class Request implements AutoCloseable {
} }
} }
@Override
public void close() throws IOException { public void close() throws IOException {
release(); release();
} }

View file

@ -63,21 +63,19 @@ public class RestClient {
HttpMethod httpMethod) throws IOException { HttpMethod httpMethod) throws IOException {
URL url = URL.create(urlString); URL url = URL.create(urlString);
RestClient restClient = new RestClient(); RestClient restClient = new RestClient();
try (Client client = Client.builder() Client client = Client.builder()
.setThreadCount(2) // for redirect .setThreadCount(2) // for redirect
.build()) { .build();
Request.Builder requestBuilder = Request.builder(httpMethod).url(url); Request.Builder requestBuilder = Request.builder(httpMethod).url(url);
if (body != null) { if (body != null) {
ByteBuf byteBuf = client.getByteBufAllocator().buffer(); ByteBuf byteBuf = client.getByteBufAllocator().buffer();
byteBuf.writeCharSequence(body, charset); byteBuf.writeCharSequence(body, charset);
requestBuilder.content(byteBuf); requestBuilder.content(byteBuf);
}
client.newTransport(HttpAddress.http1(url))
.execute(requestBuilder.setResponseListener(restClient::setResponse).build())
.close();
} catch (Exception e) {
throw new IOException(e);
} }
client.newTransport(HttpAddress.http1(url))
.execute(requestBuilder.setResponseListener(restClient::setResponse).build())
.close();
client.close();
return restClient; return restClient;
} }
} }

View file

@ -61,7 +61,7 @@ import java.util.function.Function;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public final class Client implements AutoCloseable { public final class Client {
private static final Logger logger = Logger.getLogger(Client.class.getName()); private static final Logger logger = Logger.getLogger(Client.class.getName());
@ -366,7 +366,6 @@ public final class Client implements AutoCloseable {
closeAndRemove(transport); closeAndRemove(transport);
} }
@Override
public void close() throws IOException { public void close() throws IOException {
shutdownGracefully(); shutdownGracefully();
} }

View file

@ -128,17 +128,21 @@ class RequestBuilderTest {
.url("https://google.com? a = b") .url("https://google.com? a = b")
.build(); .build();
assertEquals("google.com", request.url().getHost()); assertEquals("google.com", request.url().getHost());
assertEquals("https://google.com?%20a%20=%20b", request.absolute()); assertEquals("https://google.com? a = b", request.absolute());
assertEquals("?%20a%20=%20b", request.relative()); assertEquals("? a = b", request.relative());
assertEquals(" a = b", request.url().getQuery());
assertEquals(" a = b", request.url().getDecodedQuery());
assertEquals("https://google.com? a = b", request.url().toString()); assertEquals("https://google.com? a = b", request.url().toString());
assertEquals("https://google.com?%20a%20=%20b", request.url().toExternalForm()); assertEquals("https://google.com? a = b", request.url().toExternalForm());
request = Request.get() request = Request.get()
.url("https://google.com?%20a%20=%20b") .url("https://google.com?%20a%20=%20b")
.build(); .build();
assertEquals("google.com", request.url().getHost()); assertEquals("google.com", request.url().getHost());
assertEquals("https://google.com?%20a%20=%20b", request.absolute()); assertEquals("https://google.com?%20a%20=%20b", request.absolute());
assertEquals("?%20a%20=%20b", request.relative()); assertEquals("?%20a%20=%20b", request.relative());
assertEquals("https://google.com? a = b", request.url().toString()); assertEquals("%20a%20=%20b", request.url().getQuery());
assertEquals(" a = b", request.url().getDecodedQuery());
assertEquals("https://google.com?%20a%20=%20b", request.url().toString());
assertEquals("https://google.com?%20a%20=%20b", request.url().toExternalForm()); assertEquals("https://google.com?%20a%20=%20b", request.url().toExternalForm());
} }
@ -149,6 +153,10 @@ class RequestBuilderTest {
requestBuilder.addParameter("param" + i, "value" + i); requestBuilder.addParameter("param" + i, "value" + i);
} }
Request request = requestBuilder.build(); Request request = requestBuilder.build();
assertEquals(18276, request.absolute().length()); assertEquals(37796, request.absolute().length());
Request parsedRequest = Request.get()
.url(request.url())
.build();
assertEquals(37796, parsedRequest.absolute().length());
} }
} }

View file

@ -22,14 +22,14 @@ class FileDescriptorLeakTest {
if (os instanceof UnixOperatingSystemMXBean) { if (os instanceof UnixOperatingSystemMXBean) {
logger.info("before: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount()); logger.info("before: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount());
} }
try (Client client = Client.builder().setThreadCount(1).build()) { Client client = Client.builder().setThreadCount(1).build();
Request request = Request.get().url("http://xbib.org") Request request = Request.get().url("http://xbib.org")
.setResponseListener(resp -> { .setResponseListener(resp -> {
logger.log(Level.INFO, "status = " + resp.getStatus()); logger.log(Level.INFO, "status = " + resp.getStatus());
}) })
.build(); .build();
client.execute(request); client.execute(request);
} client.close();
if (os instanceof UnixOperatingSystemMXBean){ if (os instanceof UnixOperatingSystemMXBean){
logger.info("after: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount()); logger.info("after: number of open file descriptor : " + ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount());
} }

View file

@ -13,17 +13,17 @@ public class XbibTest {
@Test @Test
void testXbib() throws Exception { void testXbib() throws Exception {
try (Client client = Client.builder() Client client = Client.builder()
.enableDebug() .enableDebug()
.build()) { .build();
Request request = Request.get() Request request = Request.get()
.url("https://xbib.org/") .url("https://xbib.org/")
.setVersion("HTTP/2.0") .setVersion("HTTP/2.0")
.setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " + .setResponseListener(resp -> logger.log(Level.INFO, "got HTTP/2 response: " +
resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8))) resp.getHeaders() + resp.getBodyAsString(StandardCharsets.UTF_8)))
.build(); .build();
client.execute(request).get().close(); client.execute(request).get().close();
} client.close();
} }
} }

View file

@ -1,9 +1,13 @@
package org.xbib.netty.http.common; package org.xbib.netty.http.common;
import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaderValues;
import org.xbib.datastructures.common.MultiMap;
import org.xbib.datastructures.common.Pair;
import org.xbib.datastructures.common.TreeMultiMap;
import org.xbib.net.PercentDecoder;
import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoder;
import org.xbib.net.PercentEncoders; import org.xbib.net.PercentEncoders;
import org.xbib.netty.http.common.util.CaseInsensitiveParameters;
import java.nio.charset.CharacterCodingException; import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException; import java.nio.charset.MalformedInputException;
@ -13,26 +17,26 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* A limited multi-map of HTTP request parameters. Each key references a * A multi-map of HTTP request parameters. Each key references a
* limited set of parameters collected from the request during message * set of parameters collected from the request during message
* signing. Parameter values are sorted as per * signing.
* <a href="http://oauth.net/core/1.0a/#anchor13">OAuth specification</a>.
* Every key/value pair will be percent-encoded upon insertion. * Every key/value pair will be percent-encoded upon insertion.
* This class has special semantics tailored to
* being useful for message signing; it's not a general purpose collection class
* to handle request parameters.
*/ */
public class HttpParameters extends CaseInsensitiveParameters { @SuppressWarnings("serial")
public class HttpParameters {
private static final String EQUALS = "="; private static final Logger logger = Logger.getLogger(HttpParameters.class.getName());
private static final String AMPERSAND = "&"; private static final char EQUAL_CHAR = '=';
private static final char AMPERSAND_CHAR = '&';
private final int sizeLimit; private final MultiMap<String, String> multiMap;
private final int elementSizeLimit; private final PercentDecoder percentDecoder;
private final PercentEncoder percentEncoder; private final PercentEncoder percentEncoder;
@ -41,24 +45,16 @@ public class HttpParameters extends CaseInsensitiveParameters {
private final Charset encoding; private final Charset encoding;
public HttpParameters() { public HttpParameters() {
this(1024, 1024, 65536, this(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8);
HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8);
} }
public HttpParameters(CharSequence contentType) { public HttpParameters(CharSequence contentType) {
this(1024, 1024, 65536, this(contentType, StandardCharsets.UTF_8);
contentType, StandardCharsets.UTF_8);
} }
public HttpParameters(CharSequence contentType, Charset charset) { public HttpParameters(CharSequence contentType, Charset charset) {
this(1024, 1024, 65536, this.multiMap = new TreeMultiMap<>();
contentType, charset); this.percentDecoder = new PercentDecoder();
}
public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit,
CharSequence contentType, Charset charset) {
this.sizeLimit = sizeLimit;
this.elementSizeLimit = elementSizeLimit;
this.percentEncoder = PercentEncoders.getQueryEncoder(charset); this.percentEncoder = PercentEncoders.getQueryEncoder(charset);
this.contentType = contentType; this.contentType = contentType;
this.encoding = charset; this.encoding = charset;
@ -72,12 +68,26 @@ public class HttpParameters extends CaseInsensitiveParameters {
return encoding; return encoding;
} }
public Collection<String> put(String key, Collection<String> values, boolean percentEncode) { public String get(String key) {
remove(key); Collection<String> collection = multiMap.get(key);
if (collection != null) {
Iterator<String> iterator = collection.iterator();
if (iterator.hasNext()) {
return iterator.next();
}
}
return null;
}
public Collection<String> getAll(String key) {
return multiMap.get(key);
}
public void put(String key, Collection<String> values, boolean percentEncode) {
multiMap.remove(key);
for (String v : values) { for (String v : values) {
add(key, v, percentEncode); add(key, v, percentEncode);
} }
return getAll(key);
} }
/** /**
@ -121,7 +131,7 @@ public class HttpParameters extends CaseInsensitiveParameters {
try { try {
String k = percentEncode ? percentEncoder.encode(key) : key; String k = percentEncode ? percentEncoder.encode(key) : key;
String v = percentEncode ? percentEncoder.encode(value) : value; String v = percentEncode ? percentEncoder.encode(value) : value;
super.add(k, v); multiMap.put(k, v);
} catch (CharacterCodingException e) { } catch (CharacterCodingException e) {
throw new IllegalArgumentException(e); throw new IllegalArgumentException(e);
} }
@ -130,10 +140,10 @@ public class HttpParameters extends CaseInsensitiveParameters {
public String getAsQueryString(boolean percentEncode) throws MalformedInputException, UnmappableCharacterException { public String getAsQueryString(boolean percentEncode) throws MalformedInputException, UnmappableCharacterException {
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
for (String key : super.names()) { for (String key : multiMap.keySet()) {
list.add(getAsQueryString(key, percentEncode)); list.add(getAsQueryString(key, percentEncode));
} }
return String.join(AMPERSAND, list); return String.join(String.valueOf(AMPERSAND_CHAR), list);
} }
/** /**
@ -164,20 +174,46 @@ public class HttpParameters extends CaseInsensitiveParameters {
public String getAsQueryString(String key, boolean percentEncode) public String getAsQueryString(String key, boolean percentEncode)
throws MalformedInputException, UnmappableCharacterException { throws MalformedInputException, UnmappableCharacterException {
String k = percentEncode ? percentEncoder.encode(key) : key; String k = percentEncode ? percentEncoder.encode(key) : key;
Collection<String> values = getAll(k); Collection<String> values = multiMap.asMap().get(key);
if (values == null) {
return k + EQUALS;
}
Iterator<String> it = values.iterator(); Iterator<String> it = values.iterator();
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
while (it.hasNext()) { while (it.hasNext()) {
String v = it.next(); String v = it.next();
v = percentEncode ? percentEncoder.encode(v) : v; v = percentEncode ? percentEncoder.encode(v) : v;
sb.append(k).append(EQUALS).append(v); sb.append(k).append(EQUAL_CHAR).append(v);
if (it.hasNext()) { if (it.hasNext()) {
sb.append(AMPERSAND); sb.append(AMPERSAND_CHAR);
} }
} }
return sb.toString(); return sb.toString();
} }
public void addPercentEncodedBody(String body) {
if (body == null) {
return;
}
// watch out for "plus" encoding, replace it with a space character
String s = body.replace('+', ' ');
while (s != null) {
Pair<String, Object> pairs = indexOf(AMPERSAND_CHAR, s);
Pair<String, Object> pair = indexOf(EQUAL_CHAR, pairs.getKey());
if (pair.getKey() != null && !pair.getKey().isEmpty()) {
try {
String key = percentDecoder.decode(pair.getKey());
String value = pair.getValue() instanceof CharSequence ? percentDecoder.decode((CharSequence) pair.getValue()) : pair.getValue().toString();
addRaw(key, value);
} catch (MalformedInputException | UnmappableCharacterException e) {
logger.log(Level.WARNING, "unable to decode parameter in body: " + e.getMessage(), e);
}
}
s = pairs.getValue() !=null ? pairs.getValue().toString() : null;
}
}
private static Pair<String, Object> indexOf(char ch, String input) {
int i = input.indexOf(ch);
String k = i >= 0 ? input.substring(0, i) : input;
Object v = i >= 0 ? input.substring(i + 1) : null;
return Pair.of(k, v);
}
} }

View file

@ -0,0 +1,5 @@
handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
.level=ALL
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter
jdk.event.security.level=INFO

View file

@ -7,10 +7,6 @@ import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpUtil;
import org.xbib.datastructures.common.Pair;
import org.xbib.net.Parameter;
import org.xbib.net.ParameterBuilder;
import org.xbib.net.PercentDecoder;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.common.HttpParameters; import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.server.api.Domain; import org.xbib.netty.http.server.api.Domain;
@ -363,42 +359,31 @@ public class HttpServerRequest implements ServerRequest {
public ServerRequest build() { public ServerRequest build() {
// build URL and parameters // build URL and parameters
Charset charset = HttpUtil.getCharset(fullHttpRequest, StandardCharsets.UTF_8); Charset charset = HttpUtil.getCharset(fullHttpRequest, StandardCharsets.UTF_8);
CharSequence mimeType = HttpUtil.getMimeType(fullHttpRequest);
this.parameters = new HttpParameters(mimeType, charset);
// creates path, query params, fragment // creates path, query params, fragment
this.url = URL.builder() this.url = URL.builder()
.charset(charset, CodingErrorAction.REPLACE) .charset(charset, CodingErrorAction.REPLACE)
.path(fullHttpRequest.uri()) // creates path, query params, fragment .path(fullHttpRequest.uri()) // creates path, query params, fragment
.build(); .build();
ParameterBuilder queryParameters = Parameter.builder(); url.getQueryParams().forEach(p -> parameters.add(p.getKey(), p.getValue().toString()));
//url.getQueryParams(); if (fullHttpRequest.method().equals(HttpMethod.POST)) {
CharSequence mimeType = HttpUtil.getMimeType(fullHttpRequest); ByteBuf byteBuf = fullHttpRequest.content();
ByteBuf byteBuf = fullHttpRequest.content(); if (byteBuf != null) {
if (byteBuf != null) { Charset htmlCharset = HttpUtil.getCharset(fullHttpRequest, StandardCharsets.ISO_8859_1);
if (fullHttpRequest.method().equals(HttpMethod.POST)) { String body = byteBuf.toString(htmlCharset);
String params;
// https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4 // https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4
if (mimeType != null && HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString().equals(mimeType.toString())) { if (mimeType != null && HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString().equals(mimeType.toString())) {
Charset htmlCharset = HttpUtil.getCharset(fullHttpRequest, StandardCharsets.ISO_8859_1); logger.log(Level.INFO, "html form, charset = " + htmlCharset + " body = " + body);
params = byteBuf.toString(htmlCharset).replace('+', ' '); parameters.addPercentEncodedBody(body);
if (logger.isLoggable(Level.FINER)) { } else {
logger.log(Level.FINER, "html form, charset = " + htmlCharset + " param body = " + params); logger.log(Level.WARNING, "unable to process POST request, mime type = " + mimeType);
}
queryParameters.addPercentEncodedBody(params);
} }
} else {
logger.log(Level.WARNING, "unable to process POST request,body is empty");
} }
} }
// copy to HTTP parameters but percent-decoded (looks very clumsy) logger.log(Level.FINER, "HTTP server complete");
PercentDecoder percentDecoder = new PercentDecoder(charset.newDecoder()
.onMalformedInput(CodingErrorAction.REPLACE)
.onUnmappableCharacter(CodingErrorAction.REPLACE));
this.parameters = new HttpParameters(mimeType, charset);
for (Pair<String, Object> pair : queryParameters.build()) {
try {
parameters.addRaw(percentDecoder.decode(pair.getKey()), percentDecoder.decode(pair.getValue().toString()));
} catch (Exception e) {
// does not happen
throw new IllegalArgumentException(pair.toString());
}
}
return new HttpServerRequest(this); return new HttpServerRequest(this);
} }

View file

@ -56,7 +56,7 @@ import java.util.logging.Logger;
/** /**
* HTTP server. * HTTP server.
*/ */
public final class Server implements AutoCloseable { public final class Server {
private static final Logger logger = Logger.getLogger(Server.class.getName()); private static final Logger logger = Logger.getLogger(Server.class.getName());
@ -168,7 +168,6 @@ public final class Server implements AutoCloseable {
accept().channel().closeFuture().sync(); accept().channel().closeFuture().sync();
} }
@Override
public void close() { public void close() {
try { try {
shutdownGracefully(); shutdownGracefully();

View file

@ -1,5 +1,6 @@
package org.xbib.netty.http.server.test.http1; package org.xbib.netty.http.server.test.http1;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -34,7 +35,9 @@ class PostTest {
HttpServerDomain domain = HttpServerDomain.builder(httpAddress) HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "server got request " + parameters +
" body = " + req.getContent(StandardCharsets.UTF_8) +
" withspace = " + parameters.get("withspace"));
if ("Hello World".equals(parameters.get("withspace"))) { if ("Hello World".equals(parameters.get("withspace"))) {
success2.set(true); success2.set(true);
} }
@ -86,7 +89,7 @@ class PostTest {
HttpServerDomain domain = HttpServerDomain.builder(httpAddress) HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "server got request " + parameters + " body = " + req.getContent(StandardCharsets.UTF_8));
if ("Hello World".equals(parameters.get("withspace"))) { if ("Hello World".equals(parameters.get("withspace"))) {
success2.set(true); success2.set(true);
} }
@ -128,7 +131,7 @@ class PostTest {
} }
@Test @Test
void testFormPostHttp1() throws Exception { void testFormPostHttp1WithCorrectContentType() throws Exception {
final AtomicBoolean success1 = new AtomicBoolean(false); final AtomicBoolean success1 = new AtomicBoolean(false);
final AtomicBoolean success2 = new AtomicBoolean(false); final AtomicBoolean success2 = new AtomicBoolean(false);
final AtomicBoolean success3 = new AtomicBoolean(false); final AtomicBoolean success3 = new AtomicBoolean(false);
@ -137,7 +140,7 @@ class PostTest {
HttpServerDomain domain = HttpServerDomain.builder(httpAddress) HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "server got request " + parameters + " body = " + req.getContent(StandardCharsets.UTF_8));
if ("Hello World".equals(parameters.get("withplus"))) { if ("Hello World".equals(parameters.get("withplus"))) {
success2.set(true); success2.set(true);
} }
@ -164,6 +167,7 @@ class PostTest {
}; };
Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1) Request postRequest = Request.post().setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt"))
// the corrent content type
.contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8) .contentType(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED, StandardCharsets.UTF_8)
.addParameter("a", "b") .addParameter("a", "b")
// test 'plus' encoding // test 'plus' encoding
@ -185,7 +189,7 @@ class PostTest {
} }
@Test @Test
void testTextPlainPostHttp1() throws Exception { void testTextPlainPostHttp1MustFail() throws Exception {
final AtomicBoolean success1 = new AtomicBoolean(false); final AtomicBoolean success1 = new AtomicBoolean(false);
final AtomicBoolean success2 = new AtomicBoolean(false); final AtomicBoolean success2 = new AtomicBoolean(false);
final AtomicBoolean success3 = new AtomicBoolean(false); final AtomicBoolean success3 = new AtomicBoolean(false);
@ -194,7 +198,7 @@ class PostTest {
HttpServerDomain domain = HttpServerDomain.builder(httpAddress) HttpServerDomain domain = HttpServerDomain.builder(httpAddress)
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "server got request " + parameters + " body = " + req.getContent(StandardCharsets.UTF_8));
if ("Hello World".equals(parameters.get("withoutplus"))) { if ("Hello World".equals(parameters.get("withoutplus"))) {
success2.set(true); success2.set(true);
} }
@ -223,7 +227,7 @@ class PostTest {
.setVersion(HttpVersion.HTTP_1_1) .setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base().resolve("/post/test.txt")) .url(server.getServerConfig().getAddress().base().resolve("/post/test.txt"))
.contentType(HttpHeaderValues.TEXT_PLAIN, StandardCharsets.UTF_8) .contentType(HttpHeaderValues.TEXT_PLAIN, StandardCharsets.UTF_8)
// you can not pass form parameters on content type "text/plain" // you can not pass form parameters on content type "text/plain" :-)
.addParameter("a", "b") .addParameter("a", "b")
.addParameter("my param", "my value") .addParameter("my param", "my value")
.addParameter("withoutplus", "Hello World") .addParameter("withoutplus", "Hello World")
@ -237,9 +241,9 @@ class PostTest {
logger.log(Level.INFO, "server and client shut down"); logger.log(Level.INFO, "server and client shut down");
} }
assertTrue(success1.get()); assertTrue(success1.get());
assertTrue(success2.get()); assertFalse(success2.get());
assertTrue(success3.get()); assertFalse(success3.get());
assertTrue(success4.get()); assertFalse(success4.get());
} }
@Test @Test
@ -252,10 +256,11 @@ class PostTest {
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK");
// unable to decode without underflow
if ("myÿvalue".equals(parameters.get("my param"))) { if ("myÿvalue".equals(parameters.get("my param"))) {
success1.set(true); success1.set(true);
} }
if ("bÿc".equals(parameters.get("a"))) { if ("b%25YYc".equals(parameters.get("a"))) {
success2.set(true); success2.set(true);
} }
resp.getBuilder().setStatus(HttpResponseStatus.OK.code()).build().flush(); resp.getBuilder().setStatus(HttpResponseStatus.OK.code()).build().flush();
@ -285,9 +290,8 @@ class PostTest {
client.shutdownGracefully(); client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down"); logger.log(Level.INFO, "server and client shut down");
} }
assertTrue(success1.get()); assertFalse(success1.get());
assertTrue(success2.get()); assertTrue(success2.get());
assertTrue(success3.get()); assertTrue(success3.get());
} }
} }

View file

@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(NettyHttpTestExtension.class) @ExtendWith(NettyHttpTestExtension.class)
@ -240,9 +241,9 @@ class PostTest {
logger.log(Level.INFO, "server and client shut down"); logger.log(Level.INFO, "server and client shut down");
} }
assertTrue(success1.get()); assertTrue(success1.get());
assertTrue(success2.get()); assertFalse(success2.get());
assertTrue(success3.get()); assertFalse(success3.get());
assertTrue(success4.get()); assertFalse(success4.get());
} }
@Test @Test
@ -255,10 +256,11 @@ class PostTest {
.singleEndpoint("/post", "/**", (req, resp) -> { .singleEndpoint("/post", "/**", (req, resp) -> {
HttpParameters parameters = req.getParameters(); HttpParameters parameters = req.getParameters();
logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK"); logger.log(Level.INFO, "got request " + parameters.toString() + ", sending OK");
// unable to decode
if ("myÿvalue".equals(parameters.get("my param"))) { if ("myÿvalue".equals(parameters.get("my param"))) {
success1.set(true); success1.set(true);
} }
if ("bÿc".equals(parameters.get("a"))) { if ("b%25YYc".equals(parameters.get("a"))) {
success2.set(true); success2.set(true);
} }
resp.getBuilder().setStatus(HttpResponseStatus.OK.code()).build().flush(); resp.getBuilder().setStatus(HttpResponseStatus.OK.code()).build().flush();
@ -289,7 +291,7 @@ class PostTest {
client.shutdownGracefully(); client.shutdownGracefully();
logger.log(Level.INFO, "server and client shut down"); logger.log(Level.INFO, "server and client shut down");
} }
assertTrue(success1.get()); assertFalse(success1.get());
assertTrue(success2.get()); assertTrue(success2.get());
assertTrue(success3.get()); assertTrue(success3.get());
} }

View file

@ -0,0 +1,5 @@
handlers=java.util.logging.FileHandler, java.util.logging.ConsoleHandler
.level=ALL
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=org.xbib.net.util.ThreadLoggingFormatter
jdk.event.security.level=INFO

View file

@ -10,10 +10,10 @@ dependencyResolutionManagement {
versionCatalogs { versionCatalogs {
libs { libs {
version('gradle', '7.5.1') version('gradle', '7.5.1')
version('groovy', '3.0.10') version('groovy', '4.0.11')
version('spock', '2.0-groovy-3.0') version('spock', '2.4-M1-groovy-4.0')
version('junit', '5.9.1') version('junit', '5.9.2')
version('netty', '4.1.89.Final') version('netty', '4.1.90.Final')
version('netty-tcnative', '2.0.59.Final') version('netty-tcnative', '2.0.59.Final')
library('groovy-core', 'org.codehaus.groovy', 'groovy').versionRef('groovy') library('groovy-core', 'org.codehaus.groovy', 'groovy').versionRef('groovy')
library('spock-core', 'org.spockframework', 'spock-core').versionRef('spock') library('spock-core', 'org.spockframework', 'spock-core').versionRef('spock')
@ -27,13 +27,13 @@ dependencyResolutionManagement {
library('netty-epoll', 'io.netty', 'netty-transport-native-epoll').versionRef('netty') library('netty-epoll', 'io.netty', 'netty-transport-native-epoll').versionRef('netty')
library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty') library('netty-kqueue', 'io.netty', 'netty-transport-native-kqueue').versionRef('netty')
library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative') library('netty-boringssl', 'io.netty', 'netty-tcnative-boringssl-static').versionRef('netty-tcnative')
library('net', 'org.xbib', 'net').version('3.0.3')
library('net-path', 'org.xbib', 'net-path').version('3.0.3')
library('bouncycastle', 'org.bouncycastle', 'bcpkix-jdk18on').version('1.72') library('bouncycastle', 'org.bouncycastle', 'bcpkix-jdk18on').version('1.72')
library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2') library('conscrypt', 'org.conscrypt', 'conscrypt-openjdk-uber').version('2.5.2')
library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.12.7') library('jackson', 'com.fasterxml.jackson.core', 'jackson-databind').version('2.12.7')
library('guice', 'org.xbib', 'guice').version('5.0.1.0')
library('javassist', 'org.javassist', 'javassist').version('3.29.1-GA') library('javassist', 'org.javassist', 'javassist').version('3.29.1-GA')
library('guice', 'org.xbib', 'guice').version('5.0.1.0')
library('net', 'org.xbib', 'net').version('3.1.0')
library('net-path', 'org.xbib', 'net-path').version('3.1.0')
} }
} }
} }