File

shared/common/socket-redis-io-adapter.ts

Description

Class of websocket redis adapter for Nest app. Verify jwt token on connection, and decode token data for socket

Extends

IoAdapter

Index

Properties
Methods

Properties

Private Readonly logger
Default value : new Logger(RedisIoAdapter.name)

logger

Methods

createIOServer
createIOServer(port: number, options: object)

Create server method

@param port {number} - server port @param options {Object} - options object

@return {Server} io server

Parameters :
Name Type Optional Default value Description
port number No
  • server port
options object No {}
  • options object
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;
  }
}

results matching ""

    No results matching ""