io.netty.handler.ssl.SniHandler Maven / Gradle / Ivy
/*
* 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.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderException;
import io.netty.util.AsyncMapping;
import io.netty.util.DomainNameMapping;
import io.netty.util.Mapping;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
/**
* Enables SNI
* (Server Name Indication) extension for server side SSL. For clients
* support SNI, the server could have multiple host name bound on a single IP.
* The client will send host name in the handshake data so server could decide
* which certificate to choose for the host name.
*/
public class SniHandler extends AbstractSniHandler {
private static final Selection EMPTY_SELECTION = new Selection(null, null);
protected final AsyncMapping mapping;
private volatile Selection selection = EMPTY_SELECTION;
/**
* Creates a SNI detection handler with configured {@link SslContext}
* maintained by {@link Mapping}
*
* @param mapping the mapping of domain name to {@link SslContext}
*/
public SniHandler(Mapping super String, ? extends SslContext> mapping) {
this(new AsyncMappingAdapter(mapping));
}
/**
* Creates a SNI detection handler with configured {@link SslContext}
* maintained by {@link DomainNameMapping}
*
* @param mapping the mapping of domain name to {@link SslContext}
*/
public SniHandler(DomainNameMapping extends SslContext> mapping) {
this((Mapping) mapping);
}
/**
* Creates a SNI detection handler with configured {@link SslContext}
* maintained by {@link AsyncMapping}
*
* @param mapping the mapping of domain name to {@link SslContext}
*/
@SuppressWarnings("unchecked")
public SniHandler(AsyncMapping super String, ? extends SslContext> mapping) {
this.mapping = (AsyncMapping) ObjectUtil.checkNotNull(mapping, "mapping");
}
/**
* @return the selected hostname
*/
public String hostname() {
return selection.hostname;
}
/**
* @return the selected {@link SslContext}
*/
public SslContext sslContext() {
return selection.context;
}
/**
* The default implementation will simply call {@link AsyncMapping#map(Object, Promise)} but
* users can override this method to implement custom behavior.
*
* @see AsyncMapping#map(Object, Promise)
*/
@Override
protected Future lookup(ChannelHandlerContext ctx, String hostname) throws Exception {
return mapping.map(hostname, ctx.executor().newPromise());
}
@Override
protected final void onLookupComplete(ChannelHandlerContext ctx,
String hostname, Future future) throws Exception {
if (!future.isSuccess()) {
final Throwable cause = future.cause();
if (cause instanceof Error) {
throw (Error) cause;
}
throw new DecoderException("failed to get the SslContext for " + hostname, cause);
}
SslContext sslContext = future.getNow();
selection = new Selection(sslContext, hostname);
try {
replaceHandler(ctx, hostname, sslContext);
} catch (Throwable cause) {
selection = EMPTY_SELECTION;
PlatformDependent.throwException(cause);
}
}
/**
* The default implementation of this method will simply replace {@code this} {@link SniHandler}
* instance with a {@link SslHandler}. Users may override this method to implement custom behavior.
*
* Please be aware that this method may get called after a client has already disconnected and
* custom implementations must take it into consideration when overriding this method.
*
* It's also possible for the hostname argument to be {@code null}.
*/
protected void replaceHandler(ChannelHandlerContext ctx, String hostname, SslContext sslContext) throws Exception {
SslHandler sslHandler = null;
try {
sslHandler = sslContext.newHandler(ctx.alloc());
ctx.pipeline().replace(this, SslHandler.class.getName(), sslHandler);
sslHandler = null;
} finally {
// Since the SslHandler was not inserted into the pipeline the ownership of the SSLEngine was not
// transferred to the SslHandler.
// See https://github.com/netty/netty/issues/5678
if (sslHandler != null) {
ReferenceCountUtil.safeRelease(sslHandler.engine());
}
}
}
private static final class AsyncMappingAdapter implements AsyncMapping {
private final Mapping super String, ? extends SslContext> mapping;
private AsyncMappingAdapter(Mapping super String, ? extends SslContext> mapping) {
this.mapping = ObjectUtil.checkNotNull(mapping, "mapping");
}
@Override
public Future map(String input, Promise promise) {
final SslContext context;
try {
context = mapping.map(input);
} catch (Throwable cause) {
return promise.setFailure(cause);
}
return promise.setSuccess(context);
}
}
private static final class Selection {
final SslContext context;
final String hostname;
Selection(SslContext context, String hostname) {
this.context = context;
this.hostname = hostname;
}
}
}