/*
 *  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.harmony.xnet.provider.jsse;

import java.io.IOException;
import javax.net.ssl.SSLProtocolException;

/**
 * This class performs functionality dedicated to SSL record layer.
 * It unpacks and routes income data to the appropriate
 * client protocol (handshake, alert, application data protocols)
 * and packages outcome data into SSL/TLS records.
 * Initially created object has null connection state and does not
 * perform any cryptography computations over the income/outcome data.
 * After handshake protocol agreed upon security parameters they are placed
 * into SSLSessionImpl object and available for record protocol as
 * pending session. The order of setting up of the pending session
 * as an active session differs for client and server modes.
 * So for client mode the parameters are provided by handshake protocol
 * during retrieving of change_cipher_spec message to be sent (by calling of
 * getChangeCipherSpecMesage method).
 * For server side mode record protocol retrieves the parameters from
 * handshake protocol after receiving of client's change_cipher_spec message.
 * After the pending session has been setted up as a curent session,
 * new connectin state object is created and used for encryption/decryption
 * of the messages.
 * Among with base functionality this class provides the information about
 * constrains on the data length, and information about correspondance
 * of plain and encrypted data lengths.
 * For more information on TLS v1 see http://www.ietf.org/rfc/rfc2246.txt,
 * on SSL v3 see http://wp.netscape.com/eng/ssl3,
 * on SSL v2 see http://wp.netscape.com/eng/security/SSL_2.html.
 */
public class SSLRecordProtocol {

    /**
     * Maximum length of allowed plain data fragment
     * as specified by TLS specification.
     */
    protected static int MAX_DATA_LENGTH = 16384; // 2^14
    /**
     * Maximum length of allowed compressed data fragment
     * as specified by TLS specification.
     */
    protected static int MAX_COMPRESSED_DATA_LENGTH
                                    = MAX_DATA_LENGTH + 1024;
    /**
     * Maximum length of allowed ciphered data fragment
     * as specified by TLS specification.
     */
    protected static int MAX_CIPHERED_DATA_LENGTH
                                    = MAX_COMPRESSED_DATA_LENGTH + 1024;
    /**
     * Maximum length of ssl record. It is counted as:
     * type(1) + version(2) + length(2) + MAX_CIPHERED_DATA_LENGTH
     */
    protected static int MAX_SSL_PACKET_SIZE
                                    = MAX_CIPHERED_DATA_LENGTH + 5;
    // the SSL session used for connection
    private SSLSessionImpl session;
    // protocol version of the connection
    private byte[] version;
    // input stream of record protocol
    private SSLInputStream in;
    // handshake protocol object to which handshaking data will be transmitted
    private HandshakeProtocol handshakeProtocol;
    // alert protocol to indicate alerts occured/received
    private AlertProtocol alertProtocol;
    // application data object to which application data will be transmitted
    private org.apache.harmony.xnet.provider.jsse.Appendable appData;
    // connection state holding object
    private ConnectionState
        activeReadState, activeWriteState, pendingConnectionState;

    // logger
    private Logger.Stream logger = Logger.getStream("record");

    // flag indicating if session object has been changed after
    // handshake phase (to distinguish session pending state)
    private boolean sessionWasChanged = false;

    // change cipher spec message content
    private static final byte[] change_cipher_spec_byte = new byte[] {1};

    /**
     * Creates an instance of record protocol and tunes
     * up the client protocols to use ut.
     * @param   handshakeProtocol:  HandshakeProtocol
     * @param   alertProtocol:  AlertProtocol
     * @param   in: SSLInputStream
     * @param   appData:    Appendable
     */
    protected SSLRecordProtocol(HandshakeProtocol handshakeProtocol,
            AlertProtocol alertProtocol,
            SSLInputStream in,
            Appendable appData) {
        this.handshakeProtocol = handshakeProtocol;
        this.handshakeProtocol.setRecordProtocol(this);
        this.alertProtocol = alertProtocol;
        this.alertProtocol.setRecordProtocol(this);
        this.in = in;
        this.appData = appData;
    }

    /**
     * Returns the session obtained during the handshake negotiation.
     * If the handshake process was not compleated, method returns null.
     * @return the session in effect.
     */
    protected SSLSessionImpl getSession() {
        return session;
    }

    /**
     * Returns the minimum possible length of the SSL record.
     * @return
     */
    protected int getMinRecordSize() {
        return (activeReadState == null)
            ? 6 // type + version + length + 1 byte of data
            : 5 + activeReadState.getMinFragmentSize();
    }

    /**
     * Returns the record length for the specified incoming data length.
     * If actual resulting record length is greater than
     * MAX_CIPHERED_DATA_LENGTH, MAX_CIPHERED_DATA_LENGTH is returned.
     */
    protected int getRecordSize(int data_size) {
        if (activeWriteState == null) {
            return 5+data_size; // type + version + length + data_size
        } else {
            int res = 5 + activeWriteState.getFragmentSize(data_size);
            return (res > MAX_CIPHERED_DATA_LENGTH)
                ? MAX_CIPHERED_DATA_LENGTH // so the source data should be
                                           // splitted into several packets
                : res;
        }
    }

    /**
     * Returns the upper bound of length of data containing in the record with
     * specified length.
     * If the provided record_size is greater or equal to
     * MAX_CIPHERED_DATA_LENGTH the returned value will be
     * MAX_DATA_LENGTH
     * counted as for data with
     * MAX_CIPHERED_DATA_LENGTH length.
     */
    protected int getDataSize(int record_size) {
        record_size -= 5; // - (type + version + length + data_size)
        if (record_size > MAX_CIPHERED_DATA_LENGTH) {
            // the data of such size consists of the several packets
            return MAX_DATA_LENGTH;
        }
        if (activeReadState == null) {
            return record_size;
        }
        return activeReadState.getContentSize(record_size);
    }

    /**
     * Depending on the Connection State (Session) encrypts and compress
     * the provided data, and packs it into TLSCiphertext structure.
     * @param   content_type: int
     * @param   fragment: byte[]
     * @return  ssl packet created over the current connection state
     */
    protected byte[] wrap(byte content_type, DataStream dataStream) {
        byte[] fragment = dataStream.getData(MAX_DATA_LENGTH);
        return wrap(content_type, fragment, 0, fragment.length);
    }

    /**
     * Depending on the Connection State (Session) encrypts and compress
     * the provided data, and packs it into TLSCiphertext structure.
     * @param   content_type: int
     * @param   fragment: byte[]
     * @return  ssl packet created over the current connection state
     */
    protected byte[] wrap(byte content_type,
                       byte[] fragment, int offset, int len) {
        if (logger != null) {
            logger.println("SSLRecordProtocol.wrap: TLSPlaintext.fragment["
                    +len+"]:");
            logger.print(fragment, offset, len);
        }
        if (len > MAX_DATA_LENGTH) {
            throw new AlertException(
                AlertProtocol.INTERNAL_ERROR,
                new SSLProtocolException(
                    "The provided chunk of data is too big: " + len
                    + " > MAX_DATA_LENGTH == "+MAX_DATA_LENGTH));
        }
        byte[] ciphered_fragment = fragment;
        if (activeWriteState != null) {
            ciphered_fragment =
                activeWriteState.encrypt(content_type, fragment, offset, len);
            if (ciphered_fragment.length > MAX_CIPHERED_DATA_LENGTH) {
                throw new AlertException(
                    AlertProtocol.INTERNAL_ERROR,
                    new SSLProtocolException(
                        "The ciphered data increased more than on 1024 bytes"));
            }
            if (logger != null) {
                logger.println("SSLRecordProtocol.wrap: TLSCiphertext.fragment["
                        +ciphered_fragment.length+"]:");
                logger.print(ciphered_fragment);
            }
        }
        return packetize(content_type, version, ciphered_fragment);
    }

    private byte[] packetize(byte type, byte[] version, byte[] fragment) {
        byte[] buff = new byte[5+fragment.length];
        buff[0] = type;
        if (version != null) {
            buff[1] = version[0];
            buff[2] = version[1];
        } else {
            buff[1] = 3;
            buff[2] = 1;
        }
        buff[3] = (byte) ((0x00FF00 & fragment.length) >> 8);
        buff[4] = (byte) (0x0000FF & fragment.length);
        System.arraycopy(fragment, 0, buff, 5, fragment.length);
        return buff;
    }

    /**
     * Set the ssl session to be used after sending the changeCipherSpec message
     * @param   session:    SSLSessionImpl
     */
    private void setSession(SSLSessionImpl session) {
        if (!sessionWasChanged) {
            // session was not changed for current handshake process
            if (logger != null) {
                logger.println("SSLRecordProtocol.setSession: Set pending session");
                logger.println("  cipher name: " + session.getCipherSuite());
            }
            this.session = session;
            // create new connection state
            pendingConnectionState = ((version == null) || (version[1] == 1))
                ? (ConnectionState) new ConnectionStateTLS(getSession())
                : (ConnectionState) new ConnectionStateSSLv3(getSession());
            sessionWasChanged = true;
        } else {
            // wait for rehandshaking's session
            sessionWasChanged = false;
        }
    }

    /**
     * Returns the change cipher spec message to be sent to another peer.
     * The pending connection state will be built on the base of provided
     * session object
     * The calling of this method triggers pending write connection state to
     * be active.
     * @return ssl record containing the "change cipher spec" message.
     */
    protected byte[] getChangeCipherSpecMesage(SSLSessionImpl session) {
        // make change_cipher_spec_message:
        byte[] change_cipher_spec_message;
        if (activeWriteState == null) {
            change_cipher_spec_message = new byte[] {
                    ContentType.CHANGE_CIPHER_SPEC, version[0],
                        version[1], 0, 1, 1
                };
        } else {
            change_cipher_spec_message =
                packetize(ContentType.CHANGE_CIPHER_SPEC, version,
                        activeWriteState.encrypt(ContentType.CHANGE_CIPHER_SPEC,
                            change_cipher_spec_byte, 0, 1));
        }
        setSession(session);
        activeWriteState = pendingConnectionState;
        if (logger != null) {
            logger.println("SSLRecordProtocol.getChangeCipherSpecMesage");
            logger.println("activeWriteState = pendingConnectionState");
            logger.print(change_cipher_spec_message);
        }
        return change_cipher_spec_message;
    }

    /**
     * Retrieves the fragment field of TLSCiphertext, and than
     * depending on the established Connection State
     * decrypts and decompresses it. The following structure is expected
     * on the input at the moment of the call:
     *
     *  struct {
     *      ContentType type;
     *      ProtocolVersion version;
     *      uint16 length;
     *      select (CipherSpec.cipher_type) {
     *          case stream: GenericStreamCipher;
     *          case block: GenericBlockCipher;
     *      } fragment;
     *  } TLSCiphertext;
     *
     * (as specified by RFC 2246, TLS v1 Protocol specification)
     *
     * In addition this method can recognize SSLv2 hello message which
     * are often used to establish the SSL/TLS session.
     *
     * @throws IOException if some io errors have been occured
     * @throws EndOfSourceException if underlying input stream
     *                              has ran out of data.
     * @throws EndOfBufferException if there was not enought data
     *                              to build complete ssl packet.
     * @return the type of unwrapped message.
     */
    protected int unwrap() throws IOException {
        if (logger != null) {
            logger.println("SSLRecordProtocol.unwrap: BEGIN [");
        }
        int type = in.readUint8();
        if ((type < ContentType.CHANGE_CIPHER_SPEC)
                || (type > ContentType.APPLICATION_DATA)) {
            if (logger != null) {
                logger.println("Non v3.1 message type:" + type);
            }
            if (type >= 0x80) {
                // it is probably SSL v2 client_hello message
                // (see SSL v2 spec at:
                // http://wp.netscape.com/eng/security/SSL_2.html)
                int length = (type & 0x7f) << 8 | in.read();
                byte[] fragment = in.read(length);
                handshakeProtocol.unwrapSSLv2(fragment);
                if (logger != null) {
                    logger.println(
                            "SSLRecordProtocol:unwrap ] END, SSLv2 type");
                }
                return ContentType.HANDSHAKE;
            }
            throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE,
                    new SSLProtocolException(
                        "Unexpected message type has been received: "+type));
        }
        if (logger != null) {
            logger.println("Got the message of type: " + type);
        }
        if (version != null) {
            if ((in.read() != version[0])
                    || (in.read() != version[1])) {
                throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE,
                        new SSLProtocolException(
                            "Unexpected message type has been received: " +
                            type));
            }
        } else {
            in.skip(2); // just skip the version number
        }
        int length = in.readUint16();
        if (logger != null) {
            logger.println("TLSCiphertext.fragment["+length+"]: ...");
        }
        if (length > MAX_CIPHERED_DATA_LENGTH) {
            throw new AlertException(AlertProtocol.RECORD_OVERFLOW,
                    new SSLProtocolException(
                        "Received message is too big."));
        }
        byte[] fragment = in.read(length);
        if (logger != null) {
            logger.print(fragment);
        }
        if (activeReadState != null) {
            fragment = activeReadState.decrypt((byte) type, fragment);
            if (logger != null) {
                logger.println("TLSPlaintext.fragment:");
                logger.print(fragment);
            }
        }
        if (fragment.length > MAX_DATA_LENGTH) {
            throw new AlertException(AlertProtocol.DECOMPRESSION_FAILURE,
                    new SSLProtocolException(
                        "Decompressed plain data is too big."));
        }
        switch (type) {
            case ContentType.CHANGE_CIPHER_SPEC:
                // notify handshake protocol:
                handshakeProtocol.receiveChangeCipherSpec();
                setSession(handshakeProtocol.getSession());
                // change cipher spec message has been received, so:
                if (logger != null) {
                    logger.println("activeReadState = pendingConnectionState");
                }
                activeReadState = pendingConnectionState;
                break;
            case ContentType.ALERT:
                alert(fragment[0], fragment[1]);
                break;
            case ContentType.HANDSHAKE:
                handshakeProtocol.unwrap(fragment);
                break;
            case ContentType.APPLICATION_DATA:
                if (logger != null) {
                    logger.println(
                            "TLSCiphertext.unwrap: APP DATA["+length+"]:");
                    logger.println(new String(fragment));
                }
                appData.append(fragment);
                break;
            default:
                throw new AlertException(AlertProtocol.UNEXPECTED_MESSAGE,
                        new SSLProtocolException(
                            "Unexpected message type has been received: " +
                            type));
        }
        if (logger != null) {
            logger.println("SSLRecordProtocol:unwrap ] END, type: " + type);
        }
        return type;
    }

    /**
     * Passes the alert information to the alert protocol.
     * @param   level:  byte
     * @param   description:    byte
     */
    protected void alert(byte level, byte description) {
        if (logger != null) {
            logger.println("SSLRecordProtocol.allert: "+level+" "+description);
        }
        alertProtocol.alert(level, description);
    }

    /**
     * Sets up the SSL version used in this connection.
     * This method is calling from the handshake protocol after
     * it becomes known witch protocol version will be used.
     * @param   ver:    byte[]
     * @return
     */
    protected void setVersion(byte[] ver) {
        this.version = ver;
    }

    /**
     * Shutdownes the protocol. It will be impossiblke to use the instance
     * after the calling of this method.
     */
    protected void shutdown() {
        session = null;
        version = null;
        in = null;
        handshakeProtocol = null;
        alertProtocol = null;
        appData = null;
        if (pendingConnectionState != null) {
            pendingConnectionState.shutdown();
        }
        pendingConnectionState = null;
        if (activeReadState != null) {
            activeReadState.shutdown();
        }
        activeReadState = null;
        if (activeReadState != null) {
            activeReadState.shutdown();
        }
        activeWriteState = null;
    }
}
