From 906334ed8d22ca758145c2b1697793cc0ca6c4d1 Mon Sep 17 00:00:00 2001 From: yejin-shin Date: Fri, 26 Jan 2024 17:44:41 +0900 Subject: [PATCH] Reuse VirtualHostBuilder on the same hostnamePattern --- .../armeria/server/ServerBuilder.java | 24 +++ .../linecorp/armeria/server/VirtualHost.java | 16 ++ .../armeria/server/VirtualHostBuilder.java | 35 ++-- .../server/HostnameBasedVirtualHostTest.java | 151 ++++++++++++++++++ 4 files changed, 213 insertions(+), 13 deletions(-) create mode 100644 core/src/test/java/com/linecorp/armeria/server/HostnameBasedVirtualHostTest.java diff --git a/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java b/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java index ed91d00fbc61..19de7bcd7177 100644 --- a/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java @@ -1563,6 +1563,17 @@ public ServerBuilder withVirtualHost(Consumer custom * @return {@link VirtualHostBuilder} for building the virtual host */ public VirtualHostBuilder virtualHost(String hostnamePattern) { + requireNonNull(hostnamePattern, "hostnamePattern"); + + final Optional vhost = + virtualHostBuilders.stream() + .filter(v -> !v.defaultVirtualHost() && + v.equalsHostnamePattern(hostnamePattern)) + .findFirst(); + if (vhost.isPresent()) { + return vhost.get(); + } + final VirtualHostBuilder virtualHostBuilder = new VirtualHostBuilder(this, false).hostnamePattern(hostnamePattern); virtualHostBuilders.add(virtualHostBuilder); @@ -1577,6 +1588,19 @@ public VirtualHostBuilder virtualHost(String hostnamePattern) { * @return {@link VirtualHostBuilder} for building the virtual host */ public VirtualHostBuilder virtualHost(String defaultHostname, String hostnamePattern) { + requireNonNull(defaultHostname, "defaultHostname"); + requireNonNull(hostnamePattern, "hostnamePattern"); + + final Optional vhost = + virtualHostBuilders.stream() + .filter(v -> !v.defaultVirtualHost()) + .filter(v -> v.equalsDefaultHostname(defaultHostname) && + v.equalsHostnamePattern(hostnamePattern)) + .findFirst(); + if (vhost.isPresent()) { + return vhost.get(); + } + final VirtualHostBuilder virtualHostBuilder = new VirtualHostBuilder(this, false) .defaultHostname(defaultHostname) .hostnamePattern(hostnamePattern); diff --git a/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java b/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java index 96027f72c0fd..70e230cf4c21 100644 --- a/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java +++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHost.java @@ -15,6 +15,7 @@ */ package com.linecorp.armeria.server; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Objects.requireNonNull; @@ -205,6 +206,21 @@ static String normalizeHostnamePattern(String hostnamePattern) { return Ascii.toLowerCase(hostnamePattern); } + static void validateHostnamePattern(String hostnamePattern) { + final boolean validHostnamePattern; + if (hostnamePattern.charAt(0) == '*') { + validHostnamePattern = + hostnamePattern.length() >= 3 && + hostnamePattern.charAt(1) == '.' && + HOSTNAME_WITH_NO_PORT_PATTERN.matcher(hostnamePattern.substring(2)).matches(); + } else { + validHostnamePattern = HOSTNAME_WITH_NO_PORT_PATTERN.matcher(hostnamePattern).matches(); + } + + checkArgument(validHostnamePattern, + "hostnamePattern: %s (expected: *. or )", hostnamePattern); + } + /** * Ensure that 'hostnamePattern' matches 'defaultHostname'. */ diff --git a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java index a64b58e125ac..4eb5427887c4 100644 --- a/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/VirtualHostBuilder.java @@ -25,10 +25,10 @@ import static com.linecorp.armeria.server.ServerSslContextUtil.validateSslContext; import static com.linecorp.armeria.server.ServiceConfig.validateMaxRequestLength; import static com.linecorp.armeria.server.ServiceConfig.validateRequestTimeoutMillis; -import static com.linecorp.armeria.server.VirtualHost.HOSTNAME_WITH_NO_PORT_PATTERN; import static com.linecorp.armeria.server.VirtualHost.ensureHostnamePatternMatchesDefaultHostname; import static com.linecorp.armeria.server.VirtualHost.normalizeDefaultHostname; import static com.linecorp.armeria.server.VirtualHost.normalizeHostnamePattern; +import static com.linecorp.armeria.server.VirtualHost.validateHostnamePattern; import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.isPseudoHeader; import static java.util.Objects.requireNonNull; @@ -248,18 +248,7 @@ public VirtualHostBuilder hostnamePattern(String hostnamePattern) { hostnamePattern = hostAndPort.getHost(); } - final boolean validHostnamePattern; - if (hostnamePattern.charAt(0) == '*') { - validHostnamePattern = - hostnamePattern.length() >= 3 && - hostnamePattern.charAt(1) == '.' && - HOSTNAME_WITH_NO_PORT_PATTERN.matcher(hostnamePattern.substring(2)).matches(); - } else { - validHostnamePattern = HOSTNAME_WITH_NO_PORT_PATTERN.matcher(hostnamePattern).matches(); - } - - checkArgument(validHostnamePattern, - "hostnamePattern: %s (expected: *. or )", hostnamePattern); + validateHostnamePattern(hostnamePattern); this.hostnamePattern = normalizeHostnamePattern(hostnamePattern); return this; @@ -1524,6 +1513,26 @@ private SelfSignedCertificate selfSignedCertificate() throws CertificateExceptio return selfSignedCertificate; } + boolean equalsDefaultHostname(String defaultHostname) { + return normalizeDefaultHostname(defaultHostname).equals(this.defaultHostname); + } + + boolean equalsHostnamePattern(String hostnamePattern) { + checkArgument(!hostnamePattern.isEmpty(), "hostnamePattern is empty."); + + final HostAndPort hostAndPort = HostAndPort.fromString(hostnamePattern); + if (hostAndPort.hasPort()) { + if (port != hostAndPort.getPort()) { + return false; + } + hostnamePattern = hostAndPort.getHost(); + } + + validateHostnamePattern(hostnamePattern); + + return hostnamePattern.equals(this.hostnamePattern); + } + int port() { return port; } diff --git a/core/src/test/java/com/linecorp/armeria/server/HostnameBasedVirtualHostTest.java b/core/src/test/java/com/linecorp/armeria/server/HostnameBasedVirtualHostTest.java new file mode 100644 index 000000000000..2f001c7fe583 --- /dev/null +++ b/core/src/test/java/com/linecorp/armeria/server/HostnameBasedVirtualHostTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation licenses this file to you 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 com.linecorp.armeria.server; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.ServerSocket; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.linecorp.armeria.client.ClientFactory; +import com.linecorp.armeria.client.WebClient; +import com.linecorp.armeria.common.AggregatedHttpResponse; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.internal.testing.MockAddressResolverGroup; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; + +class HostnameBasedVirtualHostTest { + + private static int fooHostPort; + + @RegisterExtension + static ServerExtension serverWithPortMapping = new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) throws Exception { + + try (ServerSocket ss = new ServerSocket(0)) { + fooHostPort = ss.getLocalPort(); + } + + sb.http(fooHostPort) + .virtualHost("foo.com:" + fooHostPort) + .service("/foo", (ctx, req) -> HttpResponse.of("foo with port")) + .and() + .virtualHost("foo.com:" + fooHostPort) + .service("/bar", (ctx, req) -> HttpResponse.of("bar with port")) + .and() + .virtualHost("foo.bar.com") + .service("/foo-bar", (ctx, req) -> HttpResponse.of("foo bar")) + .and() + .virtualHost("def.bar.com", "*.bar.com") + .service("/foo-all", (ctx, req) -> HttpResponse.of("foo all")) + .and() + .build(); + } + }; + + @Test + void testHostnamePattern() { + try (ClientFactory factory = ClientFactory.builder() + .addressResolverGroupFactory( + unused -> MockAddressResolverGroup.localhost()) + .build()) { + + final WebClient client = WebClient.builder("http://foo.com:" + fooHostPort) + .factory(factory) + .build(); + AggregatedHttpResponse response = client.get("/foo").aggregate().join(); + assertThat(response.contentUtf8()).isEqualTo("foo with port"); + + response = client.get("/bar").aggregate().join(); + assertThat(response.contentUtf8()).isEqualTo("bar with port"); + } + } + + @Test + void testDefaultHostname() { + try (ClientFactory factory = ClientFactory.builder() + .addressResolverGroupFactory( + unused -> MockAddressResolverGroup.localhost()) + .build()) { + + final WebClient fooBarClient = WebClient.builder("http://foo.bar.com:" + fooHostPort) + .factory(factory) + .build(); + AggregatedHttpResponse response = fooBarClient.get("/foo-bar").aggregate().join(); + assertThat(response.contentUtf8()).isEqualTo("foo bar"); + + response = fooBarClient.get("/foo-all").aggregate().join(); + assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND); + + final WebClient barBarClient = WebClient.builder("http://bar.bar.com:" + fooHostPort) + .factory(factory) + .build(); + + response = barBarClient.get("/foo-all").aggregate().join(); + assertThat(response.contentUtf8()).isEqualTo("foo all"); + + response = barBarClient.get("/foo-bar").aggregate().join(); + assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND); + + final WebClient defBarClient = WebClient.builder("http://def.bar.com:" + fooHostPort) + .factory(factory) + .build(); + + response = defBarClient.get("/foo-all").aggregate().join(); + assertThat(response.contentUtf8()).isEqualTo("foo all"); + + response = defBarClient.get("/foo-bar").aggregate().join(); + assertThat(response.status()).isEqualTo(HttpStatus.NOT_FOUND); + } + } + + @Test + void shouldReturnSameInstanceForHostnameBasedVirtualHost() { + final ServerBuilder serverBuilder = Server.builder(); + final VirtualHostBuilder virtualHost1 = serverBuilder.virtualHost("foo.com"); + final VirtualHostBuilder virtualHost2 = serverBuilder.virtualHost("foo.com"); + assertThat(virtualHost1).isSameAs(virtualHost2); + final VirtualHostBuilder virtualHost3 = serverBuilder.virtualHost("foo.com:18080"); + assertThat(virtualHost2).isNotSameAs(virtualHost3); + final VirtualHostBuilder virtualHost4 = serverBuilder.virtualHost("bar.com"); + assertThat(virtualHost2).isNotSameAs(virtualHost4); + } + + @Test + void shouldReturnSameInstanceForHostnameBasedVirtualHostWithPort() { + final ServerBuilder serverBuilder = Server.builder(); + final VirtualHostBuilder virtualHost1 = serverBuilder.virtualHost("foo.com:18080"); + final VirtualHostBuilder virtualHost2 = serverBuilder.virtualHost("foo.com:18080"); + assertThat(virtualHost1).isSameAs(virtualHost2); + final VirtualHostBuilder virtualHost3 = serverBuilder.virtualHost("foo.com:18081"); + assertThat(virtualHost2).isNotSameAs(virtualHost3); + } + + @Test + void shouldReturnSameInstanceForDefaultHostnameAndHostnameBasedVirtualHost() { + final ServerBuilder serverBuilder = Server.builder(); + final VirtualHostBuilder virtualHost1 = serverBuilder.virtualHost("def.foo.com", "*.foo.com"); + final VirtualHostBuilder virtualHost2 = serverBuilder.virtualHost("def.foo.com", "*.foo.com"); + assertThat(virtualHost1).isSameAs(virtualHost2); + final VirtualHostBuilder virtualHost3 = serverBuilder.virtualHost("def2.foo.com", "*.foo.com"); + assertThat(virtualHost2).isNotSameAs(virtualHost3); + } +}