renaming getPublishURL() to getContextURL(), enabling multi domain context path resolving of requests

This commit is contained in:
Jörg Prante 2020-07-23 14:35:49 +02:00
parent e329bfff46
commit 4fc61db40d
11 changed files with 242 additions and 75 deletions

View file

@ -1,6 +1,6 @@
group = org.xbib
name = netty-http
version = 4.1.51.0
version = 4.1.51.1
gradle.wrapper.version = 6.4.1
netty.version = 4.1.51.Final

View file

@ -35,6 +35,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
@ -48,6 +49,8 @@ public class Domain {
private static final Logger logger = Logger.getLogger(Domain.class.getName());
private static final String EMPTY = "";
private final String name;
private final Set<String> aliases;
@ -75,16 +78,12 @@ public class Domain {
List<HttpEndpointResolver> httpEndpointResolvers,
SslContext sslContext,
Collection<? extends X509Certificate> certificates) {
this.httpAddress = httpAddress;
this.name = name;
this.aliases = aliases;
this.httpAddress = httpAddress;
this.httpEndpointResolvers = httpEndpointResolvers;
this.sslContext = sslContext;
this.certificates = certificates;
Objects.requireNonNull(httpEndpointResolvers);
if (httpEndpointResolvers.isEmpty()) {
throw new IllegalArgumentException("domain must have at least one endpoint resolver");
}
}
public static Builder builder(HttpAddress httpAddress) {
@ -143,26 +142,54 @@ public class Domain {
}
/**
* Handle server requests.
* Evaluate the context path of a given request.
* The request is not dispatched.
* URI request parameters are evaluated.
* @param serverRequest the server request
* @return the context path
* @throws IOException if handling fails
*/
public String findContextOf(ServerRequest serverRequest) throws IOException {
if (serverRequest == null) {
return EMPTY;
}
Map.Entry<HttpEndpointResolver, List<HttpEndpoint>> resolved = resolve(serverRequest);
if (resolved != null) {
resolved.getKey().handle(resolved.getValue(), serverRequest, null, false);
return serverRequest.getContextPath();
}
return null;
}
/**
* Handle server requests by resolving and handling a server request.
* @param serverRequest the server request
* @param serverResponse the server response
* @throws IOException if handling server request fails
*/
public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
boolean found = false;
Map.Entry<HttpEndpointResolver, List<HttpEndpoint>> resolved = resolve(serverRequest);
if (resolved != null) {
resolved.getKey().handle(resolved.getValue(), serverRequest, serverResponse, true);
} else {
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_IMPLEMENTED,
"text/plain", "No endpoint match for request " + serverRequest);
}
}
/**
* Just resolve a server request to a matching endpoint resolver with endpoints matched.
* @param serverRequest the server request
* @return the endpoint resolver together with the matching endpoints
*/
public Map.Entry<HttpEndpointResolver, List<HttpEndpoint>> resolve(ServerRequest serverRequest) {
for (HttpEndpointResolver httpEndpointResolver : httpEndpointResolvers) {
List<HttpEndpoint> matchingEndpoints = httpEndpointResolver.matchingEndpointsFor(serverRequest);
if (matchingEndpoints != null && !matchingEndpoints.isEmpty()) {
httpEndpointResolver.handle(matchingEndpoints, serverRequest, serverResponse);
found = true;
break;
if (!matchingEndpoints.isEmpty()) {
return Map.entry(httpEndpointResolver, matchingEndpoints);
}
}
if (!found) {
ServerResponse.write(serverResponse, HttpResponseStatus.NOT_IMPLEMENTED,
"text/plain", "No endpoint match for request " + serverRequest +
" endpoints = " + httpEndpointResolvers);
}
return null;
}
@Override
@ -373,6 +400,9 @@ public class Domain {
}
public Domain build() {
if (httpEndpointResolvers.isEmpty()) {
throw new IllegalArgumentException("domain must have at least one endpoint resolver");
}
if (httpAddress.isSecure() ) {
try {
if (sslContext == null && privateKey != null && keyCertChain != null) {

View file

@ -194,27 +194,44 @@ public final class Server implements AutoCloseable {
}
/**
* Returns the domain with the given host name.
* Returns the domain for the given server request.
*
* @param dnsName the name of the virtual host with optional port to return or null for the
* @param serverRequest the server request
* @return the domain
*/
public Domain getDomain(ServerRequest serverRequest) {
return getDomain(getBaseURL(serverRequest));
}
/**
* Returns the domain of the given URL.
* @param url the URL
* @return the domain
*/
public Domain getDomain(URL url) {
return getDomain(hostAndPort(url));
}
/**
* Returns the domain for the given host name.
*
* @param name the name of the virtual host with optional port, or null for the
* default domain
* @return the virtual host with the given name or the default domain
*/
public Domain getDomain(String dnsName) {
return serverConfig.getDomain(dnsName);
public Domain getDomain(String name) {
return serverConfig.getDomain(name);
}
public Domain getDomain(URL url) {
return getDomain(url.getHost());
}
public URL getPublishURL() {
return getPublishURL(null);
}
public URL getPublishURL(ServerRequest serverRequest) {
Domain domain = serverRequest != null ? getDomain(serverRequest.getURL()) : serverConfig.getDefaultDomain();
URL bindURL = domain.getHttpAddress().base();
/**
* Return the base URL regarding to a server request.
* The base URL depends on the host and port defined in a reqeust,
* if no request is defined, the bind URL is taken.
* @param serverRequest the server request
* @return the URL
*/
public URL getBaseURL(ServerRequest serverRequest) {
URL bindURL = serverConfig.getDefaultDomain().getHttpAddress().base();
String scheme = serverRequest != null ? serverRequest.getHeaders().get("x-forwarded-proto") : null;
if (scheme == null) {
scheme = bindURL.getScheme();
@ -234,24 +251,40 @@ public final class Server implements AutoCloseable {
port = bindURL.getPort() != null ? Integer.toString(bindURL.getPort()) : null;
}
}
String path = serverRequest != null ? serverRequest.getHeaders().get("x-forwarded-path") : null;
URL.Builder builder = URL.builder().scheme(scheme).host(host);
if (port != null) {
if (path != null) {
return builder.port(Integer.parseInt(port)).path(path).build();
} else {
return builder.port(Integer.parseInt(port)).build();
builder.port(Integer.parseInt(port));
}
}
if (path != null) {
return builder.path(path).build();
} else {
return builder.build();
}
/**
* Return the context URL of this server. This is equivalent to the bindURL
* @return the context URL
* @throws IOException should not happen
*/
public URL getContextURL() throws IOException {
return getContextURL(null);
}
/**
* Get context URL of this server regarding to a given request.
* The context URL is the base URL with the path given in the matching
* domain prefix setting of the endpoint resolver.
* @param serverRequest the server request
* @return the context URL
* @throws IOException if context path finding fails
*/
public URL getContextURL(ServerRequest serverRequest) throws IOException {
URL baseURL = getBaseURL(serverRequest);
Domain domain = getDomain(baseURL);
String context = domain.findContextOf(serverRequest);
return baseURL.resolve(context);
}
public void handle(HttpServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
Domain domain = getDomain(serverRequest.getURL());
Domain domain = getDomain(serverRequest);
logger.log(Level.FINEST, () -> "found domain " + domain + " for " + serverRequest);
if (executor != null) {
executor.submit(() -> {
try {
@ -339,6 +372,10 @@ public final class Server implements AutoCloseable {
return i >= 0 ? hostMaybePort.substring(i + 1) : null;
}
private static String hostAndPort(URL url) {
return url == null ? null : url.getPort() != -1 ? url.getHost() + ":" + url.getPort() : url.getHost();
}
private HttpChannelInitializer findChannelInitializer(int majorVersion,
HttpAddress httpAddress,
Mapping<String, SslContext> domainNameMapping) {

View file

@ -96,18 +96,22 @@ public class HttpEndpoint implements Endpoint<HttpEndpointDescriptor> {
@Override
public void before(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
serverRequest.setContext(pathMatcher.tokenizePath(getPrefix()));
if (serverResponse != null) {
for (Filter filter : beforeFilters) {
filter.handle(serverRequest, serverResponse);
}
}
}
@Override
public void after(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
serverRequest.setContext(pathMatcher.tokenizePath(getPrefix()));
if (serverResponse != null) {
for (Filter filter : afterFilters) {
filter.handle(serverRequest, serverResponse);
}
}
}
@Override
public String toString() {

View file

@ -50,35 +50,37 @@ public class HttpEndpointResolver {
}
public void handle(List<HttpEndpoint> matchingEndpoints,
ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
ServerRequest serverRequest,
ServerResponse serverResponse,
boolean dispatch) throws IOException {
Objects.requireNonNull(matchingEndpoints);
for (HttpEndpoint endpoint : matchingEndpoints) {
endpoint.resolveUriTemplate(serverRequest);
endpoint.before(serverRequest, serverResponse);
if (dispatch) {
endpointDispatcher.dispatch(endpoint, serverRequest, serverResponse);
endpoint.after(serverRequest, serverResponse);
if (serverResponse.getStatus() != null) {
if (serverResponse != null && serverResponse.getStatus() != null) {
break;
}
} else {
break;
}
}
}
public Map<HttpEndpointDescriptor, List<HttpEndpoint>> getEndpointDescriptors() {
return endpointDescriptors;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final List<HttpEndpoint> endpoints;
private int limit;
private String prefix;
private final List<HttpEndpoint> endpoints;
private EndpointDispatcher<HttpEndpoint> endpointDispatcher;
Builder() {
@ -149,6 +151,9 @@ public class HttpEndpointResolver {
}
public HttpEndpointResolver build() {
if (endpoints.isEmpty()) {
throw new IllegalArgumentException("no endpoints configured");
}
return new HttpEndpointResolver(endpoints, endpointDispatcher, limit);
}
}

View file

@ -8,6 +8,7 @@ import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.api.ServerResponse;
import java.io.IOException;
import java.net.InetSocketAddress;
public class Http1Transport extends BaseTransport {
@ -17,7 +18,9 @@ public class Http1Transport extends BaseTransport {
@Override
public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException {
HttpServerRequest serverRequest = new HttpServerRequest(server, fullHttpRequest, ctx);
HttpServerRequest serverRequest = new HttpServerRequest(fullHttpRequest,
(InetSocketAddress) ctx.channel().localAddress(),
(InetSocketAddress) ctx.channel().remoteAddress());
serverRequest.setSequenceId(sequenceId);
serverRequest.setRequestId(server.getRequestCounter().incrementAndGet());
SslHandler sslHandler = ctx.channel().pipeline().get(SslHandler.class);

View file

@ -9,6 +9,7 @@ import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.api.ServerResponse;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -22,7 +23,9 @@ public class Http2Transport extends BaseTransport {
@Override
public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException {
HttpServerRequest serverRequest = new HttpServerRequest(server, fullHttpRequest, ctx);
HttpServerRequest serverRequest = new HttpServerRequest(fullHttpRequest,
(InetSocketAddress) ctx.channel().localAddress(),
(InetSocketAddress) ctx.channel().remoteAddress());
serverRequest.setSequenceId(sequenceId);
serverRequest.setRequestId(server.getRequestCounter().incrementAndGet());
serverRequest.setStreamId(fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()));

View file

@ -2,7 +2,6 @@ package org.xbib.netty.http.server.transport;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
@ -13,7 +12,6 @@ import org.xbib.net.PercentDecoder;
import org.xbib.net.QueryParameters;
import org.xbib.net.URL;
import org.xbib.netty.http.common.HttpParameters;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.api.ServerRequest;
import javax.net.ssl.SSLSession;
@ -40,14 +38,16 @@ public class HttpServerRequest implements ServerRequest {
private final FullHttpRequest httpRequest;
private final ChannelHandlerContext ctx;
private final InetSocketAddress localAddress;
private final InetSocketAddress remoteAddress;
private final Map<String, String> pathParameters;
private List<String> context;
private String contextPath;
private Map<String, String> pathParameters;
private HttpParameters parameters;
private URL url;
@ -60,10 +60,16 @@ public class HttpServerRequest implements ServerRequest {
private SSLSession sslSession;
HttpServerRequest(Server server, FullHttpRequest fullHttpRequest,
ChannelHandlerContext ctx) {
this.httpRequest = fullHttpRequest.retainedDuplicate();
this.ctx = ctx;
public HttpServerRequest(FullHttpRequest fullHttpRequest) {
this( fullHttpRequest ,null, null);
}
public HttpServerRequest(FullHttpRequest fullHttpRequest,
InetSocketAddress localAddress,
InetSocketAddress remoteAddress) {
this.httpRequest = fullHttpRequest != null ? fullHttpRequest.retainedDuplicate() : null;
this.localAddress = localAddress;
this.remoteAddress = remoteAddress;
this.pathParameters = new LinkedHashMap<>();
}
@ -204,12 +210,12 @@ public class HttpServerRequest implements ServerRequest {
@Override
public InetSocketAddress getLocalAddress() {
return (InetSocketAddress) ctx.channel().localAddress();
return localAddress;
}
@Override
public InetSocketAddress getRemoteAddress() {
return (InetSocketAddress) ctx.channel().remoteAddress();
return remoteAddress;
}
@Override

View file

@ -0,0 +1,75 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.net.URL;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Domain;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.api.ServerRequest;
import org.xbib.netty.http.server.endpoint.HttpEndpoint;
import org.xbib.netty.http.server.endpoint.HttpEndpointResolver;
import org.xbib.netty.http.server.transport.HttpServerRequest;
@ExtendWith(NettyHttpTestExtension.class)
public class ContextURLTest {
@Test
void testServerPublishURL() throws Exception {
HttpAddress httpAddress = HttpAddress.http1("localhost", 8008);
HttpEndpointResolver endpointResolver1 = HttpEndpointResolver.builder()
.addEndpoint(HttpEndpoint.builder().setPrefix("/one").setPath("/**").build())
.setDispatcher((endpoint, serverRequest, serverResponse) -> {})
.build();
HttpEndpointResolver endpointResolver2 = HttpEndpointResolver.builder()
.addEndpoint(HttpEndpoint.builder().setPrefix("/two").setPath("/**").build())
.setDispatcher((endpoint, serverRequest, serverResponse) -> {})
.build();
HttpEndpointResolver endpointResolver3 = HttpEndpointResolver.builder()
.addEndpoint(HttpEndpoint.builder().setPrefix("/three").setPath("/**").build())
.setDispatcher((endpoint, serverRequest, serverResponse) -> {})
.build();
Domain one = Domain.builder(httpAddress, "domain.one:8008")
.addEndpointResolver(endpointResolver1)
.build();
Domain two = Domain.builder(one)
.setServerName("domain.two:8008")
.addEndpointResolver(endpointResolver2)
.addEndpointResolver(endpointResolver3)
.build();
Server server = Server.builder(one)
.addDomain(two)
.build();
URL url0 = server.getContextURL();
assertEquals("http://localhost:8008", url0.toString());
DefaultFullHttpRequest fullHttpRequest1 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/one");
fullHttpRequest1.headers().add("host", "domain.one:8008");
ServerRequest serverRequest1 = new HttpServerRequest(fullHttpRequest1);
URL url1 = server.getContextURL(serverRequest1);
assertEquals("domain.one", url1.getHost());
assertEquals("/one", url1.getPath());
DefaultFullHttpRequest fullHttpRequest2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/two");
fullHttpRequest2.headers().add("host", "domain.two:8008");
ServerRequest serverRequest2 = new HttpServerRequest(fullHttpRequest2);
URL url2 = server.getContextURL(serverRequest2);
assertEquals("domain.two", url2.getHost());
assertEquals("/two", url2.getPath());
DefaultFullHttpRequest fullHttpRequest3 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/three");
fullHttpRequest3.headers().add("host", "domain.two:8008");
ServerRequest serverRequest3 = new HttpServerRequest(fullHttpRequest3);
URL url3 = server.getContextURL(serverRequest3);
assertEquals("domain.two", url3.getHost());
assertEquals("/three", url3.getPath());
}
}

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.server.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpAddress;
@ -15,6 +16,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
@Disabled
@ExtendWith(NettyHttpTestExtension.class)
class MultiDomainSecureServerTest {
private static final Logger logger = Logger.getLogger(MultiDomainSecureServerTest.class.getName());
@ -52,7 +54,7 @@ class MultiDomainSecureServerTest {
.url("https://fl.hbz-nrw.de:8443")
.setResponseListener(resp -> {
String response = resp.getBodyAsString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "got response: " + response + " status=" + resp.getStatus());
logger.log(Level.INFO, "fl: got response: " + response + " status=" + resp.getStatus());
assertEquals("Hello fl.hbz-nrw.de", response);
})
.build();
@ -62,7 +64,7 @@ class MultiDomainSecureServerTest {
.url("https://zfl2.hbz-nrw.de:8443")
.setResponseListener(resp -> {
String response = resp.getBodyAsString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "got response: " + response + " status=" + resp.getStatus());
logger.log(Level.INFO, "zfl2: got response: " + response + " status=" + resp.getStatus());
assertEquals("Hello zfl2.hbz-nrw.de", response);
})
.build();

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.server.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.api.Request;
import org.xbib.netty.http.common.HttpAddress;
@ -14,6 +15,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
@Disabled
@ExtendWith(NettyHttpTestExtension.class)
class MultiDomainServerTest {
private static final Logger logger = Logger.getLogger(MultiDomainServerTest.class.getName());
@ -39,7 +41,7 @@ class MultiDomainServerTest {
.url("http://fl.hbz-nrw.de:8008")
.setResponseListener(resp -> {
String response = resp.getBodyAsString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "got response: " + response + " status=" + resp.getStatus());
logger.log(Level.INFO, "fl: got response: " + response + " status=" + resp.getStatus());
assertEquals("Hello fl.hbz-nrw.de", response);
})
.build();
@ -48,7 +50,7 @@ class MultiDomainServerTest {
.url("http://zfl2.hbz-nrw.de:8008")
.setResponseListener(resp -> {
String response = resp.getBodyAsString(StandardCharsets.UTF_8);
logger.log(Level.INFO, "got response: " + response + " status=" + resp.getStatus());
logger.log(Level.INFO, "zfl2: got response: " + response + " status=" + resp.getStatus());
assertEquals("Hello zfl2.hbz-nrw.de", response);
})
.build();