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 7d8873a490
commit 0f8ccbb1ac
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; 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> { public interface Mapping<IN, OUT> {
/** /**
* Returns mapped value of input. * Returns mapped value of the specified input.
*/ */
OUT map(IN 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.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import io.netty.util.DomainNameMapping;
import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory; import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.IDN;
import java.util.List; import java.util.List;
import java.util.Locale;
/** /**
* <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI * <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 = private static final InternalLogger logger =
InternalLoggerFactory.getInstance(SniHandler.class); InternalLoggerFactory.getInstance(SniHandler.class);
private final DomainNameMapping mapping; private final DomainNameMapping<SslContext> mapping;
private String hostname;
private boolean handshaken; private boolean handshaken;
private SslContext defaultContext; private volatile String hostname;
private SslContext selectedContext; private volatile SslContext selectedContext;
/** /**
* Create a SNI detection handler with configured {@link SslContext} * 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} * @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) { if (mapping == null) {
throw new NullPointerException("mapping"); throw new NullPointerException("mapping");
} }
this.mapping = mapping; this.mapping = (DomainNameMapping<SslContext>) mapping;
handshaken = false; handshaken = false;
} }
@ -76,18 +80,13 @@ public class SniHandler extends ByteToMessageDecoder {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (!handshaken && in.readableBytes() >= 5) { if (!handshaken && in.readableBytes() >= 5) {
String hostname = sniHostNameFromHandshakeInfo(in); String hostname = sniHostNameFromHandshakeInfo(in);
if (hostname != null) { if (hostname != null) {
if (logger.isDebugEnabled()) { hostname = IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US);
logger.debug("Using hostname: {}", hostname);
}
// toASCII conversion and case normalization
this.hostname = DomainNameMapping.normalizeHostname(hostname);
} }
this.hostname = hostname;
// the mapping will return default context when this.hostname is null // the mapping will return default context when this.hostname is null
selectedContext = mapping.map(this.hostname); selectedContext = mapping.map(hostname);
} }
if (handshaken) { if (handshaken) {

View File

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