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:
parent
ae1240822f
commit
845260a14e
16 changed files with 176 additions and 136 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
netty-http-common/src/test/resources/logging.properties
Normal file
5
netty-http-common/src/test/resources/logging.properties
Normal 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
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
5
netty-http-server/src/test/resources/logging.properties
Normal file
5
netty-http-server/src/test/resources/logging.properties
Normal 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
|
|
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue