Skip to content

Commit

Permalink
Merge pull request #57 from hubotio/reject-unauthorized
Browse files Browse the repository at this point in the history
fix: Enable connecting to Redis with the rediss:// (SSL/TLS) with a s…
  • Loading branch information
joeyguerra authored Jul 23, 2023
2 parents 8089baa + 4971ede commit a7f7f4a
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 29 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"scripts": {
"pretest": "standard",
"test": "nyc --reporter=html --reporter=text mocha --exit",
"e2e": "mocha --exit test/e2e",
"test:watch": "mocha --watch --exit",
"e2e": "mocha test/e2e --exit",
"standard": "standard"
},
"dependencies": {
Expand Down
47 changes: 36 additions & 11 deletions src/redis-brain.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,12 @@
const URL = require('url').URL
const Redis = require('redis')

module.exports = function (robot) {
let client, prefix
module.exports = function (robot, redis = Redis) {
const redisUrlEnv = getRedisEnv()
const redisUrl = process.env[redisUrlEnv] || 'redis://localhost:6379'

if (redisUrlEnv) {
robot.logger.info(`hubot-redis-brain: Discovered redis from ${redisUrlEnv} environment variable`)
robot.logger.info(`hubot-redis-brain: Discovered redis from ${redisUrlEnv} environment variable: ${redisUrl}`)
} else {
robot.logger.info('hubot-redis-brain: Using default redis on localhost:6379')
}
Expand All @@ -31,17 +30,43 @@ module.exports = function (robot) {
robot.logger.info('Turning off redis ready checks')
}

const info = new URL(redisUrl)
if (info.hostname === '') {
client = Redis.createClient(info.pathname)
prefix = (info.query ? info.query.toString() : undefined) || 'hubot'
} else {
client = (info.auth || process.env.REDIS_NO_CHECK)
? Redis.createClient(info.port, info.hostname, { no_ready_check: true })
: Redis.createClient(info.port, info.hostname)
let info = null
let prefix = ''
try {
info = new URL(redisUrl)
prefix = (info.pathname ? info.pathname.replace('/', '') : undefined) || 'hubot'
} catch (err) {
if (err.code === 'ERR_INVALID_URL') {
const urlPath = redisUrl.replace(/rediss?:\/{2}:?(.*@)?/, '')
info = new URL(`redis://${urlPath}`)
prefix = info.search?.replace('?', '') || 'hubot'
}
}

let redisOptions = {
url: redisUrl
}

let redisSocket = null

if (info.protocol === 'rediss:') {
redisSocket = { tls: true }
}

if (process.env.REDIS_REJECT_UNAUTHORIZED) {
redisSocket.rejectUnauthorized = process.env.REDIS_REJECT_UNAUTHORIZED === 'true'
}

if (info.auth || process.env.REDIS_NO_CHECK) {
redisOptions = Object.assign(redisOptions || {}, { no_ready_check: true })
}

if (redisSocket) {
redisOptions = Object.assign(redisOptions || {}, { socket: redisSocket })
}

const client = redis.createClient(redisOptions)

robot.brain.setAutoSave(false)

const getData = () => {
Expand Down
138 changes: 121 additions & 17 deletions test/redis-brain-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

/* global describe, it */
/* eslint-disable no-unused-expressions */
const Redis = require('redis')
const shell = require('hubot/src/adapters/shell')
const Adapter = require('hubot/src/adapter')

const path = require('path')
const redisBrain = require('../src/redis-brain.js')
const EventEmitter = require('events')

const chai = require('chai')
const sinon = require('sinon')
Expand All @@ -17,32 +16,137 @@ const Robot = Hubot.Robot

chai.use(require('sinon-chai'))

class RedisMock extends EventEmitter {
constructor (delegate) {
super()
this.data = {}
this.delegate = delegate
}

async connect () {
this.emit('connect')
}

async get (key) {
if (this.delegate?.get) return this.delegate.get(key)
return this.data[key]
}

async set (key, value) {
if (this.delegate?.set) return this.delegate.set(key)
this.data[key] = value
}

quit () {
if (this.delegate?.quit) return this.delegate.quit()
}
}

// The mock-adapter-v3 has old dependencies with security issues, so don't use that.
// Instead, we'll use the shell adapter and mock the use() method because the current version of Hubot
// doesn't have a way to specify an adapter via a path.

shell.use = robot => {
return new Adapter()
}

describe('redis-brain', () => {
it('exports a function', () => {
expect(require('../index')).to.be.a('Function')
})

it('connects to redis', () => {
// The mock-adapter-v3 has old dependencies with security issues, so don't use that.
// Instead, we'll use the shell adapter and mock the use() method because the current version of Hubot
// doesn't have a way to specify an adapter via a path.
it('Hostname should never be empty', () => {
process.env.REDIS_URL = 'redis://'
const robot = new Robot(null, 'shell', false, 'hubot')
sinon.spy(robot.logger, 'info')
redisBrain(robot, {
createClient: (options) => {
return new RedisMock()
}
})
robot.run()
expect(robot.logger.info).to.have.been.calledWith('hubot-redis-brain: Discovered redis from REDIS_URL environment variable: redis://')
robot.shutdown()
sinon.restore()
delete process.env.REDIS_URL
})

it('Connect to redis without setting the REDIS_URL environment variable', () => {
delete process.env.REDIS_URL
const robot = new Robot(null, 'shell', false, 'hubot')
sinon.spy(robot.logger, 'info')
redisBrain(robot, {
createClient: (options) => {
return new RedisMock()
}
})
robot.run()
expect(robot.logger.info).to.have.been.calledWith('hubot-redis-brain: Using default redis on localhost:6379')
robot.shutdown()
sinon.restore()
})

it('Connect vis SSL: Check that the options are set by environment variables', () => {
shell.use = robot => {
return new Adapter()
}

const robot = new Robot(null, 'shell', false, 'hubot')

sinon.spy(Redis, 'createClient')
sinon.spy(robot.logger, 'info')

robot.loadFile(path.resolve('src/'), 'redis-brain.js')
process.env.REDIS_URL = 'rediss://localhost:6379'
process.env.REDIS_REJECT_UNAUTHORIZED = 'false'
process.env.REDIS_NO_CHECK = 'true'
redisBrain(robot, {
createClient: (options) => {
expect(options.url).to.equal(process.env.REDIS_URL)
expect(options.socket.tls).to.be.true
expect(options.no_ready_check).to.be.true
expect(options.socket.rejectUnauthorized).to.be.false
return new RedisMock()
}
})
robot.run()
robot.shutdown()
delete process.env.REDIS_URL
delete process.env.REDIS_REJECT_UNAUTHORIZED
delete process.env.REDIS_NO_CHECK
})

expect(Redis.createClient).to.have.been.calledOnce
expect(Redis.createClient).to.have.been.calledWith('6379', 'localhost')
expect(robot.logger.info).to.have.been.calledWith('hubot-redis-brain: Using default redis on localhost:6379')
it('Setting the prefix with redis://localhost:6379/prefix-for-redis-key', () => {
process.env.REDIS_URL = 'redis://localhost:6379/prefix-for-redis-key'
const robot = new Robot(null, 'shell', false, 'hubot')
const delegate = {
data: {},
async get (key) {
expect(key).to.equal('prefix-for-redis-key:storage')
robot.shutdown()
delete process.env.REDIS_URL
return this.data[key]
}
}
redisBrain(robot, {
createClient: (options) => {
return new RedisMock(delegate)
}
})
robot.run()
})

robot.shutdown()
it('Setting the prefix in the query string redis://:password@/var/run/redis.sock?prefix-for-redis-key', () => {
process.env.REDIS_URL = 'redis://username:test@/var/run/redis.sock?prefix-for-redis-key'
const robot = new Robot(null, 'shell', false, 'hubot')
const delegate = {
data: {},
async get (key) {
expect(key).to.equal('prefix-for-redis-key:storage')
robot.shutdown()
delete process.env.REDIS_URL
return this.data[key]
}
}
redisBrain(robot, {
createClient: (options) => {
return new RedisMock(delegate)
}
})
robot.run()
})
})

0 comments on commit a7f7f4a

Please sign in to comment.