Introduce DomainWildcardMappingBuilder to fix wildcard matching accor… (#10132)

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
This commit is contained in:
Norman Maurer 2020-03-31 16:57:42 +02:00 committed by GitHub
parent 7309f2a13d
commit 4b235a9600
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 270 additions and 1 deletions

View File

@ -20,7 +20,7 @@ package io.netty.util;
* Builder for immutable {@link DomainNameMapping} instances.
*
* @param <V> concrete type of value objects
* @deprecated Use {@link DomainNameMappingBuilder} instead.
* @deprecated Use {@link DomainWildcardMappingBuilder} instead.
*/
@Deprecated
public final class DomainMappingBuilder<V> {

View File

@ -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}.
* </p>
* @deprecated Use {@link DomainWildcardMappingBuilder}}
*/
@Deprecated
public class DomainNameMapping<V> implements Mapping<String, V> {
final V defaultValue;

View File

@ -27,7 +27,9 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull;
* Builder for immutable {@link DomainNameMapping} instances.
*
* @param <V> concrete type of value objects
* @deprecated Use {@link DomainWildcardMappingBuilder}
*/
@Deprecated
public final class DomainNameMappingBuilder<V> {
private final V defaultValue;

View File

@ -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
* <a href="https://tools.ietf.org/search/rfc6125#section-6.4">DNS wildcard</a> matching.
* @param <V> the type of the value that we map to.
*/
public class DomainWildcardMappingBuilder<V> {
private final V defaultValue;
private final Map<String, V> 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<String, V>(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.
* <p>
* <a href="https://tools.ietf.org/search/rfc6125#section-6.4">DNS wildcard</a> 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:
*
* <p>
* *.netty.io will match xyz.netty.io but NOT abc.xyz.netty.io
* </p>
*
* @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<V> 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<String, V> build() {
return new ImmutableDomainWildcardMapping<V>(defaultValue, map);
}
private static final class ImmutableDomainWildcardMapping<V> implements Mapping<String, V> {
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<String, V> map;
ImmutableDomainWildcardMapping(V defaultValue, Map<String, V> map) {
this.defaultValue = defaultValue;
this.map = new LinkedHashMap<String, V>(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<String, V> 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();
}
}
}

View File

@ -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<String>(null);
}
@Test(expected = NullPointerException.class)
public void testNullDomainNamePatternsAreForbidden() {
new DomainWildcardMappingBuilder<String>("NotFound").add(null, "Some value");
}
@Test(expected = NullPointerException.class)
public void testNullValuesAreForbidden() {
new DomainWildcardMappingBuilder<String>("NotFound").add("Some key", null);
}
@Test
public void testDefaultValue() {
Mapping<String, String> mapping = new DomainWildcardMappingBuilder<String>("NotFound")
.add("*.netty.io", "Netty")
.build();
assertEquals("NotFound", mapping.map("not-existing"));
}
@Test
public void testStrictEquality() {
Mapping<String, String> mapping = new DomainWildcardMappingBuilder<String>("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<String, String> mapping = new DomainWildcardMappingBuilder<String>("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<String>("NotFound")
.add("*.netty.io", "Netty")
.add("downloads.netty.io", "Netty-Downloads")
.build()
.map("downloads.netty.io"));
assertEquals("Netty-Downloads",
new DomainWildcardMappingBuilder<String>("NotFound")
.add("downloads.netty.io", "Netty-Downloads")
.add("*.netty.io", "Netty")
.build()
.map("downloads.netty.io"));
}
@Test
public void testToString() {
Mapping<String, String> mapping = new DomainWildcardMappingBuilder<String>("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());
}
}