From 9375db3d68141d8e8ebc0fd0caf43bdfe5c47a47 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 11:25:49 +0200 Subject: [PATCH 01/20] checkpoint --- index.js | 2 -- package.json | 4 ++- src/access_accept.js | 2 +- src/access_reject.js | 2 +- src/accounting_respond.js | 2 +- src/acct_on_message.js | 9 ++++--- src/auth_on_message.js | 13 +++++++--- src/coa_on_message.js | 5 ++-- src/decode.js | 5 ++-- src/encode_request.js | 8 +++--- src/encode_response.js | 8 +++--- src/index.js | 37 ++++++++++++++++++--------- src/node_radius_shim.js | 2 +- src/send.js | 2 +- test/dictionaries/mikrotik.dictionary | 21 +++++++-------- test/index.js | 11 ++++---- 16 files changed, 80 insertions(+), 53 deletions(-) delete mode 100644 index.js diff --git a/index.js b/index.js deleted file mode 100644 index 6b3a552..0000000 --- a/index.js +++ /dev/null @@ -1,2 +0,0 @@ - -module.exports = require('./src') diff --git a/package.json b/package.json index 78efe58..97ef100 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,10 @@ "url": "https://github.com/io-digital/tephra" }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" }, + "type": "module", + "module": "src/index.js", "devDependencies": { "chai": "5.1.2", "mocha": "11.1.0" diff --git a/src/access_accept.js b/src/access_accept.js index 8b59586..d9334d6 100644 --- a/src/access_accept.js +++ b/src/access_accept.js @@ -1,5 +1,5 @@ -module.exports = function access_accept( +export default function access_accept( decoded, rinfo, attributes, diff --git a/src/access_reject.js b/src/access_reject.js index 939ec30..dc2778e 100644 --- a/src/access_reject.js +++ b/src/access_reject.js @@ -1,5 +1,5 @@ -module.exports = function access_reject( +export default function access_reject( decoded, rinfo, attributes, diff --git a/src/accounting_respond.js b/src/accounting_respond.js index ba9c97b..529a208 100644 --- a/src/accounting_respond.js +++ b/src/accounting_respond.js @@ -1,5 +1,5 @@ -module.exports = function accounting_respond( +export default function accounting_respond( decoded, rinfo, attributes, diff --git a/src/acct_on_message.js b/src/acct_on_message.js index 408e509..cf3f14b 100644 --- a/src/acct_on_message.js +++ b/src/acct_on_message.js @@ -1,8 +1,11 @@ -var decode = require('./decode') -var accounting_respond = require('./accounting_respond') +// var decode = require('./decode') +import decode from './decode.js' -module.exports = function acct_on_message(message, rinfo) { +// var accounting_respond = require('./accounting_respond') +import accounting_respond from './accounting_respond.js' + +export default function acct_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/auth_on_message.js b/src/auth_on_message.js index 316419f..020d88b 100644 --- a/src/auth_on_message.js +++ b/src/auth_on_message.js @@ -1,9 +1,14 @@ -var decode = require('./decode') -var access_accept = require('./access_accept') -var access_reject = require('./access_reject') +// var decode = require('./decode') +import decode from './decode.js' -module.exports = function auth_on_message(message, rinfo) { +// var access_accept = require('./access_accept') +import access_accept from './access_accept.js' + +// var access_reject = require('./access_reject') +import access_reject from './access_reject.js' + +export default function auth_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/coa_on_message.js b/src/coa_on_message.js index bbe5a15..8918910 100644 --- a/src/coa_on_message.js +++ b/src/coa_on_message.js @@ -1,7 +1,8 @@ -var decode = require('./decode') +// var decode = require('./decode') +import decode from './decode.js' -module.exports = function coa_on_message(message, rinfo) { +export default function coa_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/decode.js b/src/decode.js index c679a06..7de9b41 100644 --- a/src/decode.js +++ b/src/decode.js @@ -1,7 +1,8 @@ -var radius = require('radius') +// var radius = require('radius') +var radius = (await import('radius')).default -module.exports = function decode(message, guard, on_error) { +export default function decode(message, guard, on_error) { try { var decoded = radius.decode({ packet: message, diff --git a/src/encode_request.js b/src/encode_request.js index 54178e4..8cc80ab 100644 --- a/src/encode_request.js +++ b/src/encode_request.js @@ -1,9 +1,11 @@ -var radius = require('radius') +// var radius = require('radius') +var radius = (await import('radius')).default -var node_radius_shim = require('./node_radius_shim') +// var node_radius_shim = require('./node_radius_shim') +import node_radius_shim from './node_radius_shim.js' -module.exports = function encode_request( +export default function encode_request( code, attributes, vendor_attributes, diff --git a/src/encode_response.js b/src/encode_response.js index 8214d3b..849be8c 100644 --- a/src/encode_response.js +++ b/src/encode_response.js @@ -1,9 +1,11 @@ -var radius = require('radius') +// var radius = require('radius') +var radius = (await import('radius')).default -var node_radius_shim = require('./node_radius_shim') +// var node_radius_shim = require('./node_radius_shim') +import node_radius_shim from './node_radius_shim.js' -module.exports = function encode_response( +export default function encode_response( packet, code, attributes, diff --git a/src/index.js b/src/index.js index 2d17bbe..9d476c9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,19 +1,32 @@ -'use strict' +// var EventEmitter = require('events') +import {EventEmitter} from 'events' -var EventEmitter = require('events') -var dgram = require('dgram') +// var dgram = require('dgram') +import dgram from 'dgram' -var radius = require('radius') +// var radius = require('radius') +var radius = (await import('radius')).default -var send = require('./send') -var encode_request = require('./encode_request') -var encode_response = require('./encode_response') -var auth_on_message = require('./auth_on_message') -var acct_on_message = require('./acct_on_message') -var coa_on_message = require('./coa_on_message') +// var send = require('./send') +import send from './send.js' -module.exports = (class extends EventEmitter { +// var encode_request = require('./encode_request') +import encode_request from './encode_request.js' + +// var encode_response = require('./encode_response') +import encode_response from './encode_response.js' + +// var auth_on_message = require('./auth_on_message') +import auth_on_message from './auth_on_message.js' + +// var acct_on_message = require('./acct_on_message') +import acct_on_message from './acct_on_message.js' + +// var coa_on_message = require('./coa_on_message') +import coa_on_message from './coa_on_message.js' + +export default class extends EventEmitter { constructor( SHARED_SECRET, @@ -149,4 +162,4 @@ module.exports = (class extends EventEmitter { on_sent ) } -}) +} diff --git a/src/node_radius_shim.js b/src/node_radius_shim.js index 1c45cdc..d0cc081 100644 --- a/src/node_radius_shim.js +++ b/src/node_radius_shim.js @@ -1,5 +1,5 @@ -module.exports = function node_radius_shim( +export default function node_radius_shim( attributes, vendor_attributes ) { diff --git a/src/send.js b/src/send.js index 80a1acd..2da4879 100644 --- a/src/send.js +++ b/src/send.js @@ -1,5 +1,5 @@ -module.exports = function send(buffer, rinfo, on_sent) { +export default function send(buffer, rinfo, on_sent) { this.send( buffer, 0, diff --git a/test/dictionaries/mikrotik.dictionary b/test/dictionaries/mikrotik.dictionary index 040a26f..cb4caa8 100644 --- a/test/dictionaries/mikrotik.dictionary +++ b/test/dictionaries/mikrotik.dictionary @@ -12,7 +12,7 @@ # other damages that may result from the use of this software, including, but # not limited to, loss of data, time and (or) profits. # -# $Id: dictionary.mikrotik,v 1.7 2011/11/25 08:00:00 normis Exp $ +# $Id: dictionary.mikrotik,v 1.8 2019/12/20 11:02:37 strods Exp $ # # MikroTik Attributes @@ -42,13 +42,14 @@ ATTRIBUTE Mikrotik-Address-List 19 string ATTRIBUTE Mikrotik-Wireless-MPKey 20 string ATTRIBUTE Mikrotik-Wireless-Comment 21 string ATTRIBUTE Mikrotik-Delegated-IPv6-Pool 22 string -ATTRIBUTE Mikrotik_DHCP_Option_Set 23 string -ATTRIBUTE Mikrotik_DHCP_Option_Param_STR1 24 string -ATTRIBUTE Mikortik_DHCP_Option_Param_STR2 25 string -ATTRIBUTE Mikrotik_Wireless_VLANID 26 integer -ATTRIBUTE Mikrotik_Wireless_VLANIDtype 27 integer -ATTRIBUTE Mikrotik_Wireless_Minsignal 28 string -ATTRIBUTE Mikrotik_Wireless_Maxsignal 29 string +ATTRIBUTE Mikrotik-DHCP-Option-Set 23 string +ATTRIBUTE Mikrotik-DHCP-Option-Param-STR1 24 string +ATTRIBUTE Mikrotik-DHCP-Option-Param-STR2 25 string +ATTRIBUTE Mikrotik-Wireless-VLANID 26 integer +ATTRIBUTE Mikrotik-Wireless-VLANIDtype 27 integer +ATTRIBUTE Mikrotik-Wireless-Minsignal 28 string +ATTRIBUTE Mikrotik-Wireless-Maxsignal 29 string +ATTRIBUTE Mikrotik-Switching-Filter 30 string # MikroTik Values @@ -57,7 +58,7 @@ VALUE Mikrotik-Wireless-Enc-Algo 40-bit-WEP VALUE Mikrotik-Wireless-Enc-Algo 104-bit-WEP 2 VALUE Mikrotik-Wireless-Enc-Algo AES-CCM 3 VALUE Mikrotik-Wireless-Enc-Algo TKIP 4 -VALUE Mikrotik_Wireless_VLANIDtype 802.1q 0 -VALUE Mikrotik_Wireless_VLANIDtype 802.1ad 1 +VALUE Mikrotik-Wireless-VLANIDtype 802.1q 0 +VALUE Mikrotik-Wireless-VLANIDtype 802.1ad 1 END-VENDOR Mikrotik diff --git a/test/index.js b/test/index.js index 7ebf1a2..790c0ce 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,4 @@ -'use strict' - function radclient( address, packet_type, @@ -17,9 +15,11 @@ function radclient( }) } -var cp = require('child_process') -var {expect} = require('chai') -var tephra = require('../') +import cp from 'child_process' + +import {expect} from 'chai' + +import tephra from '../src/index.js' // some fixtures var test_secret = 'shared_secret' @@ -108,7 +108,6 @@ describe('tephra', function() { var server beforeEach(function(done) { - delete require.cache[require.resolve('..')] try { server = new tephra( test_secret, From 07ee59f70abd5457de019c8e8e03ccdde551f480 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 12:55:38 +0200 Subject: [PATCH 02/20] checkpoint --- src/access_accept.js | 2 +- src/access_reject.js | 2 +- src/accounting_respond.js | 2 +- src/acct_on_message.js | 6 +- src/auth_on_message.js | 7 +- src/coa_on_message.js | 5 +- src/decode.js | 5 +- src/encode_request.js | 5 +- src/encode_response.js | 5 +- src/index.js | 169 +++++++++++++++++++++++++------------- src/node_radius_shim.js | 9 +- test/index.js | 163 +++++++++++++++++++++--------------- 12 files changed, 234 insertions(+), 146 deletions(-) diff --git a/src/access_accept.js b/src/access_accept.js index d9334d6..34c9f83 100644 --- a/src/access_accept.js +++ b/src/access_accept.js @@ -7,7 +7,7 @@ export default function access_accept( on_accepted ) { this.respond( - 'auth', + 'authentication', decoded, 'Access-Accept', rinfo, diff --git a/src/access_reject.js b/src/access_reject.js index dc2778e..1583205 100644 --- a/src/access_reject.js +++ b/src/access_reject.js @@ -7,7 +7,7 @@ export default function access_reject( on_rejected ) { this.respond( - 'auth', + 'authentication', decoded, 'Access-Reject', rinfo, diff --git a/src/accounting_respond.js b/src/accounting_respond.js index 529a208..ecaa2eb 100644 --- a/src/accounting_respond.js +++ b/src/accounting_respond.js @@ -7,7 +7,7 @@ export default function accounting_respond( on_responded ) { this.respond( - 'acct', + 'accounting', decoded, 'Accounting-Response', rinfo, diff --git a/src/acct_on_message.js b/src/acct_on_message.js index cf3f14b..fd2ff1a 100644 --- a/src/acct_on_message.js +++ b/src/acct_on_message.js @@ -1,8 +1,5 @@ -// var decode = require('./decode') import decode from './decode.js' - -// var accounting_respond = require('./accounting_respond') import accounting_respond from './accounting_respond.js' export default function acct_on_message(message, rinfo) { @@ -14,7 +11,9 @@ export default function acct_on_message(message, rinfo) { }, this.emit.bind(this, 'error#decode#acct') ) + if (!decoded) return + // emit accounting-request this.emit( decoded.code, @@ -22,6 +21,7 @@ export default function acct_on_message(message, rinfo) { rinfo, accounting_respond.bind(this, decoded, rinfo) ) + // as well as accounting-request-{{status-type}} this.emit( `${decoded.code}-${decoded.attributes['Acct-Status-Type'] || 'unknown'}`, diff --git a/src/auth_on_message.js b/src/auth_on_message.js index 020d88b..8fa96f4 100644 --- a/src/auth_on_message.js +++ b/src/auth_on_message.js @@ -1,11 +1,6 @@ -// var decode = require('./decode') import decode from './decode.js' - -// var access_accept = require('./access_accept') import access_accept from './access_accept.js' - -// var access_reject = require('./access_reject') import access_reject from './access_reject.js' export default function auth_on_message(message, rinfo) { @@ -17,10 +12,12 @@ export default function auth_on_message(message, rinfo) { }, this.emit.bind(this, 'error#decode#auth') ) + if (!decoded) { // seems sensible to default to access-reject here return access_reject.call(this, decoded, rinfo) } + this.emit( decoded.code, decoded, diff --git a/src/coa_on_message.js b/src/coa_on_message.js index 8918910..3a45598 100644 --- a/src/coa_on_message.js +++ b/src/coa_on_message.js @@ -1,5 +1,4 @@ -// var decode = require('./decode') import decode from './decode.js' export default function coa_on_message(message, rinfo) { @@ -12,10 +11,12 @@ export default function coa_on_message(message, rinfo) { 'Disconnect-NAK', 'CoA-ACK', 'CoA-NAK' - ].indexOf(packet.code) !== -1 + ].includes(packet.code) }, this.emit.bind(this, 'error#decode#coa') ) + if (!decoded) return + this.emit(decoded.code, decoded, rinfo) } diff --git a/src/decode.js b/src/decode.js index 7de9b41..c7b6b42 100644 --- a/src/decode.js +++ b/src/decode.js @@ -1,20 +1,21 @@ -// var radius = require('radius') var radius = (await import('radius')).default export default function decode(message, guard, on_error) { try { var decoded = radius.decode({ packet: message, - secret: this.SHARED_SECRET + secret: this.shared_secret }) } catch (err) { on_error(err) return } + if (!guard(decoded)) { on_error(new Error('packed decode guard failed')) return } + return decoded } diff --git a/src/encode_request.js b/src/encode_request.js index 8cc80ab..7d709b9 100644 --- a/src/encode_request.js +++ b/src/encode_request.js @@ -1,8 +1,6 @@ -// var radius = require('radius') var radius = (await import('radius')).default -// var node_radius_shim = require('./node_radius_shim') import node_radius_shim from './node_radius_shim.js' export default function encode_request( @@ -14,12 +12,13 @@ export default function encode_request( try { var encoded = radius.encode({ attributes: node_radius_shim.call(this, attributes, vendor_attributes), - secret: this.SHARED_SECRET, + secret: this.shared_secret, code: code }) } catch (err) { on_error(err) return } + return encoded } diff --git a/src/encode_response.js b/src/encode_response.js index 849be8c..15c8413 100644 --- a/src/encode_response.js +++ b/src/encode_response.js @@ -1,8 +1,6 @@ -// var radius = require('radius') var radius = (await import('radius')).default -// var node_radius_shim = require('./node_radius_shim') import node_radius_shim from './node_radius_shim.js' export default function encode_response( @@ -17,11 +15,12 @@ export default function encode_response( packet: packet, code: code, attributes: node_radius_shim.call(this, attributes, vendor_attributes), - secret: this.SHARED_SECRET + secret: this.shared_secret }) } catch (err) { on_error(err) return } + return encoded } diff --git a/src/index.js b/src/index.js index 9d476c9..098a205 100644 --- a/src/index.js +++ b/src/index.js @@ -1,90 +1,140 @@ -// var EventEmitter = require('events') import {EventEmitter} from 'events' - -// var dgram = require('dgram') import dgram from 'dgram' -// var radius = require('radius') var radius = (await import('radius')).default -// var send = require('./send') import send from './send.js' - -// var encode_request = require('./encode_request') import encode_request from './encode_request.js' - -// var encode_response = require('./encode_response') import encode_response from './encode_response.js' - -// var auth_on_message = require('./auth_on_message') import auth_on_message from './auth_on_message.js' - -// var acct_on_message = require('./acct_on_message') import acct_on_message from './acct_on_message.js' - -// var coa_on_message = require('./coa_on_message') import coa_on_message from './coa_on_message.js' +function validate_vendor_dictionary(vendor_dictionary) { + return ( + typeof vendor_dictionary.name === 'string' && + vendor_dictionary.name.length && + typeof vendor_dictionary.path === 'string' && + vendor_dictionary.path.length && + !isNaN(vendor_dictionary.id) && + Number.isInteger(vendor_dictionary.id) && + vendor_dictionary.id + ) +} + +function validate_port(port) { + return ( + !isNaN(port) && + Number.isInteger(port) && + port >= 0 && + port <= 65535 + ) +} + export default class extends EventEmitter { constructor( - SHARED_SECRET, - AUTH_PORT, - ACCT_PORT, - COA_PORT, - VENDOR_DICTIONARIES + options = { + shared_secret: '', + sharedSecret: '', + ports: { + auth: false, + authentication: false, + acct: false, + accounting: false, + coa: false, + change_of_authorisation: false, + changeOfAuthorisation: false, + changeOfAuthorization: false + }, + vendor_dictionaries: null, + vendorDictionaries: null + } ) { super() - if (!(SHARED_SECRET && AUTH_PORT && ACCT_PORT && COA_PORT)) { - throw new Error('Missing SHARED_SECRET, AUTH_PORT, ACCT_PORT or COA_PORT arguments') + var shared_secret = (options.shared_secret || options.sharedSecret) + + if (!shared_secret) { + throw new Error('Missing shared secret') } - this.SHARED_SECRET = SHARED_SECRET - this.AUTH_PORT = AUTH_PORT - this.ACCT_PORT = ACCT_PORT - this.COA_PORT = COA_PORT - this.VENDOR_IDS = {} - - if (Array.isArray(VENDOR_DICTIONARIES) && VENDOR_DICTIONARIES.length) { - VENDOR_DICTIONARIES.forEach((dict, idx) => { - if (!( - typeof dict.vendor === 'string' && - dict.vendor.length && - typeof dict.path === 'string' && - dict.path.length && - typeof dict.id === 'number' && - dict.id - )) { + this.shared_secret = shared_secret + + var vendor_dictionaries = (options.vendor_dictionaries || options.vendorDictionaries) + + this.vendor_ids = {} + + if (vendor_dictionaries && Array.isArray(vendor_dictionaries)) { + vendor_dictionaries.forEach(function(vendor_dictionary, idx) { + if (!validate_vendor_dictionary(vendor_dictionary)) { throw new Error( - `Expected {vendor: String, path: String, id: Number} at index ${idx} in VENDOR_DICTIONARIES` + `Vendor dictionary at index ${idx} is malformed. Expected {name: String, path: String, id: Number} but got ${JSON.stringify(vendor_dictionary)}` ) } - radius.add_dictionary(dict.path) - this.VENDOR_IDS[dict.vendor] = dict.id - }) + + radius.add_dictionary(vendor_dictionary.path) + this.vendor_ids[vendor_dictionary.name] = vendor_dictionary.id + }, this) + } + + if (!options.ports) { + throw new Error('At least one port is required') } - this.SOCKETS = { - AUTH: dgram.createSocket('udp4', auth_on_message.bind(this)), - ACCT: dgram.createSocket('udp4', acct_on_message.bind(this)), - COA: dgram.createSocket('udp4', coa_on_message.bind(this)) + var authentication_port = +(options.ports.auth || options.ports.authentication) + var accounting_port = +(options.ports.acct || options.ports.accounting) + var change_of_authorisation_port = +( + options.ports.coa || + options.ports.change_of_authorisation || + options.ports.changeOfAuthorisation || + options.ports.changeOfAuthorization + ) + + if (!(authentication_port || accounting_port || change_of_authorisation_port)) { + throw new Error('At least one port is required') } + + this.sockets = {} + + void [ + {name: 'authentication', value: authentication_port, socket: 'authentication', callback: auth_on_message}, + {name: 'accounting', value: accounting_port, socket: 'accounting', callback: acct_on_message}, + {name: 'change of authorisation', value: change_of_authorisation_port, socket: 'change_of_authorisation', callback: coa_on_message} + ].forEach(function(port) { + var {name, value, socket, callback} = port + + if (!value) return + + if (!validate_port(value)) { + throw new Error(`Invalid port specified for ${name} socket`) + } + + this[socket] = value + this.sockets[socket] = dgram.createSocket('udp4', callback.bind(this)) + }, this) } bind(on_bound) { - this.SOCKETS.AUTH.bind(this.AUTH_PORT) - this.SOCKETS.ACCT.bind(this.ACCT_PORT) - this.SOCKETS.COA.bind(this.COA_PORT) + Object.keys( + this.sockets + ).forEach(function(socket) { + this.sockets[socket].bind(this[socket]) + }, this) + return typeof on_bound === 'function' ? on_bound() : this } unbind(on_unbound) { - this.SOCKETS.AUTH.close() - this.SOCKETS.ACCT.close() - this.SOCKETS.COA.close() + Object.keys( + this.sockets + ).forEach(function(socket) { + this.sockets[socket].close() + }, this) + this.removeAllListeners() + return typeof on_unbound === 'function' ? on_unbound() : this } @@ -99,6 +149,7 @@ export default class extends EventEmitter { if (typeof type !== 'string') { throw new Error('Missing required string argument type') } + var encoded = encode_request.call( this, code, @@ -106,9 +157,11 @@ export default class extends EventEmitter { vendor_attributes, on_sent ) + if (!encoded) return + send.call( - this.SOCKETS[type.toUpperCase()], + this.sockets[type.toLowerCase()], encoded, rinfo, on_sent @@ -127,6 +180,7 @@ export default class extends EventEmitter { if (typeof type !== 'string') { throw new Error('Missing required string argument type') } + var encoded = encode_response.call( this, packet, @@ -135,9 +189,11 @@ export default class extends EventEmitter { vendor_attributes, on_responded ) + if (!encoded) return + send.call( - this.SOCKETS[type.toUpperCase()], + this.sockets[type.toLowerCase()], encoded, rinfo, on_responded @@ -150,11 +206,10 @@ export default class extends EventEmitter { vendor_attributes, on_sent ) { - // override the reply port for the sake of convenience - rinfo.port = this.COA_PORT + rinfo.port = this.change_of_authorisation this.send( - 'coa', + 'change_of_authorisation', 'Disconnect-Request', rinfo, attributes, diff --git a/src/node_radius_shim.js b/src/node_radius_shim.js index d0cc081..97a2bc8 100644 --- a/src/node_radius_shim.js +++ b/src/node_radius_shim.js @@ -4,17 +4,20 @@ export default function node_radius_shim( vendor_attributes ) { var shimmed = [] + if (Array.isArray(attributes) && attributes.length) { shimmed = shimmed.concat(attributes) } + Object.keys( vendor_attributes - ).forEach(vendor_name => { + ).forEach(function(vendor_name) { shimmed.push([ 'Vendor-Specific', - this.VENDOR_IDS[vendor_name], + this.vendor_ids[vendor_name], vendor_attributes[vendor_name] ]) - }) + }, this) + return shimmed } diff --git a/test/index.js b/test/index.js index 790c0ce..e93cce8 100644 --- a/test/index.js +++ b/test/index.js @@ -33,88 +33,120 @@ describe('tephra', function() { this.timeout(5000) - describe('lifecycle', function() { + describe('constructor', function() { - var server - - it('#constructor should throw if required arguments are missing', function() { + it('should throw if required arguments are missing', function() { expect(function() { new tephra - }).to.throw(/Missing SHARED_SECRET/) + }).to.throw(/Missing shared secret/) }) - describe('vendor dictionaries', function() { - - it('#constructor should throw if vendor dictionary arguments are invalid', function() { - expect(function() { - new tephra( - test_secret, - 1812, - 1813, - 1814, - [ - {} - ] - ) - }).to.throw( - /\{vendor\:\ String\,\ path\:\ String\,\ id\:\ Number\}/ - ) - }) + it('should throw an error if no ports are specified', function() { + expect(function() { + new tephra({ + shared_secret: test_secret + }) + }).to.throw(/At least one port is required/) - it('should hold an internal representation of vendor dictionaries, mapping vendor name to vendor id', function() { - var t = new tephra( - test_secret, - 1812, - 1813, - 1814, - [ - { - vendor: 'telkom', - path: './test/dictionaries/telkom.dictionary', - id: 1431 - }, - { - vendor: 'mikrotik', - path: './test/dictionaries/mikrotik.dictionary', - id: 14988 - } - ] - ) - expect(t.VENDOR_IDS.mikrotik).to.equal(14988) - expect(t.VENDOR_IDS.telkom).to.equal(1431) - }) + expect(function() { + new tephra({ + shared_secret: test_secret, + ports: {} + }) + }).to.throw(/At least one port is required/) }) - describe('sockets', function() { - server = new tephra( - test_secret, - 1812, - 1813, - 1814 + it('should throw if vendor dictionary arguments are invalid', function() { + expect(function() { + new tephra({ + shared_secret: test_secret, + ports: { + auth: 1812, + acct: 1813, + coa: 1814 + }, + vendor_dictionaries: [ + {} + ] + }) + }).to.throw( + /Vendor dictionary at index 0 is malformed/ ) + }) - it('should bind', function(done) { - server.bind(done) + it('should hold an internal representation of vendor dictionaries, mapping vendor name to vendor id', function() { + var t = new tephra({ + shared_secret: test_secret, + ports: { + auth: 1812, + acct: 1813, + coa: 1814 + }, + vendor_dictionaries: [ + { + name: 'telkom', + path: './test/dictionaries/telkom.dictionary', + id: 1431 + }, + { + name: 'mikrotik', + path: './test/dictionaries/mikrotik.dictionary', + id: 14988 + } + ] }) - it('should unbind', function(done) { - server.unbind(done) + expect(t.vendor_ids.mikrotik).to.equal(14988) + expect(t.vendor_ids.telkom).to.equal(1431) + }) + + }) + + describe('socket permutations (authentication, accounting, change of authorisation)', function() { + + var test_cases = [ + [1812, 1813, 1814], + [1812, 1813, false], + [1812, false, 1814], + [1812, false, false], + [false, 1813, 1814], + [false, 1813, false], + [false, false, 1814] + ] + + test_cases.forEach(function(test_case, idx) { + it(`permutation ${idx + 1} (${JSON.stringify(test_case)}) should bind and unbind successfully`, function(done) { + var t = new tephra({ + shared_secret: test_secret, + ports: { + auth: test_case[0], + acct: test_case[1], + coa: test_case[2] + } + }) + + t.bind(function() { + t.unbind(done) + }) }) }) }) - describe('auth, acct and coa packet transmission', function() { + describe('authentication, accounting, and change of authorisation packet transmission', function() { var server beforeEach(function(done) { try { - server = new tephra( - test_secret, - 1812, - 1813, - 1814 - ) + server = new tephra({ + shared_secret: test_secret, + ports: { + auth: 1812, + acct: 1813, + coa: 1814 + } + }) + server.bind(done) } catch (e) { return done(e) @@ -171,7 +203,7 @@ describe('tephra', function() { it('should send a response for accounting packets', function(done) { server.on('Accounting-Request', function(request, rinfo) { - server.respond('acct', request, 'Accounting-Response', rinfo, [], {}, done) + server.respond('accounting', request, 'Accounting-Response', rinfo, [], {}, done) }) radclient( @@ -226,7 +258,7 @@ describe('tephra', function() { it('should send a response for access-request packets', function(done) { server.on('Access-Request', function(request, rinfo, accept, reject) { - server.respond('auth', request, 'Access-Accept', rinfo, [], {}, done) + server.respond('authentication', request, 'Access-Accept', rinfo, [], {}, done) }) radclient( @@ -244,6 +276,7 @@ describe('tephra', function() { server.on('Access-Request', function(request, rinfo, accept, reject) { accept([], {}, done) }) + radclient( 'localhost:1812', 'auth', @@ -287,7 +320,7 @@ describe('tephra', function() { it('#send should yield an error if supplied non-array type', function(done) { server.send( - 'acct', + 'accounting', 0, {address: '0.0.0.0', port: 12345}, null, @@ -310,7 +343,7 @@ describe('tephra', function() { it('#respond should yield an error if no packet is given', function(done) { server.respond( - 'acct', + 'accounting', null, 0, {address: '0.0.0.0', port: 12345}, From 864c4d676f75c89e3393a2ff490403b8f2e365d9 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 13:25:25 +0200 Subject: [PATCH 03/20] checkpoint --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 33c293a..e56f095 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-framework based on [node-radius](https://github.com/retailnext/node-radius). Now it's easier than ever to write a RADIUS server that isn't standards-compliant! `;)` +## Configuration + +Key | Type | Required | Notes +--- | ---- | -------- | ----- +`sharedSecret` | `String` | + | +`ports` | `Object` | | +`ports.authentication` | `Number` | | Must be a valid port number (0 - 65535 inclusive) +`ports.accounting` | `Number` | | Must be a valid port number (0 - 65535 inclusive) +`ports.changeOfAuthorisation` | `Number` | | Must be a valid port number (0 - 65535 inclusive) +`vendorDictionaries` | `Array` | | Elements of the array must be objects that conform to `{name: String, path: String, id: Number}` + ## Example ```javascript @@ -10,19 +22,21 @@ var tephra = require('tephra') var users = {user1: 'secret_password'} -var server = new tephra( - 'shared_secret', - 1812, // authentication port - 1813, // accounting port - 3799, // change of authorisation port - [ // add dictionaries for vendor-specific attributes +var server = new tephra({ + sharedSecret: 'shared_secret', + ports: { + authentication: 1812, + accounting: 1813, + changeOfAuthorisation: 1814 + }, + vendorDictionaries: [ { name: 'quux_vendor', path: '/path/to/quux_vendor/dictionary', id: 12345 } ] -) +}) server.on('Access-Request', function(packet, rinfo, accept, reject) { var username = packet.attributes['User-Name'] From f8448dfb818a119516796670ed6ef59ee348ab66 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 13:26:01 +0200 Subject: [PATCH 04/20] checkpoint --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index e56f095..1ecee07 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-fram Key | Type | Required | Notes --- | ---- | -------- | ----- -`sharedSecret` | `String` | - | +`sharedSecret` | `String` | | `ports` | `Object` | | `ports.authentication` | `Number` | | Must be a valid port number (0 - 65535 inclusive) `ports.accounting` | `Number` | | Must be a valid port number (0 - 65535 inclusive) From 8b47ca3a3ad0178c2820b0ef305712beba8ca7dd Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 13:26:49 +0200 Subject: [PATCH 05/20] checkpoint --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1ecee07..b8a3167 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-fram Key | Type | Required | Notes --- | ---- | -------- | ----- -`sharedSecret` | `String` | | -`ports` | `Object` | | -`ports.authentication` | `Number` | | Must be a valid port number (0 - 65535 inclusive) -`ports.accounting` | `Number` | | Must be a valid port number (0 - 65535 inclusive) -`ports.changeOfAuthorisation` | `Number` | | Must be a valid port number (0 - 65535 inclusive) -`vendorDictionaries` | `Array` | | Elements of the array must be objects that conform to `{name: String, path: String, id: Number}` +`sharedSecret` | `String` | [x] | +`ports` | `Object` | [x] | +`ports.authentication` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) +`ports.accounting` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) +`ports.changeOfAuthorisation` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) +`vendorDictionaries` | `Array` | [ ] | Elements of the array must be objects that conform to `{name: String, path: String, id: Number}` ## Example From 9d0bfb33ce05cf63456031ec83d31aa95e9bc3d8 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 13:29:11 +0200 Subject: [PATCH 06/20] checkpoint --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index b8a3167..2a3f216 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,18 @@ # tephra -An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-framework based on [node-radius](https://github.com/retailnext/node-radius). Now it's easier than ever to write a RADIUS server that isn't standards-compliant! `;)` +An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-framework based on [node-radius](https://github.com/retailnext/node-radius). Now it's easier than ever to write a RADIUS server that isn't standards-compliant! 😉 ## Configuration Key | Type | Required | Notes --- | ---- | -------- | ----- -`sharedSecret` | `String` | [x] | -`ports` | `Object` | [x] | -`ports.authentication` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) -`ports.accounting` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) -`ports.changeOfAuthorisation` | `Number` | [ ] | Must be a valid port number (0 - 65535 inclusive) -`vendorDictionaries` | `Array` | [ ] | Elements of the array must be objects that conform to `{name: String, path: String, id: Number}` +`sharedSecret` | `String` | ✅ | +`ports` | `Object` | ✅ | +`ports.authentication` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) +`ports.accounting` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) +`ports.changeOfAuthorisation` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) +`vendorDictionaries` | `Array` | ❌ | Elements of the array must be objects that conform to `{name: String, path: String, id: Number}` ## Example From 5d14163640464136291545e0a643a6da46892a9e Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 14:23:11 +0200 Subject: [PATCH 07/20] checkpoint --- test/index.js | 160 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 37 deletions(-) diff --git a/test/index.js b/test/index.js index e93cce8..f2181b2 100644 --- a/test/index.js +++ b/test/index.js @@ -27,11 +27,19 @@ var auth_request = 'User-Name=foo,User-Password=bar' var acct_interim = 'Acct-Status-Type=Interim-Update' var acct_start = 'Acct-Status-Type=Start' var acct_stop = 'Acct-Status-Type=Stop' -var coa_disconnect = 'Acct-Session-Id=foo,User-Name=bar,NAS-IP-Address=10.0.0.1' +var port_permutations = [ + [1812, 1813, 1814], + [1812, 1813, false], + [1812, false, 1814], + [1812, false, false], + [false, 1813, 1814], + [false, 1813, false], + [false, false, 1814] +] describe('tephra', function() { - this.timeout(5000) + this.timeout(2000) describe('constructor', function() { @@ -104,24 +112,14 @@ describe('tephra', function() { describe('socket permutations (authentication, accounting, change of authorisation)', function() { - var test_cases = [ - [1812, 1813, 1814], - [1812, 1813, false], - [1812, false, 1814], - [1812, false, false], - [false, 1813, 1814], - [false, 1813, false], - [false, false, 1814] - ] - - test_cases.forEach(function(test_case, idx) { - it(`permutation ${idx + 1} (${JSON.stringify(test_case)}) should bind and unbind successfully`, function(done) { + port_permutations.forEach(function(ports, idx) { + it(`permutation ${idx + 1} (${JSON.stringify(ports)}) should bind and unbind successfully`, function(done) { var t = new tephra({ shared_secret: test_secret, ports: { - auth: test_case[0], - acct: test_case[1], - coa: test_case[2] + auth: ports[0], + acct: ports[1], + coa: ports[2] } }) @@ -149,7 +147,8 @@ describe('tephra', function() { server.bind(done) } catch (e) { - return done(e) + done(e) + return } }) @@ -167,7 +166,10 @@ describe('tephra', function() { test_secret, acct_start, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -181,7 +183,10 @@ describe('tephra', function() { test_secret, auth_request, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -196,7 +201,10 @@ describe('tephra', function() { test_secret, auth_request, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -212,7 +220,10 @@ describe('tephra', function() { test_secret, acct_interim, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -228,22 +239,47 @@ describe('tephra', function() { test_secret, acct_interim, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) it('should emit the accounting request status type when receiving an accounting request', function(done) { var emissions = 0 - var expected = 2 + var expected = 6 function emission_counter() { emissions += 1 - if (emissions === expected) return done() + if (emissions === expected) { + done() + return + } } + + // expected to be emitted three times (once per accounting status type) server.on('Accounting-Request', emission_counter) + + // each expected to be emitted just once + server.on('Accounting-Request-Start', emission_counter) server.on('Accounting-Request-Interim-Update', emission_counter) + server.on('Accounting-Request-Stop', emission_counter) + + radclient( + 'localhost:1813', + 'acct', + test_secret, + acct_start, + function(err) { + if (err && !err.killed) { + done(err) + return + } + } + ) radclient( 'localhost:1813', @@ -251,7 +287,23 @@ describe('tephra', function() { test_secret, acct_interim, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } + } + ) + + radclient( + 'localhost:1813', + 'acct', + test_secret, + acct_stop, + function(err) { + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -267,7 +319,10 @@ describe('tephra', function() { test_secret, auth_request, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -283,7 +338,10 @@ describe('tephra', function() { test_secret, auth_request, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) @@ -297,28 +355,54 @@ describe('tephra', function() { test_secret, auth_request, function(err) { - if (err && !err.killed) return done(err) + if (err && !err.killed) { + done(err) + return + } } ) }) - it('#disconnect should throw if not given rinfo', function() { + it('should emit coa ack or nack and disconnect ack or nack when receiving a change of authorisation response', function(done) { + var emissions = 0 + var expected = 4 + + function emission_counter() { + emissions += 1 + if (emissions === expected) { + done() + return + } + } + + server.on('CoA-NAK', emission_counter) + server.on('CoA-ACK', emission_counter) + server.on('Disconnect-NAK', emission_counter) + server.on('Disconnect-ACK', emission_counter) + + server.send('authentication', 'CoA-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + server.send('authentication', 'CoA-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + server.send('authentication', 'Disconnect-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + server.send('authentication', 'Disconnect-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + }) + + it('should throw when disconnect is not given rinfo', function() { expect( server.disconnect.bind(server, null, [], {}) ).to.throw() }) - it('#disconnect should not throw if given all required arguments', function() { + it('should not throw when disconnect is given all required arguments', function() { expect( server.disconnect.bind(server, {address: '0.0.0.0', port: 12345}, [], {}) ).to.not.throw }) - it('#send should throw if supplied non-string type', function() { + it('should throw when send is supplied non-string type', function() { expect(server.send).to.throw(/string argument type/) }) - it('#send should yield an error if supplied non-array type', function(done) { + it('should yield an error when send is supplied non-array type', function(done) { server.send( 'accounting', 0, @@ -326,22 +410,23 @@ describe('tephra', function() { null, null, function(err) { - return done( + done( err ? null : new Error( 'assertion failed: expected `err` to be truthy' ) ) + return } ) }) - it('#respond should throw if supplied non-string type', function() { + it('should throw when respond is supplied non-string type', function() { expect(server.respond).to.throw(/string argument type/) }) - it('#respond should yield an error if no packet is given', function(done) { + it('should yield an error when respond is not given a packet type', function(done) { server.respond( 'accounting', null, @@ -350,13 +435,14 @@ describe('tephra', function() { null, null, function(err) { - return done( + done( err ? null : new Error( 'assertion failed: expected `err` to be truthy' ) ) + return } ) }) From 0193abc931d07d96e11c26a421d58f5e14321997 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 14:26:09 +0200 Subject: [PATCH 08/20] checkpoint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a3f216..01b9ec1 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-fram Key | Type | Required | Notes --- | ---- | -------- | ----- `sharedSecret` | `String` | ✅ | -`ports` | `Object` | ✅ | +`ports` | `Object` | ✅ | All port types are optional, but at least one must be specified, so as to permit instances with different responsibilities. `ports.authentication` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) `ports.accounting` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) `ports.changeOfAuthorisation` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) From 124145a0bdbc5dbfedb4d2f621c0b018500fc5ad Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 14:33:43 +0200 Subject: [PATCH 09/20] checkpoint --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 01b9ec1..b392011 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,22 @@ server.on('Access-Request', function(packet, rinfo, accept, reject) { respond([], {}, console.log) +}).on('CoA-ACK', function(packet, rinfo) { + + console.log(packet, rinfo) + +}).on('CoA-NAK', function(packet, rinfo) { + + console.log(packet, rinfo) + +}).on('Disconnect-ACK', function(packet, rifno) { + + console.log(packet, rinfo) + +}).on('Disconnect-NAK', function(packet, rinfo) { + + console.log(packet, rinfo) + }) server.bind() From 436358f73ab8d9e1a4b0a1e972db208ba3d7fbce Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 15:27:23 +0200 Subject: [PATCH 10/20] checkpoint --- README.md | 4 +- src/decode.js | 2 +- src/encode_request.js | 2 +- src/encode_response.js | 2 +- src/index.js | 11 ++-- test/index.js | 127 ++++++++++++++++++++++++++--------------- 6 files changed, 93 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index b392011..7b92628 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ Key | Type | Required | Notes ## Example ```javascript -var tephra = require('tephra') +import tephra from 'tephra' var users = {user1: 'secret_password'} var server = new tephra({ - sharedSecret: 'shared_secret', + secret: 'shared_secret', ports: { authentication: 1812, accounting: 1813, diff --git a/src/decode.js b/src/decode.js index c7b6b42..dd4d852 100644 --- a/src/decode.js +++ b/src/decode.js @@ -5,7 +5,7 @@ export default function decode(message, guard, on_error) { try { var decoded = radius.decode({ packet: message, - secret: this.shared_secret + secret: this.secret }) } catch (err) { on_error(err) diff --git a/src/encode_request.js b/src/encode_request.js index 7d709b9..ac42734 100644 --- a/src/encode_request.js +++ b/src/encode_request.js @@ -12,7 +12,7 @@ export default function encode_request( try { var encoded = radius.encode({ attributes: node_radius_shim.call(this, attributes, vendor_attributes), - secret: this.shared_secret, + secret: this.secret, code: code }) } catch (err) { diff --git a/src/encode_response.js b/src/encode_response.js index 15c8413..e049586 100644 --- a/src/encode_response.js +++ b/src/encode_response.js @@ -15,7 +15,7 @@ export default function encode_response( packet: packet, code: code, attributes: node_radius_shim.call(this, attributes, vendor_attributes), - secret: this.shared_secret + secret: this.secret }) } catch (err) { on_error(err) diff --git a/src/index.js b/src/index.js index 098a205..78a9292 100644 --- a/src/index.js +++ b/src/index.js @@ -36,6 +36,7 @@ export default class extends EventEmitter { constructor( options = { + secret: '', shared_secret: '', sharedSecret: '', ports: { @@ -54,13 +55,13 @@ export default class extends EventEmitter { ) { super() - var shared_secret = (options.shared_secret || options.sharedSecret) + var secret = (options.secret || options.secret || options.sharedSecret) - if (!shared_secret) { + if (!secret) { throw new Error('Missing shared secret') } - this.shared_secret = shared_secret + this.secret = secret var vendor_dictionaries = (options.vendor_dictionaries || options.vendorDictionaries) @@ -202,8 +203,8 @@ export default class extends EventEmitter { disconnect( rinfo, - attributes, - vendor_attributes, + attributes = [], + vendor_attributes = {}, on_sent ) { rinfo.port = this.change_of_authorisation diff --git a/test/index.js b/test/index.js index f2181b2..a131dce 100644 --- a/test/index.js +++ b/test/index.js @@ -2,12 +2,12 @@ function radclient( address, packet_type, - shared_secret, + secret, packet, on_exec ) { - // TODO add options for flooding - var cmd = `echo "${packet}" | ${process.env.TEST ? '/usr/bin/' : './test/'}radclient -n 1 -x ${address} ${packet_type} ${shared_secret}` + var cmd = `echo "${packet}" | ${process.env.TEST ? '/usr/bin/' : './test/'}radclient -n 1 -x ${address} ${packet_type} ${secret}` + cp.exec(cmd, { timeout: 1000 }, function(err, stdout, stderr) { @@ -21,8 +21,7 @@ import {expect} from 'chai' import tephra from '../src/index.js' -// some fixtures -var test_secret = 'shared_secret' +var test_secret = 'foo' var auth_request = 'User-Name=foo,User-Password=bar' var acct_interim = 'Acct-Status-Type=Interim-Update' var acct_start = 'Acct-Status-Type=Start' @@ -52,13 +51,13 @@ describe('tephra', function() { it('should throw an error if no ports are specified', function() { expect(function() { new tephra({ - shared_secret: test_secret + secret: test_secret }) }).to.throw(/At least one port is required/) expect(function() { new tephra({ - shared_secret: test_secret, + secret: test_secret, ports: {} }) }).to.throw(/At least one port is required/) @@ -67,7 +66,7 @@ describe('tephra', function() { it('should throw if vendor dictionary arguments are invalid', function() { expect(function() { new tephra({ - shared_secret: test_secret, + secret: test_secret, ports: { auth: 1812, acct: 1813, @@ -84,7 +83,7 @@ describe('tephra', function() { it('should hold an internal representation of vendor dictionaries, mapping vendor name to vendor id', function() { var t = new tephra({ - shared_secret: test_secret, + secret: test_secret, ports: { auth: 1812, acct: 1813, @@ -115,7 +114,7 @@ describe('tephra', function() { port_permutations.forEach(function(ports, idx) { it(`permutation ${idx + 1} (${JSON.stringify(ports)}) should bind and unbind successfully`, function(done) { var t = new tephra({ - shared_secret: test_secret, + secret: test_secret, ports: { auth: ports[0], acct: ports[1], @@ -132,12 +131,12 @@ describe('tephra', function() { describe('authentication, accounting, and change of authorisation packet transmission', function() { - var server + var t beforeEach(function(done) { try { - server = new tephra({ - shared_secret: test_secret, + t = new tephra({ + secret: test_secret, ports: { auth: 1812, acct: 1813, @@ -145,7 +144,7 @@ describe('tephra', function() { } }) - server.bind(done) + t.bind(done) } catch (e) { done(e) return @@ -153,11 +152,11 @@ describe('tephra', function() { }) afterEach(function(done) { - server.unbind(done) + t.unbind(done) }) it('should reject irrelevant packet types directed at the auth socket', function(done) { - server.on('error#decode#auth', done.bind(done, null)) + t.on('error#decode#auth', done.bind(done, null)) // send an ACCOUNTING packet to the AUTHENTICATION socket radclient( @@ -175,7 +174,7 @@ describe('tephra', function() { }) it('should emit Access-Request object on receiving packet', function(done) { - server.on('Access-Request', done.bind(done, null)) + t.on('Access-Request', done.bind(done, null)) radclient( 'localhost:1812', @@ -192,7 +191,7 @@ describe('tephra', function() { }) it('should reject irrelevant packet types directed at the acct socket', function(done) { - server.on('error#decode#acct', done.bind(done, null)) + t.on('error#decode#acct', done.bind(done, null)) // send an AUTHENTICATION packet to the ACCOUNTING socket radclient( @@ -210,8 +209,8 @@ describe('tephra', function() { }) it('should send a response for accounting packets', function(done) { - server.on('Accounting-Request', function(request, rinfo) { - server.respond('accounting', request, 'Accounting-Response', rinfo, [], {}, done) + t.on('Accounting-Request', function(request, rinfo) { + t.respond('accounting', request, 'Accounting-Response', rinfo, [], {}, done) }) radclient( @@ -229,7 +228,7 @@ describe('tephra', function() { }) it('should send a response for accounting packets using the event handler responder function', function(done) { - server.on('Accounting-Request', function(request, rinfo, respond) { + t.on('Accounting-Request', function(request, rinfo, respond) { respond([], {}, done) }) @@ -261,12 +260,12 @@ describe('tephra', function() { // expected to be emitted three times (once per accounting status type) - server.on('Accounting-Request', emission_counter) + t.on('Accounting-Request', emission_counter) // each expected to be emitted just once - server.on('Accounting-Request-Start', emission_counter) - server.on('Accounting-Request-Interim-Update', emission_counter) - server.on('Accounting-Request-Stop', emission_counter) + t.on('Accounting-Request-Start', emission_counter) + t.on('Accounting-Request-Interim-Update', emission_counter) + t.on('Accounting-Request-Stop', emission_counter) radclient( 'localhost:1813', @@ -308,9 +307,28 @@ describe('tephra', function() { ) }) - it('should send a response for access-request packets', function(done) { - server.on('Access-Request', function(request, rinfo, accept, reject) { - server.respond('authentication', request, 'Access-Accept', rinfo, [], {}, done) + it('should send an access-accept for access-request packets', function(done) { + t.on('Access-Request', function(request, rinfo, accept, reject) { + t.respond('authentication', request, 'Access-Accept', rinfo, [], {}, done) + }) + + radclient( + 'localhost:1812', + 'auth', + test_secret, + auth_request, + function(err) { + if (err && !err.killed) { + done(err) + return + } + } + ) + }) + + it('should send an access-reject for access-request packets', function(done) { + t.on('Access-Request', function(request, rinfo, accept, reject) { + t.respond('authentication', request, 'Access-Reject', rinfo, [], {}, done) }) radclient( @@ -327,8 +345,8 @@ describe('tephra', function() { ) }) - it('should send a response for access-request packets using the event handler responder function', function(done) { - server.on('Access-Request', function(request, rinfo, accept, reject) { + it('should send an access-accept for access-request packets using the event handler responder function', function(done) { + t.on('Access-Request', function(request, rinfo, accept, reject) { accept([], {}, done) }) @@ -346,8 +364,27 @@ describe('tephra', function() { ) }) + it('should send an access-reject for access-request packets using the event handler responder function', function(done) { + t.on('Access-Request', function(request, rinfo, accept, reject) { + reject([], {}, done) + }) + + radclient( + 'localhost:1812', + 'auth', + test_secret, + auth_request, + function(err) { + if (err && !err.killed) { + done(err) + return + } + } + ) + }) + it('should reject irrelevant packet types directed at the coa socket', function(done) { - server.on('error#decode#coa', done.bind(done, null)) + t.on('error#decode#coa', done.bind(done, null)) radclient( 'localhost:1814', @@ -375,35 +412,35 @@ describe('tephra', function() { } } - server.on('CoA-NAK', emission_counter) - server.on('CoA-ACK', emission_counter) - server.on('Disconnect-NAK', emission_counter) - server.on('Disconnect-ACK', emission_counter) + t.on('CoA-NAK', emission_counter) + t.on('CoA-ACK', emission_counter) + t.on('Disconnect-NAK', emission_counter) + t.on('Disconnect-ACK', emission_counter) - server.send('authentication', 'CoA-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) - server.send('authentication', 'CoA-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) - server.send('authentication', 'Disconnect-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) - server.send('authentication', 'Disconnect-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + t.send('authentication', 'CoA-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + t.send('authentication', 'CoA-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + t.send('authentication', 'Disconnect-ACK', {port: 1814, address: 'localhost'}, [], {}, function() {}) + t.send('authentication', 'Disconnect-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) }) it('should throw when disconnect is not given rinfo', function() { expect( - server.disconnect.bind(server, null, [], {}) + t.disconnect.bind(t, null, [], {}) ).to.throw() }) it('should not throw when disconnect is given all required arguments', function() { expect( - server.disconnect.bind(server, {address: '0.0.0.0', port: 12345}, [], {}) + t.disconnect.bind(t, {address: '0.0.0.0', port: 12345}, [], {}) ).to.not.throw }) it('should throw when send is supplied non-string type', function() { - expect(server.send).to.throw(/string argument type/) + expect(t.send).to.throw(/string argument type/) }) it('should yield an error when send is supplied non-array type', function(done) { - server.send( + t.send( 'accounting', 0, {address: '0.0.0.0', port: 12345}, @@ -423,11 +460,11 @@ describe('tephra', function() { }) it('should throw when respond is supplied non-string type', function() { - expect(server.respond).to.throw(/string argument type/) + expect(t.respond).to.throw(/string argument type/) }) it('should yield an error when respond is not given a packet type', function(done) { - server.respond( + t.respond( 'accounting', null, 0, From 8efbe793cfc194d2a5c49488b1117cca302ac475 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 15:28:54 +0200 Subject: [PATCH 11/20] checkpoint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b92628..575dd66 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # tephra -An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-framework based on [node-radius](https://github.com/retailnext/node-radius). Now it's easier than ever to write a RADIUS server that isn't standards-compliant! 😉 +An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-framework based on [node-radius](https://github.com/retailnext/node-radius). ## Configuration From 61bcc7c0c8eb60f48d474f1868c0e2a8fbe1b8f3 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 15:31:41 +0200 Subject: [PATCH 12/20] checkpoint --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 575dd66..4bb9744 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An event-driven [RADIUS](https://en.wikipedia.org/wiki/RADIUS) server micro-fram Key | Type | Required | Notes --- | ---- | -------- | ----- -`sharedSecret` | `String` | ✅ | +`secret` | `String` | ✅ | `ports` | `Object` | ✅ | All port types are optional, but at least one must be specified, so as to permit instances with different responsibilities. `ports.authentication` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) `ports.accounting` | `Number` | ❌ | Must be a valid port number (0 - 65535 inclusive) @@ -19,10 +19,10 @@ Key | Type | Required | Notes ```javascript import tephra from 'tephra' -var users = {user1: 'secret_password'} +var users = {user1: 'foo'} var server = new tephra({ - secret: 'shared_secret', + secret: 'foo', ports: { authentication: 1812, accounting: 1813, From bcdea3e378987ab6108db2ecabaab6a44f70f2f6 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 15:40:42 +0200 Subject: [PATCH 13/20] checkpoint --- src/{acct_on_message.js => accounting_on_message.js} | 2 +- ...th_on_message.js => authentication_on_message.js} | 2 +- ...sage.js => change_of_authorisation_on_message.js} | 2 +- src/index.js | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) rename src/{acct_on_message.js => accounting_on_message.js} (91%) rename src/{auth_on_message.js => authentication_on_message.js} (89%) rename src/{coa_on_message.js => change_of_authorisation_on_message.js} (82%) diff --git a/src/acct_on_message.js b/src/accounting_on_message.js similarity index 91% rename from src/acct_on_message.js rename to src/accounting_on_message.js index fd2ff1a..1a4d987 100644 --- a/src/acct_on_message.js +++ b/src/accounting_on_message.js @@ -2,7 +2,7 @@ import decode from './decode.js' import accounting_respond from './accounting_respond.js' -export default function acct_on_message(message, rinfo) { +export default function accounting_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/auth_on_message.js b/src/authentication_on_message.js similarity index 89% rename from src/auth_on_message.js rename to src/authentication_on_message.js index 8fa96f4..aa6c249 100644 --- a/src/auth_on_message.js +++ b/src/authentication_on_message.js @@ -3,7 +3,7 @@ import decode from './decode.js' import access_accept from './access_accept.js' import access_reject from './access_reject.js' -export default function auth_on_message(message, rinfo) { +export default function authentication_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/coa_on_message.js b/src/change_of_authorisation_on_message.js similarity index 82% rename from src/coa_on_message.js rename to src/change_of_authorisation_on_message.js index 3a45598..71ec8df 100644 --- a/src/coa_on_message.js +++ b/src/change_of_authorisation_on_message.js @@ -1,7 +1,7 @@ import decode from './decode.js' -export default function coa_on_message(message, rinfo) { +export default function change_of_authorisation_on_message(message, rinfo) { var decoded = decode.call( this, message, diff --git a/src/index.js b/src/index.js index 78a9292..c2b59d5 100644 --- a/src/index.js +++ b/src/index.js @@ -7,9 +7,9 @@ var radius = (await import('radius')).default import send from './send.js' import encode_request from './encode_request.js' import encode_response from './encode_response.js' -import auth_on_message from './auth_on_message.js' -import acct_on_message from './acct_on_message.js' -import coa_on_message from './coa_on_message.js' +import authentication_on_message from './authentication_on_message.js' +import accounting_on_message from './accounting_on_message.js' +import change_of_authorisation_on_message from './change_of_authorisation_on_message.js' function validate_vendor_dictionary(vendor_dictionary) { return ( @@ -100,9 +100,9 @@ export default class extends EventEmitter { this.sockets = {} void [ - {name: 'authentication', value: authentication_port, socket: 'authentication', callback: auth_on_message}, - {name: 'accounting', value: accounting_port, socket: 'accounting', callback: acct_on_message}, - {name: 'change of authorisation', value: change_of_authorisation_port, socket: 'change_of_authorisation', callback: coa_on_message} + {name: 'authentication', value: authentication_port, socket: 'authentication', callback: authentication_on_message}, + {name: 'accounting', value: accounting_port, socket: 'accounting', callback: accounting_on_message}, + {name: 'change of authorisation', value: change_of_authorisation_port, socket: 'change_of_authorisation', callback: change_of_authorisation_on_message} ].forEach(function(port) { var {name, value, socket, callback} = port From 66d1dc21b1674075bdc7bae92c6fda52021500ff Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 15:50:08 +0200 Subject: [PATCH 14/20] checkpoint --- src/index.js | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index c2b59d5..3eeba9f 100644 --- a/src/index.js +++ b/src/index.js @@ -97,23 +97,40 @@ export default class extends EventEmitter { throw new Error('At least one port is required') } + var sockets = [ + { + name: 'authentication', + port: authentication_port, + key: 'authentication', + callback: authentication_on_message + }, + { + name: 'accounting', + port: accounting_port, + key: 'accounting', + callback: accounting_on_message + }, + { + name: 'change of authorisation', + port: change_of_authorisation_port, + key: 'change_of_authorisation', + callback: change_of_authorisation_on_message + } + ] + this.sockets = {} - void [ - {name: 'authentication', value: authentication_port, socket: 'authentication', callback: authentication_on_message}, - {name: 'accounting', value: accounting_port, socket: 'accounting', callback: accounting_on_message}, - {name: 'change of authorisation', value: change_of_authorisation_port, socket: 'change_of_authorisation', callback: change_of_authorisation_on_message} - ].forEach(function(port) { - var {name, value, socket, callback} = port + sockets.forEach(function(socket) { + var {name, port, key, callback} = socket - if (!value) return + if (!port) return - if (!validate_port(value)) { + if (!validate_port(port)) { throw new Error(`Invalid port specified for ${name} socket`) } - this[socket] = value - this.sockets[socket] = dgram.createSocket('udp4', callback.bind(this)) + this[key] = port + this.sockets[key] = dgram.createSocket('udp4', callback.bind(this)) }, this) } From f66945814a0371d233d0be3ed2a91bd67be478cd Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 17:06:52 +0200 Subject: [PATCH 15/20] checkpoint --- Makefile | 5 + README.md | 26 +-- docker-compose.yaml | 2 + package-lock.json | 233 +++++++++++++++++++++- package.json | 3 +- src/access_accept.js | 6 +- src/access_reject.js | 6 +- src/accounting_on_message.js | 10 +- src/accounting_respond.js | 6 +- src/authentication_on_message.js | 10 +- src/change_of_authorisation_on_message.js | 4 +- src/index.js | 37 ++-- src/send.js | 6 +- test/index.js | 76 +++++-- 14 files changed, 356 insertions(+), 74 deletions(-) diff --git a/Makefile b/Makefile index 3f3bd47..d542fcd 100644 --- a/Makefile +++ b/Makefile @@ -2,3 +2,8 @@ .PHONY: test test: docker-compose up --build + open coverage/index.html + +.PHONY: clean +clean: + -rm -rf ./coverage ./node_modules diff --git a/README.md b/README.md index 4bb9744..8446a36 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ var server = new tephra({ ] }) -server.on('Access-Request', function(packet, rinfo, accept, reject) { +server.on('Access-Request', function(packet, remote_host, accept, reject) { var username = packet.attributes['User-Name'] var password = packet.attributes['User-Password'] @@ -59,39 +59,39 @@ server.on('Access-Request', function(packet, rinfo, accept, reject) { accept(attributes, vendor_attributes, console.log) -}).on('Accounting-Request', function(packet, rinfo, respond) { +}).on('Accounting-Request', function(packet, remote_host, respond) { // catch all accounting-requests respond([], {}, console.log) -}).on('Accounting-Request-Start', function(packet, rinfo, respond) { +}).on('Accounting-Request-Start', function(packet, remote_host, respond) { // or just catch specific accounting-request status types... respond([], {}, console.log) -}).on('Accounting-Request-Interim-Update', function(packet, rinfo, respond) { +}).on('Accounting-Request-Interim-Update', function(packet, remote_host, respond) { respond([], {}, console.log) -}).on('Accounting-Request-Stop', function(packet, rinfo, respond) { +}).on('Accounting-Request-Stop', function(packet, remote_host, respond) { respond([], {}, console.log) -}).on('CoA-ACK', function(packet, rinfo) { +}).on('CoA-ACK', function(packet, remote_host) { - console.log(packet, rinfo) + console.log(packet, remote_host) -}).on('CoA-NAK', function(packet, rinfo) { +}).on('CoA-NAK', function(packet, remote_host) { - console.log(packet, rinfo) + console.log(packet, remote_host) -}).on('Disconnect-ACK', function(packet, rifno) { +}).on('Disconnect-ACK', function(packet, remote_host) { - console.log(packet, rinfo) + console.log(packet, remote_host) -}).on('Disconnect-NAK', function(packet, rinfo) { +}).on('Disconnect-NAK', function(packet, remote_host) { - console.log(packet, rinfo) + console.log(packet, remote_host) }) diff --git a/docker-compose.yaml b/docker-compose.yaml index d8f1fd0..6160a80 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,3 +2,5 @@ services: tephra: build: . + volumes: + - ./coverage:/app/coverage diff --git a/package-lock.json b/package-lock.json index e8a3e70..4d2628d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,11 +12,22 @@ "radius": "1.1.4" }, "devDependencies": { + "c8": "10.1.3", "chai": "5.1.2", "mocha": "11.1.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=14.0.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, "node_modules/@isaacs/cliui": { @@ -37,6 +48,44 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -48,6 +97,13 @@ "node": ">=14" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -167,6 +223,71 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/c8/node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -614,6 +735,13 @@ "he": "bin/he" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -700,6 +828,74 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -1013,6 +1209,19 @@ ], "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -1205,6 +1414,28 @@ "node": ">=8.0" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 97ef100..27219b5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "accounting" ], "scripts": { - "test": "if [ $(uname) = 'Darwin' ]; then echo NOTICE: please use 'make test' to run the test suite; exit 1; else mocha test; fi" + "test": "c8 --reporter=html mocha" }, "repository": { "type": "git", @@ -28,6 +28,7 @@ "type": "module", "module": "src/index.js", "devDependencies": { + "c8": "10.1.3", "chai": "5.1.2", "mocha": "11.1.0" }, diff --git a/src/access_accept.js b/src/access_accept.js index 34c9f83..38fbc42 100644 --- a/src/access_accept.js +++ b/src/access_accept.js @@ -1,7 +1,7 @@ export default function access_accept( decoded, - rinfo, + remote_host, attributes, vendor_attributes, on_accepted @@ -10,9 +10,9 @@ export default function access_accept( 'authentication', decoded, 'Access-Accept', - rinfo, + remote_host, attributes, vendor_attributes, - on_accepted || function() {} + on_accepted ) } diff --git a/src/access_reject.js b/src/access_reject.js index 1583205..ebede0d 100644 --- a/src/access_reject.js +++ b/src/access_reject.js @@ -1,7 +1,7 @@ export default function access_reject( decoded, - rinfo, + remote_host, attributes, vendor_attributes, on_rejected @@ -10,9 +10,9 @@ export default function access_reject( 'authentication', decoded, 'Access-Reject', - rinfo, + remote_host, attributes, vendor_attributes, - on_rejected || function() {} + on_rejected ) } diff --git a/src/accounting_on_message.js b/src/accounting_on_message.js index 1a4d987..7ff74ab 100644 --- a/src/accounting_on_message.js +++ b/src/accounting_on_message.js @@ -2,7 +2,7 @@ import decode from './decode.js' import accounting_respond from './accounting_respond.js' -export default function accounting_on_message(message, rinfo) { +export default function accounting_on_message(message, remote_host) { var decoded = decode.call( this, message, @@ -18,15 +18,15 @@ export default function accounting_on_message(message, rinfo) { this.emit( decoded.code, decoded, - rinfo, - accounting_respond.bind(this, decoded, rinfo) + remote_host, + accounting_respond.bind(this, decoded, remote_host) ) // as well as accounting-request-{{status-type}} this.emit( `${decoded.code}-${decoded.attributes['Acct-Status-Type'] || 'unknown'}`, decoded, - rinfo, - accounting_respond.bind(this, decoded, rinfo) + remote_host, + accounting_respond.bind(this, decoded, remote_host) ) } diff --git a/src/accounting_respond.js b/src/accounting_respond.js index ecaa2eb..ee11c2a 100644 --- a/src/accounting_respond.js +++ b/src/accounting_respond.js @@ -1,7 +1,7 @@ export default function accounting_respond( decoded, - rinfo, + remote_host, attributes, vendor_attributes, on_responded @@ -10,9 +10,9 @@ export default function accounting_respond( 'accounting', decoded, 'Accounting-Response', - rinfo, + remote_host, attributes, vendor_attributes, - on_responded || function() {} + on_responded ) } diff --git a/src/authentication_on_message.js b/src/authentication_on_message.js index aa6c249..d0e172e 100644 --- a/src/authentication_on_message.js +++ b/src/authentication_on_message.js @@ -3,7 +3,7 @@ import decode from './decode.js' import access_accept from './access_accept.js' import access_reject from './access_reject.js' -export default function authentication_on_message(message, rinfo) { +export default function authentication_on_message(message, remote_host) { var decoded = decode.call( this, message, @@ -15,14 +15,14 @@ export default function authentication_on_message(message, rinfo) { if (!decoded) { // seems sensible to default to access-reject here - return access_reject.call(this, decoded, rinfo) + return access_reject.call(this, decoded, remote_host, [], {}, function() {}) } this.emit( decoded.code, decoded, - rinfo, - access_accept.bind(this, decoded, rinfo), - access_reject.bind(this, decoded, rinfo) + remote_host, + access_accept.bind(this, decoded, remote_host), + access_reject.bind(this, decoded, remote_host) ) } diff --git a/src/change_of_authorisation_on_message.js b/src/change_of_authorisation_on_message.js index 71ec8df..425dc05 100644 --- a/src/change_of_authorisation_on_message.js +++ b/src/change_of_authorisation_on_message.js @@ -1,7 +1,7 @@ import decode from './decode.js' -export default function change_of_authorisation_on_message(message, rinfo) { +export default function change_of_authorisation_on_message(message, remote_host) { var decoded = decode.call( this, message, @@ -18,5 +18,5 @@ export default function change_of_authorisation_on_message(message, rinfo) { if (!decoded) return - this.emit(decoded.code, decoded, rinfo) + this.emit(decoded.code, decoded, remote_host) } diff --git a/src/index.js b/src/index.js index 3eeba9f..a839684 100644 --- a/src/index.js +++ b/src/index.js @@ -157,15 +157,15 @@ export default class extends EventEmitter { } send( - type, + socket, code, - rinfo, + remote_host, attributes, vendor_attributes, on_sent ) { - if (typeof type !== 'string') { - throw new Error('Missing required string argument type') + if (!(socket in this.sockets)) { + throw new Error(`Invalid socket given: ${socket}`) } var encoded = encode_request.call( @@ -179,24 +179,24 @@ export default class extends EventEmitter { if (!encoded) return send.call( - this.sockets[type.toLowerCase()], + this.sockets[socket], encoded, - rinfo, + remote_host, on_sent ) } respond( - type, + socket, packet, code, - rinfo, + remote_host, attributes, vendor_attributes, on_responded ) { - if (typeof type !== 'string') { - throw new Error('Missing required string argument type') + if (!(socket in this.sockets)) { + throw new Error(`Invalid socket given: ${socket}`) } var encoded = encode_response.call( @@ -211,25 +211,24 @@ export default class extends EventEmitter { if (!encoded) return send.call( - this.sockets[type.toLowerCase()], + this.sockets[socket], encoded, - rinfo, + remote_host, on_responded ) } disconnect( - rinfo, - attributes = [], - vendor_attributes = {}, + remote_host, + attributes, + vendor_attributes, on_sent ) { - rinfo.port = this.change_of_authorisation - this.send( - 'change_of_authorisation', + // just send from the first available socket + this[Object.keys(this.sockets)[0]], 'Disconnect-Request', - rinfo, + remote_host, attributes, vendor_attributes, on_sent diff --git a/src/send.js b/src/send.js index 2da4879..5834b67 100644 --- a/src/send.js +++ b/src/send.js @@ -1,11 +1,11 @@ -export default function send(buffer, rinfo, on_sent) { +export default function send(buffer, remote_host, on_sent) { this.send( buffer, 0, buffer.length, - rinfo.port, - rinfo.address, + remote_host.port, + remote_host.address, on_sent ) } diff --git a/test/index.js b/test/index.js index a131dce..cffca62 100644 --- a/test/index.js +++ b/test/index.js @@ -63,6 +63,35 @@ describe('tephra', function() { }).to.throw(/At least one port is required/) }) + it('should throw an error if invalid ports are specified', function() { + expect(function() { + new tephra({ + secret: test_secret, + ports: { + acct: -1 + } + }) + }).to.throw(/Invalid port specified/) + + expect(function() { + new tephra({ + secret: test_secret, + ports: { + acct: 65536 + } + }) + }).to.throw(/Invalid port specified/) + + expect(function() { + new tephra({ + secret: test_secret, + ports: { + acct: 0.5 + } + }) + }).to.throw(/Invalid port specified/) + }) + it('should throw if vendor dictionary arguments are invalid', function() { expect(function() { new tephra({ @@ -112,7 +141,7 @@ describe('tephra', function() { describe('socket permutations (authentication, accounting, change of authorisation)', function() { port_permutations.forEach(function(ports, idx) { - it(`permutation ${idx + 1} (${JSON.stringify(ports)}) should bind and unbind successfully`, function(done) { + it(`permutation ${idx + 1} (${JSON.stringify(ports)}) should bind and unbind successfully using callbacks`, function(done) { var t = new tephra({ secret: test_secret, ports: { @@ -126,6 +155,21 @@ describe('tephra', function() { t.unbind(done) }) }) + + it(`permutation ${idx + 1} (${JSON.stringify(ports)}) should bind and unbind successfully without callbacks`, function(done) { + var t = new tephra({ + secret: test_secret, + ports: { + auth: ports[0], + acct: ports[1], + coa: ports[2] + } + }) + + t.bind() + t.unbind() + done() + }) }) }) @@ -209,8 +253,8 @@ describe('tephra', function() { }) it('should send a response for accounting packets', function(done) { - t.on('Accounting-Request', function(request, rinfo) { - t.respond('accounting', request, 'Accounting-Response', rinfo, [], {}, done) + t.on('Accounting-Request', function(request, remote_host) { + t.respond('accounting', request, 'Accounting-Response', remote_host, [], {}, done) }) radclient( @@ -228,7 +272,7 @@ describe('tephra', function() { }) it('should send a response for accounting packets using the event handler responder function', function(done) { - t.on('Accounting-Request', function(request, rinfo, respond) { + t.on('Accounting-Request', function(request, remote_host, respond) { respond([], {}, done) }) @@ -308,8 +352,8 @@ describe('tephra', function() { }) it('should send an access-accept for access-request packets', function(done) { - t.on('Access-Request', function(request, rinfo, accept, reject) { - t.respond('authentication', request, 'Access-Accept', rinfo, [], {}, done) + t.on('Access-Request', function(request, remote_host, accept, reject) { + t.respond('authentication', request, 'Access-Accept', remote_host, [], {}, done) }) radclient( @@ -327,8 +371,8 @@ describe('tephra', function() { }) it('should send an access-reject for access-request packets', function(done) { - t.on('Access-Request', function(request, rinfo, accept, reject) { - t.respond('authentication', request, 'Access-Reject', rinfo, [], {}, done) + t.on('Access-Request', function(request, remote_host, accept, reject) { + t.respond('authentication', request, 'Access-Reject', remote_host, [], {}, done) }) radclient( @@ -346,7 +390,7 @@ describe('tephra', function() { }) it('should send an access-accept for access-request packets using the event handler responder function', function(done) { - t.on('Access-Request', function(request, rinfo, accept, reject) { + t.on('Access-Request', function(request, remote_host, accept, reject) { accept([], {}, done) }) @@ -365,7 +409,7 @@ describe('tephra', function() { }) it('should send an access-reject for access-request packets using the event handler responder function', function(done) { - t.on('Access-Request', function(request, rinfo, accept, reject) { + t.on('Access-Request', function(request, remote_host, accept, reject) { reject([], {}, done) }) @@ -423,7 +467,7 @@ describe('tephra', function() { t.send('authentication', 'Disconnect-NAK', {port: 1814, address: 'localhost'}, [], {}, function() {}) }) - it('should throw when disconnect is not given rinfo', function() { + it('should throw when disconnect is not given remote host', function() { expect( t.disconnect.bind(t, null, [], {}) ).to.throw() @@ -435,11 +479,11 @@ describe('tephra', function() { ).to.not.throw }) - it('should throw when send is supplied non-string type', function() { - expect(t.send).to.throw(/string argument type/) + it('should throw when send is not supplied with a socket type', function() { + expect(t.send.bind(t)).to.throw(/Invalid socket given/) }) - it('should yield an error when send is supplied non-array type', function(done) { + it('should yield an error when send is supplied with invalid attribute argument types', function(done) { t.send( 'accounting', 0, @@ -459,8 +503,8 @@ describe('tephra', function() { ) }) - it('should throw when respond is supplied non-string type', function() { - expect(t.respond).to.throw(/string argument type/) + it('should throw when respond is not supplied with a socket type', function() { + expect(t.respond.bind(t)).to.throw(/Invalid socket given/) }) it('should yield an error when respond is not given a packet type', function(done) { From 2ac36c763b2cdbde269e068fb182ada04c81f614 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 17:10:40 +0200 Subject: [PATCH 16/20] checkpoint --- src/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index a839684..c43e209 100644 --- a/src/index.js +++ b/src/index.js @@ -222,7 +222,7 @@ export default class extends EventEmitter { remote_host, attributes, vendor_attributes, - on_sent + on_disconnect_sent ) { this.send( // just send from the first available socket @@ -231,7 +231,7 @@ export default class extends EventEmitter { remote_host, attributes, vendor_attributes, - on_sent + on_disconnect_sent ) } } From 34a28e77bd2bdcc0116899a8cc0f31f7f02e8939 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 17:15:30 +0200 Subject: [PATCH 17/20] checkpoint --- src/accounting_on_message.js | 2 +- test/index.js | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/accounting_on_message.js b/src/accounting_on_message.js index 7ff74ab..50b344f 100644 --- a/src/accounting_on_message.js +++ b/src/accounting_on_message.js @@ -24,7 +24,7 @@ export default function accounting_on_message(message, remote_host) { // as well as accounting-request-{{status-type}} this.emit( - `${decoded.code}-${decoded.attributes['Acct-Status-Type'] || 'unknown'}`, + `${decoded.code}-${decoded.attributes['Acct-Status-Type']}`, decoded, remote_host, accounting_respond.bind(this, decoded, remote_host) diff --git a/test/index.js b/test/index.js index cffca62..3e640c1 100644 --- a/test/index.js +++ b/test/index.js @@ -252,6 +252,24 @@ describe('tephra', function() { ) }) + it('should emit a packet decode error if the request parameters are incorrect', function(done) { + t.on('error#decode#acct', done.bind(done, null)) + + radclient( + 'localhost:1813', + 'acct', + // using the wrong secret intentionally to test the packet decode error handling + test_secret + test_secret, + auth_request, + function(err) { + if (err && !err.killed) { + done(err) + return + } + } + ) + }) + it('should send a response for accounting packets', function(done) { t.on('Accounting-Request', function(request, remote_host) { t.respond('accounting', request, 'Accounting-Response', remote_host, [], {}, done) From 3a3b68a3cc916af28719c14e07058ffce8ac03d6 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 17:40:54 +0200 Subject: [PATCH 18/20] checkpoint --- src/index.js | 26 +++----------------------- src/validate.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 23 deletions(-) create mode 100644 src/validate.js diff --git a/src/index.js b/src/index.js index c43e209..53134e9 100644 --- a/src/index.js +++ b/src/index.js @@ -10,27 +10,7 @@ import encode_response from './encode_response.js' import authentication_on_message from './authentication_on_message.js' import accounting_on_message from './accounting_on_message.js' import change_of_authorisation_on_message from './change_of_authorisation_on_message.js' - -function validate_vendor_dictionary(vendor_dictionary) { - return ( - typeof vendor_dictionary.name === 'string' && - vendor_dictionary.name.length && - typeof vendor_dictionary.path === 'string' && - vendor_dictionary.path.length && - !isNaN(vendor_dictionary.id) && - Number.isInteger(vendor_dictionary.id) && - vendor_dictionary.id - ) -} - -function validate_port(port) { - return ( - !isNaN(port) && - Number.isInteger(port) && - port >= 0 && - port <= 65535 - ) -} +import * as validate from './validate.js' export default class extends EventEmitter { @@ -69,7 +49,7 @@ export default class extends EventEmitter { if (vendor_dictionaries && Array.isArray(vendor_dictionaries)) { vendor_dictionaries.forEach(function(vendor_dictionary, idx) { - if (!validate_vendor_dictionary(vendor_dictionary)) { + if (!validate.vendor_dictionary(vendor_dictionary)) { throw new Error( `Vendor dictionary at index ${idx} is malformed. Expected {name: String, path: String, id: Number} but got ${JSON.stringify(vendor_dictionary)}` ) @@ -125,7 +105,7 @@ export default class extends EventEmitter { if (!port) return - if (!validate_port(port)) { + if (!validate.port(port)) { throw new Error(`Invalid port specified for ${name} socket`) } diff --git a/src/validate.js b/src/validate.js new file mode 100644 index 0000000..17426f1 --- /dev/null +++ b/src/validate.js @@ -0,0 +1,21 @@ + +export function vendor_dictionary(vendor_dictionary) { + return ( + typeof vendor_dictionary.name === 'string' && + vendor_dictionary.name.length && + typeof vendor_dictionary.path === 'string' && + vendor_dictionary.path.length && + !isNaN(vendor_dictionary.id) && + Number.isInteger(vendor_dictionary.id) && + vendor_dictionary.id + ) +} + +export function port(port) { + return ( + !isNaN(port) && + Number.isInteger(port) && + port >= 0 && + port <= 65535 + ) +} From cb15f1cc3a9effce20eab4660010a11813bd0277 Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 18:05:08 +0200 Subject: [PATCH 19/20] checkpoint --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 53134e9..05ca225 100644 --- a/src/index.js +++ b/src/index.js @@ -35,7 +35,7 @@ export default class extends EventEmitter { ) { super() - var secret = (options.secret || options.secret || options.sharedSecret) + var secret = (options.secret || options.shared_secret || options.sharedSecret) if (!secret) { throw new Error('Missing shared secret') From 4129f1c7ffed25127499e78cb4f9aad27495f60f Mon Sep 17 00:00:00 2001 From: Ant Cosentino Date: Thu, 6 Feb 2025 18:11:26 +0200 Subject: [PATCH 20/20] checkpoint --- src/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 05ca225..452ad2f 100644 --- a/src/index.js +++ b/src/index.js @@ -103,7 +103,9 @@ export default class extends EventEmitter { sockets.forEach(function(socket) { var {name, port, key, callback} = socket - if (!port) return + // check if we can return early but also make sure the + // port isn't set to zero because zero is a valid port number + if (!port && port !== 0) return if (!validate.port(port)) { throw new Error(`Invalid port specified for ${name} socket`)