Overall clean-up of the initial SniHandler/DomainNameMapping work

- Parameterize DomainNameMapping to make it useful for other use cases
  than just mapping to SslContext
- Move DomainNameMapping to io.netty.util
- Clean-up the API documentation
- Make SniHandler.hostname and sslContext volatile because they can be
  accessed by non-I/O threads
This commit is contained in:
Trustin Lee 2014-12-04 18:19:50 +09:00
parent 078072632a
commit 0ee7e61734
5 changed files with 171 additions and 157 deletions

View File

@ -0,0 +1,144 @@
/*
* Copyright 2014 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 io.netty.util.internal.StringUtil;
import java.net.IDN;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Maps a domain name to its associated value object.
* <p>
* 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>
*/
public class DomainNameMapping<V> implements Mapping<String, V> {
private static final Pattern DNS_WILDCARD_PATTERN = Pattern.compile("^\\*\\..*");
private final Map<String, V> map;
private final V defaultValue;
/**
* Creates a default, order-sensitive mapping. If your hostnames are in conflict, the mapping
* will choose the one you add first.
*
* @param defaultValue the default value for {@link #map(String)} to return when nothing matches the input
*/
public DomainNameMapping(V defaultValue) {
this(4, defaultValue);
}
/**
* Creates a default, order-sensitive mapping. If your hostnames are in conflict, the mapping
* will choose the one you add first.
*
* @param initialCapacity initial capacity for the internal map
* @param defaultValue the default value for {@link #map(String)} to return when nothing matches the input
*/
public DomainNameMapping(int initialCapacity, V defaultValue) {
if (defaultValue == null) {
throw new NullPointerException("defaultValue");
}
map = new LinkedHashMap<String, V>(initialCapacity);
this.defaultValue = defaultValue;
}
/**
* Adds a mapping that maps the specified (optionally wildcard) host name to the specified output value.
* <p>
* <a href="http://en.wikipedia.org/wiki/Wildcard_DNS_record">DNS wildcard</a> is supported as hostname.
* For example, you can use {@code *.netty.io} to match {@code netty.io} and {@code downloads.netty.io}.
* </p>
*
* @param hostname the host name (optionally wildcard)
* @param output the output value that will be returned by {@link #map(String)} when the specified host name
* matches the specified input host name
*/
public DomainNameMapping<V> add(String hostname, V output) {
if (hostname == null) {
throw new NullPointerException("input");
}
if (output == null) {
throw new NullPointerException("output");
}
map.put(normalizeHostname(hostname), output);
return this;
}
/**
* Simple function to match <a href="http://en.wikipedia.org/wiki/Wildcard_DNS_record">DNS wildcard</a>.
*/
private static boolean matches(String hostNameTemplate, String hostName) {
// note that inputs are converted and lowercased already
if (DNS_WILDCARD_PATTERN.matcher(hostNameTemplate).matches()) {
return hostNameTemplate.substring(2).equals(hostName) ||
hostName.endsWith(hostNameTemplate.substring(1));
} else {
return hostNameTemplate.equals(hostName);
}
}
/**
* IDNA ASCII conversion and case normalization
*/
private static String normalizeHostname(String hostname) {
if (needsNormalization(hostname)) {
hostname = IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED);
}
return hostname.toLowerCase(Locale.US);
}
private static boolean needsNormalization(String hostname) {
final int length = hostname.length();
for (int i = 0; i < length; i ++) {
int c = hostname.charAt(i);
if (c > 0x7F) {
return true;
}
}
return false;
}
@Override
public V map(String input) {
if (input != null) {
input = normalizeHostname(input);
for (Map.Entry<String, V> entry : map.entrySet()) {
if (matches(entry.getKey(), input)) {
return entry.getValue();
}
}
}
return defaultValue;
}
public String toString() {
return StringUtil.simpleClassName(this) + "(default: " + defaultValue + ", map: " + map + ')';
}
}

View File

@ -16,12 +16,12 @@
package io.netty.util;
/**
* An mapping which maintains a relationship from type of IN to type of OUT.
* Maintains the mapping from the objects of one type to the objects of the other type.
*/
public interface Mapping<IN, OUT> {
/**
* Returns mapped value of input.
* Returns mapped value of the specified input.
*/
OUT map(IN input);
}

View File

@ -1,126 +0,0 @@
/*
* Copyright 2014 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.handler.ssl;
import io.netty.util.Mapping;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.IDN;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
* <p>This class maps a domain name to a configured {@link SslContext}.</p>
*
* <p>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>
*/
public class DomainNameMapping implements Mapping<String, SslContext> {
private static final InternalLogger logger =
InternalLoggerFactory.getInstance(DomainNameMapping.class);
private static final Pattern DNS_WILDCARD_PATTERN = Pattern.compile("^\\*\\..*");
private final Map<String, SslContext> userProvidedContexts;
private final SslContext defaultContext;
/**
* Create a default, order-sensitive mapping. If your hostnames are in conflict, the mapping
* will choose the one you add first.
*
* @param defaultContext default {@link SslContext} when the nothing matches input.
*/
public DomainNameMapping(SslContext defaultContext) {
this(4, defaultContext);
}
/**
* Create a default, order-sensitive mapping. If your hostnames are in conflict, the mapping
* will choose the one you add first.
*
* @param initialCapacity initial capacity for internal map
* @param defaultContext default {@link SslContext} when the handler fails to detect SNI extension
*/
public DomainNameMapping(int initialCapacity, SslContext defaultContext) {
if (defaultContext == null) {
throw new NullPointerException("defaultContext");
}
userProvidedContexts = new LinkedHashMap<String, SslContext>(initialCapacity);
this.defaultContext = defaultContext;
}
/**
* Add a {@link SslContext} to the handler.
*
* <a href="http://en.wikipedia.org/wiki/Wildcard_DNS_record">DNS wildcard</a> is supported as hostname.
* For example, you can use {@code *.netty.io} to match {@code netty.io} and {@code downloads.netty.io}.
*
* @param hostname hostname for the certificate.
* @param context the {@link SslContext}
*/
public DomainNameMapping addContext(String hostname, SslContext context) {
if (hostname == null) {
throw new NullPointerException("hostname");
}
if (context == null) {
throw new NullPointerException("context");
}
userProvidedContexts.put(normalizeHostname(hostname), context);
return this;
}
/**
* <p>Simple function to match <a href="http://en.wikipedia.org/wiki/Wildcard_DNS_record">DNS wildcard</a>.
* </p>
*/
private static boolean matches(String hostNameTemplate, String hostName) {
// note that inputs are converted and lowercased already
if (DNS_WILDCARD_PATTERN.matcher(hostNameTemplate).matches()) {
return hostNameTemplate.substring(2).equals(hostName) ||
hostName.endsWith(hostNameTemplate.substring(1));
} else {
return hostNameTemplate.equals(hostName);
}
}
/**
* IDNA ASCII conversion and case normalization
*/
static String normalizeHostname(String hostname) {
return IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED).toLowerCase();
}
@Override
public SslContext map(String hostname) {
if (hostname != null) {
for (Map.Entry<String, SslContext> entry : userProvidedContexts.entrySet()) {
if (matches(entry.getKey(), hostname)) {
return entry.getValue();
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using default SslContext");
}
return defaultContext;
}
}

View File

@ -20,10 +20,13 @@ import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.CharsetUtil;
import io.netty.util.DomainNameMapping;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.IDN;
import java.util.List;
import java.util.Locale;
/**
* <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
@ -37,11 +40,11 @@ public class SniHandler extends ByteToMessageDecoder {
private static final InternalLogger logger =
InternalLoggerFactory.getInstance(SniHandler.class);
private final DomainNameMapping mapping;
private String hostname;
private final DomainNameMapping<SslContext> mapping;
private boolean handshaken;
private SslContext defaultContext;
private SslContext selectedContext;
private volatile String hostname;
private volatile SslContext selectedContext;
/**
* Create a SNI detection handler with configured {@link SslContext}
@ -49,12 +52,13 @@ public class SniHandler extends ByteToMessageDecoder {
*
* @param mapping the mapping of domain name to {@link SslContext}
*/
public SniHandler(DomainNameMapping mapping) {
@SuppressWarnings("unchecked")
public SniHandler(DomainNameMapping<? extends SslContext> mapping) {
if (mapping == null) {
throw new NullPointerException("mapping");
}
this.mapping = mapping;
this.mapping = (DomainNameMapping<SslContext>) mapping;
handshaken = false;
}
@ -76,18 +80,13 @@ public class SniHandler extends ByteToMessageDecoder {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (!handshaken && in.readableBytes() >= 5) {
String hostname = sniHostNameFromHandshakeInfo(in);
if (hostname != null) {
if (logger.isDebugEnabled()) {
logger.debug("Using hostname: {}", hostname);
}
// toASCII conversion and case normalization
this.hostname = DomainNameMapping.normalizeHostname(hostname);
hostname = IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US);
}
this.hostname = hostname;
// the mapping will return default context when this.hostname is null
selectedContext = mapping.map(this.hostname);
selectedContext = mapping.map(hostname);
}
if (handshaken) {

View File

@ -19,16 +19,14 @@ package io.netty.handler.ssl;
import io.netty.buffer.Unpooled;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.util.DomainNameMapping;
import org.junit.Test;
import javax.xml.bind.DatatypeConverter;
import java.io.File;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
public class SniHandlerTest {
@ -45,15 +43,15 @@ public class SniHandlerTest {
SslContext leanContext = makeSslContext();
SslContext leanContext2 = makeSslContext();
DomainNameMapping mapping = new DomainNameMapping(nettyContext);
mapping.addContext("*.netty.io", nettyContext);
DomainNameMapping<SslContext> mapping = new DomainNameMapping<SslContext>(nettyContext);
mapping.add("*.netty.io", nettyContext);
// input with custom cases
mapping.addContext("*.LEANCLOUD.CN", leanContext);
mapping.add("*.LEANCLOUD.CN", leanContext);
// a hostname conflict with previous one, since we are using order-sensitive config, the engine won't
// be used with the handler.
mapping.addContext("chat4.leancloud.cn", leanContext2);
mapping.add("chat4.leancloud.cn", leanContext2);
SniHandler handler = new SniHandler(mapping);
EmbeddedChannel ch = new EmbeddedChannel(handler);
@ -89,21 +87,21 @@ public class SniHandlerTest {
SslContext leanContext = makeSslContext();
SslContext leanContext2 = makeSslContext();
DomainNameMapping mapping = new DomainNameMapping(nettyContext);
mapping.addContext("*.netty.io", nettyContext);
DomainNameMapping<SslContext> mapping = new DomainNameMapping<SslContext>(nettyContext);
mapping.add("*.netty.io", nettyContext);
// input with custom cases
mapping.addContext("*.LEANCLOUD.CN", leanContext);
mapping.add("*.LEANCLOUD.CN", leanContext);
// a hostname conflict with previous one, since we are using order-sensitive config, the engine won't
// be used with the handler.
mapping.addContext("chat4.leancloud.cn", leanContext2);
mapping.add("chat4.leancloud.cn", leanContext2);
SniHandler handler = new SniHandler(mapping);
EmbeddedChannel ch = new EmbeddedChannel(handler);
// invalid
byte[] message = new byte[] {22, 3, 1, 0, 0};
byte[] message = { 22, 3, 1, 0, 0 };
try {
// Push the handshake message.
@ -116,5 +114,4 @@ public class SniHandlerTest {
assertThat(handler.hostname(), nullValue());
assertThat(handler.sslContext(), is(nettyContext));
}
}