├── .gitignore ├── .travis.yml ├── index.js ├── package.json ├── performance └── index.js ├── readme.md └── test ├── frame.js ├── index.js └── tcp.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.sw[op] 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | - "8" 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // vim: set ft=javascript tabstop=4 softtabstop=4 shiftwidth=4 autoindent: 2 | var dgram = require('dgram') 3 | var debug = require('debug')('syslogd') 4 | 5 | module.exports = exports = Syslogd 6 | 7 | function noop() {} 8 | 9 | function Syslogd(fn, opt) { 10 | if (!(this instanceof Syslogd)) { 11 | return new Syslogd(fn, opt) 12 | } 13 | this.opt = opt || {} 14 | this.handler = fn 15 | 16 | this.server = dgram.createSocket('udp4') 17 | } 18 | 19 | var proto = Syslogd.prototype 20 | 21 | proto.listen = function(port, cb) { 22 | var server = this.server 23 | if (this.port) { 24 | debug('server has binded to %s', port) 25 | return 26 | } 27 | debug('try bind to %s', port) 28 | cb = cb || noop 29 | this.port = port || 514 // default is 514 30 | var me = this 31 | server 32 | .on('error', function(err) { 33 | debug('binding error: %o', err) 34 | cb(err) 35 | }) 36 | .on('listening', function() { 37 | debug('binding ok') 38 | cb(null) 39 | }) 40 | .on('message', function(msg, rinfo) { 41 | var info = parser(msg, rinfo) 42 | me.handler(info) 43 | }) 44 | .bind(port, this.opt.address ) 45 | 46 | return this 47 | } 48 | 49 | var timeMaxLen = 'Dec 15 10:58:44'.length 50 | 51 | var Severity = {} 52 | 'Emergency Alert Critical Error Warning Notice Informational Debug'.split(' ').forEach(function(x, i) { 53 | Severity[x.toUpperCase()] = i 54 | }) 55 | 56 | exports.Severity = Severity 57 | 58 | var Facility = {} // to much 59 | 60 | function parsePRI(raw) { 61 | // PRI means Priority, includes Facility and Severity 62 | // e.g. 10110111 = 10110: facility 111: severity 63 | var binary = (~~raw).toString(2) 64 | var facility = parseInt(binary.substr(binary.length - 3), 2) 65 | var severity = parseInt(binary.substring(0, binary.length - 3), 2) 66 | return [facility, severity] 67 | } 68 | 69 | function parser(msg, rinfo) { 70 | // https://tools.ietf.org/html/rfc5424 71 | // e.g. time hostname tag: info 72 | msg = msg + '' 73 | var tagIndex = msg.indexOf(': ') 74 | var format = msg.substr(0, tagIndex) 75 | var priIndex = format.indexOf('>') 76 | var pri = format.substr(1, priIndex - 1) 77 | pri = parsePRI(pri) 78 | var lastSpaceIndex = format.lastIndexOf(' ') 79 | var tag = format.substr(lastSpaceIndex + 1) 80 | var last2SpaceIndex = format.lastIndexOf(' ', lastSpaceIndex - 1) // hostname cannot contain ' ' 81 | var hostname = format.substring(last2SpaceIndex + 1, lastSpaceIndex) 82 | // time is complex because don't know if it has year 83 | var time = format.substring(priIndex + 1, last2SpaceIndex) 84 | time = new Date(time) 85 | time.setYear(new Date().getFullYear()) // fix year to now 86 | return { 87 | facility: pri[0] 88 | , severity: pri[1] 89 | , tag: tag 90 | , time: time 91 | , hostname: hostname 92 | , address: rinfo.address 93 | , family: rinfo.family 94 | , port: rinfo.port 95 | , size: rinfo.size 96 | , msg: msg.substr(tagIndex + 2) 97 | } 98 | } 99 | 100 | exports.parser = parser 101 | 102 | /* 103 | * SOCK_STREAM service 104 | */ 105 | const net = require('net') 106 | 107 | function SimpleStreamService( messageReceived, options ) { 108 | return new StreamService( messageReceived, options ); 109 | } 110 | 111 | function StreamService(fn, opt) { 112 | this.opt = opt || {} 113 | this.handler = fn 114 | 115 | this.server = net.createServer( ( connection ) => { 116 | debug('New connection from ' + connection.remoteAddress + ":" + connection.remotePort ) 117 | var state = new ConnectionState( this, connection ); 118 | connection.on('data', ( buffer ) => { state.more_data( buffer ) } ) 119 | connection.on('end', () => { state.closed() } ) 120 | }) 121 | return this; 122 | } 123 | 124 | StreamService.prototype.listen = function( port, callback ){ 125 | var server = this.server 126 | callback = callback || noop 127 | this.port = port || 514 // default is 514 128 | debug('Binding to ' + this.port) 129 | var me = this 130 | server 131 | .on('error', function(err) { 132 | debug('binding error: %o', err) 133 | callback(err) 134 | }) 135 | .on('listening', function() { 136 | debug('tcp binding ok') 137 | me.port = server.address().port 138 | callback(null, me) 139 | }) 140 | .listen( port, this.opt.address ) 141 | 142 | return this 143 | } 144 | 145 | class ConnectionState { 146 | constructor( service, connection ){ 147 | this.service = service 148 | this.info = { 149 | address: connection.remoteAddress, 150 | family: connection.family, 151 | port: connection.remotePort 152 | } 153 | this.frameParser = new FrameParser( ( frame ) => { 154 | this.dispatch_message( frame ) 155 | }) 156 | } 157 | 158 | more_data( buffer ) { 159 | this.frameParser.feed( buffer ) 160 | } 161 | 162 | dispatch_message( frame ) { 163 | let clientInfo = { 164 | address: this.info.address, 165 | family: this.info.family, 166 | family: this.info.remotePort, 167 | size: frame.length 168 | } 169 | let message = parser( frame, clientInfo ) 170 | this.service.handler( message ) 171 | } 172 | 173 | closed(){ 174 | this.frameParser.done() 175 | } 176 | } 177 | 178 | let FRAME_TYPE_UNKNOWN = 0; 179 | let FRAME_TYPE_NEWLINE = 1; 180 | let FRAME_TYPE_OCTET = 2; 181 | 182 | class FrameParser { 183 | constructor( callback ){ 184 | this.buffer = Buffer.from( "" ) 185 | this.callback = callback; 186 | this.frame_state = FRAME_TYPE_UNKNOWN ; 187 | } 188 | 189 | feed( data ){ 190 | this.buffer = Buffer.concat( [ this.buffer, data ] ) 191 | this.check_framing() 192 | } 193 | 194 | done() { 195 | if( this.buffer.length > 0 ){ 196 | this.callback( this.buffer.toString() ) 197 | } 198 | this.buffer = Buffer.from( "" ) 199 | } 200 | 201 | check_framing(){ 202 | if( this.frame_state == FRAME_TYPE_UNKNOWN ) { 203 | this.decide_on_frame_type(); 204 | } else if( this.frame_state == FRAME_TYPE_NEWLINE ) { 205 | this.check_newline_framing(); 206 | } else if( this.frame_state == FRAME_TYPE_OCTET ) { 207 | this.check_octet_frame() 208 | } else { 209 | throw "Invalid farme state"; 210 | } 211 | } 212 | 213 | decide_on_frame_type() { 214 | // do nothing if buffer is too short 215 | if( this.buffer.length < 8 ) { 216 | return 217 | } 218 | // shrink our check buffer 219 | let check = this.buffer.slice( 0, 8 ) 220 | // Do we have spaces? 221 | let space = check.indexOf( " " ) 222 | if( space == -1 ){ 223 | this.frame_state = FRAME_TYPE_NEWLINE 224 | return this.check_framing() 225 | } 226 | 227 | // Check output if we can convert it to a number 228 | let size = parseInt( check.slice( 0, space ), 10 ) 229 | if( isNaN( size ) || size < 2 ) { 230 | this.frame_state = FRAME_TYPE_NEWLINE 231 | return this.check_framing() 232 | } 233 | 234 | // Octet framing 235 | this.octets = size 236 | this.frame_state = FRAME_TYPE_OCTET 237 | this.buffer = this.buffer.slice( space + 1 ) 238 | return this.check_framing() 239 | } 240 | 241 | check_newline_framing() { 242 | let indexOfNewLine = this.buffer.indexOf( "\n" ) 243 | if( indexOfNewLine == -1 ) { return; } 244 | 245 | let frame = this.buffer.slice( 0, indexOfNewLine ) 246 | this.buffer = this.buffer.slice( indexOfNewLine + 1 ) 247 | 248 | this._emit_and_reset( frame ) 249 | } 250 | 251 | check_octet_frame() { 252 | let size = this.octets 253 | if( !size ) { throw "Not currently in octet strategy" } 254 | 255 | if( this.buffer.length < size ) { return } 256 | 257 | let frame = this.buffer.slice( 0, size ) 258 | this.buffer = this.buffer.slice( size ) 259 | 260 | this._emit_and_reset( frame ) 261 | } 262 | 263 | _emit_and_reset( frame ){ 264 | this.callback( frame.toString() ) 265 | 266 | this.frame_state = FRAME_TYPE_UNKNOWN 267 | this.check_framing() 268 | } 269 | } 270 | 271 | exports.StreamService = SimpleStreamService 272 | exports.FrameParser = FrameParser 273 | 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syslogd", 3 | "version": "1.1.2", 4 | "description": "syslog server and parser", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha", 11 | "performance": "node performance" 12 | }, 13 | "keywords": [ 14 | "syslog", 15 | "parser", 16 | "server" 17 | ], 18 | "author": "ft", 19 | "license": "ISC", 20 | "dependencies": { 21 | "debug": "^2.1.0" 22 | }, 23 | "devDependencies": { 24 | "mocha" : "3.4.2" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chunpu/syslogd.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/chunpu/syslogd/issues" 32 | }, 33 | "homepage": "https://github.com/chunpu/syslogd", 34 | "engines" : { "node" : ">=6.0.0" } 35 | } 36 | -------------------------------------------------------------------------------- /performance/index.js: -------------------------------------------------------------------------------- 1 | var parser = require('../').parser 2 | 3 | console.time('performance') 4 | for (var i = 0; i < 500000; i++) { 5 | parser('<183>Dec 15 10:58:44 hostname tag: info', { 6 | address: '127.0.0.1' 7 | , family: 'IPv4' 8 | , port: 60097 9 | , size: 39 10 | }) 11 | } 12 | console.timeEnd('performance') 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Syslogd 2 | === 3 | 4 | [![NPM version][npm-image]][npm-url] 5 | [![build status][travis-image]][travis-url] 6 | 7 | nodejs syslog server, including syslog message parser 8 | 9 | Install 10 | --- 11 | 12 | ```sh 13 | npm install syslogd 14 | ``` 15 | 16 | Usage 17 | --- 18 | 19 | ```js 20 | var Syslogd = require('syslogd') 21 | Syslogd(function(info) { 22 | /* 23 | info = { 24 | facility: 7 25 | , severity: 22 26 | , tag: 'tag' 27 | , time: Mon Dec 15 2014 10:58:44 GMT-0800 (PST) 28 | , hostname: 'hostname' 29 | , address: '127.0.0.1' 30 | , family: 'IPv4' 31 | , port: null 32 | , size: 39 33 | , msg: 'info' 34 | } 35 | */ 36 | }).listen(514, function(err) { 37 | console.log('start') 38 | }) 39 | ``` 40 | 41 | Check parser performance by `npm run performance`, which will run 500000 times 42 | 43 | [npm-image]: https://img.shields.io/npm/v/syslogd.svg?style=flat-square 44 | [npm-url]: https://npmjs.org/package/syslogd 45 | [travis-image]: https://img.shields.io/travis/chunpu/syslogd.svg?style=flat 46 | [travis-url]: https://travis-ci.org/chunpu/syslogd 47 | -------------------------------------------------------------------------------- /test/frame.js: -------------------------------------------------------------------------------- 1 | let mocha = require( "mocha" ) 2 | let assert = require('assert') 3 | let syslogd = require('../') 4 | let FrameParser = syslogd.FrameParser 5 | 6 | describe( "StreamFrameParser", () => { 7 | var frames 8 | var parser 9 | 10 | beforeEach( () => { 11 | frames = [] 12 | parser = new FrameParser( ( frame ) => { 13 | frames.push( frame ) 14 | //console.log( "Frame", frames.length, " '"+ frame + "'" ) 15 | } ) 16 | }) 17 | 18 | describe( "when closed before receiving data", () => { 19 | it( "doesn't emit frames", () => { assert.equal( frames.length, 0 ) } ) 20 | }) 21 | 22 | describe( "given a complete new line frame", () => { 23 | beforeEach( () => { 24 | parser.feed( Buffer.from( "nontransparent newline\n" ) ) 25 | }) 26 | 27 | it( "adds a frame", () => { assert.equal( frames.length, 1) }) 28 | it( "correctly copies the frame", () => { assert.equal( frames[0], "nontransparent newline" ) }) 29 | 30 | describe( "when the stream is complete", () => { 31 | it( "doesn't emit a new frame", () => { assert.equal( frames.length, 1 ) } ) 32 | }) 33 | }) 34 | 35 | describe( "when given a partial new line frame", () => { 36 | beforeEach( () => { 37 | parser.feed( Buffer.from( "not done" ) ) 38 | }) 39 | 40 | it( "does not emit the frame yet", () => { assert.equal( frames.length, 0 ) }) 41 | 42 | describe( "when given the completed part", () => { 43 | beforeEach( () => { 44 | parser.feed( Buffer.from( " yet\n" ) ) 45 | }) 46 | 47 | it( "completes the frame", () => { assert.equal( frames.length, 1) } ) 48 | it( "ensure frame content correct", () => { assert.equal( frames[0], "not done yet" ) } ) 49 | }) 50 | 51 | describe( "when finished with additional frames", () => { 52 | beforeEach( () => { parser.feed( Buffer.from( "here\nwith another\nframe" ) ) }) 53 | 54 | it( "it only completed two frames", () => { assert.equal( frames.length, 2) } ) 55 | it( "frame 1 is complete", () => { assert.equal( frames[0], "not donehere" ) } ) 56 | it( "frame 2 is correct", () => { assert.equal( frames[1], "with another" ) } ) 57 | 58 | describe( "when the stream is done", () => { 59 | beforeEach( () => { parser.done() } ) 60 | it( "yeilds another frame", () => { assert.equal( frames.length, 3 ) } ) 61 | it( "yeilds leftover content", () => { assert.equal( frames[2], "frame" ) } ) 62 | }) 63 | }) 64 | }) 65 | 66 | describe( "when given multiple new line frames", () => { 67 | beforeEach( () => { 68 | parser.feed( Buffer.from( "multiple frames\nwithin a single\nbuffer" ) ) 69 | } ) 70 | 71 | it( "emits all frames", () => { assert.equal( frames.length, 2 ) }) 72 | it( "emits the frames in correct order", () => { 73 | assert.equal( frames[0], "multiple frames" ) 74 | assert.equal( frames[1], "within a single" ) 75 | }) 76 | }) 77 | 78 | describe( "when given an octet counted frame", () => { 79 | const message = "<12>1 2017-05-26T14:05:00.000Z host proc 42 - - - Some message" 80 | beforeEach( () => { 81 | let length = message.length 82 | parser.feed( Buffer.from( length + " " + message ) ) 83 | }) 84 | 85 | it( "emits a frame", () => { assert.equal( frames.length, 1 ) } ) 86 | it( "emits only the framed contents", () => { assert.equal( frames[0], message ) } ) 87 | }) 88 | 89 | describe( "when given mutliple octet frames", () => { 90 | const message1 = "<12>1 2017-05-26T14:05:00.000Z host proc 42 - - - Some message" 91 | const message2 = "<18>1 2017-05-26T14:31:00.123Z host proc 42 - - - Secon messages" 92 | beforeEach( () => { 93 | parser.feed( Buffer.from( message1.length + " " + message1 + message2.length + " " + message2 ) ) 94 | }) 95 | 96 | it( "emits a frame", () => { assert.equal( frames.length, 2 ) } ) 97 | it( "emits the first message", () => { assert.equal( frames[0], message1 ) } ) 98 | it( "emits the second message", () => { assert.equal( frames[1], message2 ) } ) 99 | }) 100 | }) 101 | 102 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var dgram = require('dgram') 2 | var assert = require('assert') 3 | let mocha = require( 'mocha' ) 4 | var Syslogd = require('../') 5 | 6 | describe( "given a syslogd service", () => { 7 | it( "recieves and processes messages", (done) => { 8 | var time = 'Dec 15 10:58:44' 9 | var testMsg = '<183>' + time + ' hostname tag: info' 10 | const port = 10514 11 | 12 | Syslogd(function(info) { 13 | //console.log(info) 14 | info.port = null // port is random 15 | var shouldRet = { 16 | facility: 7 17 | , severity: 22 18 | , tag: 'tag' 19 | , time: new Date(time + ' ' + new Date().getFullYear()) 20 | , hostname: 'hostname' 21 | , address: '127.0.0.1' 22 | , family: 'IPv4' 23 | , port: null 24 | , size: 39 25 | , msg: 'info' 26 | } 27 | assert.deepEqual(shouldRet, info) 28 | done() 29 | }).listen( port, function(err) { // sudo 30 | //console.log('listen', err) 31 | assert(!err) 32 | var client = dgram.createSocket('udp4') 33 | var buffer = new Buffer(testMsg) 34 | client.send(buffer, 0, buffer.length, port, 'localhost', function(err, bytes) { 35 | //console.log('send', err, bytes) 36 | }) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/tcp.js: -------------------------------------------------------------------------------- 1 | // vim: set ft=javascript tabstop=4 softtabstop=4 shiftwidth=4 autoindent: 2 | var assert = require('assert') 3 | let mocha = require( 'mocha' ) 4 | var net = require('net') 5 | 6 | describe( "given a TCP Syslog Server", () => { 7 | it( "Receives TCP/IP messages", (done) => { 8 | const StreamSyslogd = require('../').StreamService 9 | assert( StreamSyslogd, "StreamService not defined" ) 10 | 11 | var time = 'Dec 15 10:58:44' 12 | var testMsg = '<183>' + time + ' hostname tag: info' 13 | const port = 0 14 | 15 | StreamSyslogd(function(info) { 16 | info.port = null // port is random 17 | info.address = null 18 | info.family = null 19 | var shouldRet = { 20 | facility: 7 21 | , severity: 22 22 | , tag: 'tag' 23 | , time: new Date(time + ' ' + new Date().getFullYear()) 24 | , hostname: 'hostname' 25 | , address: null 26 | , family: null 27 | , port: null 28 | , size: testMsg.length 29 | , msg: 'info' 30 | } 31 | assert.deepEqual(shouldRet, info) 32 | done() 33 | }).listen( port, function(err, service ) { // sudo 34 | assert.ifError( err ) 35 | var buffer = new Buffer(testMsg) 36 | var client = net.connect( service.port, 'localhost', function() { 37 | client.write(buffer, function(err, bytes) { 38 | assert.ifError( err ) 39 | client.end() 40 | }) 41 | }); 42 | }) 43 | }) 44 | }) 45 | 46 | --------------------------------------------------------------------------------