com.chiorichan.http.ssl.SniNegotiator.java Source code

Java tutorial

Introduction

Here is the source code for com.chiorichan.http.ssl.SniNegotiator.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2016 Chiori Greene a.k.a. Chiori-chan <me@chiorichan.com>
 * All Right Reserved.
 */
package com.chiorichan.http.ssl;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.util.CharsetUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.IDN;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

import javax.net.ssl.SSLEngine;

import org.apache.commons.io.FileUtils;

import com.chiorichan.AppConfig;
import com.chiorichan.lang.StartupException;
import com.chiorichan.net.NetworkManager;
import com.chiorichan.util.FileFunc;
import com.google.common.collect.Lists;

/**
 * <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.
 *
 * Original code from io.netty.handler.ssl.SniHandler, modified for Chiori-chan's Web Server use case.
 * </p>
 */
public class SniNegotiator extends ByteToMessageDecoder {
    /**
     * Constants for SSL packets.
     */
    final class SslConstants {
        /**
         * change cipher spec
         */
        public static final int SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC = 20;

        /**
         * alert
         */
        public static final int SSL_CONTENT_TYPE_ALERT = 21;

        /**
         * handshake
         */
        public static final int SSL_CONTENT_TYPE_HANDSHAKE = 22;

        /**
         * application data
         */
        public static final int SSL_CONTENT_TYPE_APPLICATION_DATA = 23;

        private SslConstants() {
        }
    }

    private static final InternalLogger logger = InternalLoggerFactory.getInstance(SniHandler.class);

    static List<String> enabledCipherSuites = Lists.newCopyOnWriteArrayList();

    static {
        try {
            File cipherSuitesFile = new File(AppConfig.get().getDirectory().getAbsolutePath(),
                    "EnabledCipherSuites.txt");

            if (!cipherSuitesFile.exists())
                FileFunc.putResource("com/chiorichan/EnabledCipherSuites.txt", cipherSuitesFile);

            List<String> contents = FileUtils.readLines(cipherSuitesFile);
            boolean resave = false;

            for (String line : contents) {
                if (line.startsWith("#") || line.length() == 0)
                    continue;

                if (!enabledCipherSuites.contains(line))
                    enabledCipherSuites.add(line);
                else
                    resave = true;
            }

            if (resave) {
                StringBuilder sb = new StringBuilder();
                sb.append("\n# Chiori-chan's Web Server Enabled SSL/TLS Cipher Suites");
                sb.append("\n# Cipher Suites are in order of priority");

                for (String line : enabledCipherSuites)
                    sb.append("\n" + line);

                sb.append("\n");

                FileUtils.writeStringToFile(cipherSuitesFile, sb.toString());
            }
        } catch (IOException e) {
            SslManager.getLogger().severe("Could not load the EnabledCipherSuites file", e);
        }

        if (enabledCipherSuites.size() == 0)
            throw new StartupException(
                    "There were no cipher suites enabled, please check your EnabledCipherSuites file and/or consider adding additional ciphers.");
    }

    public static List<String> enabledCipherSuites() {
        return enabledCipherSuites;
    }

    private boolean handshaken = false;
    private volatile String hostname;
    private volatile SslContext selectedContext;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (!handshaken && in.readableBytes() >= 5) {
            String hostname = sniHostNameFromHandshakeInfo(in);
            if (hostname != null)
                hostname = IDN.toASCII(hostname, IDN.ALLOW_UNASSIGNED).toLowerCase(Locale.US);
            this.hostname = hostname;

            selectedContext = SslManager.instance().map(hostname);

            if (handshaken) {
                SSLEngine engine = selectedContext.newEngine(ctx.alloc());

                List<String> supportedCipherSuites = Arrays.asList(engine.getSupportedCipherSuites());

                if (!supportedCipherSuites.containsAll(enabledCipherSuites))
                    for (String cipher : enabledCipherSuites)
                        if (!supportedCipherSuites.contains(cipher)) {
                            NetworkManager.getLogger()
                                    .severe(String.format(
                                            "The SSL/TLS cipher suite '%s' is not supported by SSL Provider %s",
                                            cipher, SslContext.defaultServerProvider().name()));
                            enabledCipherSuites.remove(cipher);
                        }

                engine.setUseClientMode(false);
                engine.setEnabledCipherSuites(enabledCipherSuites.toArray(new String[0]));

                ctx.pipeline().replace(this, ctx.name(), new SslExceptionHandler(engine));
            }
        }
    }

    /**
     * @return the selected hostname
     */
    public String hostname() {
        return hostname;
    }

    private String sniHostNameFromHandshakeInfo(ByteBuf in) {
        int readerIndex = in.readerIndex();
        try {
            int command = in.getUnsignedByte(readerIndex);

            // tls, but not handshake command
            switch (command) {
            case SslConstants.SSL_CONTENT_TYPE_CHANGE_CIPHER_SPEC:
            case SslConstants.SSL_CONTENT_TYPE_ALERT:
            case SslConstants.SSL_CONTENT_TYPE_APPLICATION_DATA:
                return null;
            case SslConstants.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;
        }
    }

    /**
     * @return the selected sslcontext
     */
    public SslContext sslContext() {
        return selectedContext;
    }
}