package serguei.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

class ClientHello {

    private static final int TLS_HEADER_LENGTH = 5;
    private static final int CH_HEADER_LEN = 6;
    private static final int CH_RANDOM_LEN = 32;
    private static final int SNI_EXTENSION_CODE = 0;
    private static final int SNI_HOST_NAME_CODE = 0;
    private static final int TLS_MESSAGE_BUFFER_SIZE = 16384;

    private InputStream inputStream;

    private String sniHostName = "";
    private TlsVersion protocolVersion = TlsVersion.UNDEFINED;
    private TlsVersion recordProtocolVersion = TlsVersion.UNDEFINED;

    private byte[] buffer = new byte[512];
    private int bufferSize = 0;

    static ClientHello read(MarkAndResetInputStream inputStream) throws IOException {
        ClientHello clientHello = new ClientHello(inputStream);
        inputStream.mark(0);
        clientHello.parse();
        inputStream.reset();
        clientHello.inputStream = null; // we don't want to keep reference as it might be closed before ClientHello
                                        // object is freed
        clientHello.buffer = null;
        return clientHello;
    }

    public String getSniHostName() {
        return sniHostName;
    }

    public TlsVersion getProtocolVersion() {
        return protocolVersion;
    }

    public TlsVersion getRecordProtocolVersion() {
        return recordProtocolVersion;
    }

    private ClientHello(MarkAndResetInputStream inputStream) {
        this.inputStream = inputStream;
    }

    private void parse() throws IOException {
        int read = readFromStream(inputStream, buffer, bufferSize, TLS_HEADER_LENGTH);
        if (read < TLS_HEADER_LENGTH) {
            return;
        }
        if (!isTlsHandshake(buffer)) {
            tryReadingAsSsl2();
            return;
        }
        int messageLen = calcMessageLength();
        if (messageLen > TLS_MESSAGE_BUFFER_SIZE) {
            return;
        }
        resizeBufferIfNecessary(messageLen, TLS_HEADER_LENGTH);
        int toRead = messageLen - TLS_HEADER_LENGTH;
        read = readFromStream(inputStream, buffer, TLS_HEADER_LENGTH, toRead);
        if (read < toRead) {
            return;
        }
        processProtocolVersion();
        int extensionStartPos = calcExtensionsStartPos();
        if (extensionStartPos < messageLen) {
            processExtentions(extensionStartPos);
        }
    }

    private boolean isTlsHandshake(byte[] buffer) {
        return buffer[0] == 0x16;
    }

    private int calcMessageLength() {
        return getInt16(3) + TLS_HEADER_LENGTH;
    }

    private int getInt8(int pos) {
        if (pos < buffer.length) {
            return (int)buffer[pos] & 0xFF;
        } else {
            return 0;
        }
    }

    private int getInt16(int pos) {
        return (getInt8(pos) << 8) | getInt8(pos + 1);
    }

    private static int readFromStream(InputStream inputStream, byte[] buffer, int offset, int len) throws IOException {
        int result = 0;
        int read;
        do {
            read = inputStream.read(buffer, offset, len);
            result += read;
        } while (read > 0 && result < len);
        return result;
    }

    private void resizeBufferIfNecessary(int requiredLength, int toCopy) {
        if (buffer.length < requiredLength) {
            byte[] newBuffer = new byte[requiredLength];
            System.arraycopy(buffer, 0, newBuffer, 0, toCopy);
            buffer = newBuffer;
        }
    }

    private void processProtocolVersion() {
        recordProtocolVersion = getProtocolVersion(1);
        protocolVersion = getProtocolVersion(TLS_HEADER_LENGTH + 3 + 1);
    }

    private TlsVersion getProtocolVersion(int pos) {
        int major = getInt8(pos);
        int minor = getInt8(pos + 1);
        return new TlsVersion(major, minor);
    }

    private int calcExtensionsStartPos() {
        int pos = TLS_HEADER_LENGTH + CH_HEADER_LEN + CH_RANDOM_LEN;
        int sessionIdLen = getInt8(pos);
        pos += 1 + sessionIdLen;
        int cipherSuiteLen = getInt16(pos);
        pos += 2 + cipherSuiteLen;
        int compressionMethodLen = getInt8(pos);
        pos += 1 + compressionMethodLen;
        return pos;
    }

    private void processExtentions(int startPos) {
        int extLength = getInt16(startPos);
        int pos = startPos + 2;
        while (pos < startPos + extLength) {
            pos += processExtention(pos);
        }
    }

    private int processExtention(int pos) {
        int extType = getInt16(pos);
        int extLen = getInt16(pos + 2);
        if (extType == SNI_EXTENSION_CODE) {
            processSniExtension(pos + 4);
        }
        return 4 + extLen;
    }

    private void processSniExtension(int startPos) {
        int listLength = getInt16(startPos);
        int pos = startPos + 2;
        if (pos < startPos + listLength) {
            processOneSni(pos);
        }
    }

    private int processOneSni(int pos) {
        int nameType = getInt8(pos);
        int nameLen = getInt16(pos + 1);
        if (nameType == SNI_HOST_NAME_CODE) {
            try {
                String name = new String(buffer, pos + 3, nameLen, "ISO_8859_1");
                if (name != null && name.length() > 0) {
                    sniHostName = name;
                }
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e.getMessage(), e);
            }
        }
        return 3 + nameLen;
    }

    private boolean tryReadingAsSsl2() {
        if ((buffer[0] & 0x80) == 0) {
            return false;
        }
        if (getInt8(2) != 1) {
            return false;
        }
        int major = getInt8(3);
        int minor = getInt8(4);
        if (major != 3 || minor < 0 || minor > 3) {
            return false;
        }
        protocolVersion = new TlsVersion(major, minor);
        recordProtocolVersion = new TlsVersion(2, 0);
        return true;
    }
}

@@ -118,16 +118,23 @@
 ption;%0A%0A
+public 
 class Cl
@@ -748,16 +748,52 @@
 e = 0;%0A%0A
+    private ClientHello() %7B%0A%0A    %7D%0A%0A
     stat

Make ClientHello public, but keep constructor private