/*
 * Copyright (c) 1996, 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.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import javax.crypto.BadPaddingException;
import sun.security.ssl.SSLCipher.SSLReadCipher;

/**
 * {@code InputRecord} takes care of the management of SSL/TLS input
 * records, including buffering, decryption, handshake messages marshal, etc.
 *
 * @author David Brownell
 */
abstract class InputRecord implements Record, Closeable {
    SSLReadCipher       readCipher;
    // Needed for KeyUpdate, used after Handshake.Finished
    TransportContext            tc;

    final HandshakeHash handshakeHash;
    boolean             isClosed;

    // The ClientHello version to accept. If set to ProtocolVersion.SSL20Hello
    // and the first message we read is a ClientHello in V2 format, we convert
    // it to V3. Otherwise we throw an exception when encountering a V2 hello.
    ProtocolVersion     helloVersion;

    // fragment size
    int                 fragmentSize;

    InputRecord(HandshakeHash handshakeHash, SSLReadCipher readCipher) {
        this.readCipher = readCipher;
        this.helloVersion = ProtocolVersion.TLS10;
        this.handshakeHash = handshakeHash;
        this.isClosed = false;
        this.fragmentSize = Record.maxDataSize;
    }

    void setHelloVersion(ProtocolVersion helloVersion) {
        this.helloVersion = helloVersion;
    }

    boolean seqNumIsHuge() {
        return (readCipher.authenticator != null) &&
                        readCipher.authenticator.seqNumIsHuge();
    }

    /**
     * Prevent any more data from being read into this record,
     * and flag the record as holding no data.
     */
    @Override
    public synchronized void close() throws IOException {
        if (!isClosed) {
            isClosed = true;
            readCipher.dispose();
        }
    }

    synchronized boolean isClosed() {
        return isClosed;
    }

    // apply to SSLSocket and SSLEngine
    void changeReadCiphers(SSLReadCipher readCipher) {

        /*
         * Dispose of any intermediate state in the underlying cipher.
         * For PKCS11 ciphers, this will release any attached sessions,
         * and thus make finalization faster.
         *
         * Since MAC's doFinal() is called for every SSL/TLS packet, it's
         * not necessary to do the same with MAC's.
         */
        readCipher.dispose();

        this.readCipher = readCipher;
    }

    // change fragment size
    void changeFragmentSize(int fragmentSize) {
        this.fragmentSize = fragmentSize;
    }

    /*
     * Check if there is enough inbound data in the ByteBuffer to make
     * a inbound packet.
     *
     * @return -1 if there are not enough bytes to tell (small header),
     */
    // apply to SSLEngine only
    int bytesInCompletePacket(
        ByteBuffer[] srcs, int srcsOffset, int srcsLength) throws IOException {

        throw new UnsupportedOperationException("Not supported yet.");
    }

    // apply to SSLSocket only
    int bytesInCompletePacket() throws IOException {
        throw new UnsupportedOperationException();
    }

    // apply to SSLSocket only
    void setReceiverStream(InputStream inputStream) {
        throw new UnsupportedOperationException();
    }

    // read, decrypt and decompress the network record.
    //
    abstract Plaintext[] decode(ByteBuffer[] srcs, int srcsOffset,
            int srcsLength) throws IOException, BadPaddingException;

    // apply to SSLSocket only
    void setDeliverStream(OutputStream outputStream) {
        throw new UnsupportedOperationException();
    }

    // calculate plaintext fragment size
    //
    // apply to SSLEngine only
    int estimateFragmentSize(int packetSize) {
        throw new UnsupportedOperationException();
    }

    //
    // shared helpers
    //

    static ByteBuffer convertToClientHello(ByteBuffer packet) {
        int srcPos = packet.position();

        byte firstByte = packet.get();
        byte secondByte = packet.get();
        int recordLen = (((firstByte & 0x7F) << 8) | (secondByte & 0xFF)) + 2;

        packet.position(srcPos + 3);        // the V2ClientHello record header

        byte majorVersion = packet.get();
        byte minorVersion = packet.get();

        int cipherSpecLen = ((packet.get() & 0xFF) << 8) +
                             (packet.get() & 0xFF);
        int sessionIdLen  = ((packet.get() & 0xFF) << 8) +
                             (packet.get() & 0xFF);
        int nonceLen      = ((packet.get() & 0xFF) << 8) +
                             (packet.get() & 0xFF);

        // Required space for the target SSLv3 ClientHello message.
        //  5: record header size
        //  4: handshake header size
        //  2: ClientHello.client_version
        // 32: ClientHello.random
        //  1: length byte of ClientHello.session_id
        //  2: length bytes of ClientHello.cipher_suites
        //  2: empty ClientHello.compression_methods
        int requiredSize = 48 + sessionIdLen + ((cipherSpecLen * 2 ) / 3);
        byte[] converted = new byte[requiredSize];

        /*
         * Build the first part of the V3 record header from the V2 one
         * that's now buffered up.  (Lengths are fixed up later).
         */
        // Note: need not to set the header actually.
        converted[0] = ContentType.HANDSHAKE.id;
        converted[1] = majorVersion;
        converted[2] = minorVersion;
        // header [3..4] for handshake message length
        // required size is 5;

        /*
         * Store the generic V3 handshake header:  4 bytes
         */
        converted[5] = 1;    // HandshakeMessage.ht_client_hello
        // buf [6..8] for length of ClientHello (int24)
        // required size += 4;

        /*
         * ClientHello header starts with SSL version
         */
        converted[9] = majorVersion;
        converted[10] = minorVersion;
        // required size += 2;
        int pointer = 11;

        /*
         * Copy Random value/nonce ... if less than the 32 bytes of
         * a V3 "Random", right justify and zero pad to the left.  Else
         * just take the last 32 bytes.
         */
        int offset = srcPos + 11 + cipherSpecLen + sessionIdLen;

        if (nonceLen < 32) {
            for (int i = 0; i < (32 - nonceLen); i++) {
                converted[pointer++] = 0;
            }
            packet.position(offset);
            packet.get(converted, pointer, nonceLen);

            pointer += nonceLen;
        } else {
            packet.position(offset + nonceLen - 32);
            packet.get(converted, pointer, 32);

            pointer += 32;
        }

        /*
         * Copy session ID (only one byte length!)
         */
        offset -= sessionIdLen;
        converted[pointer++] = (byte)(sessionIdLen & 0xFF);
        packet.position(offset);
        packet.get(converted, pointer, sessionIdLen);

        /*
         * Copy and translate cipher suites ... V2 specs with first byte zero
         * are really V3 specs (in the last 2 bytes), just copy those and drop
         * the other ones.  Preference order remains unchanged.
         *
         * Example:  Netscape Navigator 3.0 (exportable) says:
         *
         * 0/3,     SSL_RSA_EXPORT_WITH_RC4_40_MD5
         * 0/6,     SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5
         *
         * Microsoft Internet Explorer 3.0 (exportable) supports only
         *
         * 0/3,     SSL_RSA_EXPORT_WITH_RC4_40_MD5
         */
        int j;

        offset -= cipherSpecLen;
        packet.position(offset);

        j = pointer + 2;
        for (int i = 0; i < cipherSpecLen; i += 3) {
            if (packet.get() != 0) {
                // Ignore version 2.0 specific cipher suite.  Clients
                // should also include the version 3.0 equivalent in
                // the V2ClientHello message.
                packet.get();           // ignore the 2nd byte
                packet.get();           // ignore the 3rd byte
                continue;
            }

            converted[j++] = packet.get();
            converted[j++] = packet.get();
        }

        j -= pointer + 2;
        converted[pointer++] = (byte)((j >>> 8) & 0xFF);
        converted[pointer++] = (byte)(j & 0xFF);
        pointer += j;

        /*
         * Append compression methods (default/null only)
         */
        converted[pointer++] = 1;
        converted[pointer++] = 0;      // Session.compression_null

        /*
         * Fill in lengths of the messages we synthesized (nested:
         * V3 handshake message within V3 record).
         */
        // Note: need not to set the header actually.
        int fragLen = pointer - 5;                      // TLSPlaintext.length
        converted[3] = (byte)((fragLen >>> 8) & 0xFF);
        converted[4] = (byte)(fragLen & 0xFF);

        /*
         * Handshake.length, length of ClientHello message
         */
        fragLen = pointer - 9;                          // Handshake.length
        converted[6] = (byte)((fragLen >>> 16) & 0xFF);
        converted[7] = (byte)((fragLen >>> 8) & 0xFF);
        converted[8] = (byte)(fragLen & 0xFF);

        // consume the full record
        packet.position(srcPos + recordLen);

        // Need no header bytes.
        return ByteBuffer.wrap(converted, 5, pointer - 5);  // 5: header size
    }

    // Extract an SSL/TLS record from the specified source buffers.
    static ByteBuffer extract(
            ByteBuffer[] buffers, int offset, int length, int headerSize) {

        boolean hasFullHeader = false;
        int contentLen = -1;
        for (int i = offset, j = 0;
                i < (offset + length) && j < headerSize; i++) {
            int remains = buffers[i].remaining();
            int pos = buffers[i].position();
            for (int k = 0; k < remains && j < headerSize; j++, k++) {
                byte b = buffers[i].get(pos + k);
                if (j == (headerSize - 2)) {
                    contentLen = ((b & 0xFF) << 8);
                } else if (j == (headerSize -1)) {
                    contentLen |= (b & 0xFF);
                    hasFullHeader = true;
                    break;
                }
            }
        }

        if (!hasFullHeader) {
            throw new BufferUnderflowException();
        }

        int packetLen = headerSize + contentLen;
        int remains = 0;
        for (int i = offset; i < offset + length; i++) {
            remains += buffers[i].remaining();
            if (remains >= packetLen) {
                break;
            }
        }

        if (remains < packetLen) {
            throw new BufferUnderflowException();
        }

        byte[] packet = new byte[packetLen];
        int packetOffset = 0;
        int packetSpaces = packetLen;
        for (int i = offset; i < offset + length; i++) {
            if (buffers[i].hasRemaining()) {
                int len = Math.min(packetSpaces, buffers[i].remaining());
                buffers[i].get(packet, packetOffset, len);
                packetOffset += len;
                packetSpaces -= len;
            }

            if (packetSpaces <= 0) {
                break;
            }
        }

        return ByteBuffer.wrap(packet);
    }
}
