/*
 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.security.ssl;

import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSocket;
import sun.security.ssl.SupportedGroupsExtension.NamedGroup;

/**
 * SSL/TLS transportation context.
 */
class TransportContext implements ConnectionContext {
    final SSLTransport              transport;

    // registered plaintext consumers
    final Map<Byte, SSLConsumer>    consumers;
    final AccessControlContext      acc;

    final SSLContextImpl            sslContext;
    final SSLConfiguration          sslConfig;
    final InputRecord               inputRecord;
    final OutputRecord              outputRecord;

    // connection status
    boolean                         isUnsureMode;
    boolean                         isNegotiated = false;
    boolean                         isBroken = false;
    boolean                         isInputCloseNotified = false;
    boolean                         peerUserCanceled = false;
    Exception                       closeReason = null;
    Exception                       delegatedThrown = null;

    // negotiated security parameters
    SSLSessionImpl                  conSession;
    ProtocolVersion                 protocolVersion;
    String                          applicationProtocol= null;

    // handshake context
    HandshakeContext                handshakeContext = null;

    // connection reserved status for handshake.
    boolean                         secureRenegotiation = false;
    byte[]                          clientVerifyData;
    byte[]                          serverVerifyData;

    // connection sensitive configuration
    List<NamedGroup>                serverRequestedNamedGroups;

    CipherSuite cipherSuite;
    private static final byte[] emptyByteArray = new byte[0];

    // Please never use the transport parameter other than storing a
    // reference to this object.
    //
    // Called by SSLEngineImpl
    TransportContext(SSLContextImpl sslContext, SSLTransport transport,
            InputRecord inputRecord, OutputRecord outputRecord) {
        this(sslContext, transport, new SSLConfiguration(sslContext, true),
                inputRecord, outputRecord, true);
    }

    // Please never use the transport parameter other than storing a
    // reference to this object.
    //
    // Called by SSLSocketImpl
    TransportContext(SSLContextImpl sslContext, SSLTransport transport,
            InputRecord inputRecord, OutputRecord outputRecord,
            boolean isClientMode) {
        this(sslContext, transport,
                new SSLConfiguration(sslContext, isClientMode),
                inputRecord, outputRecord, false);
    }

    // Please never use the transport parameter other than storing a
    // reference to this object.
    //
    // Called by SSLSocketImpl with an existing SSLConfig
    TransportContext(SSLContextImpl sslContext, SSLTransport transport,
            SSLConfiguration sslConfig,
            InputRecord inputRecord, OutputRecord outputRecord) {
        this(sslContext, transport, (SSLConfiguration)sslConfig.clone(),
                inputRecord, outputRecord, false);
    }

    private TransportContext(SSLContextImpl sslContext, SSLTransport transport,
            SSLConfiguration sslConfig, InputRecord inputRecord,
            OutputRecord outputRecord, boolean isUnsureMode) {
        this.transport = transport;
        this.sslContext = sslContext;
        this.inputRecord = inputRecord;
        this.outputRecord = outputRecord;
        this.sslConfig = sslConfig;
        if (this.sslConfig.maximumPacketSize == 0) {
            this.sslConfig.maximumPacketSize = outputRecord.getMaxPacketSize();
        }
        this.isUnsureMode = isUnsureMode;

        // initial security parameters
        this.conSession = new SSLSessionImpl();
        this.protocolVersion = this.sslConfig.maximumProtocolVersion;
        this.clientVerifyData = emptyByteArray;
        this.serverVerifyData = emptyByteArray;

        this.acc = AccessController.getContext();
        this.consumers = new HashMap<>();
    }

    // Dispatch plaintext to a specific consumer.
    void dispatch(Plaintext plaintext) throws IOException {
        if (plaintext == null) {
            return;
        }

        ContentType ct = ContentType.valueOf(plaintext.contentType);
        if (ct == null) {
            throw fatal(Alert.UNEXPECTED_MESSAGE,
                "Unknown content type: " + plaintext.contentType);
        }

        switch (ct) {
            case HANDSHAKE:
                byte type = HandshakeContext.getHandshakeType(this,
                        plaintext);
                if (handshakeContext == null) {
                    if (type == SSLHandshake.KEY_UPDATE.id ||
                            type == SSLHandshake.NEW_SESSION_TICKET.id) {
                        if (!isNegotiated) {
                            throw fatal(Alert.UNEXPECTED_MESSAGE,
                                    "Unexpected unnegotiated post-handshake" +
                                            " message: " +
                                            SSLHandshake.nameOf(type));
                        }

                        if (!PostHandshakeContext.isConsumable(this, type)) {
                            throw fatal(Alert.UNEXPECTED_MESSAGE,
                                    "Unexpected post-handshake message: " +
                                    SSLHandshake.nameOf(type));
                        }

                        handshakeContext = new PostHandshakeContext(this);
                    } else {
                        handshakeContext = sslConfig.isClientMode ?
                                new ClientHandshakeContext(sslContext, this) :
                                new ServerHandshakeContext(sslContext, this);
                    }
                }
                handshakeContext.dispatch(type, plaintext);
                break;
            case ALERT:
                Alert.alertConsumer.consume(this, plaintext.fragment);
                break;
            default:
                SSLConsumer consumer = consumers.get(plaintext.contentType);
                if (consumer != null) {
                    consumer.consume(this, plaintext.fragment);
                } else {
                    throw fatal(Alert.UNEXPECTED_MESSAGE,
                        "Unexpected content: " + plaintext.contentType);
                }
        }
    }

    void kickstart() throws IOException {
        if (isUnsureMode) {
            throw new IllegalStateException("Client/Server mode not yet set.");
        }

        if (outputRecord.isClosed() || inputRecord.isClosed() || isBroken) {
            if (closeReason != null) {
                throw new SSLException(
                        "Cannot kickstart, the connection is broken or closed",
                        closeReason);
            } else {
                throw new SSLException(
                        "Cannot kickstart, the connection is broken or closed");
            }
        }

        // initialize the handshaker if necessary
        if (handshakeContext == null) {
            //  TLS1.3 post-handshake
            if (isNegotiated && protocolVersion.useTLS13PlusSpec()) {
                handshakeContext = new PostHandshakeContext(this);
            } else {
                handshakeContext = sslConfig.isClientMode ?
                        new ClientHandshakeContext(sslContext, this) :
                        new ServerHandshakeContext(sslContext, this);
            }
        }

        // kickstart the handshake if needed
        //
        // Need no kickstart message on server side unless the connection
        // has been established.
        if(isNegotiated || sslConfig.isClientMode) {
           handshakeContext.kickstart();
        }
    }

    boolean isPostHandshakeContext() {
        return handshakeContext != null &&
                (handshakeContext instanceof PostHandshakeContext);
    }

    // Note: close_notify is delivered as a warning alert.
    void warning(Alert alert) {
        // For initial handshaking, don't send a warning alert message to peer
        // if handshaker has not started.
        if (isNegotiated || handshakeContext != null) {
            try {
                outputRecord.encodeAlert(Alert.Level.WARNING.level, alert.id);
            } catch (IOException ioe) {
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.warning(
                        "Warning: failed to send warning alert " + alert, ioe);
                }
            }
        }
    }

    SSLException fatal(Alert alert,
            String diagnostic) throws SSLException {
        return fatal(alert, diagnostic, null);
    }

    SSLException fatal(Alert alert, Throwable cause) throws SSLException {
        return fatal(alert, null, cause);
    }

    SSLException fatal(Alert alert,
            String diagnostic, Throwable cause) throws SSLException {
        return fatal(alert, diagnostic, false, cause);
    }

    // Note: close_notify is not delivered via fatal() methods.
    SSLException fatal(Alert alert, String diagnostic,
            boolean recvFatalAlert, Throwable cause) throws SSLException {
        // If we've already shutdown because of an error, there is nothing we
        // can do except rethrow the exception.
        //
        // Most exceptions seen here will be SSLExceptions. We may find the
        // occasional Exception which hasn't been converted to a SSLException,
        // so we'll do it here.
        if (closeReason != null) {
            if (cause == null) {
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.warning(
                            "Closed transport, general or untracked problem");
                }
                throw alert.createSSLException(
                        "Closed transport, general or untracked problem");
            }

            if (cause instanceof SSLException) {
                throw (SSLException)cause;
            } else {    // unlikely, but just in case.
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.warning(
                            "Closed transport, unexpected rethrowing", cause);
                }
                throw alert.createSSLException("Unexpected rethrowing", cause);
            }
        }

        // If we have no further information, make a general-purpose
        // message for folks to see.  We generally have one or the other.
        if (diagnostic == null) {
            if (cause == null) {
                diagnostic = "General/Untracked problem";
            } else {
                diagnostic = cause.getMessage();
            }
        }

        if (cause == null) {
            cause = alert.createSSLException(diagnostic);
        }

        // shutdown the transport
        if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
            SSLLogger.severe("Fatal (" + alert + "): " + diagnostic, cause);
        }

        // remember the close reason
        if (cause instanceof SSLException) {
            closeReason = (SSLException)cause;
        } else {
            // Including RuntimeException, but we'll throw those down below.
            closeReason = alert.createSSLException(diagnostic, cause);
        }

        // close inbound
        try {
            inputRecord.close();
        } catch (IOException ioe) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.warning("Fatal: input record closure failed", ioe);
            }

            closeReason.addSuppressed(ioe);
        }

        // invalidate the session
        if (conSession != null) {
            conSession.invalidate();
        }

        if (handshakeContext != null &&
                handshakeContext.handshakeSession != null) {
            handshakeContext.handshakeSession.invalidate();
        }

        // send fatal alert
        //
        // If we haven't even started handshaking yet, or we are the recipient
        // of a fatal alert, no need to generate a fatal close alert.
        if (!recvFatalAlert && !isOutboundClosed() && !isBroken &&
                (isNegotiated || handshakeContext != null)) {
            try {
                outputRecord.encodeAlert(Alert.Level.FATAL.level, alert.id);
            } catch (IOException ioe) {
                if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                    SSLLogger.warning(
                        "Fatal: failed to send fatal alert " + alert, ioe);
                }

                closeReason.addSuppressed(ioe);
            }
        }

        // close outbound
        try {
            outputRecord.close();
        } catch (IOException ioe) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.warning("Fatal: output record closure failed", ioe);
            }

            closeReason.addSuppressed(ioe);
        }

        // terminate the handshake context
        if (handshakeContext != null) {
            handshakeContext = null;
        }

        // terminate the transport
        try {
            transport.shutdown();
        } catch (IOException ioe) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.warning("Fatal: transport closure failed", ioe);
            }

            closeReason.addSuppressed(ioe);
        } finally {
            isBroken = true;
        }

        if (closeReason instanceof SSLException) {
            throw (SSLException)closeReason;
        } else {
            throw (RuntimeException)closeReason;
        }
    }

    void setUseClientMode(boolean useClientMode) {
        // Once handshaking has begun, the mode can not be reset for the
        // life of this engine.
        if (handshakeContext != null || isNegotiated) {
            throw new IllegalArgumentException(
                    "Cannot change mode after SSL traffic has started");
        }

        /*
         * If we need to change the client mode and the enabled
         * protocols and cipher suites haven't specifically been
         * set by the user, change them to the corresponding
         * default ones.
         */
        if (sslConfig.isClientMode != useClientMode) {
            if (sslContext.isDefaultProtocolVesions(
                    sslConfig.enabledProtocols)) {
                sslConfig.enabledProtocols =
                        sslContext.getDefaultProtocolVersions(!useClientMode);
            }

            if (sslContext.isDefaultCipherSuiteList(
                    sslConfig.enabledCipherSuites)) {
                sslConfig.enabledCipherSuites =
                        sslContext.getDefaultCipherSuites(!useClientMode);
            }

            sslConfig.toggleClientMode();
        }

        isUnsureMode = false;
    }

    // The OutputRecord is closed and not buffered output record.
    boolean isOutboundDone() {
        return outputRecord.isClosed() && outputRecord.isEmpty();
    }

    // The OutputRecord is closed, but buffered output record may be still
    // waiting for delivery to the underlying connection.
    boolean isOutboundClosed() {
        return outputRecord.isClosed();
    }

    boolean isInboundClosed() {
        return inputRecord.isClosed();
    }

    // Close inbound, no more data should be delivered to the underlying
    // transportation connection.
    void closeInbound() throws SSLException {
        if (isInboundClosed()) {
            return;
        }

        try {
            // Important note: check if the initial handshake is started at
            // first so that the passiveInboundClose() implementation need not
            // to consider the case any more.
            if (!isInputCloseNotified) {
                // the initial handshake is not started
                initiateInboundClose();
            } else {
                passiveInboundClose();
            }
        } catch (IOException ioe) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.warning("inbound closure failed", ioe);
            }
        }
    }

    // Close the connection passively.  The closure could be kickoff by
    // receiving a close_notify alert or reaching end_of_file of the socket.
    //
    // Note that this method is called only if the initial handshake has
    // started or completed.
    private void passiveInboundClose() throws IOException {
        if (!isInboundClosed()) {
            inputRecord.close();
        }

        // For TLS 1.2 and prior version, it is required to respond with
        // a close_notify alert of its own and close down the connection
        // immediately, discarding any pending writes.
        if (!isOutboundClosed()) {
            boolean needCloseNotify = SSLConfiguration.acknowledgeCloseNotify;
            if (!needCloseNotify) {
                if (isNegotiated) {
                    if (!protocolVersion.useTLS13PlusSpec()) {
                        needCloseNotify = true;
                    }
                } else if (handshakeContext != null) {  // initial handshake
                    ProtocolVersion pv = handshakeContext.negotiatedProtocol;
                    if (pv == null || (!pv.useTLS13PlusSpec())) {
                        needCloseNotify = true;
                    }
                }
            }

            if (needCloseNotify) {
                synchronized (outputRecord) {
                    try {
                        // send a close_notify alert
                        warning(Alert.CLOSE_NOTIFY);
                    } finally {
                        outputRecord.close();
                    }
                }
            }
        }
    }

    // Initiate a inbound close when the handshake is not started.
    private void initiateInboundClose() throws IOException {
        if (!isInboundClosed()) {
            inputRecord.close();
        }
    }

    // Close outbound, no more data should be received from the underlying
    // transportation connection.
    void closeOutbound() {
        if (isOutboundClosed()) {
            return;
        }

        try {
             initiateOutboundClose();
        } catch (IOException ioe) {
            if (SSLLogger.isOn && SSLLogger.isOn("ssl")) {
                SSLLogger.warning("outbound closure failed", ioe);
            }
        }
    }

    // Initiate a close by sending a close_notify alert.
    private void initiateOutboundClose() throws IOException {
        boolean useUserCanceled = false;
        if (!isNegotiated && (handshakeContext != null) && !peerUserCanceled) {
            // initial handshake
            useUserCanceled = true;
        }

        // Need a lock here so that the user_canceled alert and the
        // close_notify alert can be delivered together.
        synchronized (outputRecord) {
            try {
                // send a user_canceled alert if needed.
                if (useUserCanceled) {
                    warning(Alert.USER_CANCELED);
                }

                // send a close_notify alert
                warning(Alert.CLOSE_NOTIFY);
            } finally {
                outputRecord.close();
            }
        }
    }

    // Note; HandshakeStatus.FINISHED status is retrieved in other places.
    HandshakeStatus getHandshakeStatus() {
        if (!outputRecord.isEmpty()) {
            // If no handshaking, special case to wrap alters or
            // post-handshake messages.
            return HandshakeStatus.NEED_WRAP;
        } else if (isOutboundClosed() && isInboundClosed()) {
            return HandshakeStatus.NOT_HANDSHAKING;
        } else if (handshakeContext != null) {
            if (!handshakeContext.delegatedActions.isEmpty()) {
                return HandshakeStatus.NEED_TASK;
            } else if (!isInboundClosed()) {
                return HandshakeStatus.NEED_UNWRAP;
            } else if (!isOutboundClosed()) {
                // Special case that the inbound was closed, but outbound open.
                return HandshakeStatus.NEED_WRAP;
            }   // Otherwise, both inbound and outbound are closed.
        }

        return HandshakeStatus.NOT_HANDSHAKING;
    }

    HandshakeStatus finishHandshake() {
        if (protocolVersion.useTLS13PlusSpec()) {
            outputRecord.tc = this;
            inputRecord.tc = this;
            cipherSuite = handshakeContext.negotiatedCipherSuite;
            inputRecord.readCipher.baseSecret =
                    handshakeContext.baseReadSecret;
            outputRecord.writeCipher.baseSecret =
                    handshakeContext.baseWriteSecret;
        }

        handshakeContext = null;
        outputRecord.handshakeHash.finish();
        isNegotiated = true;

        // Tell folk about handshake completion, but do it in a separate thread.
        if (transport instanceof SSLSocket &&
                sslConfig.handshakeListeners != null &&
                !sslConfig.handshakeListeners.isEmpty()) {
            HandshakeCompletedEvent hce =
                new HandshakeCompletedEvent((SSLSocket)transport, conSession);
            Thread thread = new Thread(
                null,
                new NotifyHandshake(sslConfig.handshakeListeners, hce),
                "HandshakeCompletedNotify-Thread",
                0);
            thread.start();
        }

        return HandshakeStatus.FINISHED;
    }

    HandshakeStatus finishPostHandshake() {
        handshakeContext = null;

        // Note: May need trigger handshake completion even for post-handshake
        // authentication in the future.

        return HandshakeStatus.FINISHED;
    }

    // A separate thread is allocated to deliver handshake completion
    // events.
    private static class NotifyHandshake implements Runnable {
        private final Set<Map.Entry<HandshakeCompletedListener,
                AccessControlContext>> targets;         // who gets notified
        private final HandshakeCompletedEvent event;    // the notification

        NotifyHandshake(
                Map<HandshakeCompletedListener,AccessControlContext> listeners,
                HandshakeCompletedEvent event) {
            this.targets = new HashSet<>(listeners.entrySet());     // clone
            this.event = event;
        }

        @Override
        public void run() {
            // Don't need to synchronize, as it only runs in one thread.
            for (Map.Entry<HandshakeCompletedListener,
                    AccessControlContext> entry : targets) {
                final HandshakeCompletedListener listener = entry.getKey();
                AccessControlContext acc = entry.getValue();
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    @Override
                    public Void run() {
                        listener.handshakeCompleted(event);
                        return null;
                    }
                }, acc);
            }
        }
    }
}
