From 4b235a96001e107401dce12aad520a42b2bbbc6f Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Tue, 31 Mar 2020 16:57:42 +0200 Subject: [PATCH] =?UTF-8?q?Introduce=20DomainWildcardMappingBuilder=20to?= =?UTF-8?q?=20fix=20wildcard=20matching=20accor=E2=80=A6=20(#10132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: How we did wildcard matching was not correct according to RFC6125. Beside this our implementation was quite CPU heavy. Modifications: - Add new DomainWildcardMappingBuilder which correctly does wildcard matching. See https://tools.ietf.org/search/rfc6125#section-6.4 - Add unit tests - Deprecate old implementations Result: Correctly implement wildcard matching and improve performance --- .../io/netty/util/DomainMappingBuilder.java | 2 +- .../java/io/netty/util/DomainNameMapping.java | 2 + .../netty/util/DomainNameMappingBuilder.java | 2 + .../util/DomainWildcardMappingBuilder.java | 159 ++++++++++++++++++ .../DomainWildcardMappingBuilderTest.java | 106 ++++++++++++ 5 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/io/netty/util/DomainWildcardMappingBuilder.java create mode 100644 common/src/test/java/io/netty/util/DomainWildcardMappingBuilderTest.java diff --git a/common/src/main/java/io/netty/util/DomainMappingBuilder.java b/common/src/main/java/io/netty/util/DomainMappingBuilder.java index e0dcf7dafb..69063f892a 100644 --- a/common/src/main/java/io/netty/util/DomainMappingBuilder.java +++ b/common/src/main/java/io/netty/util/DomainMappingBuilder.java @@ -20,7 +20,7 @@ package io.netty.util; * Builder for immutable {@link DomainNameMapping} instances. * * @param concrete type of value objects - * @deprecated Use {@link DomainNameMappingBuilder} instead. + * @deprecated Use {@link DomainWildcardMappingBuilder} instead. */ @Deprecated public final class DomainMappingBuilder { diff --git a/common/src/main/java/io/netty/util/DomainNameMapping.java b/common/src/main/java/io/netty/util/DomainNameMapping.java index d1734aa712..62c4ebd09d 100644 --- a/common/src/main/java/io/netty/util/DomainNameMapping.java +++ b/common/src/main/java/io/netty/util/DomainNameMapping.java @@ -33,7 +33,9 @@ import static io.netty.util.internal.StringUtil.commonSuffixOfLength; * DNS wildcard is supported as hostname, so you can use {@code *.netty.io} to match both {@code netty.io} * and {@code downloads.netty.io}. *

+ * @deprecated Use {@link DomainWildcardMappingBuilder}} */ +@Deprecated public class DomainNameMapping implements Mapping { final V defaultValue; diff --git a/common/src/main/java/io/netty/util/DomainNameMappingBuilder.java b/common/src/main/java/io/netty/util/DomainNameMappingBuilder.java index 4bf360c5fa..1e86900418 100644 --- a/common/src/main/java/io/netty/util/DomainNameMappingBuilder.java +++ b/common/src/main/java/io/netty/util/DomainNameMappingBuilder.java @@ -27,7 +27,9 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; * Builder for immutable {@link DomainNameMapping} instances. * * @param concrete type of value objects + * @deprecated Use {@link DomainWildcardMappingBuilder} */ +@Deprecated public final class DomainNameMappingBuilder { private final V defaultValue; diff --git a/common/src/main/java/io/netty/util/DomainWildcardMappingBuilder.java b/common/src/main/java/io/netty/util/DomainWildcardMappingBuilder.java new file mode 100644 index 0000000000..ee196c2f6e --- /dev/null +++ b/common/src/main/java/io/netty/util/DomainWildcardMappingBuilder.java @@ -0,0 +1,159 @@ +/* + * Copyright 2020 The Netty Project + * + * The Netty Project 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: + * + * http://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 io.netty.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Builder that allows to build {@link Mapping}s that support + * DNS wildcard matching. + * @param the type of the value that we map to. + */ +public class DomainWildcardMappingBuilder { + + private final V defaultValue; + private final Map map; + + /** + * Constructor with default initial capacity of the map holding the mappings + * + * @param defaultValue the default value for {@link Mapping#map(Object)} )} to return + * when nothing matches the input + */ + public DomainWildcardMappingBuilder(V defaultValue) { + this(4, defaultValue); + } + + /** + * Constructor with initial capacity of the map holding the mappings + * + * @param initialCapacity initial capacity for the internal map + * @param defaultValue the default value for {@link Mapping#map(Object)} to return + * when nothing matches the input + */ + public DomainWildcardMappingBuilder(int initialCapacity, V defaultValue) { + this.defaultValue = checkNotNull(defaultValue, "defaultValue"); + map = new LinkedHashMap(initialCapacity); + } + + /** + * Adds a mapping that maps the specified (optionally wildcard) host name to the specified output value. + * {@code null} values are forbidden for both hostnames and values. + *

+ * DNS wildcard is supported as hostname. The + * wildcard will only match one sub-domain deep and only when wildcard is used as the most-left label. + * + * For example: + * + *

+ * *.netty.io will match xyz.netty.io but NOT abc.xyz.netty.io + *

+ * + * @param hostname the host name (optionally wildcard) + * @param output the output value that will be returned by {@link Mapping#map(Object)} + * when the specified host name matches the specified input host name + */ + public DomainWildcardMappingBuilder add(String hostname, V output) { + map.put(normalizeHostName(hostname), + checkNotNull(output, "output")); + return this; + } + + private String normalizeHostName(String hostname) { + checkNotNull(hostname, "hostname"); + if (hostname.isEmpty() || hostname.charAt(0) == '.') { + throw new IllegalArgumentException("Hostname '" + hostname + "' not valid"); + } + hostname = ImmutableDomainWildcardMapping.normalize(checkNotNull(hostname, "hostname")); + if (hostname.charAt(0) == '*') { + if (hostname.length() < 3 || hostname.charAt(1) != '.') { + throw new IllegalArgumentException("Wildcard Hostname '" + hostname + "'not valid"); + } + return hostname.substring(1); + } + return hostname; + } + /** + * Creates a new instance of an immutable {@link Mapping}. + * + * @return new {@link Mapping} instance + */ + public Mapping build() { + return new ImmutableDomainWildcardMapping(defaultValue, map); + } + + private static final class ImmutableDomainWildcardMapping implements Mapping { + private static final String REPR_HEADER = "ImmutableDomainWildcardMapping(default: "; + private static final String REPR_MAP_OPENING = ", map: "; + private static final String REPR_MAP_CLOSING = ")"; + + private final V defaultValue; + private final Map map; + + ImmutableDomainWildcardMapping(V defaultValue, Map map) { + this.defaultValue = defaultValue; + this.map = new LinkedHashMap(map); + } + + @Override + public V map(String hostname) { + if (hostname != null) { + hostname = normalize(hostname); + + // Let's try an exact match first + V value = map.get(hostname); + if (value != null) { + return value; + } + + // No exact match, let's try a wildcard match. + int idx = hostname.indexOf('.'); + if (idx != -1) { + value = map.get(hostname.substring(idx)); + if (value != null) { + return value; + } + } + } + + return defaultValue; + } + + @SuppressWarnings("deprecation") + static String normalize(String hostname) { + return DomainNameMapping.normalizeHostname(hostname); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(REPR_HEADER).append(defaultValue).append(REPR_MAP_OPENING).append('{'); + + for (Map.Entry entry : map.entrySet()) { + String hostname = entry.getKey(); + if (hostname.charAt(0) == '.') { + hostname = '*' + hostname; + } + sb.append(hostname).append('=').append(entry.getValue()).append(", "); + } + sb.setLength(sb.length() - 2); + return sb.append('}').append(REPR_MAP_CLOSING).toString(); + } + } +} diff --git a/common/src/test/java/io/netty/util/DomainWildcardMappingBuilderTest.java b/common/src/test/java/io/netty/util/DomainWildcardMappingBuilderTest.java new file mode 100644 index 0000000000..374cd5c64d --- /dev/null +++ b/common/src/test/java/io/netty/util/DomainWildcardMappingBuilderTest.java @@ -0,0 +1,106 @@ +/* +* Copyright 2015 The Netty Project +* +* The Netty Project 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: +* +* http://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 io.netty.util; + +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class DomainWildcardMappingBuilderTest { + + @Test(expected = NullPointerException.class) + public void testNullDefaultValue() { + new DomainWildcardMappingBuilder(null); + } + + @Test(expected = NullPointerException.class) + public void testNullDomainNamePatternsAreForbidden() { + new DomainWildcardMappingBuilder("NotFound").add(null, "Some value"); + } + + @Test(expected = NullPointerException.class) + public void testNullValuesAreForbidden() { + new DomainWildcardMappingBuilder("NotFound").add("Some key", null); + } + + @Test + public void testDefaultValue() { + Mapping mapping = new DomainWildcardMappingBuilder("NotFound") + .add("*.netty.io", "Netty") + .build(); + + assertEquals("NotFound", mapping.map("not-existing")); + } + + @Test + public void testStrictEquality() { + Mapping mapping = new DomainWildcardMappingBuilder("NotFound") + .add("netty.io", "Netty") + .add("downloads.netty.io", "Netty-Downloads") + .build(); + + assertEquals("Netty", mapping.map("netty.io")); + assertEquals("Netty-Downloads", mapping.map("downloads.netty.io")); + + assertEquals("NotFound", mapping.map("x.y.z.netty.io")); + } + + @Test + public void testWildcardMatchesNotAnyPrefix() { + Mapping mapping = new DomainWildcardMappingBuilder("NotFound") + .add("*.netty.io", "Netty") + .build(); + + assertEquals("NotFound", mapping.map("netty.io")); + assertEquals("Netty", mapping.map("downloads.netty.io")); + assertEquals("NotFound", mapping.map("x.y.z.netty.io")); + + assertEquals("NotFound", mapping.map("netty.io.x")); + } + + @Test + public void testExactMatchWins() { + assertEquals("Netty-Downloads", + new DomainWildcardMappingBuilder("NotFound") + .add("*.netty.io", "Netty") + .add("downloads.netty.io", "Netty-Downloads") + .build() + .map("downloads.netty.io")); + + assertEquals("Netty-Downloads", + new DomainWildcardMappingBuilder("NotFound") + .add("downloads.netty.io", "Netty-Downloads") + .add("*.netty.io", "Netty") + .build() + .map("downloads.netty.io")); + } + + @Test + public void testToString() { + Mapping mapping = new DomainWildcardMappingBuilder("NotFound") + .add("*.netty.io", "Netty") + .add("downloads.netty.io", "Netty-Download") + .build(); + + assertEquals( + "ImmutableDomainWildcardMapping(default: NotFound, map: " + + "{*.netty.io=Netty, downloads.netty.io=Netty-Download})", + mapping.toString()); + } +}