/**
 * This module is part of the TraCINg CIDS and introduces mitigation methodologies
 * against the probe response attack by analyzing statistical anomalies. On identification
 * of an attack, countermeasures are introduced which modify the way attack events get
 * presented to the user (see postHandler.js).
 */

// load node modules
var Chance = require("chance");
var fs = require('fs');
var path = require('path');
var Map = require("collections/map");
var Set = require("collections/set");
var es = require("event-stream");

var debugInstance = 0;
var Debug = function(){
        var output = "";
        var instance = debugInstance++;

        this.get = function(){
                return output;
        };

        this.add = function(text){
                text = "Proberesponse["+instance+"]: " + text;
                console.log(text);
                output += text + "\n";
        };
};

var debug = new Debug();
var chance = new Chance();


// TBA: duration of one time slot in a window
// 10 fits for destination port, 6ms
var TIMESLOT_SECONDS = 60;

// TBA: amount of "TIMESLOT_SECONDS" for a time window to save, total seconds = WINDOW_MAX_TIMESLOTS_RATIO * TIMESLOT_SECONDS
var WINDOW_MAX_TIMESLOTS_RATIO = 60;
// Attack count for every source IP address: { ip_source : { ip_destination : count}, ... }
var ip_source_dest_count = {};
// List of ip_source_dest_count for every slot in the time window [ ip_count1, ip_count2, ... ]
// This is a round robbin buffer where the oldest entries get overwritten by the newest.
var window_ip_source_dest = [];

// TBA; Ratio of (attacks/unique monitors) for the whole time window which triggers an alarm.
// There should be a save margin upwards to a "normal" ratio.
// ratio can not be lower than 1 (at minimum 1 attack per monitor)
var THRESHOLD_ATTACKS_MONITOR_RATIO = 3;	// depends on TIMESLOT_SECONDS and WINDOW_MAX_TIMESLOTS_RATIO and amount of events/s

// >>> TBA: Alarm thresholds for the whole time window WINDOW_MAX_TIMESLOTS_RATIO
// depents on TIMESLOT_SECONDS * WINDOW_MAX_TIMESLOTS_RATIO
// TBA: Amount of unique monitors per source (adress:monitors -> 1:n -> check n)
var THRESHOLD_MONITORS_PER_SOURCE_IP = 50000;	// ~20% of all monitor nodes.
// TBA: Amount of unique sources for one monitors (monitor:address -> 1:n -> check n)
var THRESHOLD_SOURCE_IPS_PER_MONITOR = 100;
// TBA: Amount of attacks from one source to a different monitors (monitor:address -> 1:1 -> check amount of attacks)
var THRESHOLD_SOURCE_IPS_MONITOR_ATTACKS = 6000;
// TBA: Amount of total attacks for one monitors (monitor:attacks -> 1:n -> check n)
var THRESHOLD_ATTACKS_PER_MONITOR = 15000;


// >>> Alarm thresholds for destination port statistics
// TBA: amount of "TIMESLOT_SECONDS" for a time windowto save, total seconds = WINDOW_MAX_TIMESLOTS_RATIO * TIMESLOT_SECONDS
var WINDOW_MAX_TIMESLOTS_PORT_DST = 60;
// Attack count for every destination port: { port : count, ...}
var port_dest_count = new Map();
// List of dicts of ports [ port_dest_count1, port_dest_count2, ... ]
// This is a round robbin buffer where the oldest entries get overwritten by the newest.
var window_port_dest = [];
// TBA: max amount of attacked destination ports in TIMESLOT_SECONDS*WINDOW_MAX_TIMESLOTS_PORT_DST seconds
var THRESHOLD_PORT_DEST_ATTACKED = 7000;

// TBA: Ban duration AFTTER source count falls below SOURCE_IP_THRESHOLD
var SOURCE_IP_BAN_MINUTES = 1;
// { ip : ban_time_minutes }
var banned_ips = {}

// Sampling of events: just take 0% -> 100% of all events
var sampling_percent = 100;
var sampling_active = false;
// noise information
var events_dshield_total = 0;
var events_dshield_added = 0;
var events_external_total = 0;
var events_external_added = 0;

// handle mitigation if activated (does NOT influence metrics -> those are further calculated but external events are ignored)
var is_active = true;

var dshield_entries = [];
// 24 events per second
var noise_interval_ms = 41;
//var noise_interval_ms = 7;
var FILE_STATS = "attack_stats.txt";
var dshield_read_linenr = 0;
var init_started = false;


function init_stats_window() {
	// Amount of events for: TIMESLOT_SECONDS * WINDOW_MAX_TIMESLOTS_RATIO
	// 27 events/s = 97200 events/window (60*60 seconds) = ~ ratio 5.8
	//var attacks_init_win = 97200;
	var attacks_init_win = Math.round(1000/noise_interval_ms)*TIMESLOT_SECONDS*WINDOW_MAX_TIMESLOTS_RATIO;

	var attacks_per_slot = Math.round(attacks_init_win / WINDOW_MAX_TIMESLOTS_RATIO);
	var port_count = new Map();

	debug.add("amount of dshield entries: "+ dshield_entries.length);
	debug.add("initiating window ip: "+WINDOW_MAX_TIMESLOTS_RATIO+"*"+TIMESLOT_SECONDS+
		" sec, window port: "+WINDOW_MAX_TIMESLOTS_PORT_DST+"*"+TIMESLOT_SECONDS+" sec");

	var cnt = 0;

	for(var x=0; x < WINDOW_MAX_TIMESLOTS_RATIO; ++x){
		//window_ip_source_dest[x] = {"0.0.0.0" : {"0.0.0.0" : 0}};
		window_ip_source_dest[x] = {};
		window_port_dest[x] = new Map();

		for (var y=0; y < attacks_per_slot; ++y) {
			entry = dshield_entries.pop();
			//entry = dshield_entries.shift();
			//l = [ipsrc, ipmon, chance.integer({min:0, max:65535})];
			l = [entry[0], entry[2], entry[3]];

			if (l in window_ip_source_dest[x]) {
				window_ip_source_dest[x][l] += 1;
				//debug.add("adding, total: "+ window_ip_source_dest[x][l]);
			}
			else
				window_ip_source_dest[x][l] = 1;

			if (port_count.get(entry[3]) == undefined) {
				port_count.set(entry[3], 1);
			} else {
				port_count.set(entry[3], port_count.get(entry[3]) + 1);
			}

			if (window_port_dest[x].get(entry[3]) == undefined) {
				window_port_dest[x].set(entry[3], 1);
			} else {
				window_port_dest[x].set(entry[3], window_port_dest[x].get(entry[3]) + 1);
			}
			cnt += 1;
		}
	}

	var port_cnt_check = 0;
	port_count.forEach( function merge(count, port, mapObj) {
		port_cnt_check += count;
	});


	debug.add("unique ports: "+ port_count.length);
	debug.add("finished initiating, count: "+attacks_init_win+"="+port_cnt_check+"=~"+cnt);
	// add noise using DShield data
	var noise_ref = setInterval(add_entry_dshield, noise_interval_ms);
	// mitigation check-logic
	//var check_attack_status_ref = setInterval(check_attack_status, TIMESLOT_SECONDS * 1000);
}

var FILE_DSHIELD_EVENTS = path.join(__dirname, 'dshield_report.csv');
debug.add("reading DShield events from "+FILE_DSHIELD_EVENTS+", standy by");

s = fs.createReadStream(FILE_DSHIELD_EVENTS)
    .pipe(es.split())
    .pipe(es.mapSync(function(line){
        // pause the readstream
        s.pause();
        dshield_read_linenr += 1;

        (function(){
                        var columns = line.toString().split("\t", -1);

                        if (columns.length == 6) {
                        try {
                                // 3 4 [8 -> IP] 5 8: ip_src port_src ip_dst port_dst id
                                // 0 1 [5 -> IP] 2 5: ip_src port_src ip_dst port_dst id
                                // convert monitor ID to an IP address
                                a = parseInt(columns[5].substring(2, 4), 16);
                                b = parseInt(columns[5].substring(4, 6), 16);
                                c = parseInt(columns[5].substring(6, 8), 16);
                                d = parseInt(columns[5].substring(8, 10), 16);
                                //debug.add("IP is: " + a+"."+b+"."+c+"."+d);

                                dshield_entries.push([columns[0],
                                                parseInt(columns[1]),
                                                a+"."+b+"."+c+"."+d,
                                                columns[2],
                                                columns[5]]);
                        } catch (err) { /* ignore malformed lines */ }
	               }

            // process line here and call s.resume() when rdy
            //logMemoryUsage(lineNr);
            if (dshield_read_linenr % 200000 == 0) debug.add("read DShield events: "+dshield_read_linenr);
	    if (!init_started && dshield_read_linenr > 2000000) {
		s.destroy(); init_stats_window(); init_started = true;
		}

            // resume the readstream
            s.resume();

        })();
    })
    .on('error', function(){ debug.add('Error while reading DShield report'); })
    .on('end', function(){ debug.add('Read entire DShield report') })
);


function update_slot(monitor_id, ip_source, ip_dest, port_dest) {
	//debug.add("updating window: "+monitor_id+" / "+ip_source+" / "+ip_dest+" / "+port_dest);
	//if (!is_active) { return; }

	/*
	if (!(ip_source === undefined) && (ip_source in banned_ips)) {
		// don't count banned IP addresses
		//debug.add("won't count banned ip: "+ ip_source)
		return;
	}
	*/

	//l = [ip_source, ip_dest, port_dest];
	l = [ip_source, monitor_id, port_dest];

	if (ip_source_dest_count[l] == undefined) { ip_source_dest_count[l] = 0; }

	ip_source_dest_count[l] += 1;

	if (port_dest_count.get(port_dest) == undefined) {
		port_dest_count.set(port_dest, 1);
	} else {
		port_dest_count.set(port_dest, port_dest_count.get(port_dest) + 1);
	}
}


// clear old attack stats file
fs.writeFile(FILE_STATS, "ratio\tports\talarm_ratio\talarm_port\tevents_dshield_added\tevents_dshield_total\tevents_external_added\tevents_extern_total\n", function (err) { if (err) throw err; });

// update ban status of any source IP address
function check_attack_status() {
	//debug.add("checking attack status");
	// >>> Update window with new slot
	var time_seconds_now = Math.round(new Date().getTime() / 1000);
	var time_minutes_now = Math.round(time_seconds_now / 60);
	var timeslot_ratio = Math.round(time_seconds_now / TIMESLOT_SECONDS) % WINDOW_MAX_TIMESLOTS_RATIO;
	// window_ip_source_dest[x] = [a, b, c]
	window_ip_source_dest[timeslot_ratio] = ip_source_dest_count;
	// reset count for next slot
	ip_source_dest_count = {};

	var timeslot_ports = Math.round(time_seconds_now / TIMESLOT_SECONDS) % WINDOW_MAX_TIMESLOTS_PORT_DST;
	window_port_dest[timeslot_ports] = port_dest_count;
	// reset count for next slot
	port_dest_count = new Map();

	var alarm_ratio_triggered = false;
	var alarm_portdest_triggered = false;
	var alarm_attacks_per_monitor_triggered = false;

	var sampling = 100;

	// >>> check threshold: amount of unique attacked destination ports
	//debug.add("counting ports...");
	var port_dest_count_merged = new Map();
	var top_ports = new Set();
	alarm_portdest_triggered = false;

	/**/
	// MITIGATION block start: comment in to activate mitigation based on port frequency if needed
	for (var timeslot in window_port_dest) {
		window_port_dest[timeslot].forEach( function merge(count, port, mapObj) {
			if (port_dest_count_merged.get(port) == undefined) {
				port_dest_count_merged.set(port, count);
				//debug.add("setting port: "+port+" = count "+count);
			} else {
				port_dest_count_merged.set(port, port_dest_count_merged.get(port) + count);
			}
		});
	}
	// MITIGATION block end
	/**/

	// sort keys by value descending
	var top_ports_all = port_dest_count_merged.keys().sort(function(a,b){
		return port_dest_count_merged.get(b) - port_dest_count_merged.get(a)}
	);

	for (var x=0; x < 2 && x < top_ports_all.length; ++x) {
		top_ports.add(top_ports_all[x]);
	}

	debug.add("port: slot="+timeslot_ports+", unique dest ports: "+port_dest_count_merged.length);
		//", top 3 ports="+top_ports_all[0]+","+top_ports_all[1]+","+top_ports_all[2]);


	if (port_dest_count_merged.length > THRESHOLD_PORT_DEST_ATTACKED) {
		//debug.add("PRA-WARNING: too many unique destination ports: " +
		//" ("+ port_dest_count_merged.length +" > "+ THRESHOLD_PORT_DEST_ATTACKED +")");
		sampling = ((THRESHOLD_PORT_DEST_ATTACKED / port_dest_count_merged.length) * 100)
		alarm_portdest_triggered = true;
	}

	// amount of attacks for one monitor {id : count}
	var attacks_per_monitor = new Map();
	var attacks_total = 0;
	//debug.add("counting monitors...");
	var skipped_top_ports = 0;

	/*
	// MITIGATION block start: comment in to activate mitigation based on attack/monitor ratio if needed
	for (var slot_search in window_ip_source_dest) {
		for (var ipsrc_ipdst_portdst in window_ip_source_dest[slot_search]) {
			ipsrc_ipdst_portdst_split = ipsrc_ipdst_portdst.split(",");

			// TODO: activate for port normalizing (optional)
			//if (top_ports.contains(ipsrc_ipdst_portdst_split[2])) {
			//	// skip top x ports
			//	//debug.add("skipping port: "+ipsrc_ipdst_portdst_split[2]);
			//	skipped_top_ports += 1;
			//	continue;
			//}
			ip_dst = ipsrc_ipdst_portdst_split[1];
			count = window_ip_source_dest[slot_search][ipsrc_ipdst_portdst];

			//if (count > 100) {debug.add(ip_dst+"="+count);}
			if (attacks_per_monitor.get(ip_dst) == undefined) {
				//debug.add(ip_dst);
				attacks_per_monitor.set(ip_dst, count);
			} else {
				attacks_per_monitor.set(ip_dst, attacks_per_monitor.get(ip_dst) + count);
			}
			attacks_total += count;
		}
	}
	// MITIGATION block end
	*/

	//debug.add("output...");
	// >>> check treshold:
	// ratio -> attacks / unique monitors
	var unique_monitors_total = attacks_per_monitor.length;
	var ratio = attacks_total / unique_monitors_total;
	var alarm_ratio_triggered = false;

	/**/
	if (isNaN(ratio)) {
		// ratio 0 means: no attacks and/or no monitors -> nothing happened -> no attack
		ratio = 0;
	}

	/*
	if (is_active) {
		debug.add("mon: slot="+timeslot_ratio+", attacks="+attacks_total+", mon="+unique_monitors_total+
		", ratio="+ratio+", top removed="+skipped_top_ports);
	}*/

	if (ratio != 0 && ratio <= THRESHOLD_ATTACKS_MONITOR_RATIO) {
		//debug.add("PRA-WARNING: Ratio attacks/monitors is too low: "+ratio+" <= "+THRESHOLD_ATTACKS_MONITOR_RATIO);
		// ratio is at minimum 1, so minimum is: 1/(threshold_ratio) -> set minimum to 0/(threshold_ratio-1)
		sampling_ratio = (((ratio-1)/(THRESHOLD_ATTACKS_MONITOR_RATIO-1)) * 100);

		if (sampling_ratio < sampling)  {
			sampling = sampling_ratio;
		}
		alarm_ratio_triggered = true;
	}


	alarm_attacks_per_monitor_triggered = false;

	/*
	// >>> check threshold: attacks per monitor
	attacks_per_monitor.forEach(function each(amount, monitor, mapObj) {
		//debug.add(monitor+"="+amount);
		if (amount > THRESHOLD_ATTACKS_PER_MONITOR) {
			//debug.add("PRA-WARNING: Monitor "+monitor+" is attacked too many times("+
			//amount+" > "+THRESHOLD_ATTACKS_PER_MONITOR+")");
			sampling_attack_per_mon = ((THRESHOLD_ATTACKS_PER_MONITOR/amount) * 100);
			if (sampling_attack_per_mon < sampling)  {
				sampling = sampling_attack_per_mon;
			}
			alarm_attacks_per_monitor_triggered = true;
		}
	});
	*/

	if (is_active) {
		sampling_percent = sampling;

		if ((!alarm_ratio_triggered) && (!alarm_portdest_triggered) && (!alarm_attacks_per_monitor_triggered)) {
			sampling_percent = 100;
			sampling_active = false;
		} else if (sampling_percent < 100) {
			debug.add("sampling="+sampling_percent+"%, ratio="+alarm_ratio_triggered+
			", port_dest="+alarm_portdest_triggered+", atttacks_per_mon="+alarm_attacks_per_monitor_triggered+
			", noise added/total="+events_dshield_added+"/"+events_dshield_total);
			sampling_active = true;
		}
	} else {
		ratio = 0;
	}
	// unban IPs which ban timeout exceeded
	/*
	for (var ip in banned_ips) {
		if ( (time_minutes_now - banned_ips[ip]) >= SOURCE_IP_BAN_MINUTES) {
			debug.add("unbanning ip: "+ ip +" ("+ (time_minutes_now - banned_ips[ip]) +" minutes passed)");
			delete banned_ips[ip];
		}
	}
	*/
	fs.appendFile(FILE_STATS, ratio+"\t"+port_dest_count_merged.length+"\t"+alarm_ratio_triggered+"\t"
		+alarm_portdest_triggered+"\t"+events_dshield_added+"\t"
		+events_dshield_total+"\t"+events_external_added+"\t"+events_external_total+"\n", function (err) {});

	//debug.add("finished checking");
}

// this is exclusively used by addNoiseEvent()
var data = {
	sensorname: null,
	sensortype: "RandomType",
	src: {
		ip: null,
		port: null,
		ll: [0,0],
		country: "",
		cc: 0,
		city: ""
	},
	dst: {
		ip: null,
		port: null,
		ll: [0,0],
		country: "",
		cc: 0,
		city: ""
	},
	type: 11,
	log: null,
	md5sum: "0123456789",
	date: new Date(),
	authorized: false
};

function addNoiseEvent(ip_src, port_src, ip_dst, port_dst, monitorname, doEmit) {
	if (typeof io == 'undefined') {
		//debug.add("no io, no emit -> I quit");
		return;
	}

	//debug.add("adding, name/ip/port: " + data.sensorname + " / " + data.src.ip + " / " +
	//	data.dst.ip + " / " + data.src.port + " / " + data.dst.port);
	/*
	// not needed for PRA
	ipa = lookUpIp(ip_src);

	if(ipa){
		data.src["ll"] = [ipa.longitude, ipa.latitude];
		data.src["country"] = ipa.country_name;
		data.src["cc"] = ipa.country_code;
		data.src["city"] = ipa.city || "";
	}

	ipd = lookUpIp(ip_dst);

	if(ipd){
		data.dst["ll"] = [ipd.longitude, ipd.latitude];
		data.dst["country"] = ipd.country_name;
		data.dst["cc"] = ipd.country_code;
		data.dst["city"] = ipd.city || "";
	}
	*/
	//debug.add("> " + chance.integer({min: 0, max: 65535}));
	//debug.add("> " + chance.ip());
	//debug.add("Sending random entry: " + data["src"]["ip"]);

	update_slot(monitorname, ip_src, ip_dst, port_dst);

	// do emit data to client if sampled
	if (doEmit) {
		data.src.ip = ip_src;
		data.src.port = port_src;
		data.dst.ip = ip_dst;
		data.dst.port = port_dst;
		data.sensorname = monitorname;
		io.sockets.emit("markIncident", data);
	}
}

// add artificial noise to the report
function add_entry_random() {
	addNoiseEvent(chance.ip(),
		chance.integer({min: 0, max: 65535}),
		chance.ip(),
		chance.integer({min: 0, max: 65535}),
		"RandomMonitor_" + chance.integer({min: 0, max: 100}),
		true
	);
}

function add_entry_dshield() {
	e = dshield_entries.pop();
	//e = dshield_entries.shift();

	//debug.add("adding noise using dshield data: "+e);
	// we can not differentiate between PRA and non-PRA events so we have to sample both
	// accept entry if: sampling not active (sample 100%) or random value tells us so
	// add noise if: attack mitigation is not active, mitigation not triggered or random tells us to
	sample_entry = (!is_active || !sampling_active || chance.floating({min: 1, max: 100}) <= sampling_percent);

	try {
		addNoiseEvent(e[0], e[1], e[2], e[3], e[4], sample_entry);
		if (sample_entry) {
			events_dshield_added += 1;
		}
		events_dshield_total += 1;
	}
	catch (err) {
	// no events left
	}
}

function applyMitigation(ip_src, ip_dst, port_dst, monitor_id) {
	// praMitigation.applyMitigation(items[i].src.ip, items[i].dst.ip, items[i].dst.port
	events_external_total += 1;

	if (!is_active) {
		return false;
	} else {
		update_slot(monitor_id, ip_src, ip_dst, port_dst);

		// allow entry if sampling is not active or random tells us to
		is_sampled = (!sampling_active || chance.floating({min: 1, max: 100}) > sampling_percent);

		if (is_sampled) {
			events_external_added += 1;
		}

		return !is_sampled;
	}
}

function setActive(activeState) {
	debug.add("setting new PRA mitigation active state: " + activeState);
	is_active = activeState;
}

module.exports = {
	//init: init,
	setActive: setActive,
	applyMitigation: applyMitigation
};
