/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF 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 org.apache.zookeeper.server.admin;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSession;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.server.AbstractConnectionFactory;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The purpose of this class is to dynamically determine whether to create
 * a plaintext or SSL connection whenever newConnection() is called. It works
 * in conjunction with ReadAheadEndpoint to inspect bytes on the incoming
 * connection.
 */
public class UnifiedConnectionFactory extends AbstractConnectionFactory {

    private static final Logger LOG = LoggerFactory.getLogger(UnifiedConnectionFactory.class);

    private final SslContextFactory sslContextFactory;
    private final String nextProtocol;

    public UnifiedConnectionFactory(String nextProtocol) {
        this(null, nextProtocol);
    }

    public UnifiedConnectionFactory(SslContextFactory factory, String nextProtocol) {
        super("SSL");
        this.sslContextFactory = (factory == null) ? new SslContextFactory.Server() : factory;
        this.nextProtocol = nextProtocol;
        this.addBean(this.sslContextFactory);
    }

    @Override
    protected void doStart() throws Exception {
        super.doStart();
        SSLEngine engine = this.sslContextFactory.newSSLEngine();
        SSLSession session = engine.getSession();
        engine.setUseClientMode(false);
        if (session.getPacketBufferSize() > this.getInputBufferSize()) {
            this.setInputBufferSize(session.getPacketBufferSize());
        }
    }

    @Override
    public Connection newConnection(Connector connector, EndPoint realEndPoint) {
        ReadAheadEndpoint aheadEndpoint = new ReadAheadEndpoint(realEndPoint, 1);
        byte[] bytes = aheadEndpoint.getBytes();
        boolean isSSL;

        if (bytes == null || bytes.length == 0) {
            isSSL = false;
            LOG.warn("Incoming connection has no data");
        } else {
            byte b = bytes[0]; // TLS first byte is 0x16, let's not support SSLv3 and below
            isSSL = b == 0x16; // matches SSL detection in NettyServerCnxnFactory.java
        }

        LOG.debug(String.format("UnifiedConnectionFactory: newConnection() with SSL = %b", isSSL));

        EndPoint plainEndpoint;
        SslConnection sslConnection;

        if (isSSL) {
            SSLEngine engine = this.sslContextFactory.newSSLEngine(aheadEndpoint.getRemoteAddress());
            engine.setUseClientMode(false);
            sslConnection = this.newSslConnection(connector, aheadEndpoint, engine);
            sslConnection.setRenegotiationAllowed(this.sslContextFactory.isRenegotiationAllowed());
            this.configure(sslConnection, connector, aheadEndpoint);
            plainEndpoint = sslConnection.getDecryptedEndPoint();
        } else {
            sslConnection = null;
            plainEndpoint = aheadEndpoint;
        }

        ConnectionFactory next = connector.getConnectionFactory(nextProtocol);
        Connection connection = next.newConnection(connector, plainEndpoint);
        plainEndpoint.setConnection(connection);

        return (sslConnection == null) ? connection : sslConnection;
    }

    protected SslConnection newSslConnection(
        final Connector connector,
        final EndPoint endPoint,
        final SSLEngine engine) {
        return new SslConnection(connector.getByteBufferPool(), connector.getExecutor(), endPoint, engine);
    }

    @Override
    public String toString() {
        return String.format(
            "%s@%x{%s->%s}",
            this.getClass().getSimpleName(),
            this.hashCode(),
            this.getProtocol(),
            this.nextProtocol);
    }

}
