<commit_before>/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var createMatrix = require('regl-scattermatrix');

var Lib = require('../../lib');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
var calcColorscales = require('../scatter/colorscale_calc');
var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;

var BADNUM = require('../../constants/numerical').BADNUM;
var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;

function calc(gd, trace) {
    var stash = {};
    var opts = {};
    var i, xa, ya;

    var dimLength = trace.dimensions.length;
    var hasTooManyPoints = (dimLength * trace._commonLength) > TOO_MANY_POINTS;
    var matrix = opts.data = new Array(dimLength);

    for(i = 0; i < dimLength; i++) {
        // using xa or ya should make no difference here
        xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        matrix[i] = makeCalcdata(xa, trace, trace.dimensions[i]);
    }

    calcColorscales(trace);
    Lib.extendFlat(opts, convertMarkerStyle(trace));

    for(i = 0; i < dimLength; i++) {
        xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        ya = AxisIDs.getFromId(gd, trace.yaxes[i]);

        // Re-use SVG scatter axis expansion routine except
        // for graph with very large number of points where it
        // performs poorly.
        // In big data case, fake Axes.expand outputs with data bounds,
        // and an average size for array marker.size inputs.
        var ppad;
        if(hasTooManyPoints) {
            ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
        } else {
            ppad = calcMarkerSize(trace, trace._commonLength);
        }
        calcAxisExpansion(gd, trace, xa, ya, matrix[i], matrix[i], ppad);
    }

    var scene = stash.scene = sceneUpdate(gd, stash);
    if(!scene.matrix) scene.matrix = true;
    scene.matrixOptions = opts;

    return [{x: false, y: false, t: stash, trace: trace}];
}

function makeCalcdata(ax, trace, dim) {
    var i;

    var cdata = ax.makeCalcdata({
        v: dim.values,
        vcalendar: trace.calendar
    }, 'v');

    for(i = 0; i < cdata.length; i++) {
        cdata[i] = cdata[i] === BADNUM ? NaN : cdata[i];
    }

    if(ax.type === 'log') {
        for(i = 0; i < cdata.length; i++) {
            cdata[i] = ax.c2l(cdata[i]);
        }
    }

    return cdata;
}

// TODO do we need this?
function sceneUpdate(gd, stash) {
    var scene = stash._scene;

    var reset = {
        dirty: true,
        opts: null
    };

    var first = {
        selectBatch: null,
        unselectBatch: null,
        matrix: false,
        select: null
    };

    if(!scene) {
        scene = stash._scene = Lib.extendFlat({}, reset, first);

        // TODO should we use something like this on drag?
        scene.update = function update(opt) {
            if(scene.matrix) scene.matrix.update(opt);
            scene.draw();
        };

        scene.draw = function draw() {
            if(scene.matrix) scene.matrix.draw();

            // TODO selection stuff

            // do we need to use this flag anywhere??
            scene.dirty = false;
        };

        // make sure canvas is clear
        scene.clear = function clear() {
            // TODO
        };

        // remove selection
        scene.clearSelect = function clearSelect() {
            if(!scene.selectBatch) return;
            scene.selectBatch = null;
            scene.unselectBatch = null;
            scene.matrix.update(scene.opts);
            scene.clear();
            scene.draw();
        };

        // remove scene resources
        scene.destroy = function destroy() {
            if(scene.matrix) scene.matrix.destroy();

            scene.opts = null;
            scene.selectBatch = null;
            scene.unselectBatch = null;

            stash._scene = null;
        };
    }

    // In case if we have scene from the last calc - reset data
    if(!scene.dirty) {
        Lib.extendFlat(scene, reset);
    }

    return scene;
}

function plot(gd, _, cdata) {
    if(!cdata.length) return;

    for(var i = 0; i < cdata.length; i++) {
        plotOne(gd, cdata[i][0]);
    }
}

function plotOne(gd, cd0) {
    var fullLayout = gd._fullLayout;
    var gs = fullLayout._size;
    var scene = cd0.t.scene;
    var trace = cd0.trace;
    var regl = fullLayout._glcanvas.data()[0].regl;

    var dimLength = trace.dimensions.length;
    var viewOpts = {
        ranges: new Array(dimLength),
        domains: new Array(dimLength)
    };

    for(var i = 0; i < dimLength; i++) {
        var xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        var ya = AxisIDs.getFromId(gd, trace.yaxes[i]);
        viewOpts.ranges[i] = [xa.range[0], ya.range[0], xa.range[1], ya.range[1]];
        viewOpts.domains[i] = [xa.domain[0], ya.domain[0], xa.domain[1], ya.domain[1]];
    }

    viewOpts.viewport = [gs.l, gs.b, fullLayout.width - gs.r, fullLayout.height - gs.t];

    if(scene.matrix === true) {
        scene.matrix = createMatrix(regl);
    }

    scene.matrix.update(scene.matrixOptions);
    scene.matrix.update(viewOpts);
    scene.matrix.draw();
}

// TODO splom 'needs' the grid component, register it here?

function hoverPoints() {

}

function selectPoints() {

}

module.exports = {
    moduleType: 'trace',
    name: 'splom',

    basePlotModule: require('./base_plot'),
    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],

    attributes: require('./attributes'),
    supplyDefaults: require('./defaults'),

    calc: calc,
    plot: plot,
    hoverPoints: hoverPoints,
    selectPoints: selectPoints,
    style: function() {},

    meta: {
        description: [
            'SPLOM !!!'
        ].join(' ')
    }
};
<commit_msg>fixup _scene ref in calcdata stash<commit_after>/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var createMatrix = require('regl-scattermatrix');

var Lib = require('../../lib');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
var calcColorscales = require('../scatter/colorscale_calc');
var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;

var BADNUM = require('../../constants/numerical').BADNUM;
var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;

function calc(gd, trace) {
    var stash = {};
    var opts = {};
    var i, xa, ya;

    var dimLength = trace.dimensions.length;
    var hasTooManyPoints = (dimLength * trace._commonLength) > TOO_MANY_POINTS;
    var matrix = opts.data = new Array(dimLength);

    for(i = 0; i < dimLength; i++) {
        // using xa or ya should make no difference here
        xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        matrix[i] = makeCalcdata(xa, trace, trace.dimensions[i]);
    }

    calcColorscales(trace);
    Lib.extendFlat(opts, convertMarkerStyle(trace));

    for(i = 0; i < dimLength; i++) {
        xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        ya = AxisIDs.getFromId(gd, trace.yaxes[i]);

        // Re-use SVG scatter axis expansion routine except
        // for graph with very large number of points where it
        // performs poorly.
        // In big data case, fake Axes.expand outputs with data bounds,
        // and an average size for array marker.size inputs.
        var ppad;
        if(hasTooManyPoints) {
            ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
        } else {
            ppad = calcMarkerSize(trace, trace._commonLength);
        }
        calcAxisExpansion(gd, trace, xa, ya, matrix[i], matrix[i], ppad);
    }

    var scene = stash._scene = sceneUpdate(gd, stash);
    if(!scene.matrix) scene.matrix = true;
    scene.matrixOptions = opts;

    return [{x: false, y: false, t: stash, trace: trace}];
}

function makeCalcdata(ax, trace, dim) {
    var i;

    var cdata = ax.makeCalcdata({
        v: dim.values,
        vcalendar: trace.calendar
    }, 'v');

    for(i = 0; i < cdata.length; i++) {
        cdata[i] = cdata[i] === BADNUM ? NaN : cdata[i];
    }

    if(ax.type === 'log') {
        for(i = 0; i < cdata.length; i++) {
            cdata[i] = ax.c2l(cdata[i]);
        }
    }

    return cdata;
}

// TODO do we need this?
function sceneUpdate(gd, stash) {
    var scene = stash._scene;

    var reset = {
        dirty: true,
        opts: null
    };

    var first = {
        selectBatch: null,
        unselectBatch: null,
        matrix: false,
        select: null
    };

    if(!scene) {
        scene = stash._scene = Lib.extendFlat({}, reset, first);

        // TODO should we use something like this on drag?
        scene.update = function update(opt) {
            if(scene.matrix) scene.matrix.update(opt);
            scene.draw();
        };

        scene.draw = function draw() {
            if(scene.matrix) scene.matrix.draw();

            // TODO selection stuff

            // do we need to use this flag anywhere??
            scene.dirty = false;
        };

        // make sure canvas is clear
        scene.clear = function clear() {
            // TODO
        };

        // remove selection
        scene.clearSelect = function clearSelect() {
            if(!scene.selectBatch) return;
            scene.selectBatch = null;
            scene.unselectBatch = null;
            scene.matrix.update(scene.opts);
            scene.clear();
            scene.draw();
        };

        // remove scene resources
        scene.destroy = function destroy() {
            if(scene.matrix) scene.matrix.destroy();

            scene.opts = null;
            scene.selectBatch = null;
            scene.unselectBatch = null;

            stash._scene = null;
        };
    }

    // In case if we have scene from the last calc - reset data
    if(!scene.dirty) {
        Lib.extendFlat(scene, reset);
    }

    return scene;
}

function plot(gd, _, cdata) {
    if(!cdata.length) return;

    for(var i = 0; i < cdata.length; i++) {
        plotOne(gd, cdata[i][0]);
    }
}

function plotOne(gd, cd0) {
    var fullLayout = gd._fullLayout;
    var gs = fullLayout._size;
    var scene = cd0.t._scene;
    var trace = cd0.trace;
    var regl = fullLayout._glcanvas.data()[0].regl;

    var dimLength = trace.dimensions.length;
    var viewOpts = {
        ranges: new Array(dimLength),
        domains: new Array(dimLength)
    };

    for(var i = 0; i < dimLength; i++) {
        var xa = AxisIDs.getFromId(gd, trace.xaxes[i]);
        var ya = AxisIDs.getFromId(gd, trace.yaxes[i]);
        viewOpts.ranges[i] = [xa.range[0], ya.range[0], xa.range[1], ya.range[1]];
        viewOpts.domains[i] = [xa.domain[0], ya.domain[0], xa.domain[1], ya.domain[1]];
    }

    viewOpts.viewport = [gs.l, gs.b, fullLayout.width - gs.r, fullLayout.height - gs.t];

    if(scene.matrix === true) {
        scene.matrix = createMatrix(regl);
    }

    scene.matrix.update(scene.matrixOptions);
    scene.matrix.update(viewOpts);
    scene.matrix.draw();
}

// TODO splom 'needs' the grid component, register it here?

function hoverPoints() {

}

function selectPoints() {

}

module.exports = {
    moduleType: 'trace',
    name: 'splom',

    basePlotModule: require('./base_plot'),
    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],

    attributes: require('./attributes'),
    supplyDefaults: require('./defaults'),

    calc: calc,
    plot: plot,
    hoverPoints: hoverPoints,
    selectPoints: selectPoints,
    style: function() {},

    meta: {
        description: [
            'SPLOM !!!'
        ].join(' ')
    }
};
<|endoftext|>


<commit_before>// karma - reward good and penalize bad mail senders

var ipaddr = require('ipaddr.js');
var redis = require('redis');
var db;

exports.register = function () {
    var plugin = this;

    this.register_hook('init_master',  'karma_onInit');
    this.register_hook('init_child',   'karma_onInit');
    this.register_hook('deny',         'karma_onDeny');
    this.register_hook('lookup_rdns',  'karma_onConnect');
    this.register_hook('mail',         'karma_onMailFrom');
    this.register_hook('rcpt',         'karma_onRcptTo');
    this.register_hook('data',         'karma_onData');
    this.register_hook('data_post',    'karma_onDataPost');
    this.register_hook('disconnect',   'karma_onDisconnect');
};

exports.karma_onInit = function (next, server) {
    initRedisConnection(this);
    return next();
};

function initRedisConnection(self) {
    if (db && db.ping()) return;   // connection is good

    var config     = self.config.get('karma.ini');
    var redis_ip  = '127.0.0.1';
    var redis_port = '6379';
    if (config.redis) {
        redis_ip = config.redis.server_ip || '127.0.0.1';
        redis_port = config.redis.server_port || '6379';
    };

    db = redis.createClient(redis_port, redis_ip);
    db.on('error', function (error) {
        self.logerror('Redis error: ' + error.message);
        db.end();
        db = null;
    });
};

exports.karma_onConnect = function (next, connection) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    initRedisConnection(this);
    initConnectionNote(connection, config);

    var r_ip = connection.remote_ip;
    var dbkey = 'karma|' + r_ip;
    var expire = (config.main.expire_days || 60) * 86400; // convert to days

    function initRemoteIP () {
        db.multi()
            .hmset(dbkey, {'penalty_start_ts': 0, 'bad': 0, 'good': 0, 'connections': 1})
            .expire(dbkey, expire)
            .exec();
        connection.logdebug(plugin,"first connect");
    };

    db.multi()
        .get('concurrent|' + r_ip)
        .hgetall(dbkey)
        .exec(function redisResults (err,replies) {
            if (err) {
                connection.logdebug(plugin,"err: " + err);
                return next();
            };

            if (replies[1] === null) { initRemoteIP(); return next(); };

            db.hincrby(dbkey, 'connections', 1); // increment total connections
            db.expire(dbkey, expire);            // extend expiration date

            var dbr = replies[1]; // 2nd element of DB reply is our karma object
            var history = (dbr.good || 0) - (dbr.bad || 0);
            connection.notes.karma.history = history;

            var summary = dbr.bad + " bad, " + dbr.good + " good, "
                         + dbr.connections + " connects, " + history + " history";

            var too_many = checkConcurrency(plugin, 'concurrent|' + r_ip, replies[0], history);
            if (too_many) {
                connection.loginfo(plugin, too_many + ", (" + summary + ")");
                return next(DENYSOFT, too_many);
            };

            if (dbr.penalty_start_ts === '0') {
                connection.loginfo(plugin, "no penalty, " + karmaSummary(connection));
                return next();
            }

            var days_old = (Date.now() - Date.parse(dbr.penalty_start_ts)) / 86.4;
            var penalty_days = config.main.penalty_days;
            if (days_old >= penalty_days) {
                connection.loginfo(plugin, "penalty expired, " + karmaSummary(connection));
                return next();
            }

            var left = +(penalty_days - days_old).toFixed(2);
            var mess = "Bad karma, you can try again in " + left + " more days.";

            return next(DENY, mess);
        });

    checkAwards(config, connection, plugin);
};

exports.karma_onDeny = function (next, connection, params) {
    /* params
    ** [0] = plugin return value (constants.deny or constants.denysoft)
    ** [1] = plugin return message
    */

    var pi_name     = params[2];
    var pi_function = params[3];
    var pi_params   = params[4];
    var pi_hook     = params[5];

    var plugin = this;
    var transaction = connection.transaction;

    var config = this.config.get('karma.ini');
    initConnectionNote(connection, config);

    // CONSIDER: decrement karma two points for a 5XX deny?
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push(pi_name);

    connection.loginfo(plugin, 'deny, ' + karmaSummary(connection));

    checkAwards(config, connection, plugin);
    return next();
};

exports.karma_onMailFrom = function (next, connection, params) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    checkSpammyTLD(params[0], config, connection, plugin);
    checkSyntaxMailFrom(connection, plugin);

    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, karmaSummary(connection));
    return next();
};

exports.karma_onRcptTo = function (next, connection, params) {
    var plugin = this;
    var rcpt = params[0];
    var config = this.config.get('karma.ini');

    checkSyntaxRcptTo(connection, plugin);
    var too_many = checkMaxRecipients(connection, plugin, config);
    if (too_many) return next(DENY, too_many);

    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, karmaSummary(connection));
    return next();
};

exports.karma_onData = function (next, connection) {
// cut off bad senders at DATA to prevent transferring the message
    var config = this.config.get('karma.ini');
    var negative_limit = config.thresholds.negative || -5;
    var karma = connection.notes.karma * 1;

    if (karma.connection <= negative_limit) {
        return next(DENY, "very bad karma score: " + karma);
    }

    checkAwards(config, connection, this);
    return next();
}

exports.karma_onDataPost = function (next, connection) {
    connection.transaction.add_header('X-Haraka-Karma',
        karmaSummary(connection)
    );
    var config = this.config.get('karma.ini');
    checkAwards(config, connection, this);
    return next();
}

exports.karma_onDisconnect = function (next, connection) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    initRedisConnection(this);
    if (config.concurrency) db.incrby('concurrent|' + connection.remote_ip, -1);

    var k = connection.notes.karma;
    if (!k) { connection.logerror(plugin, "karma note missing!"); return next(); };

    if (!k.connection) {
        connection.loginfo(plugin, "neutral: " + karmaSummary(connection));
        return next();
    };

    var key = 'karma|' + connection.remote_ip;
    var history = k.history;

    if (config.threshold) {
        var pos_lim = config.thresholds.positive || 2;

        if (k.connection > pos_lim) {
            db.hincrby(key, 'good', 1);
            connection.loginfo(plugin, "positive: " + karmaSummary(connection));
            return next();
        };

        var bad_limit = config.thresholds.negative || -3;
        if (k.connection < bad_limit) {
            db.hincrby(key, 'bad', 1);
            history--;

            if (history <= config.thresholds.history_negative) {
                if (history < -5) {
                    db.hset(key, 'penalty_start_ts', addDays(Date(), history * -1));
                    connection.loginfo(plugin, "penalty box bonus!: " + karmaSummary(connection));
                }
                else {
                    db.hset(key, 'penalty_start_ts', Date());
                    connection.loginfo(plugin, "penalty box: " + karmaSummary(connection));
                }
                return next();
            }
        }
    };
    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, "no action, " + karmaSummary(connection));
    return next();
};

function karmaSummary(c) {
    var k = c.notes.karma;
    return '('
        + 'conn:' + k.connection
        + ', hist: ' + k.history
        + (k.penalties.length ? ', penalties: '+ k.penalties : '')
        + (k.awards.length    ? ', awards: '   + k.awards    : '')
        + ')';
}

function addDays(date, days) {
    var result = new Date(date);
    result.setDate(date.getDate() + days);
    return result;
}

function checkAwards (config, connection, plugin) {
    if (!connection.notes.karma) return;
    if (!connection.notes.karma.todo) return;
    if (!plugin) plugin = connection;
    var awards = config.awards;

    Object.keys(connection.notes.karma.todo).forEach(function(key) {
        var e = key.split('@').slice(0,2);
        var suffix = e[0];
        var wants = e[1];

        // assemble the object path using the note name
        var note = assembleNoteObj(connection, suffix);
        if (note == null || note === false) {
            // connection.logdebug(plugin, "no connection note: " + key);
            if (!connection.transaction) return;
            note = assembleNoteObj(connection.transaction, suffix);
            if (note == null || note === false) {
                // connection.logdebug(plugin, "no transaction note: " + key);
                return;
            }
        };

        if (wants && note && (!note.toString().match(new RegExp(wants, 'i')))) {
            // connection.logdebug(plugin, "key " + suffix + " wants: " + wants + " but saw: " + note);
            return;
        };

        var karma_to_apply = connection.notes.karma.todo[key];
        if (!karma_to_apply) return;
        if (Number(karma_to_apply) === 'NaN') return;  // garbage in config

        connection.notes.karma.connection += karma_to_apply * 1;
        connection.loginfo(plugin, "applied " + key + " karma: " + karma_to_apply);
        delete connection.notes.karma.todo[key];

        var trimmed = key.substring(0,5) === 'notes' ? key.substring(6) : key;
        if (karma_to_apply > 0) { connection.notes.karma.awards.push(trimmed); };
        if (karma_to_apply < 0) { connection.notes.karma.penalties.push(trimmed); };
    });
}

function checkConcurrency(plugin, con_key, val, history) {
    var config = plugin.config.get('karma.ini');

    if (!config.concurrency) return;

    var count = val || 0;    // add this connection
    count++;
    db.incr(con_key);        // increment Redis, (creates if needed)
    db.expire(con_key, 4 * 60);     // expire after 4 min

    var reject=0;
    if (history <  0 && count > config.concurrency.bad) reject++;
    if (history >  0 && count > config.concurrency.good)    reject++;
    if (history == 0 && count > config.concurrency.neutral) reject++;
    if (reject) return "too many connections for you: " + count;
    return;
};

function checkMaxRecipients(connection, plugin, config) {
    if (!config.recipients) return;     // disabled in config file
    var c = connection.rcpt_count;
    var count = c.accept + c.tempfail + c.reject + 1;
    if (count <= 1) return;           // everybody is allowed one

    connection.logdebug(plugin, "recipient count: " + count );

    var desc = history > 3 ? 'good' : history >= 0 ? 'neutral' : 'bad';

    var cr = config.recipients;

    // the deeds of their past shall not go unnoticed!
    var history = connection.notes.karma.history;
    if (history > 3 && count <= cr.good) return;
    if (history > -1 && count <= cr.neutral) return;

    // this is *more* strict than history, b/c they have fewer opportunity
    // to score positive karma this early in the connection. senders with
    // good history will rarely see these limits.
    var karma = connection.notes.karma.connection;
    if (karma >  3 && count <= cr.good) return;
    if (karma >= 0 && count <= cr.neutral) return;

    return 'too many recipients (' + count + ') for ' + desc + ' karma';
}

function checkSpammyTLD(mail_from, config, connection, plugin) {
    if (!config.spammy_tlds) return;
    if (mail_from.isNull()) return;    // null sender (bounce)

    var from_tld = mail_from.host.split('.').pop();
    // connection.logdebug(plugin, "from_tld: " + from_tld);

    var tld_penalty = (config.spammy_tlds[from_tld] || 0) * 1; // force numeric
    if (tld_penalty === 0) return;

    connection.loginfo(plugin, "spammy TLD: " + tld_penalty);
    connection.notes.karma.connection += tld_penalty;
    connection.notes.karma.penalties.push('spammy.TLD');
};

function checkSyntaxMailFrom(connection, plugin) {
    var full_from = connection.current_line;
    // connection.logdebug(plugin, "mail_from: " + full_from);

// look for an illegal (RFC 5321,2821,821) space in envelope from
    if (full_from.toUpperCase().substring(0,11) === 'MAIL FROM:<') return;

    connection.loginfo(plugin, "illegal envelope address format: " + full_from );
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push('rfc5321.MailFrom');
};

function checkSyntaxRcptTo(connection, plugin) {
    // check for an illegal RFC (2)821 space in envelope recipient
    var full_rcpt = connection.current_line;
    if (full_rcpt.toUpperCase().substring(0,9) === 'RCPT TO:<') return;

    connection.loginfo(plugin, "illegal envelope address format: " + full_rcpt );
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push('rfc5321.RcptTo');
};

function initConnectionNote(connection, config) {
    if (connection.notes.karma) return; // init once per connection
    connection.notes.karma = {
        connection: 0,
        history: 0,
        awards: [],
        penalties: [ ],
        todo: {},
    };

    // todo is a list of connection/transaction notes to 'watch' for.
    // When discovered, award their karma points to the connection
    // and remove them from todo.
    var awards = config.awards;
    if (!awards) return;
    Object.keys(awards).forEach(function(key) {
        connection.notes.karma.todo[key] = awards[key];
    });
};

function assembleNoteObj(prefix, key) {
    var note = prefix;
    var parts = key.split('.');
    while(parts.length > 0) {
        note = note[parts.shift()];
        if (note == null) break;
    }
    return note;
};
<commit_msg>karma: return a 4XX error on max recipients<commit_after>// karma - reward good and penalize bad mail senders

var ipaddr = require('ipaddr.js');
var redis = require('redis');
var db;

exports.register = function () {
    var plugin = this;

    this.register_hook('init_master',  'karma_onInit');
    this.register_hook('init_child',   'karma_onInit');
    this.register_hook('deny',         'karma_onDeny');
    this.register_hook('lookup_rdns',  'karma_onConnect');
    this.register_hook('mail',         'karma_onMailFrom');
    this.register_hook('rcpt',         'karma_onRcptTo');
    this.register_hook('data',         'karma_onData');
    this.register_hook('data_post',    'karma_onDataPost');
    this.register_hook('disconnect',   'karma_onDisconnect');
};

exports.karma_onInit = function (next, server) {
    initRedisConnection(this);
    return next();
};

function initRedisConnection(self) {
    if (db && db.ping()) return;   // connection is good

    var config     = self.config.get('karma.ini');
    var redis_ip  = '127.0.0.1';
    var redis_port = '6379';
    if (config.redis) {
        redis_ip = config.redis.server_ip || '127.0.0.1';
        redis_port = config.redis.server_port || '6379';
    };

    db = redis.createClient(redis_port, redis_ip);
    db.on('error', function (error) {
        self.logerror('Redis error: ' + error.message);
        db.end();
        db = null;
    });
};

exports.karma_onConnect = function (next, connection) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    initRedisConnection(this);
    initConnectionNote(connection, config);

    var r_ip = connection.remote_ip;
    var dbkey = 'karma|' + r_ip;
    var expire = (config.main.expire_days || 60) * 86400; // convert to days

    function initRemoteIP () {
        db.multi()
            .hmset(dbkey, {'penalty_start_ts': 0, 'bad': 0, 'good': 0, 'connections': 1})
            .expire(dbkey, expire)
            .exec();
        connection.logdebug(plugin,"first connect");
    };

    db.multi()
        .get('concurrent|' + r_ip)
        .hgetall(dbkey)
        .exec(function redisResults (err,replies) {
            if (err) {
                connection.logdebug(plugin,"err: " + err);
                return next();
            };

            if (replies[1] === null) { initRemoteIP(); return next(); };

            db.hincrby(dbkey, 'connections', 1); // increment total connections
            db.expire(dbkey, expire);            // extend expiration date

            var dbr = replies[1]; // 2nd element of DB reply is our karma object
            var history = (dbr.good || 0) - (dbr.bad || 0);
            connection.notes.karma.history = history;

            var summary = dbr.bad + " bad, " + dbr.good + " good, "
                         + dbr.connections + " connects, " + history + " history";

            var too_many = checkConcurrency(plugin, 'concurrent|' + r_ip, replies[0], history);
            if (too_many) {
                connection.loginfo(plugin, too_many + ", (" + summary + ")");
                return next(DENYSOFT, too_many);
            };

            if (dbr.penalty_start_ts === '0') {
                connection.loginfo(plugin, "no penalty, " + karmaSummary(connection));
                return next();
            }

            var days_old = (Date.now() - Date.parse(dbr.penalty_start_ts)) / 86.4;
            var penalty_days = config.main.penalty_days;
            if (days_old >= penalty_days) {
                connection.loginfo(plugin, "penalty expired, " + karmaSummary(connection));
                return next();
            }

            var left = +(penalty_days - days_old).toFixed(2);
            var mess = "Bad karma, you can try again in " + left + " more days.";

            return next(DENY, mess);
        });

    checkAwards(config, connection, plugin);
};

exports.karma_onDeny = function (next, connection, params) {
    /* params
    ** [0] = plugin return value (constants.deny or constants.denysoft)
    ** [1] = plugin return message
    */

    var pi_name     = params[2];
    var pi_function = params[3];
    var pi_params   = params[4];
    var pi_hook     = params[5];

    var plugin = this;
    var transaction = connection.transaction;

    var config = this.config.get('karma.ini');
    initConnectionNote(connection, config);

    // CONSIDER: decrement karma two points for a 5XX deny?
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push(pi_name);

    connection.loginfo(plugin, 'deny, ' + karmaSummary(connection));

    checkAwards(config, connection, plugin);
    return next();
};

exports.karma_onMailFrom = function (next, connection, params) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    checkSpammyTLD(params[0], config, connection, plugin);
    checkSyntaxMailFrom(connection, plugin);

    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, karmaSummary(connection));
    return next();
};

exports.karma_onRcptTo = function (next, connection, params) {
    var plugin = this;
    var rcpt = params[0];
    var config = this.config.get('karma.ini');

    checkSyntaxRcptTo(connection, plugin);
    var too_many = checkMaxRecipients(connection, plugin, config);
    if (too_many) return next(DENYSOFT, too_many);

    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, karmaSummary(connection));
    return next();
};

exports.karma_onData = function (next, connection) {
// cut off bad senders at DATA to prevent transferring the message
    var config = this.config.get('karma.ini');
    var negative_limit = config.thresholds.negative || -5;
    var karma = connection.notes.karma * 1;

    if (karma.connection <= negative_limit) {
        return next(DENY, "very bad karma score: " + karma);
    }

    checkAwards(config, connection, this);
    return next();
}

exports.karma_onDataPost = function (next, connection) {
    connection.transaction.add_header('X-Haraka-Karma',
        karmaSummary(connection)
    );
    var config = this.config.get('karma.ini');
    checkAwards(config, connection, this);
    return next();
}

exports.karma_onDisconnect = function (next, connection) {
    var plugin = this;
    var config = this.config.get('karma.ini');

    initRedisConnection(this);
    if (config.concurrency) db.incrby('concurrent|' + connection.remote_ip, -1);

    var k = connection.notes.karma;
    if (!k) { connection.logerror(plugin, "karma note missing!"); return next(); };

    if (!k.connection) {
        connection.loginfo(plugin, "neutral: " + karmaSummary(connection));
        return next();
    };

    var key = 'karma|' + connection.remote_ip;
    var history = k.history;

    if (config.threshold) {
        var pos_lim = config.thresholds.positive || 2;

        if (k.connection > pos_lim) {
            db.hincrby(key, 'good', 1);
            connection.loginfo(plugin, "positive: " + karmaSummary(connection));
            return next();
        };

        var bad_limit = config.thresholds.negative || -3;
        if (k.connection < bad_limit) {
            db.hincrby(key, 'bad', 1);
            history--;

            if (history <= config.thresholds.history_negative) {
                if (history < -5) {
                    db.hset(key, 'penalty_start_ts', addDays(Date(), history * -1));
                    connection.loginfo(plugin, "penalty box bonus!: " + karmaSummary(connection));
                }
                else {
                    db.hset(key, 'penalty_start_ts', Date());
                    connection.loginfo(plugin, "penalty box: " + karmaSummary(connection));
                }
                return next();
            }
        }
    };
    checkAwards(config, connection, plugin);
    connection.loginfo(plugin, "no action, " + karmaSummary(connection));
    return next();
};

function karmaSummary(c) {
    var k = c.notes.karma;
    return '('
        + 'conn:' + k.connection
        + ', hist: ' + k.history
        + (k.penalties.length ? ', penalties: '+ k.penalties : '')
        + (k.awards.length    ? ', awards: '   + k.awards    : '')
        + ')';
}

function addDays(date, days) {
    var result = new Date(date);
    result.setDate(date.getDate() + days);
    return result;
}

function checkAwards (config, connection, plugin) {
    if (!connection.notes.karma) return;
    if (!connection.notes.karma.todo) return;
    if (!plugin) plugin = connection;
    var awards = config.awards;

    Object.keys(connection.notes.karma.todo).forEach(function(key) {
        var e = key.split('@').slice(0,2);
        var suffix = e[0];
        var wants = e[1];

        // assemble the object path using the note name
        var note = assembleNoteObj(connection, suffix);
        if (note == null || note === false) {
            // connection.logdebug(plugin, "no connection note: " + key);
            if (!connection.transaction) return;
            note = assembleNoteObj(connection.transaction, suffix);
            if (note == null || note === false) {
                // connection.logdebug(plugin, "no transaction note: " + key);
                return;
            }
        };

        if (wants && note && (!note.toString().match(new RegExp(wants, 'i')))) {
            // connection.logdebug(plugin, "key " + suffix + " wants: " + wants + " but saw: " + note);
            return;
        };

        var karma_to_apply = connection.notes.karma.todo[key];
        if (!karma_to_apply) return;
        if (Number(karma_to_apply) === 'NaN') return;  // garbage in config

        connection.notes.karma.connection += karma_to_apply * 1;
        connection.loginfo(plugin, "applied " + key + " karma: " + karma_to_apply);
        delete connection.notes.karma.todo[key];

        var trimmed = key.substring(0,5) === 'notes' ? key.substring(6) : key;
        if (karma_to_apply > 0) { connection.notes.karma.awards.push(trimmed); };
        if (karma_to_apply < 0) { connection.notes.karma.penalties.push(trimmed); };
    });
}

function checkConcurrency(plugin, con_key, val, history) {
    var config = plugin.config.get('karma.ini');

    if (!config.concurrency) return;

    var count = val || 0;    // add this connection
    count++;
    db.incr(con_key);        // increment Redis, (creates if needed)
    db.expire(con_key, 4 * 60);     // expire after 4 min

    var reject=0;
    if (history <  0 && count > config.concurrency.bad) reject++;
    if (history >  0 && count > config.concurrency.good)    reject++;
    if (history == 0 && count > config.concurrency.neutral) reject++;
    if (reject) return "too many connections for you: " + count;
    return;
};

function checkMaxRecipients(connection, plugin, config) {
    if (!config.recipients) return;     // disabled in config file
    var c = connection.rcpt_count;
    var count = c.accept + c.tempfail + c.reject + 1;
    if (count <= 1) return;           // everybody is allowed one

    connection.logdebug(plugin, "recipient count: " + count );

    var desc = history > 3 ? 'good' : history >= 0 ? 'neutral' : 'bad';

    var cr = config.recipients;

    // the deeds of their past shall not go unnoticed!
    var history = connection.notes.karma.history;
    if (history > 3 && count <= cr.good) return;
    if (history > -1 && count <= cr.neutral) return;

    // this is *more* strict than history, b/c they have fewer opportunity
    // to score positive karma this early in the connection. senders with
    // good history will rarely see these limits.
    var karma = connection.notes.karma.connection;
    if (karma >  3 && count <= cr.good) return;
    if (karma >= 0 && count <= cr.neutral) return;

    return 'too many recipients (' + count + ') for ' + desc + ' karma';
}

function checkSpammyTLD(mail_from, config, connection, plugin) {
    if (!config.spammy_tlds) return;
    if (mail_from.isNull()) return;    // null sender (bounce)

    var from_tld = mail_from.host.split('.').pop();
    // connection.logdebug(plugin, "from_tld: " + from_tld);

    var tld_penalty = (config.spammy_tlds[from_tld] || 0) * 1; // force numeric
    if (tld_penalty === 0) return;

    connection.loginfo(plugin, "spammy TLD: " + tld_penalty);
    connection.notes.karma.connection += tld_penalty;
    connection.notes.karma.penalties.push('spammy.TLD');
};

function checkSyntaxMailFrom(connection, plugin) {
    var full_from = connection.current_line;
    // connection.logdebug(plugin, "mail_from: " + full_from);

// look for an illegal (RFC 5321,2821,821) space in envelope from
    if (full_from.toUpperCase().substring(0,11) === 'MAIL FROM:<') return;

    connection.loginfo(plugin, "illegal envelope address format: " + full_from );
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push('rfc5321.MailFrom');
};

function checkSyntaxRcptTo(connection, plugin) {
    // check for an illegal RFC (2)821 space in envelope recipient
    var full_rcpt = connection.current_line;
    if (full_rcpt.toUpperCase().substring(0,9) === 'RCPT TO:<') return;

    connection.loginfo(plugin, "illegal envelope address format: " + full_rcpt );
    connection.notes.karma.connection--;
    connection.notes.karma.penalties.push('rfc5321.RcptTo');
};

function initConnectionNote(connection, config) {
    if (connection.notes.karma) return; // init once per connection
    connection.notes.karma = {
        connection: 0,
        history: 0,
        awards: [],
        penalties: [ ],
        todo: {},
    };

    // todo is a list of connection/transaction notes to 'watch' for.
    // When discovered, award their karma points to the connection
    // and remove them from todo.
    var awards = config.awards;
    if (!awards) return;
    Object.keys(awards).forEach(function(key) {
        connection.notes.karma.todo[key] = awards[key];
    });
};

function assembleNoteObj(prefix, key) {
    var note = prefix;
    var parts = key.split('.');
    while(parts.length > 0) {
        note = note[parts.shift()];
        if (note == null) break;
    }
    return note;
};
<|endoftext|>


<commit_before>import { bind } from './../functions/eventListener';
import isMeta from './../functions/isMeta';
import preventDefault from './../functions/preventDefault';
import domQuery from './../functions/domQuery';

function isElement(el) {
  return (el && el.nodeType === 1);
}

function onClick(el, handler) {
  return (e) => {
    const href = el.getAttribute('href')
      || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
      || el.getAttribute('xlink:href');

    try {
      console.warn(el);
      console.warn(handler);
      handler(el);
    } catch (error) {
      console.warn(error);
      // TODO
    }

    if (href && el.target !== '_blank' && !isMeta(e)) {
      preventDefault(e);
      setTimeout(() => {
        window.location.href = href;
      }, 500);
    }
  };
}

export default function trackLink(links, handler) {
  if (!links) return;
  if (typeof links === 'string') {
    links = domQuery(links);
  } else if (isElement(links)) {
    links = [links];
  } else if (links.toArray) { // handles jquery
    links = links.toArray();
  }

  if (typeof handler !== 'function') {
    throw new TypeError('Must pass function handler to `ddManager.trackLink`.');
  }

  for (const el of links) {
    if (!isElement(el)) {
      throw new TypeError('Must pass HTMLElement to `ddManager.trackLink`.');
    }
    bind(el, 'click', onClick(el, handler));
  }

  return this;
}
<commit_msg>removed console<commit_after>import { bind } from './../functions/eventListener';
import isMeta from './../functions/isMeta';
import preventDefault from './../functions/preventDefault';
import domQuery from './../functions/domQuery';

function isElement(el) {
  return (el && el.nodeType === 1);
}

function onClick(el, handler) {
  return (e) => {
    const href = el.getAttribute('href')
      || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
      || el.getAttribute('xlink:href');

    try {
      handler(el);
    } catch (error) {
      // TODO
    }

    if (href && el.target !== '_blank' && !isMeta(e)) {
      preventDefault(e);
      setTimeout(() => {
        window.location.href = href;
      }, 500);
    }
  };
}

export default function trackLink(links, handler) {
  if (!links) return;
  if (typeof links === 'string') {
    links = domQuery(links);
  } else if (isElement(links)) {
    links = [links];
  } else if (links.toArray) { // handles jquery
    links = links.toArray();
  }

  if (typeof handler !== 'function') {
    throw new TypeError('Must pass function handler to `ddManager.trackLink`.');
  }

  for (const el of links) {
    if (!isElement(el)) {
      throw new TypeError('Must pass HTMLElement to `ddManager.trackLink`.');
    }
    bind(el, 'click', onClick(el, handler));
  }

  return this;
}
<|endoftext|>


<commit_before>import { bind, unbind } from 'driveback-utils/eventListener';
import isMeta from 'driveback-utils/isMeta';
import preventDefault from 'driveback-utils/preventDefault';
import domQuery from 'driveback-utils/domQuery';

function applyHandler(event, el, handler) {
  if (event.detail === 0) {
    return; // prevent incorrect markup clicks propogation
  }

  const href = (
    el.tagName === 'A' &&
    (
      el.getAttribute('href')
      || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
      || el.getAttribute('xlink:href')
    )
  );

  try {
    handler(el);
  } catch (error) {
    // TODO
  }

  if (
    href &&
    el.target !== '_blank' &&
    !isMeta(event) &&
    !event.defaultPrevented &&
    event.returnValue !== false
  ) {
    preventDefault(event);
    setTimeout(() => {
      window.location.href = href;
    }, 500);
  }
}

function onClick(el, handler) {
  return (event) => {
    applyHandler(event, el, handler);
  };
}

export default function trackLink(selector, handler) {
  if (!selector) return;

  if (typeof handler !== 'function') {
    throw new TypeError('Must pass function handler to `ddManager.trackLink`.');
  }

  let trackedLinks;

  bind(window.document, 'click', () => {
    const links = (window.jQuery) ? window.jQuery(selector).get() : domQuery(selector);
    trackedLinks = [];
    links.forEach((el) => {
      const onClickHandler = onClick(el, handler);
      bind(el, 'click', onClickHandler);
      trackedLinks.push([el, onClickHandler]);
    });
  }, true); // capturing phase

  bind(window.document, 'click', () => {
    trackedLinks.forEach((trackedLink) => {
      const [el, onClickHandler] = trackedLink;
      unbind(el, 'click', onClickHandler);
    });
  }); // bubbling phase
}
<commit_msg>link tracking improvement<commit_after>import { bind, unbind } from 'driveback-utils/eventListener';
import isMeta from 'driveback-utils/isMeta';
import preventDefault from 'driveback-utils/preventDefault';
import domQuery from 'driveback-utils/domQuery';

function applyHandler(event, el, handler) {
  if (event.detail === 0) {
    return; // prevent incorrect markup clicks propogation
  }

  const href = (
    el.tagName === 'A' &&
    (
      el.getAttribute('href')
      || el.getAttributeNS('http://www.w3.org/1999/xlink', 'href')
      || el.getAttribute('xlink:href')
    )
  );

  try {
    handler(el);
  } catch (error) {
    // TODO
  }

  if (
    href &&
    el.target !== '_blank' &&
    !isMeta(event) &&
    !event.defaultPrevented &&
    event.returnValue !== false
  ) {
    preventDefault(event);
    setTimeout(() => {
      window.location.href = href;
    }, 500);
  }
}

function onClick(el, handler) {
  return (event) => {
    applyHandler(event, el, handler);
  };
}

export default function trackLink(selector, handler) {
  if (!selector) return;

  if (typeof handler !== 'function') {
    throw new TypeError('Must pass function handler to `ddManager.trackLink`.');
  }

  let trackedLinks = [];

  bind(window.document, 'click', () => {
    trackedLinks.forEach((trackedLink) => {
      const [el, onClickHandler] = trackedLink;
      unbind(el, 'click', onClickHandler);
    });

    const links = (window.jQuery) ? window.jQuery(selector).get() : domQuery(selector);
    trackedLinks = [];
    links.forEach((el) => {
      const onClickHandler = onClick(el, handler);
      bind(el, 'click', onClickHandler);
      trackedLinks.push([el, onClickHandler]);
    });
  }, true); // capturing phase
}
<|endoftext|>


<commit_before>/* Description:
 *   Emits reset signals on a timer.
 *
 * Dependencies:
 *   settings, reset
 *
 * Configuration:
 *   resetTime - Time between resets on each screen, in milliseconds.
 *
 * Author:
 *    mythmon
 */

var Promise = require('es6-promise').Promise;

module.exports = function (corsica) {
  var settings = corsica.settings.setup('timer', {
    resetTime: 2 * 60 * 1000,
    jitter: 15 * 1000,
    resetOnConnect: true,
  });

  var clientCounters = {};
  var utils = corsica.utils;

  corsica.on('census.connected', function (data) {
    var name = data.name;
    if (name === undefined) {
      return;
    }
    makeTimeout(name);
    settings.get()
      .then(function(settings) {
        if (settings.resetOnConnect) {
          corsica.sendMessage('reset', {screen: name});
        }
      });
    return data;
  });

  corsica.on('census.disconnected', function (data) {
    // Invalidate the timer.
    clientCounters[data.name]++;
  });

  function makeTimeout(name) {
    if (name === undefined) {
      console.log('[timer] Error: No name.');
      return Promise.reject();
    }
    var currentCounter = clientCounters[name] = (clientCounters[name] || 0) + 1;

    /* Get the reset time, wait for that number of milliseconds, reset
     * the give screen, and then set another timeout.
     *
     * If makeTimeout gets called again, clientCounter[name] will get
     * incremented, making this promise chain invalid.
     */
    return settings.get()
      .then(function(settings) {
        var resetTime = +settings.resetTime;
        var jitter = +settings.jitter;
        var offset = jitter * (Math.random() * 2 - 1);
        var timeout = resetTime + offset;
        return utils.timerPromise(timeout);
      })
      .then(function () {
        if (clientCounters[name] !== currentCounter) {
          // The counter got incremented, this chain is no longer valid.
          return;
        }
        makeTimeout(name);
        corsica.sendMessage('reset', {'screen': name});
        console.log('[timer]', 'reset', name);
      })
      .catch(function (err) {
        console.error('Error:', err);
      });
  }

  corsica.on('content', function (content) {
    // If a screen is getting new content, start a new counter.
    var screens = content.screen;
    if (typeof screens === 'string') {
      screens = [screens];
    }
    screens.forEach(makeTimeout);
    return content;
  });

  corsica.on('timer.stop', function(content) {
    console.log('stopping timer for', content.screen);
    var screens = content.screen;
    if (!(screens instanceof Array)) {
      screens = [screens];
    }

    screens.forEach(function(screen) {
      if (clientCounters[screen]) {
        clientCounters[screen]++;
      }
    });

    return content;
  });

  corsica.on('timer.start', function(content) {
    console.log('restarting timer for', content.screen);
    corsica.sendMessage('reset', {screen: content.screen});
    return content;
  });
};
<commit_msg>Add per-screen variable timing.<commit_after>/* Description:
 *   Emits reset signals on a timer.
 *
 * Dependencies:
 *   settings, reset
 *
 * Configuration:
 *   resetTime - Time between resets on each screen, in milliseconds.
 *
 * Author:
 *    mythmon
 */

var Promise = require('es6-promise').Promise;

module.exports = function (corsica) {
  var settings = corsica.settings.setup('timer', {
    resetTime: 2 * 60 * 1000,
    jitter: 15 * 1000,
    resetOnConnect: true,
  });

  var clientCounters = {};
  var utils = corsica.utils;

  corsica.on('census.connected', function (data) {
    var name = data.name;
    if (name === undefined) {
      return;
    }
    makeTimeout(name);
    settings.get()
      .then(function(settings) {
        if (settings.resetOnConnect) {
          corsica.sendMessage('reset', {screen: name});
        }
      });
    return data;
  });

  corsica.on('census.disconnected', function (data) {
    // Invalidate the timer.
    clientCounters[data.name]++;
  });

  function makeTimeout(name) {
    if (name === undefined) {
      console.log('[timer] Error: No name.');
      return Promise.reject();
    }
    var currentCounter = clientCounters[name] = (clientCounters[name] || 0) + 1;

    /* Get the reset time, wait for that number of milliseconds, reset
     * the give screen, and then set another timeout.
     *
     * If makeTimeout gets called again, clientCounter[name] will get
     * incremented, making this promise chain invalid.
     */
    return settings.get()
      .then(function(settings) {
        var resetTime, jitter;
        if ('screens' in settings && name in settings.screens) {
          resetTime = parseInt(settings.screens[name].resetTime || settings.resetTime);
          jitter = parseInt(settings.screens[name].jitter || settings.jitter);
        } else {
          resetTime = +settings.resetTime;
          jitter = +settings.jitter;
        }
        var offset = jitter * (Math.random() * 2 - 1);
        var timeout = resetTime + offset;
        return utils.timerPromise(timeout);
      })
      .then(function () {
        if (clientCounters[name] !== currentCounter) {
          // The counter got incremented, this chain is no longer valid.
          return;
        }
        makeTimeout(name);
        corsica.sendMessage('reset', {'screen': name});
        console.log('[timer]', 'reset', name);
      })
      .catch(function (err) {
        console.error('Error:', err);
      });
  }

  corsica.on('content', function (content) {
    // If a screen is getting new content, start a new counter.
    var screens = content.screen;
    if (typeof screens === 'string') {
      screens = [screens];
    }
    screens.forEach(makeTimeout);
    return content;
  });

  corsica.on('timer.stop', function(content) {
    console.log('stopping timer for', content.screen);
    var screens = content.screen;
    if (!(screens instanceof Array)) {
      screens = [screens];
    }

    screens.forEach(function(screen) {
      if (clientCounters[screen]) {
        clientCounters[screen]++;
      }
    });

    return content;
  });

  corsica.on('timer.start', function(content) {
    console.log('restarting timer for', content.screen);
    corsica.sendMessage('reset', {screen: content.screen});
    return content;
  });
};
<|endoftext|>


<commit_before>go.app = function() {
    var vumigo = require('vumigo_v02');
    var MenuState = vumigo.states.MenuState;
    var FreeText = vumigo.states.FreeText;
    var Choice = vumigo.states.Choice;
    var SeedJsboxUtils = require('seed-jsbox-utils');
    var IdentityStore = SeedJsboxUtils.IdentityStore;
    var JsonApi = vumigo.http.api.JsonApi;
    var App = vumigo.App;

    var GoNDOH = App.extend(function(self) {
        App.call(self, 'state_start');
        var utils = SeedJsboxUtils.utils;

        //variables for services
        var is;
        self.init = function() {
            // initialise services
            is = new IdentityStore(
                new JsonApi(self.im, {}),
                self.im.config.services.identity_store.token,
                self.im.config.services.identity_store.url
            );
        };

        self.states.add("state_start", function(name) {
            self.im.user.set_answers = {};
            var msisdn = utils.normalize_msisdn(self.im.user.addr, '27');
            self.im.user.set_answer("operator_msisdn", msisdn);
            return is
            .get_or_create_identity({"msisdn": msisdn})
            .then(function(identity) {
                self.im.user.set_answer("operator", identity);
                self.im.user.set_answer("msisdn",msisdn);
            })
            .then(function() {
                return self.states.create('state_not_registered');
            });
        });

        self.states.add('state_not_registered', function(name){
            return new FreeText(name, {
                question: ("Welcome to NurseConnect, where you can stay up to date with "+
                "maternal & child health. Reply '1' to start."),
                next: 'state_nurse_connect_options'
            });
          });

          // OPTIONS MENU
        self.states.add('state_nurse_connect_options', function(name) {
            return new MenuState(name, {
                question: ('Do you want to:'),
                choices: [
                    new Choice('state_weekly_messages', ('Sign up for weekly messages')),
                    new Choice('state_change_number', ('Change your no')),
                    new Choice('state_friend_register', ('Help a friend register')),
                ],
            });
        });

    });

    return {
        GoNDOH: GoNDOH
    };
}();<commit_msg>added states for non subscribed nurses<commit_after>go.app = function() {
    var vumigo = require('vumigo_v02');
    var MenuState = vumigo.states.MenuState;
    var ChoiceState = vumigo.states.ChoiceState;
    var FreeText = vumigo.states.FreeText;
    var Choice = vumigo.states.Choice;
    var SeedJsboxUtils = require('seed-jsbox-utils');
    var IdentityStore = SeedJsboxUtils.IdentityStore;
    var JsonApi = vumigo.http.api.JsonApi;
    var App = vumigo.App;

    var GoNDOH = App.extend(function(self) {
        App.call(self, 'state_start');
        var utils = SeedJsboxUtils.utils;

        //variables for services
        var is;

        self.init = function() {
            // initialise services
            is = new IdentityStore(
                new JsonApi(self.im, {}),
                self.im.config.services.identity_store.token,
                self.im.config.services.identity_store.url
            );
        };

        self.states.add("state_start", function(name) {
            // Reset user answers when restarting the app
            self.im.user.answers = {};
            var msisdn = utils.normalize_msisdn(self.im.user.addr, '27');
            self.im.user.set_answer("operator_msisdn", msisdn);

            /*
                find identity & check subscription using CompanionApp

                will go to state_registered if found,
                else state_not_registered
            */
            return self.states.create('state_not_registered');

        });

        self.states.add('state_not_registered', function(name){
            return new FreeText(name, {
                question: ("Welcome to NurseConnect, where you can stay up to date with "+
                "maternal & child health. Reply '1' to start."),
                next: 'state_nurse_connect_options'
            });
          });

          // OPTIONS MENU
        self.states.add('state_nurse_connect_options', function(name) {
            return new MenuState(name, {
                question: ('Do you want to:'),
                choices: [
                    new Choice('state_weekly_messages', ('Sign up for weekly messages')),
                    new Choice('state_change_number', ('Change your no')),
                    new Choice('state_friend_register', ('Help a friend register')),
                ],
            });
        });

        self.states.add('state_weekly_messages', function(name) {
            return new ChoiceState(name, {
                question: ("To register, your info needs to be collected, stored and used. " +
                           "You might also receive messages on public holidays. Do you agree?"),
                choices: [
                    new Choice('yes', ('Yes')),
                    new Choice('no', ('No')),
                ],
                next: function(choice) {
                    if (choice.value === 'yes') {
                        return 'state_agree_to_register';
                    } else {
                        return 'state_no_registration';
                    }
                }
            });
        });

        self.states.add('state_friend_register', function(name) {
            return new ChoiceState(name, {
                question: ("To register, your friend's info needs to be collected, stored and used. "+
                            "They may receive messages on public holidays. Do they agree?"),
                choices: [
                    new Choice('yes', ('Yes')),
                    new Choice('no', ('No')),
                ],
                next: function(choice) {
                    if (choice.value === 'yes') {
                        return 'state_agree_to_register';
                    } else {
                        return 'state_no_registration';
                    }
                }
            });
        });

        self.states.add('state_no_registration', function(name) {
            return new MenuState(name, {
                question: ("If you/they don't agree to share info, we can't send NurseConnect messages. " +
                           "Reply '1' if you/they change your/their mind and would like to sign up."),
                choices: [
                    new Choice('state_nurse_connect_options', ('Main Menu')),
                ],
            });
        });

    });

    return {
        GoNDOH: GoNDOH
    };
}();<|endoftext|>


<commit_before>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program 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 Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

let log = require('../net2/logger.js')(__filename, 'info');

let Hook = require('./Hook.js');

let sem = require('../sensor/SensorEventManager.js').getInstance();

let HostTool = require('../net2/HostTool.js');
let hostTool = new HostTool();

let Promise = require('bluebird');

let extend = require('../util/util.js').extend;
let util = require('util');
let bone = require("../lib/Bone.js");

let flowUtil = require("../net2/FlowUtil.js");

let async = require('asyncawait/async');
let await = require('asyncawait/await');

let Samba = require('../extension/samba/samba.js');
let samba = new Samba();

let HostManager = require('../net2/HostManager.js');

var _async = require('async');

const MAX_IPV6_ADDRESSES = 8

class DeviceHook extends Hook {
  constructor() {
    super();
  }

  processDeviceUpdate(event) {
    let host = event.host
    let mac = host.mac
    let ipv4Addr = host.ipv4Addr
    let ipv6Addr = host.ipv6Addr

    return async(() => {

      // 1. if this is a brand new mac address => NewDeviceFound
      let found = await (hostTool.macExists(mac))
      if(!found) {
        sem.emitEvent({
          type: "NewDeviceFound",
          message: "A new device (mac address) found @ DeviceHook",
          host: host,
          suppressAlarm: event.suppressAlarm
        })
        return
      }

      // 2. if this is an existing mac address, and it has same ipv4 address => RegularDeviceInfoUpdate
      // it may update redis ip6 keys if additional ip addresses are added
      if(ipv4Addr) {
        let data = await(hostTool.getIPv4Entry(ipv4Addr))
        if(data && data.mac === mac) {
          sem.emitEvent({
            type: "RegularDeviceInfoUpdate",
            message: "Refresh device status @ DeviceHook",
            suppressEventLogging: true,
            host: host
          });
          return
        }

        // 3. if this is an existing mac address, and it has a different ipv4 address, (the ipv4 is owned by nobody in redis) => OldDeviceChangedToNewIP
        // it may update redis ip6 keys if additional ip addresses are added
        if(!data) {
          sem.emitEvent({
            type: "OldDeviceChangedToNewIP",
            message: "An old device used a new IP @ DeviceHook",
            host: host
          })
          return
        }

        // 4. if this is an existing mac address, and it has a different ipv4 address, (the ipv4 is already owned by someone in redis) => OldDeviceTakenOverOtherDeviceIP
        // it may update redis ip6 keys if additional ip addresses are added
        if(data && data.mac !== mac) {
          sem.emitEvent({
            type: "OldDeviceTakenOverOtherDeviceIP",
            message: "An old device used IP used to be other device @ DeviceHook",
            host: host,
            oldMac: data.mac
          })
          return
        }

      } else {
        // 5. if this is an existing mac address, and it has no ipv4 address (only ipv6 addresses)

        // Then just update the ipv6 entries
        if(ipv6Addr) {
          await (hostTool.updateIPv6Host(host)) // v6
          let newIPv6Addr = await (this.updateIPv6EntriesForMAC(ipv6Addr, mac))
          let newHost = extend({}, host, {ipv6Addr: newIPv6Addr})

          await (hostTool.updateMACKey(newHost)) // mac
        }
      }

    })().catch((err) => {
      log.error("Failed to process DeviceUpdate event:", err, {});
    })
  }

  run() {

    // DeviceUpdate event format:
    //   ipv4: ipv4Addr
    //   ipv4Addr: ipv4Addr
    //   mac: mac
    //   bname: service.name (optional)
    //   ipv6Addr =  service.ipv6Addrs (optional)

    // DeviceUpdate may be triggered by nmap scan, bonjour monitor,
    // dhcp monitor and etc...

    sem.on("DeviceUpdate", (event) => {
      let mac = event.mac;

      this.processDeviceUpdate(event)
    });

    sem.on("IPv6DeviceInfoUpdate",(event)=>{
      let host = event.host;

      if (host.ipv6Addr && host.ipv6Addr.length>0) {
        log.info(util.format("A new IPv6DeviceInfoUpdate device %s - %s - %s is found!", host.ipv6Addr, host.mac));

        _async.eachLimit(host.ipv6Addr, 1, (v6, cb) => {
          hostTool.linkMacWithIPv6(v6,host.mac,cb);
        }, (err) => {
        });
      }
    });

    sem.on("NewDeviceFound", (event) => {
      let host = event.host;

      log.info(util.format("A new device %s - %s - %s is found!", host.bname, host.ipv4Addr, host.mac));

      let enrichedHost = extend({}, host, {
        uid: host.ipv4Addr,
        firstFoundTimestamp: new Date() / 1000,
        lastActiveTimestamp: new Date() / 1000
      });

      async(() => {

        // v4
        await (hostTool.updateHost(enrichedHost));

        // v6
        await (hostTool.updateIPv6Host(enrichedHost));

        log.info("Host entry is created for this new device");

        let mac = enrichedHost.mac;

        if(!mac)
          return; // ignore if mac is undefined

        let vendor = null;

        try {
          vendor = await (this.getVendorInfoAsync(mac));
        } catch(err) {
          // do nothing
          log.error("Failed to get vendor info from cloud", err, {});
        }

        let v = "Unknown";
        if(vendor)
          v = vendor;

        enrichedHost.macVendor = v;

        if(!enrichedHost.bname) {
          let sambaName = await (samba.getSambaName(host.ipv4Addr));
          if(sambaName)
            enrichedHost.bname = sambaName;
        }

        if(!enrichedHost.bname && enrichedHost.macVendor !== "Unknown") {
          // finally, use macVendor if no name
          // if macVendor is not available, don't set the bname
          enrichedHost.bname = enrichedHost.macVendor;
        }

        enrichedHost.bnameCheckTime = Math.floor(new Date() / 1000);

        await (hostTool.updateMACKey(enrichedHost));

        if(!event.suppressAlarm) {
          await (this.createAlarm(enrichedHost));
        } else {
          log.info("Alarm is suppressed for new device", hostTool.getHostname(enrichedHost), {})
        }

      })().catch((err) => {
        log.error("Failed to handle NewDeviceFound event:", err, {});
      });
    });

    sem.on("OldDeviceChangedToNewIP", (event) => {
      // FIXME: this is typically old ip is taken by some device else, not going to delete the old ip entry
      let host = event.host;

      log.info(util.format("Device %s (%s) has a new IP: %s", host.bname, host.mac, host.ipv4Addr));

      async(() => {
        let macData = await(hostTool.getMACEntry(host.mac))

        let firstFoundTimestamp = macData.firstFoundTimestamp;
        if(!firstFoundTimestamp)
          firstFoundTimestamp = new Date() / 1000;

        let enrichedHost = extend({}, host, {
          uid: host.ipv4Addr,
          firstFoundTimestamp: firstFoundTimestamp,
          lastActiveTimestamp: new Date() / 1000
        });

        await (hostTool.updateHost(enrichedHost)) //v4
        await (hostTool.updateIPv6Host(enrichedHost)) //v6

        log.info("New host entry is created for this old device");

        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        await (hostTool.updateMACKey(enrichedHost)) // mac

        log.info("MAC entry is updated with new IP");

        let hostManager= new HostManager("cli",'server','debug');
        log.info(`Reload host info for new ip address ${host.ipv4Addr}`)
        hostManager.getHost(host.ipv4Addr);
      })().catch((err) => {
        log.error("Failed to process OldDeviceChangedToNewIP event:", err, {})
      })
    });

    sem.on("OldDeviceTakenOverOtherDeviceIP", (event) => {
      let host = event.host;

      log.info(util.format("Device %s (%s) has a new IP: %s", host.bname, host.mac, host.ipv4Addr));

      async(() => {
        let macData = await (hostTool.getMACEntry(host.mac))

        let firstFoundTimestamp = macData.firstFoundTimestamp;
        if(!firstFoundTimestamp)
          firstFoundTimestamp = new Date() / 1000;

        let enrichedHost = extend({}, host, {
          uid: host.ipv4Addr,
          firstFoundTimestamp: firstFoundTimestamp,
          lastActiveTimestamp: new Date() / 1000
        });

        await (hostTool.updateHost(enrichedHost))
        await (hostTool.updateIPv6Host(enrichedHost)) //v6

        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        await (hostTool.updateMACKey(enrichedHost))

        log.info("MAC entry is updated with new IP");

        let hostManager= new HostManager("cli",'server','debug');
        log.info(`Reload host info for new ip address ${host.ipv4Addr}`)
        hostManager.getHost(host.ipv4Addr);
      })().catch((err) => {
        log.error("Failed to process OldDeviceTakenDOverOtherDeivceIP event:", err, {})
      })
    });

    sem.on("RegularDeviceInfoUpdate", (event) => {
      let host = event.host
      let mac = host.mac

      log.debug(util.format("Regular Device Update for %s (%s - %s)", host.bname, host.ipv4Addr, host.mac));

      let enrichedHost = extend({}, host, {
        lastActiveTimestamp: new Date() / 1000
      });

      async(() => {
        // For ipv6, need to load existing ip6 address from redis, and merge together
        // One device may have multiple ipv6 addresses
        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        // FIXME: shoud not keep minimal info for host key, not all
        await (hostTool.updateHost(enrichedHost))   // host:ip4:.......

        await (hostTool.updateIPv6Host(enrichedHost)) // host:ip6:.........

        log.debug("Host entry is updated for this device");

        await (hostTool.updateMACKey(enrichedHost)) // host:mac:.....

        // log.info("RegularDeviceInfoUpdate MAC entry is updated, checking V6",host.ipv6Addr,enrichedHost.ipv6Addr);
        // if (host.ipv6Addr == null || host.ipv6Addr.length == 0) {
        //         return;
        //       }
        // if (host.ipv6Addr.length == enrichedHost.ipv6Addr.length
        //     && host.ipv6Addr.every(function(u, i) {
        //             return u === enrichedHost.ipv6Addr[i];
        //           })
        //          ) {
        //       } else {
        //         sem.emitEvent({
        //           type: "IPv6DeviceInfoUpdate",
        //           message: "IPv6 Device Update @ DeviceHook",
        //           suppressEventLogging: true,
        //           host: host
        //         });
        //       }
        //     }).catch((err) => {
        //       log.error("Failed to create mac entry:", err, err.stack, {});
        //     })

        // })
      })().catch((err) => {
        log.error("Failed to create host entry:", err, err.stack, {});
      });

    });
  }

  /*
   * ipv6 address fields works like a queue.  oldest discovered ipv6 address
   * at index 0.  any newly discovered ip must be placed at the end by taking
   * out from its old possition
   */

  updateIPv6EntriesForMAC(ipv6Addr, mac) {
    return async(() => {
      let existingIPv6Addresses = await(hostTool.getIPv6AddressesByMAC(mac)) || []
      ipv6Addr.forEach((addr) => {
        let index = existingIPv6Addresses.indexOf(addr);
        if(index > -1) {
          existingIPv6Addresses.splice(index,1);
        }
        existingIPv6Addresses.push(addr) // found new ip address
        if(existingIPv6Addresses.length > MAX_IPV6_ADDRESSES) {
            existingIPv6Addresses.shift()
        }
      })
      return existingIPv6Addresses
    })()
  }

  createAlarmAsync(host) {
    return new Promise((resolve, reject) => {
      this.createAlarmAsync(host, (err) => {
        if(err) {
          reject(err);
        } else {
          resolve();
        }
      })
    })
  }

  createAlarm(host, callback) {
    callback = callback || function() {}

    let Alarm = require('../alarm/Alarm.js');
    let AM2 = require('../alarm/AlarmManager2.js');
    let am2 = new AM2();

    let name = host.bname || host.ipv4Addr;

    let alarm = new Alarm.NewDeviceAlarm(new Date() / 1000,
                                         name,
                                         {
                                           "p.device.id": name,
                                           "p.device.name": name,
                                           "p.device.ip": host.ipv4Addr,
                                           "p.device.mac": host.mac,
                                           "p.device.vendor": host.macVendor
                                         });

    am2.checkAndSave(alarm, (err) => {
      if(err) {
        log.error("Failed to save new alarm: " + err);
      }
      callback(err);
    });
  }

  getVendorInfoAsync(mac) {
    return new Promise((resolve, reject) => {
      this.getVendorInfo(mac, (err, vendorInfo) => {
        if(err) {
          reject(err);
        } else {
          resolve(vendorInfo);
        }
      })
    })
  }

  getVendorInfo(mac, callback) {
    mac = mac.toUpperCase();
    let rawData = {
      ou: mac.slice(0,13), // use 0,13 for better OU compatibility
      uuid: flowUtil.hashMac(mac)
    };
    bone.device("identify", rawData, (err, enrichedData) => {
      if(err) {
        log.error("Failed to get vendor info for mac " + mac + ": " + err);
        callback(err);
        return;
      }

      if(enrichedData && enrichedData._vendor) {
        let v = enrichedData._vendor;
        if(v.startsWith('"'))
          v = v.slice(1); // workaround for buggy code, vendor has a unless prefix "
        callback(null, v);
      } else {
        callback(null, null);
      }
    });
  }

}

module.exports = DeviceHook;
<commit_msg>better way to rotate ipv6 link local addresses.  fixes #428<commit_after>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program 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 Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

let log = require('../net2/logger.js')(__filename, 'info');

let Hook = require('./Hook.js');

let sem = require('../sensor/SensorEventManager.js').getInstance();

let HostTool = require('../net2/HostTool.js');
let hostTool = new HostTool();

let Promise = require('bluebird');

let extend = require('../util/util.js').extend;
let util = require('util');
let bone = require("../lib/Bone.js");

let flowUtil = require("../net2/FlowUtil.js");

let async = require('asyncawait/async');
let await = require('asyncawait/await');

let Samba = require('../extension/samba/samba.js');
let samba = new Samba();

let HostManager = require('../net2/HostManager.js');

var _async = require('async');

const MAX_IPV6_ADDRESSES = 10
const MAX_LINKLOCAL_IPV6_ADDRESSES = 3

class DeviceHook extends Hook {
  constructor() {
    super();
  }

  processDeviceUpdate(event) {
    let host = event.host
    let mac = host.mac
    let ipv4Addr = host.ipv4Addr
    let ipv6Addr = host.ipv6Addr

    return async(() => {

      // 1. if this is a brand new mac address => NewDeviceFound
      let found = await (hostTool.macExists(mac))
      if(!found) {
        sem.emitEvent({
          type: "NewDeviceFound",
          message: "A new device (mac address) found @ DeviceHook",
          host: host,
          suppressAlarm: event.suppressAlarm
        })
        return
      }

      // 2. if this is an existing mac address, and it has same ipv4 address => RegularDeviceInfoUpdate
      // it may update redis ip6 keys if additional ip addresses are added
      if(ipv4Addr) {
        let data = await(hostTool.getIPv4Entry(ipv4Addr))
        if(data && data.mac === mac) {
          sem.emitEvent({
            type: "RegularDeviceInfoUpdate",
            message: "Refresh device status @ DeviceHook",
            suppressEventLogging: true,
            host: host
          });
          return
        }

        // 3. if this is an existing mac address, and it has a different ipv4 address, (the ipv4 is owned by nobody in redis) => OldDeviceChangedToNewIP
        // it may update redis ip6 keys if additional ip addresses are added
        if(!data) {
          sem.emitEvent({
            type: "OldDeviceChangedToNewIP",
            message: "An old device used a new IP @ DeviceHook",
            host: host
          })
          return
        }

        // 4. if this is an existing mac address, and it has a different ipv4 address, (the ipv4 is already owned by someone in redis) => OldDeviceTakenOverOtherDeviceIP
        // it may update redis ip6 keys if additional ip addresses are added
        if(data && data.mac !== mac) {
          sem.emitEvent({
            type: "OldDeviceTakenOverOtherDeviceIP",
            message: "An old device used IP used to be other device @ DeviceHook",
            host: host,
            oldMac: data.mac
          })
          return
        }

      } else {
        // 5. if this is an existing mac address, and it has no ipv4 address (only ipv6 addresses)

        // Then just update the ipv6 entries
        if(ipv6Addr) {
          await (hostTool.updateIPv6Host(host)) // v6
          let newIPv6Addr = await (this.updateIPv6EntriesForMAC(ipv6Addr, mac))
          let newHost = extend({}, host, {ipv6Addr: newIPv6Addr})

          await (hostTool.updateMACKey(newHost)) // mac
        }
      }

    })().catch((err) => {
      log.error("Failed to process DeviceUpdate event:", err, {});
    })
  }

  run() {

    // DeviceUpdate event format:
    //   ipv4: ipv4Addr
    //   ipv4Addr: ipv4Addr
    //   mac: mac
    //   bname: service.name (optional)
    //   ipv6Addr =  service.ipv6Addrs (optional)

    // DeviceUpdate may be triggered by nmap scan, bonjour monitor,
    // dhcp monitor and etc...

    sem.on("DeviceUpdate", (event) => {
      let mac = event.mac;

      this.processDeviceUpdate(event)
    });

    sem.on("IPv6DeviceInfoUpdate",(event)=>{
      let host = event.host;

      if (host.ipv6Addr && host.ipv6Addr.length>0) {
        log.info(util.format("A new IPv6DeviceInfoUpdate device %s - %s - %s is found!", host.ipv6Addr, host.mac));

        _async.eachLimit(host.ipv6Addr, 1, (v6, cb) => {
          hostTool.linkMacWithIPv6(v6,host.mac,cb);
        }, (err) => {
        });
      }
    });

    sem.on("NewDeviceFound", (event) => {
      let host = event.host;

      log.info(util.format("A new device %s - %s - %s is found!", host.bname, host.ipv4Addr, host.mac));

      let enrichedHost = extend({}, host, {
        uid: host.ipv4Addr,
        firstFoundTimestamp: new Date() / 1000,
        lastActiveTimestamp: new Date() / 1000
      });

      async(() => {

        // v4
        await (hostTool.updateHost(enrichedHost));

        // v6
        await (hostTool.updateIPv6Host(enrichedHost));

        log.info("Host entry is created for this new device");

        let mac = enrichedHost.mac;

        if(!mac)
          return; // ignore if mac is undefined

        let vendor = null;

        try {
          vendor = await (this.getVendorInfoAsync(mac));
        } catch(err) {
          // do nothing
          log.error("Failed to get vendor info from cloud", err, {});
        }

        let v = "Unknown";
        if(vendor)
          v = vendor;

        enrichedHost.macVendor = v;

        if(!enrichedHost.bname) {
          let sambaName = await (samba.getSambaName(host.ipv4Addr));
          if(sambaName)
            enrichedHost.bname = sambaName;
        }

        if(!enrichedHost.bname && enrichedHost.macVendor !== "Unknown") {
          // finally, use macVendor if no name
          // if macVendor is not available, don't set the bname
          enrichedHost.bname = enrichedHost.macVendor;
        }

        enrichedHost.bnameCheckTime = Math.floor(new Date() / 1000);

        await (hostTool.updateMACKey(enrichedHost));

        if(!event.suppressAlarm) {
          await (this.createAlarm(enrichedHost));
        } else {
          log.info("Alarm is suppressed for new device", hostTool.getHostname(enrichedHost), {})
        }

      })().catch((err) => {
        log.error("Failed to handle NewDeviceFound event:", err, {});
      });
    });

    sem.on("OldDeviceChangedToNewIP", (event) => {
      // FIXME: this is typically old ip is taken by some device else, not going to delete the old ip entry
      let host = event.host;

      log.info(util.format("Device %s (%s) has a new IP: %s", host.bname, host.mac, host.ipv4Addr));

      async(() => {
        let macData = await(hostTool.getMACEntry(host.mac))

        let firstFoundTimestamp = macData.firstFoundTimestamp;
        if(!firstFoundTimestamp)
          firstFoundTimestamp = new Date() / 1000;

        let enrichedHost = extend({}, host, {
          uid: host.ipv4Addr,
          firstFoundTimestamp: firstFoundTimestamp,
          lastActiveTimestamp: new Date() / 1000
        });

        await (hostTool.updateHost(enrichedHost)) //v4
        await (hostTool.updateIPv6Host(enrichedHost)) //v6

        log.info("New host entry is created for this old device");

        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        await (hostTool.updateMACKey(enrichedHost)) // mac

        log.info("MAC entry is updated with new IP");

        let hostManager= new HostManager("cli",'server','debug');
        log.info(`Reload host info for new ip address ${host.ipv4Addr}`)
        hostManager.getHost(host.ipv4Addr);
      })().catch((err) => {
        log.error("Failed to process OldDeviceChangedToNewIP event:", err, {})
      })
    });

    sem.on("OldDeviceTakenOverOtherDeviceIP", (event) => {
      let host = event.host;

      log.info(util.format("Device %s (%s) has a new IP: %s", host.bname, host.mac, host.ipv4Addr));

      async(() => {
        let macData = await (hostTool.getMACEntry(host.mac))

        let firstFoundTimestamp = macData.firstFoundTimestamp;
        if(!firstFoundTimestamp)
          firstFoundTimestamp = new Date() / 1000;

        let enrichedHost = extend({}, host, {
          uid: host.ipv4Addr,
          firstFoundTimestamp: firstFoundTimestamp,
          lastActiveTimestamp: new Date() / 1000
        });

        await (hostTool.updateHost(enrichedHost))
        await (hostTool.updateIPv6Host(enrichedHost)) //v6

        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        await (hostTool.updateMACKey(enrichedHost))

        log.info("MAC entry is updated with new IP");

        let hostManager= new HostManager("cli",'server','debug');
        log.info(`Reload host info for new ip address ${host.ipv4Addr}`)
        hostManager.getHost(host.ipv4Addr);
      })().catch((err) => {
        log.error("Failed to process OldDeviceTakenDOverOtherDeivceIP event:", err, {})
      })
    });

    sem.on("RegularDeviceInfoUpdate", (event) => {
      let host = event.host
      let mac = host.mac

      log.debug(util.format("Regular Device Update for %s (%s - %s)", host.bname, host.ipv4Addr, host.mac));

      let enrichedHost = extend({}, host, {
        lastActiveTimestamp: new Date() / 1000
      });

      async(() => {
        // For ipv6, need to load existing ip6 address from redis, and merge together
        // One device may have multiple ipv6 addresses
        if(enrichedHost.ipv6Addr) {
          enrichedHost.ipv6Addr = await (this.updateIPv6EntriesForMAC(enrichedHost.ipv6Addr, mac))
        }

        // FIXME: shoud not keep minimal info for host key, not all
        await (hostTool.updateHost(enrichedHost))   // host:ip4:.......

        await (hostTool.updateIPv6Host(enrichedHost)) // host:ip6:.........

        log.debug("Host entry is updated for this device");

        await (hostTool.updateMACKey(enrichedHost)) // host:mac:.....

        // log.info("RegularDeviceInfoUpdate MAC entry is updated, checking V6",host.ipv6Addr,enrichedHost.ipv6Addr);
        // if (host.ipv6Addr == null || host.ipv6Addr.length == 0) {
        //         return;
        //       }
        // if (host.ipv6Addr.length == enrichedHost.ipv6Addr.length
        //     && host.ipv6Addr.every(function(u, i) {
        //             return u === enrichedHost.ipv6Addr[i];
        //           })
        //          ) {
        //       } else {
        //         sem.emitEvent({
        //           type: "IPv6DeviceInfoUpdate",
        //           message: "IPv6 Device Update @ DeviceHook",
        //           suppressEventLogging: true,
        //           host: host
        //         });
        //       }
        //     }).catch((err) => {
        //       log.error("Failed to create mac entry:", err, err.stack, {});
        //     })

        // })
      })().catch((err) => {
        log.error("Failed to create host entry:", err, err.stack, {});
      });

    });
  }

  /*
   * ipv6 address fields works like a queue.  oldest discovered ipv6 address
   * at index 0.  any newly discovered ip must be placed at the end by taking
   * out from its old possition
   */

  updateIPv6EntriesForMAC(ipv6Addr, mac) {
    return async(() => {
      let existingIPv6Addresses = await(hostTool.getIPv6AddressesByMAC(mac)) || []
      let linklocalAddrs = [];
      let globalAddrs=[];

      existingIPv6Addresses.forEach((addr) => {
          if (addr.startsWith("fe80")) {
              linklocalAddrs.push(addr);
          } else {
              globalAddrs.push(addr);
          }
      });

      ipv6Addr.forEach((addr) => {
        let addrList = globalAddrs;
        let max = MAX_IPV6_ADDRESSES;
        if (addr.startsWith("fe80")) {
            addrList = linklocalAddrs;
            max = MAX_LINKLOCAL_IPV6_ADDRESSES;
        }
        let index = addrList.indexOf(addr);
        if(index > -1) {
          addrList.splice(index,1);
        }
        addrList.push(addr) // found new ip address
        if(addrList.length > max) {
            let removed = addrList.shift()
            log.info("DEVICEHOOK_DEBUG_REMOVEV6",removed);
        }
      })

      log.info("DEVICEHOOK",ipv6Addr, linklocalAddrs, globalAddrs);

      return linklocalAddrs.concat(globalAddrs);
    })()
  }

  createAlarmAsync(host) {
    return new Promise((resolve, reject) => {
      this.createAlarmAsync(host, (err) => {
        if(err) {
          reject(err);
        } else {
          resolve();
        }
      })
    })
  }

  createAlarm(host, callback) {
    callback = callback || function() {}

    let Alarm = require('../alarm/Alarm.js');
    let AM2 = require('../alarm/AlarmManager2.js');
    let am2 = new AM2();

    let name = host.bname || host.ipv4Addr;

    let alarm = new Alarm.NewDeviceAlarm(new Date() / 1000,
                                         name,
                                         {
                                           "p.device.id": name,
                                           "p.device.name": name,
                                           "p.device.ip": host.ipv4Addr,
                                           "p.device.mac": host.mac,
                                           "p.device.vendor": host.macVendor
                                         });

    am2.checkAndSave(alarm, (err) => {
      if(err) {
        log.error("Failed to save new alarm: " + err);
      }
      callback(err);
    });
  }

  getVendorInfoAsync(mac) {
    return new Promise((resolve, reject) => {
      this.getVendorInfo(mac, (err, vendorInfo) => {
        if(err) {
          reject(err);
        } else {
          resolve(vendorInfo);
        }
      })
    })
  }

  getVendorInfo(mac, callback) {
    mac = mac.toUpperCase();
    let rawData = {
      ou: mac.slice(0,13), // use 0,13 for better OU compatibility
      uuid: flowUtil.hashMac(mac)
    };
    bone.device("identify", rawData, (err, enrichedData) => {
      if(err) {
        log.error("Failed to get vendor info for mac " + mac + ": " + err);
        callback(err);
        return;
      }

      if(enrichedData && enrichedData._vendor) {
        let v = enrichedData._vendor;
        if(v.startsWith('"'))
          v = v.slice(1); // workaround for buggy code, vendor has a unless prefix "
        callback(null, v);
      } else {
        callback(null, null);
      }
    });
  }

}

module.exports = DeviceHook;
<|endoftext|>