UNPKG

fluent-logger

Version:

fluent-logger implementation for Node.js inspired by [fluent-logger-python].

211 lines (201 loc) 6.67 kB
'use strict'; const net = require('net'); const tls = require('tls'); const msgpack = require('msgpack-lite'); const crypto = require('crypto'); const zlib = require('zlib'); class MockFluentdServer { constructor(options, tlsOptions) { this._port = null; this._options = options; this._tlsOptions = tlsOptions || {}; this._received = []; this._clients = {}; this._state = null; this._nonce = null; this._userAuthSalt = null; let server = (socket) => { const clientKey = socket.remoteAddress + ':' + socket.remotePort; this._clients[clientKey] = socket; socket.on('end', () => { delete this._clients[clientKey]; }); const stream = msgpack.createDecodeStream(); socket.pipe(stream).on('data', (m) => { if (this._state === 'pingpong') { const authResult = this._checkPing(m); socket.write(msgpack.encode(this._generatePong(authResult, this._nonce, this._options.security.sharedKey))); if (authResult.succeeded) { this._state = 'established'; } else { socket.end(); } } else if (this._state === 'established') { let entries = m[1]; let options = null; if (entries instanceof Buffer) { options = m[2]; if (options.compressed === 'gzip') { entries = zlib.gunzipSync(entries); } let s = msgpack.createDecodeStream(); s.on('data', (data) => { let _time = data[0]; let record = data[1]; this._received.push({ tag: m[0], data: record, options: options }); }); s.write(entries); } else { this._received.push({ tag: m[0], time: m[1], data: m[2], options: m[3] }); options = m[3]; } if (this._options.requireAckResponse && options && options.chunk) { const response = { ack: options.chunk }; socket.write(msgpack.encode(response)); } } }); }; let connectionEventType = 'connection'; if (this._tlsOptions.tls) { connectionEventType = 'secureConnection'; this._server = tls.createServer(this._tlsOptions, server); } else { this._server = net.createServer(server); } this._server.on(connectionEventType, (socket) => { if (this._options.security && this._options.security.sharedKey && this._options.security.serverHostname) { this._state = 'helo'; this._nonce = crypto.randomBytes(16); this._userAuthSalt = crypto.randomBytes(16); } else { this._state = 'established'; } if (this._state === 'helo') { socket.write(msgpack.encode(this._generateHelo(this._nonce, this._userAuthSalt))); this._state = 'pingpong'; } }); } get port() { return this._port; } get messages() { return this._received; } _generateHelo(nonce, userAuthSalt) { // ['HELO', options(hash)] let options = { 'nonce': nonce, 'auth': this._options.security ? userAuthSalt : '', 'keepalive': false }; return ['HELO', options]; } _checkPing(m) { // this._options.checkPing() should return { succeeded: true, reason: 'why', sharedKeySalt: 'salt' } if (this._options.checkPing) { return this._options.checkPing(m); } else { // ['PING', self_hostname, shared_key_salt, sha512_hex(shared_key_salt + self_hostname + nonce + shared_key), username || '', sha512_hex(auth_salt + username + password) || ''] if (m.length !== 6) { return { succeeded: false, reason: 'Invalid ping message size' }; } if (m[0] !== 'PING') { return { succeeded: false, reason: 'Invalid ping message' }; } const _ping = m[0]; const hostname = m[1]; const sharedKeySalt = m[2]; const sharedKeyHexDigest = m[3]; const _username = m[4]; const passwordDigest = m[5]; const serverSideDigest = crypto.createHash('sha512') .update(sharedKeySalt) .update(hostname) .update(this._nonce) .update(this._options.security.sharedKey) .digest('hex'); if (sharedKeyHexDigest !== serverSideDigest) { return { succeeded: false, reason: 'shared key mismatch' }; } if (this._options.security.username && this._options.security.password) { const serverSidePasswordDigest = crypto.createHash('sha512') .update(this._userAuthSalt) .update(this._options.security.username) .update(this._options.security.password) .digest('hex'); if (passwordDigest !== serverSidePasswordDigest) { return { succeeded: false, reason: 'username/password mismatch' }; } } return { succeeded: true, sharedKeySalt: sharedKeySalt }; } } _generatePong(authResult, nonce, sharedKey) { // this._options.generatePong() should return PONG message // [ // 'PONG', // bool(authentication result), // 'reason if authentication failed', // serverHostname, // sha512_hex(salt + serverHostname + nonce + sharedkey) // ] if (authResult.succeeded) { const sharedKeyDigestHex = crypto.createHash('sha512') .update(authResult.sharedKeySalt) .update(this._options.security.serverHostname) .update(nonce) .update(sharedKey) .digest('hex'); return ['PONG', true, '', this._options.security.serverHostname, sharedKeyDigestHex]; } else { return ['PONG', false, authResult.reason, '', '']; } } listen(callback) { let options = { port: this._options.port }; this._server.listen(options, () => { this._port = this._server.address().port; callback(); }); } close(callback) { this._server.close(() => { callback(); }); for (const i in this._clients) { this._clients[i].end(); // this._clients[i].destroy(); } } } module.exports = { runServer: function(options, tlsOptions, callback) { const server = new MockFluentdServer(options, tlsOptions); server.listen(() => { callback(server, (_callback) => { // wait 100 ms to receive all messages and then close setTimeout(() => { const messages = server.messages; server.close(() => { _callback && _callback(messages); }); }, 100); }); }); } };