package divconq.web.http;
import io.netty.buffer.ByteBuf;
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.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import java.net.IDN;
import java.util.List;
import java.util.Locale;
import divconq.net.ssl.SslHandler;
import divconq.web.WebSiteManager;
/**
* <p>Enables <a href="https://tools.ietf.org/html/rfc3546#section-3.1">SNI
* (Server Name Indication)</a> 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. </p>
*/
public class SniHandler extends ByteToMessageDecoder {
public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20;
public static final int SSL_CONTENT_TYPE_ALERT = 21;
public static final int SSL_CONTENT_TYPE_HANDSHAKE = 22;
public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = 23;
protected static final InternalLogger logger = InternalLoggerFactory.getInstance(SniHandler.class);
protected boolean handshaken = false;
protected SslContextFactory selectedContext = null;
protected String hostname = null;
protected WebSiteManager man = null;
public SniHandler(WebSiteManager man) {
this.man = man;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (!handshaken && in.readableBytes() >= 5) {
this.hostname = sniHostNameFromHandshakeInfo(in);
if (this.hostname != null)
this.hostname = IDN.toASCII(this.hostname, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US);
else
this.hostname = "localhost";
// the mapping will return default context when this.hostname is null
this.selectedContext = this.man.findSslContextFactory(this.hostname);
}
if (handshaken) {
SslHandler sslHandler = new SslHandler(this.selectedContext.getServerEngine(this.hostname));
ctx.pipeline().replace("ssl", "ssl", sslHandler);
}
}
private String sniHostNameFromHandshakeInfo(ByteBuf in) {
int readerIndex = in.readerIndex();
try {
int command = in.getUnsignedByte(readerIndex);
// tls, but not handshake command
switch (command) {
case SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
case SSL_CONTENT_TYPE_ALERT:
case SSL_CONTENT_TYPE_APPLICATION_DATA:
return null;
case SSL_CONTENT_TYPE_HANDSHAKE:
break;
default:
//not tls or sslv3, do not try sni
handshaken = true;
return null;
}
int majorVersion = in.getUnsignedByte(readerIndex + 1);
// SSLv3 or TLS
if (majorVersion == 3) {
int packetLength = in.getUnsignedShort(readerIndex + 3) + 5;
if (in.readableBytes() >= packetLength) {
// decode the ssl client hello packet
// we have to skip some var-length fields
int offset = readerIndex + 43;
int sessionIdLength = in.getUnsignedByte(offset);
offset += sessionIdLength + 1;
int cipherSuitesLength = in.getUnsignedShort(offset);
offset += cipherSuitesLength + 2;
int compressionMethodLength = in.getUnsignedByte(offset);
offset += compressionMethodLength + 1;
int extensionsLength = in.getUnsignedShort(offset);
offset += 2;
int extensionsLimit = offset + extensionsLength;
while (offset < extensionsLimit) {
int extensionType = in.getUnsignedShort(offset);
offset += 2;
int extensionLength = in.getUnsignedShort(offset);
offset += 2;
// SNI
if (extensionType == 0) {
handshaken = true;
int serverNameType = in.getUnsignedByte(offset + 2);
if (serverNameType == 0) {
int serverNameLength = in.getUnsignedShort(offset + 3);
return in.toString(offset + 5, serverNameLength,
CharsetUtil.UTF_8);
} else {
// invalid enum value
return null;
}
}
offset += extensionLength;
}
handshaken = true;
return null;
} else {
// client hello incomplete
return null;
}
} else {
handshaken = true;
return null;
}
} catch (Throwable e) {
// unexpected encoding, ignore sni and use default
if (logger.isDebugEnabled()) {
logger.debug("Unexpected client hello packet: " + ByteBufUtil.hexDump(in), e);
}
handshaken = true;
return null;
}
}
}