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:
parent
7309f2a13d
commit
4b235a9600
@ -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> {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user