paulgrove/node-syslog-client

View on GitHub
index.js

Summary

Maintainability
F
3 days
Test Coverage
B
88%

// Copyright 2015-2016 Stephen Vickers <stephen.vickers.sv@gmail.com>
// with contributions 2017 by Seth Blumberg <sethb@pobox.com>

var dgram = require("dgram");
var events = require("events");
var net = require("net");
var os = require("os");
var tls = require("tls");
var util = require("util");

function _expandConstantObject(object) {
    var keys = [];
    for (var key in object)
        if (Object.hasOwnProperty.call(object, key))
            keys.push(key);
    for (var i = 0; i < keys.length; i++)
        object[object[keys[i]]] = parseInt(keys[i], 10);
}

var Transport = {
    Tcp:  1,
    Udp:  2,
    Tls:  3,
    Unix: 4
};

_expandConstantObject(Transport);

var Facility = {
    Kernel: 0,
    User:   1,
    Mail:   2,
    System: 3,
    Daemon: 3,
    Auth:   4,
    Syslog: 5,
    Lpr:    6,
    News:   7,
    Uucp:   8,
    Cron:   9,
    Authpriv: 10,
    Ftp:    11,
    Audit:  13,
    Alert:  14,
    Local0: 16,
    Local1: 17,
    Local2: 18,
    Local3: 19,
    Local4: 20,
    Local5: 21,
    Local6: 22,
    Local7: 23
};

_expandConstantObject(Facility);

var Severity = {
    Emergency:     0,
    Alert:         1,
    Critical:      2,
    Error:         3,
    Warning:       4,
    Notice:        5,
    Informational: 6,
    Debug:         7
};

_expandConstantObject(Severity);

function Client(target, options) {
    this.target = target || "127.0.0.1";

    if (!options)
        options = {}

    this.syslogHostname = options.syslogHostname || os.hostname();
    this.port = options.port || 514;
    this.tcpTimeout = options.tcpTimeout || 10000;
    this.getTransportRequests = [];
    this.facility = typeof options.facility !== "number" || Facility.Local0;
    this.severity =    typeof options.severity !== "number" || Severity.Informational;
    this.rfc3164 = typeof options.rfc3164 === 'boolean' ? options.rfc3164 : true;
    this.appName = options.appName || process.title.substring(process.title.lastIndexOf("/")+1, 48);
    this.dateFormatter = options.dateFormatter || function() { return this.toISOString(); };
    this.udpBindAddress = options.udpBindAddress;

    this.transport = Transport.Udp;
    if (options.transport &&
        options.transport === Transport.Udp ||
        options.transport === Transport.Tcp ||
        options.transport === Transport.Unix ||
        options.transport === Transport.Tls)
            this.transport = options.transport;
    if (this.transport === Transport.Tls) {
        this.tlsCA = options.tlsCA;
    }

    return this;
}

util.inherits(Client, events.EventEmitter);

Client.prototype.buildFormattedMessage = function buildFormattedMessage(message, options) {
    // Some applications, like LTE CDR collection, need to be able to
    // back-date log messages based on CDR timestamps across different
    // time zones, because of delayed record collection with 3rd parties.
    // Particular useful in when feeding CDRs to Splunk for indexing.
    var date = (typeof options.timestamp === 'undefined') ? new Date() : options.timestamp;

    var pri = (options.facility * 8) + options.severity;

    var newline = message[message.length - 1] === "\n" ? "" : "\n";

    var timestamp, formattedMessage;
    if (typeof options.rfc3164 !== 'boolean' || options.rfc3164) {
        // RFC 3164 uses an obsolete date/time format and header.
        var elems = date.toString().split(/\s+/);

        var month = elems[1];
        var day = elems[2];
        var time = elems[4];

        /**
         ** BSD syslog requires leading 0's to be a space.
         **/
        if (day[0] === "0")
            day = " " + day.substr(1, 1);

        timestamp = month + " " + day + " " + time;

        formattedMessage = "<"
                + pri
                + ">"
                + timestamp
                + " "
                + options.syslogHostname
                + " "
                + message
                + newline;
    } else {
        // RFC 5424 obsoletes RFC 3164 and requires RFC 3339
        // (ISO 8601) timestamps and slightly different header.

        var msgid = (typeof options.msgid === 'undefined') ? "-" : options.msgid;


        formattedMessage = "<"
                + pri
                + ">1"                // VERSION 1
                + " "
                + this.dateFormatter.call(date)
                + " "
                + options.syslogHostname
                + " "
                + options.appName
                + " "
                + process.pid
                + " "
                + msgid
                + " - "                // no STRUCTURED-DATA
                + message
                + newline;
    }

    return Buffer.from(formattedMessage);
};

Client.prototype.close = function close() {
    if (this.transport_) {
        if (this.transport === Transport.Tcp || this.transport === Transport.Tls || this.transport === Transport.Unix)
            this.transport_.destroy();
        if (this.transport === Transport.Udp)
            this.transport_.close();
        this.transport_ = undefined;
    } else {
        this.onClose();
    }

    return this;
};

Client.prototype.log = function log() {
    var message, options = {}, cb;

    if (typeof arguments[0] === "string")
        message = arguments[0];
    else
        throw new Error("first argument must be string");

    if (typeof arguments[1] === "function")
        cb = arguments[1];
    else if (typeof arguments[1] === "object")
        options = arguments[1];
    if (typeof arguments[2] === "function")
        cb = arguments[2];

    if (!cb)
        cb = function () {};

    if (typeof options.facility === "undefined") {
        options.facility = this.facility;
    }
    if (typeof options.severity === "undefined") {
        options.severity = this.severity;
    }
    if (typeof options.rfc3164 !== "boolean") {
        options.rfc3164 = this.rfc3164;
    }
    if (typeof options.appName === "undefined") {
        options.appName = this.appName;
    }
    if (typeof options.syslogHostname === "undefined") {
        options.syslogHostname = this.syslogHostname;
    }

    var fm = this.buildFormattedMessage(message, options);

    var me = this;

    me.getTransport(function(error, transport) {
        if (error)
            return cb(error);

        try {
            if (me.transport === Transport.Tcp || me.transport === Transport.Tls || me.transport === Transport.Unix) {
                transport.write(fm, function(error) {
                    if (error)
                        return cb(new Error("net.write() failed: " + error.message));
                    return cb();
                });
            } else if (me.transport === Transport.Udp) {
                transport.send(fm, 0, fm.length, me.port, me.target, function(error, bytes) {
                    if (error) {
                        return cb(new Error("dgram.send() failed: " + error.message));
                    }
                    return cb();
                });
            } else {
                return cb(new Error("unknown transport '%s' specified to Client", me.transport));
            }
        } catch (err) {
            me.onError(err);
            return cb(err);
        }
    });

    return this;
};

Client.prototype.getTransport = function getTransport(cb) {
    if (this.transport_ !== undefined)
        return cb(null, this.transport_);

    this.getTransportRequests.push(cb);

    if (this.connecting)
        return this;
    else
        this.connecting = true;

    var af = net.isIPv6(this.target) ? 6 : 4;

    var me = this;

    function doCb(error, transport) {
        while (me.getTransportRequests.length > 0) {
            var nextCb = me.getTransportRequests.shift();
            nextCb(error, transport);
        }

        me.connecting = false;
    }

    if (this.transport === Transport.Tcp || this.transport === Transport.Unix) {
        var options;
        if (this.transport === Transport.Unix) {
            options = {
                path: this.target
            };
        }
        else {
            options = {
                host: this.target,
                port: this.port,
                family: af
            };
        }

        var transport;
        try {
            transport = net.createConnection(options, function() {
                me.transport_ = transport;
                doCb(null, me.transport_);
            });
        } catch (err) {
            doCb(err);
            me.onError(err);
        }

        if (!transport)
            return;

        transport.setTimeout(this.tcpTimeout, function() {
            var err = new Error("connection timed out");
            transport.destroy();
            me.emit("error", err);
            doCb(err);
        });

        transport.once("connect", function () {
            transport.setTimeout(0);
        });

        transport.on("end", function() {
            var err = new Error("connection closed");
            me.emit("error", err);
            doCb(err);
        });

        transport.on("close", me.onClose.bind(me));
        transport.on("error", function (err) {
            transport.destroy();
            doCb(err);
            me.onError(err);
        });

        transport.unref();
    } else if (this.transport === Transport.Tls) {
        var tlsOptions = {
            host: this.target,
            port: this.port,
            family: af,
            ca: this.tlsCA,
            secureProtocol: 'TLSv1_2_method'
        };

        var tlsTransport;
        try {
            tlsTransport = tls.connect(tlsOptions, function() {
                me.transport_ = tlsTransport;
                doCb(null, me.transport_);
            });
        } catch (err) {
            doCb(err);
            me.onError(err);
        }

        if (!tlsTransport)

            return;

        tlsTransport.setTimeout(this.tcpTimeout, function() {
            var err = new Error("connection timed out");
            me.emit("error", err);
            doCb(err);
        });

        tlsTransport.once("connect", function () {
            tlsTransport.setTimeout(0);
        });

        tlsTransport.on("end", function() {
            var err = new Error("connection closed");
            me.emit("error", err);
            doCb(err);
        });

        tlsTransport.on("close", me.onClose.bind(me));
        tlsTransport.on("error", function (err) {
            doCb(err);
            me.onError(err);
        });

        tlsTransport.unref();
    } else if (this.transport === Transport.Udp) {
        try {
            this.transport_ = dgram.createSocket("udp" + af);

            // if not binding on a particular address
            // node will bind to 0.0.0.0
            if (this.udpBindAddress) {
                // avoid binding to all addresses
                this.transport_.bind({ address: this.udpBindAddress })
            }
        }
        catch (err) {
            try {
                this.transport_.destroy();
            } catch (err2) {
                // ignore cleanup error
            }
            doCb(err);
            this.onError(err);
        }

        if (!this.transport_)
            return;

        this.transport_.on("close", this.onClose.bind(this));
        this.transport_.on("error", function (err) {
            me.onError(err);
            doCb(err);
        });

        this.transport_.unref();

        doCb(null, this.transport_);
    } else {
        doCb(new Error("unknown transport '%s' specified to Client", this.transport));
    }
};

Client.prototype.onClose = function onClose() {
    if (this.transport_) {
        if (this.transport_.destroy)
            this.transport_.destroy();
        this.transport_ = undefined;
    }

    this.emit("close");

    return this;
};

Client.prototype.onError = function onError(error) {
    if (this.transport_) {
        if (this.transport_.destroy)
            this.transport_.destroy();
        this.transport_ = undefined;
    }

    this.emit("error", error);

    return this;
};

exports.Client = Client;

exports.createClient = function createClient(target, options) {
    return new Client(target, options);
};

exports.Transport = Transport;
exports.Facility  = Facility;
exports.Severity  = Severity;