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 = {