/*
 * Copyright 2016 The Android Open Source Project
 *
 * Licensed 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.conscrypt;

import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionBindingEvent;
import javax.net.ssl.SSLSessionBindingListener;
import javax.net.ssl.SSLSessionContext;
import javax.security.cert.CertificateException;

/**
 * Extends the base SSLSession with some methods used exclusively in Conscrypt.
 *
 * @hide
 */
@Internal
public abstract class AbstractOpenSSLSession implements SSLSession {
    private final Map<String, Object> values = new HashMap<String, Object>();

    private volatile javax.security.cert.X509Certificate[] peerCertificateChain;

    private AbstractSessionContext sessionContext;

    private boolean isValid = true;

    /**
     * Class constructor creates an SSL session context given the appropriate
     * session context.
     */
    protected AbstractOpenSSLSession(AbstractSessionContext sessionContext) {
        this.sessionContext = sessionContext;
    }

    protected abstract X509Certificate[] getX509PeerCertificates()
            throws SSLPeerUnverifiedException;

    protected abstract X509Certificate[] getX509LocalCertificates();

    /**
     * Throw SSLPeerUnverifiedException on null or empty peerCertificates array
     */
    private void checkPeerCertificatesPresent() throws SSLPeerUnverifiedException {
        X509Certificate[] peerCertificates = getX509PeerCertificates();
        if (peerCertificates == null || peerCertificates.length == 0) {
            throw new SSLPeerUnverifiedException("No peer certificates");
        }
    }

    /**
     * Return the identity of the peer in this SSL session
     * determined via certificate(s).
     * @return an array of X509 certificates (the peer's one first and then
     *         eventually that of the certification authority) or null if no
     *         certificate were used during the SSL connection.
     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
     *         was used (i.e. Kerberos certificates) or the peer could not
     *         be verified.
     */
    @Override
    public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException {
        return getX509PeerCertificates();
    }

    /**
     * Returns the certificate(s) of the peer in this SSL session
     * used in the handshaking phase of the connection.
     * Please notice hat this method is superseded by
     * <code>getPeerCertificates()</code>.
     * @return an array of X509 certificates (the peer's one first and then
     *         eventually that of the certification authority) or null if no
     *         certificate were used during the SSL connection.
     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
     *         was used (i.e. Kerberos certificates) or the peer could not
     *         be verified.
     */
    @Override
    public javax.security.cert.X509Certificate[] getPeerCertificateChain()
            throws SSLPeerUnverifiedException {
        checkPeerCertificatesPresent();
        javax.security.cert.X509Certificate[] result = peerCertificateChain;
        if (result == null) {
            // single-check idiom
            peerCertificateChain = result = createPeerCertificateChain();
        }
        return result;
    }

    /**
     * Provide a value to initialize the volatile peerCertificateChain
     * field based on the native SSL_SESSION
     */
    private javax.security.cert.X509Certificate[] createPeerCertificateChain()
            throws SSLPeerUnverifiedException {
        X509Certificate[] peerCertificates = getX509PeerCertificates();
        try {
            javax.security.cert.X509Certificate[] chain =
                    new javax.security.cert.X509Certificate[peerCertificates.length];

            for (int i = 0; i < peerCertificates.length; i++) {
                byte[] encoded = peerCertificates[i].getEncoded();
                chain[i] = javax.security.cert.X509Certificate.getInstance(encoded);
            }
            return chain;
        } catch (CertificateEncodingException e) {
            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
            exception.initCause(exception);
            throw exception;
        } catch (CertificateException e) {
            SSLPeerUnverifiedException exception = new SSLPeerUnverifiedException(e.getMessage());
            exception.initCause(exception);
            throw exception;
        }
    }

    /**
     * The identity of the principal that was used by the peer during the SSL
     * handshake phase is returned by this method.
     * @return a X500Principal of the last certificate for X509-based
     *         cipher suites.
     * @throws SSLPeerUnverifiedException if either a non-X.509 certificate
     *         was used (i.e. Kerberos certificates) or the peer does not exist.
     *
     */
    @Override
    public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
        checkPeerCertificatesPresent();
        return getX509PeerCertificates()[0].getSubjectX500Principal();
    }

    /**
     * Returns the principal (subject) of this concrete SSL session used in the
     * handshaking phase of the connection.
     * @return a X509 certificate or null if no principal was defined
     */
    @Override
    public Principal getLocalPrincipal() {
        X509Certificate[] localCertificates = getX509LocalCertificates();
        if (localCertificates != null && localCertificates.length > 0) {
            return localCertificates[0].getSubjectX500Principal();
        } else {
            return null;
        }
    }

    /**
     * Returns the certificate(s) of the principal (subject) of this concrete SSL
     * session used in the handshaking phase of the connection. The OpenSSL
     * native method supports only RSA certificates.
     * @return an array of certificates (the local one first and then eventually
     *         that of the certification authority) or null if no certificate
     *         were used during the handshaking phase.
     */
    @Override
    public Certificate[] getLocalCertificates() {
        return getX509LocalCertificates();
    }

    /**
     * Returns the largest buffer size for the application's data bound to this
     * concrete SSL session.
     * @return the largest buffer size
     */
    @Override
    public int getApplicationBufferSize() {
        return NativeConstants.SSL3_RT_MAX_PLAIN_LENGTH;
    }

    /**
     * Returns the largest SSL/TLS packet size one can expect for this concrete
     * SSL session.
     * @return the largest packet size
     */
    @Override
    public int getPacketBufferSize() {
        return NativeConstants.SSL3_RT_MAX_PACKET_SIZE;
    }

    /**
     * Returns the object which is bound to the the input parameter name.
     * This name is a sort of link to the data of the SSL session's application
     * layer, if any exists.
     *
     * @param name the name of the binding to find.
     * @return the value bound to that name, or null if the binding does not
     *         exist.
     * @throws IllegalArgumentException if the argument is null.
     */
    @Override
    public Object getValue(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name == null");
        }
        return values.get(name);
    }

    /**
     * Returns an array with the names (sort of links) of all the data
     * objects of the application layer bound into the SSL session.
     *
     * @return a non-null (possibly empty) array of names of the data objects
     *         bound to this SSL session.
     */
    @Override
    public String[] getValueNames() {
        return values.keySet().toArray(new String[values.size()]);
    }

    /**
     * A link (name) with the specified value object of the SSL session's
     * application layer data is created or replaced. If the new (or existing)
     * value object implements the <code>SSLSessionBindingListener</code>
     * interface, that object will be notified in due course.
     *
     * @param name the name of the link (no null are
     *            accepted!)
     * @param value data object that shall be bound to
     *            name.
     * @throws IllegalArgumentException if one or both argument(s) is null.
     */
    @Override
    public void putValue(String name, Object value) {
        if (name == null || value == null) {
            throw new IllegalArgumentException("name == null || value == null");
        }
        Object old = values.put(name, value);
        if (value instanceof SSLSessionBindingListener) {
            ((SSLSessionBindingListener) value).valueBound(new SSLSessionBindingEvent(this, name));
        }
        if (old instanceof SSLSessionBindingListener) {
            ((SSLSessionBindingListener) old).valueUnbound(new SSLSessionBindingEvent(this, name));
        }
    }

    /**
     * Removes a link (name) with the specified value object of the SSL
     * session's application layer data.
     *
     * <p>If the value object implements the <code>SSLSessionBindingListener</code>
     * interface, the object will receive a <code>valueUnbound</code> notification.
     *
     * @param name the name of the link (no null are
     *            accepted!)
     * @throws IllegalArgumentException if the argument is null.
     */
    @Override
    public void removeValue(String name) {
        if (name == null) {
            throw new IllegalArgumentException("name == null");
        }
        Object old = values.remove(name);
        if (old instanceof SSLSessionBindingListener) {
            SSLSessionBindingListener listener = (SSLSessionBindingListener) old;
            listener.valueUnbound(new SSLSessionBindingEvent(this, name));
        }
    }

    /**
     * Returns the context to which the actual SSL session is bound. A SSL
     * context consists of (1) a possible delegate, (2) a provider and (3) a
     * protocol.
     * @return the SSL context used for this session, or null if it is
     * unavailable.
     */
    @Override
    public SSLSessionContext getSessionContext() {
        return sessionContext;
    }

    /**
     * Returns a boolean flag signaling whether a SSL session is valid
     * and available for resuming or joining or not.
     *
     * @return true if this session may be resumed.
     */
    @Override
    public boolean isValid() {
        if (!isValid) {
            return false;
        }
        // The session has't yet been invalidated -- check whether it timed out.

        SSLSessionContext context = getSessionContext();
        if (context == null) {
            // Session not associated with a context -- no way to tell what its timeout should be.
            return true;
        }

        int timeoutSeconds = context.getSessionTimeout();
        if (timeoutSeconds == 0) {
            // Infinite timeout -- session still valid
            return true;
        }

        long creationTimestampMillis = getCreationTime();
        long ageSeconds = (System.currentTimeMillis() - creationTimestampMillis) / 1000;
        // NOTE: The age might be negative if something was/is wrong with the system clock. We time
        // out such sessions to be safe.
        if ((ageSeconds >= timeoutSeconds) || (ageSeconds < 0)) {
            // Session timed out -- no longer valid
            isValid = false;
            return false;
        }

        // Session still valid
        return true;
    }

    /**
     * It invalidates a SSL session forbidding any resumption.
     */
    @Override
    public void invalidate() {
        isValid = false;
        sessionContext = null;
    }

    /**
     * Returns the name requested by the SNI extension.
     */
    public abstract String getRequestedServerName();

    /**
     * Returns the OCSP stapled response.
     */
    public abstract List<byte[]> getStatusResponses();

    /**
     * Returns the TLS Stapled Certificate Transparency data.
     */
    public abstract byte[] getTlsSctData();

    /**
     * Sets the last accessed time for this session in milliseconds since Jan 1,
     * 1970 00:00:00 UTC.
     */
    public abstract void setLastAccessedTime(long accessTimeMillis);

    /**
     * Indicates that this session's ID may have changed and should be
     * re-cached.
     */
    abstract void resetId();
}
