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
master
David Kebler 2019-08-28 09:02:12 -07:00
parent 3299e9125e
commit 8078831971
4 changed files with 85 additions and 24 deletions

View File

@ -37,7 +37,9 @@ class Test extends Socket {
// // ca: [ fs.readFileSync('client-cert.pem') ] // // 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) let test = new Test(options)
async function processor (packet) { async function processor (packet) {
@ -64,7 +66,7 @@ test.registerPacketProcessor(processor)
// console.log('using TLS') // console.log('using TLS')
// } // }
// test.addTokens('cheetos')
await test.create() await test.create()
let count = 0 let count = 0

View File

@ -1,6 +1,6 @@
{ {
"name": "@uci/socket", "name": "@uci/socket",
"version": "0.2.18", "version": "0.2.19",
"description": "JSON packet intra(named)/inter(TCP) host communication over socket", "description": "JSON packet intra(named)/inter(TCP) host communication over socket",
"main": "src", "main": "src",
"scripts": { "scripts": {
@ -10,9 +10,11 @@
"testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true", "testci": "istanbul cover ./node_modules/.bin/_mocha --report lcovonly -- -R spec --recursive && codecov || true",
"s": "node -r esm examples/server", "s": "node -r esm examples/server",
"devs": "UCI_ENV=dev ./node_modules/.bin/nodemon -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", "devs:debug": "UCI_LOG_LEVEL=debug npm run devs",
"client": "=node -r esm examples/client", "client": "=node -r esm examples/client",
"devc": "UCI_ENV=dev ./node_modules/.bin/nodemon -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", "devc:debug": "UCI_LOG_LEVEL=debug npm run devc",
"c2": "node -r esm examples/client2" "c2": "node -r esm examples/client2"
}, },

View File

@ -93,13 +93,14 @@ class SocketConsumer extends Socket {
if (packet._handshake) { if (packet._handshake) {
clearTimeout(initTimeout) clearTimeout(initTimeout)
this._connected = true this._connected = true
let authPacket = this._authenticate() let authPacket = this._authenticate() || {}
authPacket._authenticate = true authPacket._authenticate = true
authPacket.clientName = this.id authPacket.clientName = this.id
let res = (await this._authenticateSend(authPacket)) || {} let res = (await this._authenticateSend(authPacket)) || {}
console.log('auth response', res) if (!res.authenticated) {
// if ((await this.send(authPacket)).authenticated) authenticated() this.emit('connection',{msg:`authentication failed: ${res.reason}`, id:this.id, opts:this.opts, authenticated:this._authenticated, connected:this._connected})
if (!res.authenticated) reject('unable to authenticate') reject('unable to authenticate')
}
else { else {
this._authenticated = res.authenticated this._authenticated = res.authenticated
this.removeListener('error',initialErrorHandler) this.removeListener('error',initialErrorHandler)
@ -153,9 +154,7 @@ class SocketConsumer extends Socket {
if (err) if (err)
resolve({error: 'unable to serialize packet for sending',packet: packet}) resolve({error: 'unable to serialize packet for sending',packet: packet})
await this.__write(res) await this.__write(res)
console.log(packet._header)
this.once(packet._header.id, async function(reply) { this.once(packet._header.id, async function(reply) {
console.log('returning response', reply)
let res = await this._packetProcess(reply) let res = await this._packetProcess(reply)
if (!res) { // if packetProcess was not promise if (!res) { // if packetProcess was not promise
res = reply res = reply
@ -172,12 +171,19 @@ class SocketConsumer extends Socket {
this._packetProcess = func this._packetProcess = func
} }
// PRIVATE METHODS // func should return an object the server expects
_authenticate () { registerAuthenticator (func) {
return { token:'test'} 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 => { return new Promise(async resolve => {
setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000) setTimeout(() => {resolve({ error: 'no response from socket in 10sec' })}, 10000)
let [err, res] = await btc(this.stream.serialize)(authPacket) let [err, res] = await btc(this.stream.serialize)(authPacket)
@ -206,13 +212,13 @@ class SocketConsumer extends Socket {
const handshake = async (packet) => { const handshake = async (packet) => {
if (packet._handshake) { if (packet._handshake) {
this._connected = true this._connected = true
let authPacket = this._authenticate() let authPacket = this._authenticate() || {}
authPacket._authenticate = true authPacket._authenticate = true
authPacket.clientName = this.id authPacket.clientName = this.id
let res = (await this._authenticateSend(authPacket)) || {} let res = (await this._authenticateSend(authPacket)) || {}
if (!res.authenticated) { 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 { else {
this._authenticated = res.authenticated this._authenticated = res.authenticated

View File

@ -57,7 +57,7 @@ export default function socketClass(Server) {
if (path.dirname(opts.path) === '.') // relative path sent if (path.dirname(opts.path) === '.') // relative path sent
opts.path = path.join(DEFAULT_PIPE_DIR, opts.path) 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.tokens = opts.tokens || []
this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true this.keepAlive = 'keepAlive' in opts ? opts.keepAlive : true
this.pingInterval = opts.pingInterval === false ? opts.pingInterval : (opts.pingInterval * 1000 || 5000) this.pingInterval = opts.pingInterval === false ? opts.pingInterval : (opts.pingInterval * 1000 || 5000)
@ -150,6 +150,41 @@ export default function socketClass(Server) {
clearInterval(this._ping) 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 * 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) { 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') client.stream.removeAllListeners('message')
if (!packet._authenticate) reject('first client packet was not authentication') if (!packet._authenticate) reject('first client packet was not authentication')
else { else {
let [err, res] = await btc(this._authenticate)(packet) 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 client.name = packet.clientName
packet.authenticated = client.authenticated 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 await this._send(client,packet) // send either way
if (err && !this.allowAnonymous) reject(client.authenticated) if (err && !this.allowAnonymous) {
else resolve(client.authenticated) 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)) }.bind(this))
@ -211,9 +255,16 @@ export default function socketClass(Server) {
// private methods // 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) { 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 return true
} }
@ -271,7 +322,7 @@ export default function socketClass(Server) {
// that's it. Connection is active // that's it. Connection is active
async function messageProcess(client, packet) { 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))) || {} let res = (await this._packetProcess(clone(packet))) || {}
if (Object.keys(res).length === 0) if (Object.keys(res).length === 0)
res = { res = {