From 1dbefb73a8815aabfbf9dd7604846f2dd43349ca Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Sat, 18 Feb 2023 17:48:39 -0500 Subject: [PATCH] Refactor UriEndpoint (#2700) Clean up UriEndpoint logic to be based around the java.net.URI. Move relevant methods into this class. Construct instances using static method. Only calculate derived values on demand (e.g. toExternalForm) when needed in the various request implementations. Cleaned up redirection logic. Added additional tests. Made conversion from InetSocketAddress more efficient (removed the substrings which trimmed out the ports). Fixed HttpClientTest to support ipv6 (by using NetUtil.toSocketAddressString to get the [] around the address, and also fixed FailedHttpClientRequest.uri() from returning full URI rather than just Netty definition of "uri" which is raw path and query. Fixed ClientTransportTest to support ipv6. Fixes #829 --- .../AbstractHttpClientMetricsHandler.java | 4 +- .../http/client/FailedHttpClientRequest.java | 15 +- .../reactor/netty/http/client/HttpClient.java | 6 +- .../netty/http/client/HttpClientConnect.java | 84 +--- .../http/client/HttpClientOperations.java | 10 +- .../netty/http/client/UriEndpoint.java | 203 +++++++--- .../netty/http/client/UriEndpointFactory.java | 135 ------- .../netty/http/client/HttpClientTest.java | 5 +- .../http/client/UriEndpointFactoryTest.java | 360 +++++++++++++----- 9 files changed, 447 insertions(+), 375 deletions(-) delete mode 100644 reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpointFactory.java diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/AbstractHttpClientMetricsHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/AbstractHttpClientMetricsHandler.java index 7efe2d7c74..984b4dd799 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/AbstractHttpClientMetricsHandler.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/AbstractHttpClientMetricsHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2021-2023 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -163,7 +163,7 @@ private void extractDetailsFromHttpRequest(ChannelHandlerContext ctx, HttpReques ChannelOperations channelOps = ChannelOperations.get(ctx.channel()); if (channelOps instanceof HttpClientOperations) { HttpClientOperations ops = (HttpClientOperations) channelOps; - path = uriTagValue == null ? ops.path : uriTagValue.apply(ops.path); + path = uriTagValue == null ? ops.fullPath() : uriTagValue.apply(ops.fullPath()); contextView = ops.currentContextView(); } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/FailedHttpClientRequest.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/FailedHttpClientRequest.java index bd44582b78..0d66d09184 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/FailedHttpClientRequest.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/FailedHttpClientRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2021 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.Cookie; import reactor.netty.http.Cookies; -import reactor.netty.http.HttpOperations; import reactor.util.context.Context; import reactor.util.context.ContextView; @@ -45,9 +44,8 @@ final class FailedHttpClientRequest implements HttpClientRequest { final HttpHeaders headers; final boolean isWebsocket; final HttpMethod method; - final String path; final Duration responseTimeout; - final String uri; + final UriEndpoint uriEndpoint; FailedHttpClientRequest(ContextView contextView, HttpClientConfig c) { this.contextView = contextView; @@ -55,8 +53,7 @@ final class FailedHttpClientRequest implements HttpClientRequest { this.headers = c.headers; this.isWebsocket = c.websocketClientSpec != null; this.method = c.method; - this.uri = c.uri == null ? c.uriStr : c.uri.toString(); - this.path = this.uri != null ? HttpOperations.resolvePath(this.uri) : null; + this.uriEndpoint = UriEndpoint.create(c.uri, c.baseUrl, c.uriStr, c.remoteAddress(), c.isSecure(), c.websocketClientSpec != null); this.responseTimeout = c.responseTimeout; } @@ -89,7 +86,7 @@ public ContextView currentContextView() { @Override public String fullPath() { - return path; + return uriEndpoint.getPath(); } @Override @@ -144,12 +141,12 @@ public HttpClientRequest responseTimeout(Duration maxReadOperationInterval) { @Override public String resourceUrl() { - return null; + return uriEndpoint.toExternalForm(); } @Override public String uri() { - return uri; + return uriEndpoint.getRawUri(); } @Override diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java index 52f0d67ca9..f607eccc55 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2022 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-2023 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1608,4 +1608,8 @@ static String reactorNettyVersion() { static final String WS_SCHEME = "ws"; static final String WSS_SCHEME = "wss"; + + static final int DEFAULT_PORT = 80; + + static final int DEFAULT_SECURE_PORT = 443; } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java index 5f127235e6..ff3a264df4 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java @@ -17,8 +17,6 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.net.URI; -import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -38,7 +36,6 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.ssl.SslClosedEngineException; import io.netty.resolver.AddressResolverGroup; @@ -55,7 +52,6 @@ import reactor.netty.ConnectionObserver; import reactor.netty.NettyOutbound; import reactor.netty.channel.AbortedException; -import reactor.netty.http.HttpOperations; import reactor.netty.http.HttpProtocol; import reactor.netty.tcp.TcpClientConfig; import reactor.netty.transport.AddressUtils; @@ -455,7 +451,6 @@ static final class HttpClientHandler extends SocketAddress final BiFunction> handler; final boolean compress; - final UriEndpointFactory uriEndpointFactory; final WebsocketClientSpec websocketClientSpec; final BiPredicate followRedirectPredicate; @@ -468,7 +463,6 @@ static final class HttpClientHandler extends SocketAddress final Duration responseTimeout; volatile UriEndpoint toURI; - volatile String resourceUrl; volatile UriEndpoint fromURI; volatile Supplier[] redirectedFrom; volatile boolean shouldRetry; @@ -484,34 +478,10 @@ static final class HttpClientHandler extends SocketAddress this.proxyProvider = configuration.proxyProvider(); this.responseTimeout = configuration.responseTimeout; this.defaultHeaders = configuration.headers; - - String baseUrl = configuration.baseUrl; - - this.uriEndpointFactory = - new UriEndpointFactory(configuration.remoteAddress(), configuration.isSecure(), URI_ADDRESS_MAPPER); - this.websocketClientSpec = configuration.websocketClientSpec; this.shouldRetry = !configuration.retryDisabled; this.handler = configuration.body; - - if (configuration.uri == null) { - String uri = configuration.uriStr; - - uri = uri == null ? "/" : uri; - - if (baseUrl != null && uri.startsWith("/")) { - if (baseUrl.endsWith("/")) { - baseUrl = baseUrl.substring(0, baseUrl.length() - 1); - } - uri = baseUrl + uri; - } - - this.toURI = uriEndpointFactory.createUriEndpoint(uri, configuration.websocketClientSpec != null); - } - else { - this.toURI = uriEndpointFactory.createUriEndpoint(configuration.uri, configuration.websocketClientSpec != null); - } - this.resourceUrl = toURI.toExternalForm(); + this.toURI = UriEndpoint.create(configuration.uri, configuration.baseUrl, configuration.uriStr, configuration.remoteAddress(), configuration.isSecure(), configuration.websocketClientSpec != null); } @Override @@ -526,18 +496,16 @@ public SocketAddress get() { Publisher requestWithBody(HttpClientOperations ch) { try { - ch.resourceUrl = this.resourceUrl; + UriEndpoint uriEndpoint = toURI; + ch.uriEndpoint = uriEndpoint; ch.responseTimeout = responseTimeout; - UriEndpoint uri = toURI; HttpHeaders headers = ch.getNettyRequest() - .setUri(uri.getPathAndQuery()) + .setUri(uriEndpoint.getRawUri()) .setMethod(method) .setProtocolVersion(HttpVersion.HTTP_1_1) .headers(); - ch.path = HttpOperations.resolvePath(ch.uri()); - if (!defaultHeaders.isEmpty()) { headers.set(defaultHeaders); } @@ -546,9 +514,8 @@ Publisher requestWithBody(HttpClientOperations ch) { headers.set(HttpHeaderNames.USER_AGENT, USER_AGENT); } - SocketAddress remoteAddress = uri.getRemoteAddress(); if (!headers.contains(HttpHeaderNames.HOST)) { - headers.set(HttpHeaderNames.HOST, resolveHostHeaderValue(remoteAddress)); + headers.set(HttpHeaderNames.HOST, uriEndpoint.getHostHeader()); } if (!headers.contains(HttpHeaderNames.ACCEPT)) { @@ -608,46 +575,11 @@ Publisher requestWithBody(HttpClientOperations ch) { } } - static String resolveHostHeaderValue(@Nullable SocketAddress remoteAddress) { - if (remoteAddress instanceof InetSocketAddress) { - InetSocketAddress address = (InetSocketAddress) remoteAddress; - String host = HttpUtil.formatHostnameForHttp(address); - int port = address.getPort(); - if (port != 80 && port != 443) { - host = host + ':' + port; - } - return host; - } - else { - return "localhost"; - } - } - void redirect(String to) { Supplier[] redirectedFrom = this.redirectedFrom; - UriEndpoint toURITemp; - UriEndpoint from = toURI; - SocketAddress address = from.getRemoteAddress(); - if (address instanceof InetSocketAddress) { - try { - URI redirectUri = new URI(to); - if (!redirectUri.isAbsolute()) { - URI requestUri = new URI(resourceUrl); - redirectUri = requestUri.resolve(redirectUri); - } - toURITemp = uriEndpointFactory.createUriEndpoint(redirectUri, from.isWs()); - } - catch (URISyntaxException e) { - throw new IllegalArgumentException("Cannot resolve location header", e); - } - } - else { - toURITemp = uriEndpointFactory.createUriEndpoint(from, to, () -> address); - } - fromURI = from; - toURI = toURITemp; - resourceUrl = toURITemp.toExternalForm(); - this.redirectedFrom = addToRedirectedFromArray(redirectedFrom, from); + fromURI = toURI; + toURI = toURI.redirect(to); + this.redirectedFrom = addToRedirectedFromArray(redirectedFrom, fromURI); } @SuppressWarnings({"unchecked", "rawtypes"}) diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java index 78f1316f18..f99cbe47a6 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java @@ -106,8 +106,7 @@ class HttpClientOperations extends HttpOperations final Sinks.One trailerHeaders; Supplier[] redirectedFrom = EMPTY_REDIRECTIONS; - String resourceUrl; - String path; + UriEndpoint uriEndpoint; Duration responseTimeout; volatile ResponseState responseState; @@ -140,8 +139,7 @@ class HttpClientOperations extends HttpOperations this.requestHeaders = replaced.requestHeaders; this.cookieEncoder = replaced.cookieEncoder; this.cookieDecoder = replaced.cookieDecoder; - this.resourceUrl = replaced.resourceUrl; - this.path = replaced.path; + this.uriEndpoint = replaced.uriEndpoint; this.responseTimeout = replaced.responseTimeout; this.is100Continue = replaced.is100Continue; this.trailerHeaders = replaced.trailerHeaders; @@ -504,12 +502,12 @@ public final String uri() { @Override public final String fullPath() { - return this.path; + return uriEndpoint == null ? null : uriEndpoint.getPath(); } @Override public String resourceUrl() { - return resourceUrl; + return uriEndpoint == null ? null : uriEndpoint.toExternalForm(); } @Override diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpoint.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpoint.java index 48b7be36ba..499bc5cf44 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpoint.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,82 +15,185 @@ */ package reactor.netty.http.client; +import java.net.Inet6Address; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Objects; import java.util.function.Supplier; +import java.util.regex.Pattern; -import io.netty.channel.unix.DomainSocketAddress; import io.netty.util.NetUtil; +import reactor.netty.transport.AddressUtils; +import static reactor.netty.http.client.HttpClient.DEFAULT_PORT; +import static reactor.netty.http.client.HttpClient.DEFAULT_SECURE_PORT; final class UriEndpoint { - final String scheme; - final String host; - final int port; - final Supplier remoteAddress; - final String pathAndQuery; + private static final Pattern SCHEME_PATTERN = Pattern.compile("^\\w+://.*$"); + private static final String ROOT_PATH = "/"; + private static final String COLON_DOUBLE_SLASH = "://"; - UriEndpoint(String scheme, String host, int port, Supplier remoteAddress, String pathAndQuery) { - this.host = host; - this.port = port; - this.scheme = Objects.requireNonNull(scheme, "scheme"); - this.remoteAddress = Objects.requireNonNull(remoteAddress, "remoteAddressSupplier"); - this.pathAndQuery = Objects.requireNonNull(pathAndQuery, "pathAndQuery"); + private final SocketAddress remoteAddress; + private final URI uri; + private final String scheme; + private final boolean secure; + private final String authority; + private final String rawUri; + + private UriEndpoint(URI uri) { + this(uri, null); } - boolean isWs() { - return HttpClient.WS_SCHEME.equals(scheme) || HttpClient.WSS_SCHEME.equals(scheme); + private UriEndpoint(URI uri, SocketAddress remoteAddress) { + this.uri = Objects.requireNonNull(uri, "uri"); + if (uri.isOpaque()) { + throw new IllegalArgumentException("URI is opaque: " + uri); + } + if (!uri.isAbsolute()) { + throw new IllegalArgumentException("URI is not absolute: " + uri); + } + this.scheme = uri.getScheme().toLowerCase(); + this.secure = isSecureScheme(scheme); + this.authority = authority(uri); + this.rawUri = rawUri(uri); + if (remoteAddress == null) { + int port = uri.getPort() != -1 ? uri.getPort() : (secure ? DEFAULT_SECURE_PORT : DEFAULT_PORT); + this.remoteAddress = AddressUtils.createUnresolved(uri.getHost(), port); + } + else { + this.remoteAddress = remoteAddress; + } } - boolean isSecure() { - return isSecureScheme(scheme); + static UriEndpoint create(URI uri, String baseUrl, String uriStr, Supplier remoteAddress, boolean secure, boolean ws) { + if (uri != null) { + // fast path + return new UriEndpoint(uri); + } + if (uriStr == null) { + uriStr = ROOT_PATH; + } + if (baseUrl != null && uriStr.startsWith(ROOT_PATH)) { + // support prepending a baseUrl + if (baseUrl.endsWith(ROOT_PATH)) { + // trim off trailing slash to avoid a double slash when appending uriStr + baseUrl = baseUrl.substring(0, baseUrl.length() - ROOT_PATH.length()); + } + uriStr = baseUrl + uriStr; + } + if (uriStr.startsWith(ROOT_PATH)) { + // support "/path" base by prepending scheme and host + SocketAddress socketAddress = remoteAddress.get(); + uriStr = resolveScheme(secure, ws) + COLON_DOUBLE_SLASH + socketAddressToAuthority(socketAddress, secure) + uriStr; + return new UriEndpoint(URI.create(uriStr), socketAddress); + } + if (!SCHEME_PATTERN.matcher(uriStr).matches()) { + // support "example.com/path" case by prepending scheme + uriStr = resolveScheme(secure, ws) + COLON_DOUBLE_SLASH + uriStr; + } + return new UriEndpoint(URI.create(uriStr)); } - static boolean isSecureScheme(String scheme) { - return HttpClient.HTTPS_SCHEME.equals(scheme) || HttpClient.WSS_SCHEME.equals(scheme); + private static String socketAddressToAuthority(SocketAddress socketAddress, boolean secure) { + if (!(socketAddress instanceof InetSocketAddress)) { + return "localhost"; + } + InetSocketAddress inetSocketAddress = (InetSocketAddress) socketAddress; + String host; + if (inetSocketAddress.isUnresolved()) { + host = NetUtil.getHostname(inetSocketAddress); + } + else { + InetAddress inetAddress = inetSocketAddress.getAddress(); + host = NetUtil.toAddressString(inetAddress); + if (inetAddress instanceof Inet6Address) { + host = '[' + host + ']'; + } + } + int port = inetSocketAddress.getPort(); + if ((!secure && port != DEFAULT_PORT) || (secure && port != DEFAULT_SECURE_PORT)) { + return host + ':' + port; + } + return host; } - String getPathAndQuery() { - return pathAndQuery; + private static String resolveScheme(boolean secure, boolean ws) { + if (ws) { + return secure ? HttpClient.WSS_SCHEME : HttpClient.WS_SCHEME; + } + else { + return secure ? HttpClient.HTTPS_SCHEME : HttpClient.HTTP_SCHEME; + } } - SocketAddress getRemoteAddress() { - return remoteAddress.get(); + private static boolean isSecureScheme(String scheme) { + return HttpClient.HTTPS_SCHEME.equals(scheme) || HttpClient.WSS_SCHEME.equals(scheme); } - String toExternalForm() { - StringBuilder sb = new StringBuilder(); - SocketAddress address = remoteAddress.get(); - if (address instanceof DomainSocketAddress) { - sb.append(((DomainSocketAddress) address).path()); + private static String rawUri(URI uri) { + String rawPath = uri.getRawPath(); + if (rawPath == null || rawPath.isEmpty()) { + rawPath = ROOT_PATH; } - else { - sb.append(scheme); - sb.append("://"); - sb.append(address != null - ? toSocketAddressStringWithoutDefaultPort(address, isSecure()) - : "localhost"); - sb.append(pathAndQuery); + String rawQuery = uri.getRawQuery(); + if (rawQuery == null) { + return rawPath; } - return sb.toString(); + return rawPath + '?' + rawQuery; } - static String toSocketAddressStringWithoutDefaultPort(SocketAddress address, boolean secure) { - if (!(address instanceof InetSocketAddress)) { - throw new IllegalStateException("Only support InetSocketAddress representation"); + private static String authority(URI uri) { + String host = uri.getHost(); + int port = uri.getPort(); + if (port == -1 || port == DEFAULT_PORT || port == DEFAULT_SECURE_PORT) { + return host; } - String addressString = NetUtil.toSocketAddressString((InetSocketAddress) address); - if (secure) { - if (addressString.endsWith(":443")) { - addressString = addressString.substring(0, addressString.length() - 4); + return host + ':' + port; + } + + UriEndpoint redirect(String to) { + try { + URI redirectUri = new URI(to); + if (redirectUri.isAbsolute()) { + // absolute path: treat as a brand new uri + return new UriEndpoint(redirectUri); } + // relative path: reuse the remote address + return new UriEndpoint(uri.resolve(redirectUri), remoteAddress); } - else { - if (addressString.endsWith(":80")) { - addressString = addressString.substring(0, addressString.length() - 3); - } + catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot resolve location header", e); } - return addressString; + } + + boolean isSecure() { + return secure; + } + + String getRawUri() { + return rawUri; + } + + String getPath() { + String path = uri.getPath(); + if (path == null || path.isEmpty()) { + return ROOT_PATH; + } + return path; + } + + String getHostHeader() { + return authority; + } + + SocketAddress getRemoteAddress() { + return remoteAddress; + } + + String toExternalForm() { + return scheme + COLON_DOUBLE_SLASH + authority + rawUri; } @Override @@ -107,11 +210,11 @@ public boolean equals(Object o) { return false; } UriEndpoint that = (UriEndpoint) o; - return getRemoteAddress().equals(that.getRemoteAddress()); + return remoteAddress.equals(that.remoteAddress); } @Override public int hashCode() { - return Objects.hash(getRemoteAddress()); + return remoteAddress.hashCode(); } } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpointFactory.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpointFactory.java deleted file mode 100644 index 2c32a529ee..0000000000 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/UriEndpointFactory.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2017-2021 VMware, Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package reactor.netty.http.client; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.URI; -import java.util.function.BiFunction; -import java.util.function.Supplier; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import reactor.util.annotation.Nullable; - -final class UriEndpointFactory { - final Supplier connectAddress; - final boolean defaultSecure; - final BiFunction inetSocketAddressFunction; - - static final Pattern URL_PATTERN = Pattern.compile( - "(?:(\\w+)://)?((?:\\[.+?])|(? connectAddress, boolean defaultSecure, - BiFunction inetSocketAddressFunction) { - this.connectAddress = connectAddress; - this.defaultSecure = defaultSecure; - this.inetSocketAddressFunction = inetSocketAddressFunction; - } - - UriEndpoint createUriEndpoint(String url, boolean isWs) { - return createUriEndpoint(url, isWs, connectAddress); - } - - UriEndpoint createUriEndpoint(String url, boolean isWs, Supplier connectAddress) { - if (url.startsWith("/")) { - return new UriEndpoint(resolveScheme(isWs), "localhost", 80, connectAddress, url); - } - else { - Matcher matcher = URL_PATTERN.matcher(url); - if (matcher.matches()) { - // scheme is optional in pattern. use default if it's not specified - String scheme = matcher.group(1) != null ? matcher.group(1).toLowerCase() - : resolveScheme(isWs); - String host = cleanHostString(matcher.group(2)); - - String portString = matcher.group(3); - int port = portString != null ? Integer.parseInt(portString) - : (UriEndpoint.isSecureScheme(scheme) ? 443 : 80); - String pathAndQuery = cleanPathAndQuery(matcher.group(4)); - return new UriEndpoint(scheme, host, port, - () -> inetSocketAddressFunction.apply(host, port), - pathAndQuery); - } - else { - throw new IllegalArgumentException("Unable to parse url [" + url + "]"); - } - } - } - - UriEndpoint createUriEndpoint(URI url, boolean isWs) { - if (!url.isAbsolute()) { - throw new IllegalArgumentException("URI is not absolute: " + url); - } - if (url.getHost() == null) { - throw new IllegalArgumentException("Host is not specified"); - } - String scheme = url.getScheme() != null ? url.getScheme().toLowerCase() : resolveScheme(isWs); - String host = cleanHostString(url.getHost()); - int port = url.getPort() != -1 ? url.getPort() : (UriEndpoint.isSecureScheme(scheme) ? 443 : 80); - String path = url.getRawPath() != null ? url.getRawPath() : ""; - String query = url.getRawQuery() != null ? '?' + url.getRawQuery() : ""; - return new UriEndpoint(scheme, host, port, - () -> inetSocketAddressFunction.apply(host, port), - cleanPathAndQuery(path + query)); - } - - UriEndpoint createUriEndpoint(UriEndpoint from, String to, Supplier connectAddress) { - if (to.startsWith("/")) { - return new UriEndpoint(from.scheme, from.host, from.port, connectAddress, to); - } - else { - throw new IllegalArgumentException("Must provide a relative address in parameter `to`"); - } - } - - String cleanPathAndQuery(@Nullable String pathAndQuery) { - if (pathAndQuery == null) { - pathAndQuery = "/"; - } - else { - // remove possible fragment since it shouldn't be sent to the server - int pos = pathAndQuery.indexOf('#'); - if (pos > -1) { - pathAndQuery = pathAndQuery.substring(0, pos); - } - } - if (pathAndQuery.length() == 0) { - pathAndQuery = "/"; - } - else if (pathAndQuery.charAt(0) == '?') { - pathAndQuery = "/" + pathAndQuery; - } - return pathAndQuery; - } - - String cleanHostString(String host) { - // remove brackets around IPv6 address in host name - if (host.charAt(0) == '[' && host.charAt(host.length() - 1) == ']') { - host = host.substring(1, host.length() - 1); - } - return host; - } - - String resolveScheme(boolean isWs) { - if (isWs) { - return defaultSecure ? HttpClient.WSS_SCHEME : HttpClient.WS_SCHEME; - } - else { - return defaultSecure ? HttpClient.HTTPS_SCHEME : HttpClient.HTTP_SCHEME; - } - } -} diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java index 3c1d3e7f77..e9cc93fe66 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java @@ -89,6 +89,7 @@ import io.netty.resolver.AddressResolverGroup; import io.netty.resolver.dns.DnsAddressResolverGroup; import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; import io.netty.util.concurrent.DefaultEventExecutor; import io.netty.util.concurrent.EventExecutor; import org.junit.jupiter.api.AfterAll; @@ -926,7 +927,7 @@ void testIssue473() { HttpClient.create(ConnectionProvider.newConnection()) .secure() .websocket() - .uri("wss://" + disposableServer.host() + ":" + disposableServer.port()) + .uri("wss://" + NetUtil.toSocketAddressString(disposableServer.host(), disposableServer.port())) .handle((in, out) -> Mono.empty())) .expectErrorMatches(t -> t.getCause() instanceof CertificateException) .verify(Duration.ofSeconds(30)); @@ -2533,7 +2534,7 @@ private void doTestUriWhenFailedRequest(boolean useUri) throws Exception { AtomicReference uriFailedRequest = new AtomicReference<>(); HttpClient client = createHttpClientForContextWithPort() - .doOnRequestError((req, t) -> uriFailedRequest.set(req.uri())); + .doOnRequestError((req, t) -> uriFailedRequest.set(req.resourceUrl())); String uri = "http://localhost:" + disposableServer.port() + "/"; if (useUri) { diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/client/UriEndpointFactoryTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/client/UriEndpointFactoryTest.java index 47e2e864ec..c3a6bd1180 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/client/UriEndpointFactoryTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/client/UriEndpointFactoryTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017-2021 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2017-2023 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,12 @@ import java.net.InetSocketAddress; import java.net.URI; +import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; -import java.util.function.BiFunction; -import java.util.regex.Matcher; import org.junit.jupiter.api.Test; + import reactor.netty.transport.AddressUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -32,50 +32,71 @@ class UriEndpointFactoryTest { private final UriEndpointFactoryBuilder builder = new UriEndpointFactoryBuilder(); @Test - void shouldParseUrls_1() { + void shouldParseUrls_1() throws Exception { List inputs = Arrays.asList( - new String[]{"http://localhost:80/path", "http", "localhost", "80", "/path"}, - new String[]{"http://localhost:80/path?key=val", "http", "localhost", "80", "/path?key=val"}, - new String[]{"http://localhost/path", "http", "localhost", null, "/path"}, - new String[]{"http://localhost/path?key=val", "http", "localhost", null, "/path?key=val"}, - new String[]{"http://localhost/", "http", "localhost", null, "/"}, - new String[]{"http://localhost/?key=val", "http", "localhost", null, "/?key=val"}, - new String[]{"http://localhost", "http", "localhost", null, null}, - new String[]{"http://localhost?key=val", "http", "localhost", null, "?key=val"}, - new String[]{"http://localhost:80", "http", "localhost", "80", null}, - new String[]{"http://localhost:80?key=val", "http", "localhost", "80", "?key=val"}, - new String[]{"http://localhost/:1234", "http", "localhost", null, "/:1234"}, - new String[]{"http://[::1]:80/path", "http", "[::1]", "80", "/path"}, - new String[]{"http://[::1]:80/path?key=val", "http", "[::1]", "80", "/path?key=val"}, - new String[]{"http://[::1]/path", "http", "[::1]", null, "/path"}, - new String[]{"http://[::1]/path?key=val", "http", "[::1]", null, "/path?key=val"}, - new String[]{"http://[::1]/", "http", "[::1]", null, "/"}, - new String[]{"http://[::1]/?key=val", "http", "[::1]", null, "/?key=val"}, - new String[]{"http://[::1]", "http", "[::1]", null, null}, - new String[]{"http://[::1]?key=val", "http", "[::1]", null, "?key=val"}, - new String[]{"http://[::1]:80", "http", "[::1]", "80", null}, - new String[]{"http://[::1]:80?key=val", "http", "[::1]", "80", "?key=val"}, - new String[]{"localhost:80/path", null, "localhost", "80", "/path"}, - new String[]{"localhost:80/path?key=val", null, "localhost", "80", "/path?key=val"}, - new String[]{"localhost/path", null, "localhost", null, "/path"}, - new String[]{"localhost/path?key=val", null, "localhost", null, "/path?key=val"}, - new String[]{"localhost/", null, "localhost", null, "/"}, - new String[]{"localhost/?key=val", null, "localhost", null, "/?key=val"}, - new String[]{"localhost", null, "localhost", null, null}, - new String[]{"localhost?key=val", null, "localhost", null, "?key=val"}, - new String[]{"localhost:80", null, "localhost", "80", null}, - new String[]{"localhost:80?key=val", null, "localhost", "80", "?key=val"}, - new String[]{"localhost/:1234", null, "localhost", null, "/:1234"} - ); + new String[]{"http://localhost:80/path", "http://localhost/path"}, + new String[]{"http://localhost:80/path?key=val", "http://localhost/path?key=val"}, + new String[]{"http://localhost/path", "http://localhost/path"}, + new String[]{"http://localhost/path%20", "http://localhost/path%20"}, + new String[]{"http://localhost/path?key=val", "http://localhost/path?key=val"}, + new String[]{"http://localhost/", "http://localhost/"}, + new String[]{"http://localhost/?key=val", "http://localhost/?key=val"}, + new String[]{"http://localhost", "http://localhost/"}, + new String[]{"http://localhost?key=val", "http://localhost/?key=val"}, + new String[]{"http://localhost:80", "http://localhost/"}, + new String[]{"http://localhost:80?key=val", "http://localhost/?key=val"}, + new String[]{"http://localhost:80/?key=val#fragment", "http://localhost/?key=val"}, + new String[]{"http://localhost:80/?key=%223", "http://localhost/?key=%223"}, + new String[]{"http://localhost/:1234", "http://localhost/:1234"}, + new String[]{"http://localhost:1234", "http://localhost:1234/"}, + new String[]{"http://[::1]:80/path", "http://[::1]/path"}, + new String[]{"http://[::1]:80/path?key=val", "http://[::1]/path?key=val"}, + new String[]{"http://[::1]/path", "http://[::1]/path"}, + new String[]{"http://[::1]/path%20", "http://[::1]/path%20"}, + new String[]{"http://[::1]/path?key=val", "http://[::1]/path?key=val"}, + new String[]{"http://[::1]/", "http://[::1]/"}, + new String[]{"http://[::1]/?key=val", "http://[::1]/?key=val"}, + new String[]{"http://[::1]", "http://[::1]/"}, + new String[]{"http://[::1]?key=val", "http://[::1]/?key=val"}, + new String[]{"http://[::1]:80", "http://[::1]/"}, + new String[]{"http://[::1]:80?key=val", "http://[::1]/?key=val"}, + new String[]{"http://[::1]:80/?key=val#fragment", "http://[::1]/?key=val"}, + new String[]{"http://[::1]:80/?key=%223", "http://[::1]/?key=%223"}, + new String[]{"http://[::1]:1234", "http://[::1]:1234/"}, + new String[]{"localhost:80/path", "http://localhost/path"}, + new String[]{"localhost:80/path?key=val", "http://localhost/path?key=val"}, + new String[]{"localhost/path", "http://localhost/path"}, + new String[]{"localhost/path%20", "http://localhost/path%20"}, + new String[]{"localhost/path?key=val", "http://localhost/path?key=val"}, + new String[]{"localhost/", "http://localhost/"}, + new String[]{"localhost/?key=val", "http://localhost/?key=val"}, + new String[]{"localhost", "http://localhost/"}, + new String[]{"localhost?key=val", "http://localhost/?key=val"}, + new String[]{"localhost:80", "http://localhost/"}, + new String[]{"localhost:80?key=val", "http://localhost/?key=val"}, + new String[]{"localhost:80/?key=val#fragment", "http://localhost/?key=val"}, + new String[]{"localhost:80/?key=%223", "http://localhost/?key=%223"}, + new String[]{"localhost/:1234", "http://localhost/:1234"}, + new String[]{"localhost:1234", "http://localhost:1234/"}, + new String[]{"[::1]:80/path", "http://[::1]/path"}, + new String[]{"[::1]:80/path?key=val", "http://[::1]/path?key=val"}, + new String[]{"[::1]/path", "http://[::1]/path"}, + new String[]{"[::1]/path%20", "http://[::1]/path%20"}, + new String[]{"[::1]/path?key=val", "http://[::1]/path?key=val"}, + new String[]{"[::1]/", "http://[::1]/"}, + new String[]{"[::1]/?key=val", "http://[::1]/?key=val"}, + new String[]{"[::1]", "http://[::1]/"}, + new String[]{"[::1]?key=val", "http://[::1]/?key=val"}, + new String[]{"[::1]:80", "http://[::1]/"}, + new String[]{"[::1]:80?key=val", "http://[::1]/?key=val"}, + new String[]{"[::1]:80/?key=val#fragment", "http://[::1]/?key=val"}, + new String[]{"[::1]:80/?key=%223", "http://[::1]/?key=%223"}, + new String[]{"[::1]:1234", "http://[::1]:1234/"}, + new String[]{"/?key=val#fragment", "https://example.com/?key=val"} + ); for (String[] input : inputs) { - Matcher matcher = UriEndpointFactory.URL_PATTERN - .matcher(input[0]); - assertThat(matcher.matches()).isTrue(); - assertThat(input[1]).isEqualTo(matcher.group(1)); - assertThat(input[2]).isEqualTo(matcher.group(2)); - assertThat(input[3]).isEqualTo(matcher.group(3)); - assertThat(input[4]).isEqualTo(matcher.group(4)); + assertThat(externalForm(this.builder.baseUrl("https://example.com/").build(), input[0], false)).isEqualTo(input[1]); } } @@ -114,17 +135,142 @@ void shouldParseUrls_2() throws Exception { ); for (String[] input : inputs) { - assertThat(externalForm(this.builder.build(), input[0], false, true)).isEqualTo(input[1]); + assertThat(externalForm(this.builder.baseUrl("https://example.com/").build(), input[0], true)).isEqualTo(input[1]); } } + @Test + void createUriEndpointOpaque() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> externalForm(this.builder.build(), "mailto:admin@example.com", true)); + } + + @Test + void createUriEndpointFqdn_1() { + UriEndpoint endpoint = this.builder.host("example.com").sslSupport().build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 443)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointFqdn_2() { + UriEndpoint endpoint = this.builder.host("example.com").port(8080).sslSupport().build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com:8080/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com:8080"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 8080)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointFqdn_3() { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("https://example.com:8080/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com:8080/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com:8080"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 8080)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointIpv4() { + UriEndpoint endpoint = this.builder.host("127.0.0.1").port(8080).build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("http://127.0.0.1:8080/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("127.0.0.1:8080"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("127.0.0.1", 8080)); + assertThat(endpoint.isSecure()).isFalse(); + } + + @Test + void createUriEndpointIpv6() throws UnknownHostException { + UriEndpoint endpoint = this.builder.host("::1").port(8080).build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("http://[::1]:8080/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("[::1]:8080"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("::1", 8080)); + assertThat(endpoint.isSecure()).isFalse(); + } + + @Test + void createUriEndpointRedirectAbsolute() throws UnknownHostException { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("https://source.example.com/foo/bar"); + + endpoint = endpoint.redirect("https://example.com/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 443)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointRedirectRelative() throws UnknownHostException { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("https://example.com/"); + + endpoint = endpoint.redirect("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com"); + assertThat(endpoint.getRawUri()).isEqualTo("/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 443)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointRedirectRelativeSubpath() throws UnknownHostException { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("https://example.com/subpath/"); + + endpoint = endpoint.redirect("path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("https://example.com/subpath/path%20example?key=value"); + assertThat(endpoint.getHostHeader()).isEqualTo("example.com"); + assertThat(endpoint.getRawUri()).isEqualTo("/subpath/path%20example?key=value"); + assertThat(endpoint.getPath()).isEqualTo("/subpath/path example"); + assertThat(endpoint.getRemoteAddress()).isEqualTo(AddressUtils.createUnresolved("example.com", 443)); + assertThat(endpoint.isSecure()).isTrue(); + } + + @Test + void createUriEndpointRedirectInvalid() throws UnknownHostException { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("https://example.com/"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> endpoint.redirect("path${MACRO_IS_INVALID}/test@@@@")); + } + @Test void createUriEndpointRelative() { String test1 = this.builder.build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); - String test2 = this.builder.build() - .createUriEndpoint("/foo", true) + String test2 = this.builder.webSocket(true).build() + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test1).isEqualTo("http://localhost/foo"); @@ -135,11 +281,12 @@ void createUriEndpointRelative() { void createUriEndpointRelativeSslSupport() { String test1 = this.builder.sslSupport() .build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); String test2 = this.builder.sslSupport() + .webSocket(true) .build() - .createUriEndpoint("/foo", true) + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test1).isEqualTo("https://localhost/foo"); @@ -149,10 +296,10 @@ void createUriEndpointRelativeSslSupport() { @Test void createUriEndpointRelativeNoLeadingSlash() { String test1 = this.builder.sslSupport().build() - .createUriEndpoint("example.com:8443/bar", false) + .createUriEndpoint("example.com:8443/bar") .toExternalForm(); - String test2 = this.builder.build() - .createUriEndpoint("example.com:8443/bar", true) + String test2 = this.builder.webSocket(true).build() + .createUriEndpoint("example.com:8443/bar") .toExternalForm(); assertThat(test1).isEqualTo("https://example.com:8443/bar"); @@ -164,12 +311,13 @@ void createUriEndpointRelativeAddress() { String test1 = this.builder.host("127.0.0.1") .port(8080) .build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); String test2 = this.builder.host("127.0.0.1") .port(8080) + .webSocket(true) .build() - .createUriEndpoint("/foo", true) + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test1).isEqualTo("http://127.0.0.1:8080/foo"); @@ -181,12 +329,13 @@ void createUriEndpointIPv6Address() { String test1 = this.builder.host("::1") .port(8080) .build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); String test2 = this.builder.host("::1") .port(8080) + .webSocket(true) .build() - .createUriEndpoint("/foo", true) + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test1).isEqualTo("http://[::1]:8080/foo"); @@ -199,13 +348,14 @@ void createUriEndpointRelativeAddressSsl() { .port(8080) .sslSupport() .build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); String test2 = this.builder.host("example.com") .port(8080) .sslSupport() + .webSocket(true) .build() - .createUriEndpoint("/foo", true) + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test1).isEqualTo("https://example.com:8080/foo"); @@ -219,7 +369,7 @@ void createUriEndpointRelativeWithPort() { .port(443) .sslSupport() .build() - .createUriEndpoint("/foo", false) + .createUriEndpoint("/foo") .toExternalForm(); assertThat(test).isEqualTo("https://example.com/foo"); @@ -232,11 +382,11 @@ void createUriEndpointAbsoluteHttp() throws Exception { } private void testCreateUriEndpointAbsoluteHttp(boolean useUri) throws Exception { - String test1 = externalForm(this.builder.build(), "https://localhost/foo", false, useUri); - String test2 = externalForm(this.builder.build(), "http://localhost/foo", true, useUri); + String test1 = externalForm(this.builder.build(), "https://localhost/foo", useUri); + String test2 = externalForm(this.builder.webSocket(true).build(), "http://localhost/foo", useUri); - String test3 = externalForm(this.builder.sslSupport().build(), "http://localhost/foo", false, useUri); - String test4 = externalForm(this.builder.sslSupport().build(), "https://localhost/foo", true, useUri); + String test3 = externalForm(this.builder.sslSupport().build(), "http://localhost/foo", useUri); + String test4 = externalForm(this.builder.sslSupport().webSocket(true).build(), "https://localhost/foo", useUri); assertThat(test1).isEqualTo("https://localhost/foo"); assertThat(test2).isEqualTo("http://localhost/foo"); @@ -251,45 +401,45 @@ void createUriEndpointWithQuery() throws Exception { } private void testCreateUriEndpointWithQuery(boolean useUri) throws Exception { - assertThat(externalForm(this.builder.build(), "http://localhost/foo?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost/foo?key=val", useUri)) .isEqualTo("http://localhost/foo?key=val"); - assertThat(externalForm(this.builder.build(), "http://localhost/?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost/?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "http://localhost?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "http://localhost:80/foo?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost:80/foo?key=val", useUri)) .isEqualTo("http://localhost/foo?key=val"); - assertThat(externalForm(this.builder.build(), "http://localhost:80/?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost:80/?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "http://localhost:80?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "http://localhost:80?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); if (useUri) { assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost/foo?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost/foo?key=val", useUri)); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost/?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost/?key=val", useUri)); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost?key=val", useUri)); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80/foo?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80/foo?key=val", useUri)); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80/?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80/?key=val", useUri)); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80?key=val", false, useUri)); + .isThrownBy(() -> externalForm(this.builder.build(), "localhost:80?key=val", useUri)); } else { - assertThat(externalForm(this.builder.build(), "localhost/foo?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost/foo?key=val", useUri)) .isEqualTo("http://localhost/foo?key=val"); - assertThat(externalForm(this.builder.build(), "localhost/?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost/?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "localhost?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "localhost:80/foo?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost:80/foo?key=val", useUri)) .isEqualTo("http://localhost/foo?key=val"); - assertThat(externalForm(this.builder.build(), "localhost:80/?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost:80/?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); - assertThat(externalForm(this.builder.build(), "localhost:80?key=val", false, useUri)) + assertThat(externalForm(this.builder.build(), "localhost:80?key=val", useUri)) .isEqualTo("http://localhost/?key=val"); } } @@ -301,11 +451,11 @@ void createUriEndpointAbsoluteWs() throws Exception { } private void testCreateUriEndpointAbsoluteWs(boolean useUri) throws Exception { - String test1 = externalForm(this.builder.build(), "wss://localhost/foo", false, useUri); - String test2 = externalForm(this.builder.build(), "ws://localhost/foo", true, useUri); + String test1 = externalForm(this.builder.build(), "wss://localhost/foo", useUri); + String test2 = externalForm(this.builder.webSocket(true).build(), "ws://localhost/foo", useUri); - String test3 = externalForm(this.builder.sslSupport().build(), "ws://localhost/foo", false, useUri); - String test4 = externalForm(this.builder.sslSupport().build(), "wss://localhost/foo", true, useUri); + String test3 = externalForm(this.builder.sslSupport().build(), "ws://localhost/foo", useUri); + String test4 = externalForm(this.builder.sslSupport().webSocket(true).build(), "wss://localhost/foo", useUri); assertThat(test1).isEqualTo("wss://localhost/foo"); assertThat(test2).isEqualTo("ws://localhost/foo"); @@ -313,13 +463,13 @@ private void testCreateUriEndpointAbsoluteWs(boolean useUri) throws Exception { assertThat(test4).isEqualTo("wss://localhost/foo"); } - private static String externalForm(UriEndpointFactory factory, String url, boolean isWs, boolean useUri) throws Exception { + private static String externalForm(UriEndpointFactoryBuilder.UriEndpointFactory factory, String url, boolean useUri) throws Exception { if (useUri) { - return factory.createUriEndpoint(new URI(url), isWs) + return factory.createUriEndpoint(new URI(url)) .toExternalForm(); } else { - return factory.createUriEndpoint(url, isWs) + return factory.createUriEndpoint(url) .toExternalForm(); } } @@ -328,11 +478,35 @@ private static final class UriEndpointFactoryBuilder { private boolean secure; private String host = "localhost"; private int port = -1; + private String baseUrl; + private boolean isWs; public UriEndpointFactory build() { - return new UriEndpointFactory( - () -> InetSocketAddress.createUnresolved(host, port != -1 ? port : (secure ? 443 : 80)), secure, - URI_ADDRESS_MAPPER); + return new UriEndpointFactory(); + } + + private final class UriEndpointFactory { + InetSocketAddress remoteAddress() { + return AddressUtils.createUnresolved(host, port != -1 ? port : (secure ? 443 : 80)); + } + + UriEndpoint createUriEndpoint(String uri) { + return UriEndpoint.create(null, baseUrl, uri, this::remoteAddress, secure, isWs); + } + + UriEndpoint createUriEndpoint(URI uri) { + return UriEndpoint.create(uri, baseUrl, null, this::remoteAddress, secure, isWs); + } + } + + public UriEndpointFactoryBuilder baseUrl(String baseUrl) { + this.baseUrl = baseUrl; + return this; + } + + public UriEndpointFactoryBuilder webSocket(boolean isWs) { + this.isWs = isWs; + return this; } public UriEndpointFactoryBuilder sslSupport() { @@ -350,7 +524,5 @@ public UriEndpointFactoryBuilder port(int port) { return this; } - static final BiFunction URI_ADDRESS_MAPPER = - AddressUtils::createUnresolved; } }