chore(websocket): Adding support for redis username in websocket server (#25826)

This commit is contained in:
Craig Rueda 2023-11-01 16:52:14 -07:00 committed by GitHub
parent a3686459a9
commit 6ace22da87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 52 deletions

View File

@ -1,10 +1,7 @@
{
"redis": {
"port": 6379,
"host": "127.0.0.1",
"password": "",
"db": 10,
"ssl": false
"password": "some pwd"
},
"statsd": {
"host": "127.0.0.1",

View File

@ -9,10 +9,12 @@
"version": "0.0.1",
"license": "Apache-2.0",
"dependencies": {
"@types/lodash": "^4.14.200",
"cookie": "^0.5.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"ws": "^8.14.2"
@ -1421,6 +1423,11 @@
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.14.200",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz",
"integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q=="
},
"node_modules/@types/node": {
"version": "20.8.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
@ -2431,9 +2438,9 @@
}
},
"node_modules/denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
@ -3491,9 +3498,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ioredis": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz",
"integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==",
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"dependencies": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
@ -4530,8 +4537,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.defaults": {
"version": "4.2.0",
@ -4541,7 +4547,7 @@
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
},
"node_modules/lodash.includes": {
"version": "4.3.0",
@ -7271,6 +7277,11 @@
"@types/node": "*"
}
},
"@types/lodash": {
"version": "4.14.200",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz",
"integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q=="
},
"@types/node": {
"version": "20.8.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.10.tgz",
@ -8029,9 +8040,9 @@
"dev": true
},
"denque": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
"integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
},
"detect-newline": {
"version": "3.1.0",
@ -8816,9 +8827,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ioredis": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz",
"integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==",
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",
@ -9613,8 +9624,7 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash.defaults": {
"version": "4.2.0",
@ -9624,7 +9634,7 @@
"lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
},
"lodash.includes": {
"version": "4.3.0",

View File

@ -16,10 +16,12 @@
},
"license": "Apache-2.0",
"dependencies": {
"@types/lodash": "^4.14.200",
"cookie": "^0.5.0",
"hot-shots": "^10.0.0",
"ioredis": "^4.28.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"uuid": "^9.0.1",
"winston": "^3.11.0",
"ws": "^8.14.2"

View File

@ -26,7 +26,7 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
);
expect(config.redis.host).toEqual('127.0.0.1');
expect(config.redis.port).toEqual(6379);
expect(config.redis.password).toEqual('');
expect(config.redis.password).toEqual('some pwd');
expect(config.redis.db).toEqual(10);
expect(config.redis.ssl).toEqual(false);
expect(config.statsd.host).toEqual('127.0.0.1');
@ -65,3 +65,11 @@ test('buildConfig() builds configuration and applies env var overrides', () => {
delete process.env.STATSD_PORT;
delete process.env.STATSD_GLOBAL_TAGS;
});
test('buildConfig() performs deep merge between configs', () => {
const config = buildConfig();
// We left the ssl setting the default
expect(config.redis.ssl).toEqual(false);
// We overrode the pwd
expect(config.redis.password).toEqual('some pwd');
});

View File

@ -138,36 +138,53 @@ describe('server', () => {
describe('redisUrlFromConfig', () => {
test('it builds a valid Redis URL from defaults', () => {
expect(
server.redisUrlFromConfig({
server.buildRedisOpts({
port: 6379,
host: '127.0.0.1',
username: 'test-user',
password: '',
db: 0,
ssl: false,
validateHostname: false,
}),
).toEqual('redis://127.0.0.1:6379/0');
).toEqual({ db: 0, host: '127.0.0.1', port: 6379 });
});
test('it builds a valid Redis URL with a password', () => {
expect(
server.redisUrlFromConfig({
server.buildRedisOpts({
port: 6380,
host: 'redis.local',
username: 'cool-user',
password: 'foo',
db: 1,
ssl: false,
validateHostname: false,
}),
).toEqual('redis://:foo@redis.local:6380/1');
).toEqual({
db: 1,
host: 'redis.local',
password: 'foo',
port: 6380,
username: 'cool-user',
});
});
test('it builds a valid Redis URL with SSL', () => {
expect(
server.redisUrlFromConfig({
server.buildRedisOpts({
port: 6379,
host: '127.0.0.1',
password: '',
username: 'cool-user',
db: 0,
ssl: true,
validateHostname: false,
}),
).toEqual('rediss://127.0.0.1:6379/0');
).toEqual({
db: 0,
host: '127.0.0.1',
port: 6379,
tls: { checkServerIdentity: expect.anything() },
});
});
});

View File

@ -16,6 +16,19 @@
* specific language governing permissions and limitations
* under the License.
*/
import * as _ from 'lodash';
export interface RedisConfig {
port: number;
host: string;
password: string;
username: string;
db: number;
ssl: boolean;
validateHostname: boolean;
}
type ConfigType = {
port: number;
logLevel: string;
@ -26,13 +39,7 @@ type ConfigType = {
port: number;
globalTags: Array<string>;
};
redis: {
port: number;
host: string;
password: string;
db: number;
ssl: boolean;
};
redis: RedisConfig;
redisStreamPrefix: string;
redisStreamReadCount: number;
redisStreamReadBlockMs: number;
@ -70,8 +77,10 @@ function defaultConfig(): ConfigType {
host: '127.0.0.1',
port: 6379,
password: '',
username: 'default',
db: 0,
ssl: false,
validateHostname: true,
},
};
}
@ -114,6 +123,7 @@ function applyEnvOverrides(config: ConfigType): ConfigType {
REDIS_HOST: val => (config.redis.host = val),
REDIS_PORT: val => (config.redis.port = toNumber(val)),
REDIS_PASSWORD: val => (config.redis.password = val),
REDIS_USERNAME: val => (config.redis.username = val),
REDIS_DB: val => (config.redis.db = toNumber(val)),
REDIS_SSL: val => (config.redis.ssl = toBoolean(val)),
STATSD_HOST: val => (config.statsd.host = val),
@ -132,6 +142,6 @@ function applyEnvOverrides(config: ConfigType): ConfigType {
}
export function buildConfig(): ConfigType {
const config = Object.assign(defaultConfig(), configFromFile());
const config = _.merge(defaultConfig(), configFromFile());
return applyEnvOverrides(config);
}

View File

@ -22,11 +22,12 @@ import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid';
import jwt, { Algorithm } from 'jsonwebtoken';
import cookie from 'cookie';
import Redis from 'ioredis';
import Redis, { RedisOptions } from 'ioredis';
import StatsD from 'hot-shots';
import { createLogger } from './logger';
import { buildConfig } from './config';
import { buildConfig, RedisConfig } from './config';
import { checkServerIdentity, PeerCertificate } from 'tls';
export type StreamResult = [
recordId: string,
@ -66,13 +67,6 @@ export interface SocketInstance {
channel: string;
pongTs: number;
}
interface RedisConfig {
port: number;
host: string;
password?: string | null;
db: number;
ssl: boolean;
}
interface ChannelValue {
sockets: Array<string>;
@ -103,15 +97,39 @@ export const statsd = new StatsD({
if (startServer && opts.jwtSecret.length < 32)
throw new Error('Please provide a JWT secret at least 32 bytes long');
export const redisUrlFromConfig = (redisConfig: RedisConfig): string => {
let url = redisConfig.ssl ? 'rediss://' : 'redis://';
if (redisConfig.password) url += `:${redisConfig.password}@`;
url += `${redisConfig.host}:${redisConfig.port}/${redisConfig.db}`;
return url;
export const buildRedisOpts = (baseConfig: RedisConfig) => {
const redisOpts: RedisOptions = {
port: baseConfig.port,
host: baseConfig.host,
db: baseConfig.db,
};
const passwd = baseConfig.password;
if (passwd !== '') {
redisOpts.username = baseConfig.username;
redisOpts.password = baseConfig.password;
}
if (baseConfig.ssl) {
redisOpts.tls = {
checkServerIdentity: (
hostname: string,
cert: PeerCertificate,
): Error | undefined => {
// Note, the cert chain will have been verified already. the role of this method is to
// validate that at least one of the SAN's (or subject) of the server's cert matches the provided hostname
if (baseConfig.validateHostname) {
return checkServerIdentity(hostname, cert);
}
},
};
}
return redisOpts;
};
// initialize servers
const redis = new Redis(redisUrlFromConfig(opts.redis));
const redis = new Redis(buildRedisOpts(opts.redis));
const httpServer = http.createServer();
export const wss = new WebSocket.Server({
noServer: true,