universo-platform-3d
206 строк · 6.8 Кб
1import { Injectable, Logger } from '@nestjs/common'2import { Server, WebSocket } from 'ws'3import { FirebaseAuthenticationService } from '../firebase/firebase-authentication.service'4import { RedisPubSubService } from '../redis/redis-pub-sub.service'5import { CHANNELS } from '../redis/redis.channels'6import { v4 as uuidv4 } from 'uuid'7
8/**
9* @description This is used with WsAuthGuard to ensure that the user is authed before processing a request
10* See: https://github.com/nestjs/nest/issues/882#issuecomment-1493106283
11*/
12@Injectable()13export class WsAuthHelperService {14constructor(15private readonly logger: Logger,16private readonly redisPubSubService: RedisPubSubService,17private readonly firebaseAuthService: FirebaseAuthenticationService18) {}19public initializationSuccess: { [key: string]: boolean } = {}20private initializationMap = new Map<string, Promise<any>>()21/**22* Primary property for holding subscriptions to channels
23*/
24private channelSubs: Record<string, WebSocket[]> = {}25
26handleConnectionHelper(client: WebSocket, args: any) {27this.initializationMap.set(client['id'], this.initialize(client, args))28}29
30async finishInitialization(client: WebSocket): Promise<any> {31return await this.initializationMap.get(client['id'])32}33
34private async initialize(client: WebSocket, args: any): Promise<any> {35// ensure max listeners is set high enough - nestjs bug (older version causes error)36client.setMaxListeners(20)37
38/** Crude authentication check until further defined */ const [39{ headers }40] = args41let spaceId = headers?.space42let token = headers?.authorization43
44// attach the token to the client45client['token'] = token46// assign the socket a uuid with the uuid library47client['id'] = uuidv4()48
49let isFirebaseToken = false50
51// if no token, check for Sec-WebSocket-Protocol header (this is how browsers have to send headers: see #5 here https://stackoverflow.com/a/77060459/3777933)52if (!token) {53const secWebSocketProtocol = headers['sec-websocket-protocol']54if (secWebSocketProtocol) {55// Important: if this order is changed, it must be changed in ws-auth-helper.service.ts on the react app too. it expects ordered array56const result = secWebSocketProtocol.split(',')57token = result[0]58client['token'] = token59if (result[1]) {60spaceId = result[1]61}62}63}64
65/*66check if the token is a valid firebase token
67if it is, set the user object on the client
68this data will be used to check the roles
69we can catch headers when using WS only during the handleConnection event,
70so here we check the user's token and embed the decrypted token data in the user's connection object
71when using the Firebase token, methods with role verification will be executed only
72*/
73
74/*75if the token is the WSS_SECRET, set the role to admin
76and allows to use methods with admin role
77when using the WSS_SECRET token, methods will be executed only without role verification
78*/
79if (token === process.env.WSS_SECRET) {80client['role'] = 'admin'81} else {82try {83const decodedJwt = await this.decodeJwt(token)84if (decodedJwt) {85isFirebaseToken = true86client['user'] = decodedJwt87console.log('isFirebaseToken', isFirebaseToken)88}89} catch (error) {90this.logger.log(91`Invalid tokens (WSS_SECRET or Firebase JWT) for GodotGateway handleConnection: Disconnecting`,92WsAuthHelperService.name93)94return client.close(1014, 'Invalid token')95}96}97
98// if the token is neither the WSS_SECRET nor a valid firebase token, close the connection99if (!token || (token !== process.env.WSS_SECRET && !isFirebaseToken)) {100const msg =101'Invalid tokens (WSS_SECRET or Firebase JWT) for GodotGateway handleConnection: Disconnecting'102this.logger.error(msg, WsAuthHelperService.name)103return client.close(1014, 'Invalid token')104}105
106this.logger.log(107`handleConnection: ${JSON.stringify(108{109spaceId
110},111null,1122113)}`,114WsAuthHelperService.name115)116this.setupSubscriber(client, spaceId)117
118this.initializationMap.delete(client['id'])119this.initializationSuccess[client['id']] = true120return Promise.resolve()121}122
123setupSubscriber(client: WebSocket, spaceId: string) {124if (!spaceId) {125this.logger.log(126`setupSubscriber: attempted but spaceId was falsey: ${JSON.stringify(127{128spaceId
129},130null,1312132)}`,133WsAuthHelperService.name134)135return136}137const subChannel = `${CHANNELS.SPACE}:${spaceId}`138client['subscriberChannel'] = subChannel139// Add to tracked channels140if (!this.channelSubs[subChannel]) {141this.channelSubs[subChannel] = new Array<WebSocket>()142}143this.channelSubs[subChannel].push(client)144// setup the subscription listener if this is the first subscriber to the channel.145if (this.channelSubs[subChannel].length == 1) {146this.redisPubSubService.subscriber.subscribe(subChannel, (message) => {147this.handleSubscribedChannelReceivedMessage(subChannel, message)148})149}150}151
152/**153*
154* @description Called when a Redis pubsub message is received from a channel that was subscribed to
155*/
156handleSubscribedChannelReceivedMessage(subChannel: string, message: string) {157this.logger.log(158`handleSubscribedChannelReceivedMessage: ${JSON.stringify(159{160subChannel,161message
162},163null,1642165)}\n166Sending message to subscribers: ${this.channelSubs[subChannel].length}`,167WsAuthHelperService.name168)169this.channelSubs[subChannel].forEach((client) => {170client.send(message)171})172}173
174removeSubscriber(client: WebSocket) {175// remove the client from the channel subscriptions.176const subchannel = client['subscriberChannel']177if (!subchannel || !this.channelSubs[subchannel]) {178this.logger.log(179`setupSubscriber: attempted but subchannel was falsey: ${JSON.stringify(180{181client,182subchannel
183},184null,1852186)}`,187WsAuthHelperService.name188)189return190}191this.channelSubs[subchannel] = this.channelSubs[subchannel].filter(192(c1) => c1 !== client193)194// unsubscribe from the subscription listener if this is the last subscriber to the channel.195if (this.channelSubs[subchannel].length == 0) {196this.redisPubSubService.subscriber.unsubscribe(subchannel)197}198}199
200async decodeJwt(token) {201return await this.firebaseAuthService.verifyIdToken(202token ? token.replace('Bearer ', '') : '',203true204)205}206}
207