Add SniCompletionEvent which allows to easily retrieve the hostname that was used to select the SslContext.

Motivation:

At the moment its a bit "hacky" to retrieve the hostname that was used during SNI as you need to hold a reference to SniHandler and then call hostname() once the selection is done. It would be better to fire an event to let the user know we did the selection.

Modifications:

Add a SniCompletionEvent that can be used to get the hostname that was used to do the selection and was included in the SNI extension.

Result:

Easier usage of SNI.
This commit is contained in:
Norman Maurer 2017-12-01 11:26:01 +01:00
parent ca1e1fcddf
commit aabb73a9d2
4 changed files with 87 additions and 7 deletions

View File

@ -80,7 +80,7 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme
NotSslRecordException e = new NotSslRecordException(
"not an SSL/TLS record: " + ByteBufUtil.hexDump(in));
in.skipBytes(in.readableBytes());
ctx.fireUserEventTriggered(new SniCompletionEvent(e));
SslUtils.notifyHandshakeFailure(ctx, e, true);
throw e;
}
@ -222,6 +222,7 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme
private void select(final ChannelHandlerContext ctx, final String hostname) throws Exception {
Future<T> future = lookup(ctx, hostname);
if (future.isDone()) {
fireSniCompletionEvent(ctx, hostname, future);
onLookupComplete(ctx, hostname, future);
} else {
suppressRead = true;
@ -231,6 +232,7 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme
try {
suppressRead = false;
try {
fireSniCompletionEvent(ctx, hostname, future);
onLookupComplete(ctx, hostname, future);
} catch (DecoderException err) {
ctx.fireExceptionCaught(err);
@ -250,6 +252,15 @@ public abstract class AbstractSniHandler<T> extends ByteToMessageDecoder impleme
}
}
private void fireSniCompletionEvent(ChannelHandlerContext ctx, String hostname, Future<T> future) {
Throwable cause = future.cause();
if (cause == null) {
ctx.fireUserEventTriggered(new SniCompletionEvent(hostname));
} else {
ctx.fireUserEventTriggered(new SniCompletionEvent(hostname, cause));
}
}
/**
* Kicks off a lookup for the given SNI value and returns a {@link Future} which in turn will
* notify the {@link #onLookupComplete(ChannelHandlerContext, String, Future)} on completion.

View File

@ -0,0 +1,54 @@
/*
* Copyright 2017 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.internal.UnstableApi;
/**
* Event that is fired once we did a selection of a {@link SslContext} based on the {@code SNI hostname},
* which may be because it was successful or there was an error.
*/
@UnstableApi
public final class SniCompletionEvent extends SslCompletionEvent {
private final String hostname;
SniCompletionEvent(String hostname) {
this.hostname = hostname;
}
SniCompletionEvent(String hostname, Throwable cause) {
super(cause);
this.hostname = hostname;
}
SniCompletionEvent(Throwable cause) {
this(null, cause);
}
/**
* Returns the SNI hostname send by the client if we were able to parse it, {@code null} otherwise.
*/
public String hostname() {
return hostname;
}
@Override
public String toString() {
final Throwable cause = cause();
return cause == null ? getClass().getSimpleName() + "(SUCCESS='" + hostname + "'\")":
getClass().getSimpleName() + '(' + cause + ')';
}
}

View File

@ -45,7 +45,7 @@ public abstract class SslCompletionEvent {
}
@Override
public final String toString() {
public String toString() {
final Throwable cause = cause();
return cause == null? getClass().getSimpleName() + "(SUCCESS)" :
getClass().getSimpleName() + '(' + cause + ')';

View File

@ -24,6 +24,7 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.DefaultEventLoopGroup;
@ -56,15 +57,13 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLEngine;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
@ -157,8 +156,18 @@ public class SniHandlerTest {
.add("chat4.leancloud.cn", leanContext2)
.build();
final AtomicReference<SniCompletionEvent> evtRef = new AtomicReference<SniCompletionEvent>();
SniHandler handler = new SniHandler(mapping);
EmbeddedChannel ch = new EmbeddedChannel(handler);
EmbeddedChannel ch = new EmbeddedChannel(handler, new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof SniCompletionEvent) {
assertTrue(evtRef.compareAndSet(null, (SniCompletionEvent) evt));
} else {
ctx.fireUserEventTriggered(evt);
}
}
});
try {
// hex dump of a client hello packet, which contains hostname "CHAT4.LEANCLOUD.CN"
@ -180,6 +189,12 @@ public class SniHandlerTest {
assertThat(handler.hostname(), is("chat4.leancloud.cn"));
assertThat(handler.sslContext(), is(leanContext));
SniCompletionEvent evt = evtRef.get();
assertNotNull(evt);
assertEquals("chat4.leancloud.cn", evt.hostname());
assertTrue(evt.isSuccess());
assertNull(evt.cause());
} finally {
ch.finishAndReleaseAll();
}