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..d909535467 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 Objects.hash(remoteAddress); } } 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..590535c367 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").build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("http://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", 80)); + assertThat(endpoint.isSecure()).isFalse(); + } + + @Test + void createUriEndpointFqdn_2() { + UriEndpoint endpoint = this.builder.host("example.com").port(8080).build() + .createUriEndpoint("/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("http://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()).isFalse(); + } + + @Test + void createUriEndpointFqdn_3() { + UriEndpoint endpoint = this.builder.build() + .createUriEndpoint("http://example.com:8080/path%20example?key=value#fragment"); + + assertThat(endpoint.toExternalForm()).isEqualTo("http://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()).isFalse(); + } + + @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; } }