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());
+ }
+}