shared/common/socket-redis-io-adapter.ts
Class of websocket redis adapter for Nest app. Verify jwt token on connection, and decode token data for socket
IoAdapter
Properties |
|
Methods |
Private Readonly logger |
Default value : new Logger(RedisIoAdapter.name)
|
Defined in shared/common/socket-redis-io-adapter.ts:36
|
logger |
createIOServer | |||||||||||||||
createIOServer(port: number, options: object)
|
|||||||||||||||
Defined in shared/common/socket-redis-io-adapter.ts:46
|
|||||||||||||||
Create server method @param port {number} - server port @param options {Object} - options object @return {Server} io server
Parameters :
Returns :
any
io server |
import { IoAdapter } from '@nestjs/platform-socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { Logger } from '@nestjs/common';
import { AbstractGameClient } from '../interfaces/game';
import { verify as JwtVerify } from 'jsonwebtoken';
import Redis from 'ioredis';
const { WEBSOCKET_NO_REDIS } = process.env;
let redisAdapter = null;
if (WEBSOCKET_NO_REDIS !== 'yes') {
/** redisio pub client */
const pubClient = new Redis({
host: process.env.WEBSOCKET_REDIS_HOST,
port: parseInt(process.env.WEBSOCKET_REDIS_PORT, 10),
db: parseInt(process.env.WEBSOCKET_REDIS_DB, 10) || 0,
});
/** redisio sub client */
const subClient = pubClient.duplicate();
/** socket.io redis adapter */
redisAdapter = createAdapter(pubClient, subClient);
pubClient.on('error', () => {
/* Simply catch the error, it's logged higher */
});
subClient.on('error', () => {
/* Simply catch the error, it's logged higher */
});
}
/**
* Class of websocket redis adapter for Nest app. Verify jwt token on connection, and decode token data for socket
*/
export class RedisIoAdapter extends IoAdapter {
/** logger */
private readonly logger = new Logger(RedisIoAdapter.name);
/**
* Create server method
*
* @param port {number} - server port
* @param options {Object} - options object
*
* @return {Server} io server
*/
createIOServer(port: number, options = {}): any {
this.logger.log(
`new redis websocket, <${JSON.stringify(options || null)}>`,
);
const whitelistOrigins = process.env.ALLOWED_CORS_ORIGIN;
const authSecret = process.env.AUTH_PRIVATE_KEY;
const server = super.createIOServer(port, {
// websockets are used for live games
// where "been live" is important
// cuz all games uses same websocket server
// (cuz @nestjs generates different servers only if path or port are different)
// we define "hard ping intervals" here and this options are used by all games
pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL, 10) || 1000,
pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT, 10) || 1000,
transports: ['websocket'],
allowUpgrades: false,
allowRequest: (req, callback) => {
const { origin } = req.headers;
const { jwt } = req._query;
if (!jwt || jwt === 'null') return callback(null, false);
if (whitelistOrigins.indexOf(origin) === -1)
return callback(null, false);
// health checker
if (jwt === 'healthcheck') {
return callback(null, true);
}
try {
const user = JwtVerify(jwt, authSecret);
// 401 error for not authorized - just fun, browser doesn't care
if (!user) return callback('401', false);
} catch (err) {
// TODO find way how to check
// server.jwtVerifyCounter++
this.logger.error(err, `Socket io connection jwt error`);
return callback('401', false);
}
// cuz allowRequest doesn't modify requests - user can't be applyed here
// so user will be attached in use middleware
return callback(null, true);
},
...options,
});
// TODO find way how to check
// server.jwtVerifyCounter = 0;
if (redisAdapter) {
server.adapter(redisAdapter);
server.of('/').adapter.on('error', (err) => {
this.logger.error(err, 'Websockets redis error');
});
server.of('/live').adapter.on('error', (err) => {
this.logger.error(err, 'Live websockets redis error');
});
}
// middleware to attach user from jwt
server.of('/live').use((socket, next) => {
const { jwt, clientTime } = socket.handshake.query;
if (!jwt) return next(401);
// health checker
if (jwt === 'healthcheck') {
const hc: AbstractGameClient = {
clientId: 'health-check',
gameCode: 'health-check',
region: process.env.MATH99_REGION || 'core',
isHost: false,
};
socket.data.user = hc;
next();
return;
}
const user: AbstractGameClient = JwtVerify(
jwt,
authSecret,
) as AbstractGameClient;
if (!user) return next(401);
socket.data = { user };
socket.data.user.gameCode = `${user.gameCode}`;
socket.data.user.clientTime = clientTime;
next();
});
return server;
}
}