// LICENSE_CODE ZON
'use strict'; /*zlint node*/
require('./config.js');
var crypto = require('crypto');
var pki = require('./pki.js');
var zcounter = require('./zcounter.js');
var E = exports;

E.cert_cache = {sz: 0, n: 0, hit: 0, miss: 0};

E.Handshake = Handshake;
function Handshake(opt){
    opt = opt||{};
    this.server = new Packets();
    this.client = new Packets();
    this.done = 0;
    this.opt = opt;
}

Handshake.prototype.finish = function(ret){
    this.done = 1;
    return ret;
};

function md5sum(buf){
    return crypto.createHash('md5').update(buf).digest('hex'); }

// expects chunks from server hello
Handshake.prototype.extract_cert_names = function(chunk){
    try {
        var all_names, md5;
        this.server.push(chunk);
        this.server.seek(0);
        // will exit on: success, error, notfound, hard limit (16K)
        for (;;)
        {
            var record = this.server.next_record();
            if (record.type!=22)
                throw new ParseError('not TLS handshake record');
            var message;
            while (message = record.next_message())
            {
                if (message.type==14)
                    throw new ParseError('server_hello_done msg before cert');
                if (message.type!=11)
                    continue;
                var all_certs = message.next_arr({length_width: 3});
                if (!this.opt.cert_cache ||
                    !(all_names=E.cert_cache[md5=md5sum(all_certs[0].buf)]))
                {
                    var cert = pki.x509_der_parse(all_certs[0].buf);
                    // all google certs are signed by one issuer
                    // var issuer = pki.x509_issuer(cert);
                    all_names = pki.x509_all_names(cert);
                    if (md5)
                    {
                        E.cert_cache[md5] = all_names;
                        E.cert_cache.miss++;
                        E.cert_cache.sz++;
                        E.cert_cache.n += all_names.length;
                        zcounter.inc('cert_cache_miss');
                        zcounter.inc_level('cert_cache_sz', 1);
                        zcounter.inc_level('cert_cache_n', all_names.length);
                    }
                }
                else
                {
                    E.cert_cache.hit++;
                    zcounter.inc('cert_cache_hit');
                }
                return this.finish({status: 'ok', all_names: all_names});
            }
        }
    } catch(err){
        if (err instanceof RangeError)
            return {status: 'truncated'};
        return this.finish({status: 'err', message: err.message,
            stack: err.stack});
    }
};

// expects chunks from client hello
Handshake.prototype.extract_sni = function(chunk){
    try {
        this.client.push(chunk);
        this.client.seek(0);
        if (this.client.peek_sslv2())
            return this.finish({status: 'notfound', obsolete: true});
        var record = this.client.next_record();
        if (record.type!=22)
            throw new ParseError('not TLS handshake record');
        var message = record.next_message();
        if (message.type!=1)
            throw new ParseError('unexpected msg type: '+message.type);
        // skip everything till 'session ID length'
        message.skip_bytes(0x22);
        // skip session id, cipher suite, compression methods
        message.skip_bytes(message.next_uint(1));
        message.skip_bytes(message.next_uint(2));
        message.skip_bytes(message.next_uint(1));
        // do we have extensions?
        if (message.end())
            return this.finish({status: 'notfound'});
        var extensions = message.next_extensions();
        for (var i=0; i<extensions.length; i++)
        {
            var ext = extensions[i];
            // skip non-SNI extensions
            if (ext.type>0)
                continue;
            // although it's list, only first element is used
            var sni = ext.next_sni_list()[0];
            if (sni.type)
                break;
            return this.finish({status: 'ok', hostname: sni.buf.toString()});
        }
        return this.finish({status: 'notfound'});
    } catch(err){
        if (err instanceof RangeError)
            return {status: 'truncated'};
        return this.finish({status: 'err', message: err.message,
            stack: err.stack});
    }
};

function ParseError(msg){
    this.name = 'ParseError';
    this.message = msg||'';
    this.stack = (new Error()).stack;
}
ParseError.prototype = Object.create(Error.prototype);
ParseError.prototype.constructor = ParseError;

function Packets(opt){
    this.ofs = 0;
    Object.assign(this, opt);
}

Packets.prototype.seek = function(ofs){ this.ofs = ofs; };
Packets.prototype.skip_bytes = function(length){ this.get_buf(length); };
Packets.prototype.end = function(){ return this.ofs==this.buf.length; };

Packets.prototype.push = function(chunk){
    if (this.fixed)
        throw new ParseError('buffer has fixed size');
    this.buf = this.buf ? Buffer.concat([this.buf, chunk]) : chunk;
};

Packets.prototype.get_buf = function(length){
    var end_ofs;
    if ((end_ofs = this.ofs+length)>this.buf.length)
        throw this.fixed ? new ParseError(end_ofs) : new RangeError(end_ofs);
    var res = this.buf.slice(this.ofs, end_ofs);
    this.ofs = end_ofs;
    return res;
};

// get next {header, body} object
Packets.prototype.next = function(opt){
    if (this.fixed && this.end())
        return;
    var header = this.get_buf(opt.header_size);
    var body_length = header.readUIntBE(opt.length_ofs, opt.length_width);
    if (opt.hard_limit && body_length>opt.hard_limit)
        throw new ParseError('total size exceeds hard limit');
    var body = this.get_buf(body_length);
    var type = header.readUIntBE(0, opt.type_width||1);
    return new Packets({type: type, header: header, buf: body, fixed: true});
};

// get next unsigned int of specified width (in bytes)
Packets.prototype.next_uint = function(width){
    var bytes = this.get_buf(width);
    return bytes.readUIntBE(0, width);
};

// TLS record
Packets.prototype.next_record = function(){
    return this.next({header_size: 5, length_ofs: 3, length_width: 2,
        hard_limit: 16384});
};

// TLS message
Packets.prototype.next_message = function(rec_size){
    return this.next({header_size: 4, length_ofs: 1, length_width: 3});
};

// TLS array: [total, {len1, elem1}, {len2, elem2}, ..]
Packets.prototype.next_arr = function(opt){
    opt.length_ofs = opt.length_ofs||0;
    opt.header_size = opt.header_size||opt.length_width;
    var elem, res = [];
    var total = this.next_uint(opt.length_width);
    while (total)
    {
        elem = this.next(opt);
        total = total-(elem.buf.length+elem.header.length);
        res.push(elem);
    }
    return res;
};

// client_hello extensions
Packets.prototype.next_extensions = function(type){
    return this.next_arr({header_size: 4, length_ofs: 2, type_width: 2,
        length_width: 2, filter: type});
};

// client_hello extensions: server name indication (SNI)
Packets.prototype.next_sni_list = function(){
    return this.next_arr({header_size: 3, length_ofs: 1, length_width: 2}); };

Packets.prototype.peek_sslv2 = function(){
    return (this.buf.readUInt8(0)&0x80)>0 && this.buf.readUInt8(2)==1; };
