From 80788319711eb60137561a3c29ad25a2a665e17f Mon Sep 17 00:00:00 2001 From: David Kebler Date: Wed, 28 Aug 2019 09:02:12 -0700 Subject: [PATCH] 0.2.19 register authenticator added to consumer and socket default consumer authenticator looks for token in environment or opts or sets to 'default' default socket authenticator looks for token validator method add and remove token methods default token validator method just checks sent token against list can register alt token validator improved authenticate function to handle fail reason and anonymous connect --- examples/server.js | 6 ++-- package.json | 4 ++- src/consumer.js | 32 +++++++++++++--------- src/socket-class.js | 67 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 85 insertions(+), 24 deletions(-) diff --git a/examples/server.js b/examples/server.js index cfd4c66..e3d802f 100644 --- a/examples/server.js +++ b/examples/server.js @@ -37,7 +37,9 @@ class Test extends Socket { // // ca: [ fs.readFileSync('client-cert.pem') ] // } -let options = {path:true, tokens:['test'], conPacket:{cmd:'onconnect', data:'this is a packet data sent consumer after handshake/authentification'}} +let options = {path:true} +// options.conPacket = {cmd:'onconnect', data:'this is a packet data sent consumer after handshake/authentification'} +options.tokens = ['cheetos'] let test = new Test(options) async function processor (packet) { @@ -64,7 +66,7 @@ test.registerPacketProcessor(processor) // console.log('using TLS') // } - + // test.addTokens('cheetos') await test.create() let count = 0 diff --git a/package.json b/package.json index 95951a3..1f81bd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uci/socket", - "version": "0.2.18", + "version": "0.2.19", "description": "JSON packet intra(named)/inter(TCP) host communication over socket", "main": "src", "scripts": { @@ -10,9 +10,11 @@ "testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true", "s": "node -r esm examples/server", "devs": "UCI_ENV=dev ./node_modules/.bin/nodemon -r esm examples/server", + "devs:anon": "UCI_ANON=true npm run devs", "devs:debug": "UCI_LOG_LEVEL=debug npm run devs", "client": "=node -r esm examples/client", "devc": "UCI_ENV=dev ./node_modules/.bin/nodemon -r esm examples/client", + "devc:token": "UCI_CLIENT_TOKEN='cheetos' npm run devc", "devc:debug": "UCI_LOG_LEVEL=debug npm run devc", "c2": "node -r esm examples/client2" }, diff --git a/src/consumer.js b/src/consumer.js index 9e37d0a..70546dc 100644 --- a/src/consumer.js +++ b/src/consumer.js @@ -93,13 +93,14 @@ class SocketConsumer extends Socket { if (packet._handshake) { clearTimeout(initTimeout) this._connected = true - let authPacket = this._authenticate() + let authPacket = this._authenticate() || {} authPacket._authenticate = true authPacket.clientName = this.id let res = (await this._authenticateSend(authPacket)) || {} - console.log('auth response', res) - // if ((await this.send(authPacket)).authenticated) authenticated() - if (!res.authenticated) reject('unable to authenticate') + if (!res.authenticated) { + this.emit('connection',{msg:`authentication failed: ${res.reason}`, id:this.id, opts:this.opts, authenticated:this._authenticated, connected:this._connected}) + reject('unable to authenticate') + } else { this._authenticated = res.authenticated this.removeListener('error',initialErrorHandler) @@ -153,9 +154,7 @@ class SocketConsumer extends Socket { if (err) resolve({error: 'unable to serialize packet for sending',packet: packet}) await this.__write(res) - console.log(packet._header) this.once(packet._header.id, async function(reply) { - console.log('returning response', reply) let res = await this._packetProcess(reply) if (!res) { // if packetProcess was not promise res = reply @@ -172,12 +171,19 @@ class SocketConsumer extends Socket { this._packetProcess = func } - // PRIVATE METHODS - _authenticate () { - return { token:'test'} + // func should return an object the server expects + registerAuthenticator (func) { + this._authenticate = func } - async _authenticateSend (authPacket) { + // PRIVATE METHODS + + // default authentication using a simple token + _authenticate () { + return { token: process.env.UCI_CLIENT_TOKEN || this.token || 'default' } + } + + async _authenticateSend (authPacket={}) { return new Promise(async resolve => { setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000) let [err, res] = await btc(this.stream.serialize)(authPacket) @@ -206,13 +212,13 @@ class SocketConsumer extends Socket { const handshake = async (packet) => { if (packet._handshake) { this._connected = true - let authPacket = this._authenticate() + let authPacket = this._authenticate() || {} authPacket._authenticate = true authPacket.clientName = this.id let res = (await this._authenticateSend(authPacket)) || {} if (!res.authenticated) { - this.emit('connection',{msg:'authentification failed', id:this.id, opts:this.opts, authenticated:this._authenticated, connected:this._connected, res:res}) - + this.emit('connection',{msg:`authentication failed: ${res.reason}`, id:this.id, opts:this.opts, authenticated:this._authenticated, connected:this._connected}) + this.emit('error',{code:'authentification failed'}) } else { this._authenticated = res.authenticated diff --git a/src/socket-class.js b/src/socket-class.js index ac99d3b..cd4df60 100644 --- a/src/socket-class.js +++ b/src/socket-class.js @@ -57,7 +57,7 @@ export default function socketClass(Server) { if (path.dirname(opts.path) === '.') // relative path sent opts.path = path.join(DEFAULT_PIPE_DIR, opts.path) } - this.allowAnonymous = process.env.UCI_ANON || opts.allowAnonymous || false + this.allowAnonymous = (!opts.tokens || !!process.env.UCI_ANON || opts.allowAnonymous) ? true : false this.tokens = opts.tokens || [] this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true this.pingInterval = opts.pingInterval === false ? opts.pingInterval : (opts.pingInterval * 1000 || 5000) @@ -150,6 +150,41 @@ export default function socketClass(Server) { clearInterval(this._ping) } + addTokens(tokens) { + if (typeof tokens ==='string'){ + tokens = tokens.split(',') + } + this.tokens = this.tokens.concat(tokens) + if (this.tokens.length>0) this.allowAnonymous = false + } + + removeTokens(tokens) { + if (typeof tokens ==='string'){ + if (tokens === 'all') { + this.tokens = [] + this.allowAnonymous = true + return + } + tokens = tokens.split(',') + } + this.tokens = this.tokens.filter(token => !tokens.includes(token)) + if (this.tokens.length===0) { + log.warn({msg:'all tokens have been removed, switching to allow anonymous connections'}) + this.allowAnonymous = true + } + } + + registerTokenValidator (func) { + this.allowAnonymous = false + this._validateToken = func + } + + registerAuthenticator (func) { + this.allowAnonymous = false + this._authenticate = func + } + + /** * push - pushes a supplied UCI object packet to all connected clients * @@ -193,17 +228,26 @@ export default function socketClass(Server) { } async function authenticate (client,packet) { - log.info({msg:`authentication packet from client ${client.id}`, packet:packet}) + log.debug({msg:`authentication packet from client ${client.id}`, packet:packet}) client.stream.removeAllListeners('message') if (!packet._authenticate) reject('first client packet was not authentication') else { let [err, res] = await btc(this._authenticate)(packet) - client.authenticated = this.allowAnonymous ? 'anonymous' : (err || res) + client.authenticated = this.allowAnonymous ? 'anonymous' : (err ? false : res) client.name = packet.clientName packet.authenticated = client.authenticated + packet.reason = err || null + log.debug({msg:'sending authorization result to client', packet:packet}) await this._send(client,packet) // send either way - if (err && !this.allowAnonymous) reject(client.authenticated) - else resolve(client.authenticated) + if (err && !this.allowAnonymous) { + log.info({msg:'client authentication failed', client:client.name, client_id:client.id, reason:err}) + reject(packet.reason) + } + else { + log.info({msg:'client authenticated successfuly', client:client.name, client_id:client.id}) + if (this.allowAnonymous) log.warn({msg:'client connected anonymously', client:client.name, client_id:client.id}) + resolve(client.authenticated) + } } } }.bind(this)) @@ -211,9 +255,16 @@ export default function socketClass(Server) { // private methods - // default authenticator + + // default validator + _validateToken (token) { + if (token) return this.tokens.includes(token) + return false + } + + // default authenticator - reject value should be reason which is returned to client async _authenticate (packet) { - if (this.tokens.indexOf(packet.token) === -1) return Promise.reject(false) + if (!this._validateToken(packet.token)) return Promise.reject('invalid token') return true } @@ -271,7 +322,7 @@ export default function socketClass(Server) { // that's it. Connection is active async function messageProcess(client, packet) { - log.info({method:'_listen', line:179, packet: packet, client:client.name, msg:'incoming packet on socket side'}) + log.debug({method:'_listen', line:179, packet: packet, client:client.name, msg:'incoming packet on socket side'}) let res = (await this._packetProcess(clone(packet))) || {} if (Object.keys(res).length === 0) res = {