Skip to content

Commit

Permalink
feat: user ip ban 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
hoeeeeeh committed Nov 30, 2024
1 parent e94af4c commit 299da5e
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 60 deletions.
54 changes: 54 additions & 0 deletions backend/chatServer/src/chat/chat.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { HttpStatus } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';

class ChatException extends WsException {
statusCode: number;
constructor({ statusCode, message } : ChatError , public roomId?: string) {
super({ statusCode, message, roomId });
this.statusCode = statusCode;
}

getError(): object {
return {
statusCode: this.statusCode,
msg: this.message,
roomId: this.roomId || null,
};
}
}

interface ChatError {
statusCode: number;
message: string;
}

const CHATTING_SOCKET_ERROR = {
ROOM_EMPTY: {
statusCode: HttpStatus.BAD_REQUEST,
message: '유저가 참여하고 있는 채팅방이 없습니다.'
},

ROOM_EXISTED: {
statusCode: HttpStatus.BAD_REQUEST,
message: '이미 존재하는 방입니다.'
},

INVALID_USER: {
statusCode: HttpStatus.UNAUTHORIZED,
message: '유효하지 않는 유저입니다.'
},

QUESTION_EMPTY: {
statusCode: HttpStatus.BAD_REQUEST,
message: '유효하지 않은 질문입니다.'
},

BAN_USER: {
statusCode: HttpStatus.FORBIDDEN,
message: '호스트에 의해 밴 당한 유저입니다.'
}


};
export { CHATTING_SOCKET_ERROR, ChatException };

39 changes: 29 additions & 10 deletions backend/chatServer/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,29 @@ import {
OnGatewayConnection,
OnGatewayDisconnect,
MessageBody,
ConnectedSocket,
ConnectedSocket
} from '@nestjs/websockets';
import { UseGuards } from '@nestjs/common';
import { Server, Socket } from 'socket.io';
import {
CHATTING_SOCKET_DEFAULT_EVENT,
CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_SEND_EVENT
CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_SEND_EVENT
} from '../event/constants';
import {
BanUserIncomingMessageDto,
NormalIncomingMessageDto, NoticeIncomingMessageDto, QuestionDoneIncomingMessageDto, QuestionIncomingMessageDto
} from '../event/dto/IncomingMessage.dto';
import { JoiningRoomDto } from '../event/dto/JoiningRoom.dto';
import { RoomService } from '../room/room.service';
import { createAdapter } from '@socket.io/redis-adapter';
import { HostGuard, MessageGuard } from './chat.guard';
import { BlacklistGuard, HostGuard, MessageGuard } from './chat.guard';
import { LeavingRoomDto } from '../event/dto/LeavingRoom.dto';
import {
NormalOutgoingMessageDto,
NoticeOutgoingMessageDto,
QuestionOutgoingMessageDto
} from '../event/dto/OutgoingMessage.dto';
import { QuestionDto } from '../event/dto/Question.dto';
import { ChatException, CHATTING_SOCKET_ERROR } from './chat.error';

@WebSocketGateway({ cors: true })
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
Expand Down Expand Up @@ -69,6 +70,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
}

// 특정 방에 참여하기 위한 메서드
@UseGuards(BlacklistGuard)
@SubscribeMessage(CHATTING_SOCKET_DEFAULT_EVENT.JOIN_ROOM)
async handleJoinRoom(client: Socket, payload: JoiningRoomDto) {
const { roomId, userId } = payload;
Expand All @@ -93,7 +95,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
}

// 방에 NORMAL 메시지를 보내기 위한 메서드
@UseGuards(MessageGuard)
@UseGuards(MessageGuard, BlacklistGuard)
@SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.NORMAL)
async handleNormalMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: NormalIncomingMessageDto) {
const { roomId, userId, msg } = payload;
Expand All @@ -103,7 +105,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
...user,
msg,
msgTime: new Date().toISOString(),
msgType: 'normal'
msgType: 'normal',
socketId: client.id
};
console.log('Normal Message Come In: ', normalOutgoingMessage);
const hostId = await this.roomService.getHostOfRoom(roomId);
Expand All @@ -121,7 +124,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
}

// 방에 QUESTION 메시지를 보내기 위한 메서드
@UseGuards(MessageGuard)
@UseGuards(MessageGuard,BlacklistGuard)
@SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.QUESTION)
async handleQuestionMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: QuestionIncomingMessageDto) {
const { roomId, msg } = payload;
Expand All @@ -132,7 +135,8 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
msg,
msgTime: new Date().toISOString(),
msgType: 'question',
questionDone: false
questionDone: false,
socketId: client.id
};

const question: QuestionOutgoingMessageDto = await this.roomService.addQuestion(roomId, questionWithoutId);
Expand All @@ -150,8 +154,7 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
}

// 방에 NOTICE 메시지를 보내기 위한 메서드
@UseGuards(MessageGuard)
@UseGuards(HostGuard)
@UseGuards(MessageGuard, HostGuard)
@SubscribeMessage(CHATTING_SOCKET_SEND_EVENT.NOTICE)
async handleNoticeMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: NoticeIncomingMessageDto) {
const { roomId, msg } = payload;
Expand All @@ -165,4 +168,20 @@ export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewa
};
this.server.to(roomId).emit(CHATTING_SOCKET_RECEIVE_EVENT.NOTICE, noticeOutgoingMessage);
}

@UseGuards(HostGuard)
@SubscribeMessage(CHATTING_SOCKET_DEFAULT_EVENT.BAN_USER)
async handleBanUserMessage(@ConnectedSocket() client: Socket, @MessageBody() payload: BanUserIncomingMessageDto) {
const { roomId, socketId } = payload;
const banUser = this.server.sockets.sockets.get(socketId);
const address = banUser?.handshake.address.replaceAll('::ffff:', '');

if(!address) throw new ChatException(CHATTING_SOCKET_ERROR.INVALID_USER);

const forwarded = banUser?.handshake.headers.forwarded ?? address;
console.log('ban:', roomId, address, forwarded);

await this.roomService.addUserToBlacklist(roomId, address, forwarded);
console.log(await this.roomService.getUserBlacklist(roomId, address));
}
}
30 changes: 30 additions & 0 deletions backend/chatServer/src/chat/chat.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { RoomService } from '../room/room.service';
import { Socket } from 'socket.io';

import { ChatException, CHATTING_SOCKET_ERROR } from './chat.error';

@Injectable()
export class MessageGuard implements CanActivate {
Expand All @@ -22,3 +25,30 @@ export class HostGuard implements CanActivate {
return hostId === userId;
}
}

@Injectable()
export class BlacklistGuard implements CanActivate {
constructor(private roomService: RoomService) {};
async canActivate(context: ExecutionContext) {
const payload = context.switchToWs().getData();
const { roomId } = payload;

const client: Socket = context.switchToWs().getClient<Socket>();
const address = client.handshake.address.replaceAll('::ffff:', '');
const forwarded = client.handshake.headers.forwarded?.split(',')[0] ?? address;

const isValidUser = await this.whenJoinRoom(roomId, address, forwarded);

if(!isValidUser) throw new ChatException(CHATTING_SOCKET_ERROR.BAN_USER, roomId);
return true;
}

async whenJoinRoom(roomId: string, address: string, forwarded: string) {
console.log(roomId, address, forwarded);
const blacklistInRoom = await this.roomService.getUserBlacklist(roomId, address);
console.log(blacklistInRoom);
const isInBlacklistUser = blacklistInRoom.some((blackForwarded) => blackForwarded === forwarded);
console.log('blacklistInRoom:', isInBlacklistUser);
return !isInBlacklistUser;
}
}
4 changes: 2 additions & 2 deletions backend/chatServer/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';
import { RoomModule } from '../room/room.module';
import { MessageGuard } from './chat.guard';
import { BlacklistGuard, HostGuard, MessageGuard } from './chat.guard';

@Module({
imports: [RoomModule],
providers: [ChatGateway, MessageGuard],
providers: [ChatGateway, MessageGuard, BlacklistGuard, HostGuard],
})
export class ChatModule {}
29 changes: 2 additions & 27 deletions backend/chatServer/src/event/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { HttpStatus } from '@nestjs/common';

const CHATTING_SOCKET_DEFAULT_EVENT = {
JOIN_ROOM: 'join_room',
LEAVE_ROOM: 'leave_room',
BAN_USER: 'ban_user',
};

const CHATTING_SOCKET_RECEIVE_EVENT = {
Expand All @@ -20,28 +19,4 @@ const CHATTING_SOCKET_SEND_EVENT = {
NOTICE: 'send_notice'
};

const CHATTING_SOCKET_ERROR = {
ROOM_EMPTY : {
statusCode: HttpStatus.BAD_REQUEST,
message: '유저가 참여하고 있는 채팅방이 없습니다.'
},

ROOM_EXISTED: {
statusCode: HttpStatus.BAD_REQUEST,
message: '이미 존재하는 방입니다.'
},

INVALID_USER: {
statusCode: HttpStatus.UNAUTHORIZED,
message: '유효하지 않는 유저입니다.'
},

QUESTION_EMPTY: {
statusCode: HttpStatus.BAD_REQUEST,
message: '유효하지 않은 질문입니다.'
},


};

export { CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_SEND_EVENT, CHATTING_SOCKET_RECEIVE_EVENT, CHATTING_SOCKET_ERROR};
export { CHATTING_SOCKET_DEFAULT_EVENT, CHATTING_SOCKET_SEND_EVENT, CHATTING_SOCKET_RECEIVE_EVENT};
1 change: 1 addition & 0 deletions backend/chatServer/src/event/dto/OutgoingMessage.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class DefaultOutgoingMessageDto {
roomId: string = '';
nickname: string = '';
color: string = '';
entryTime: string = '';
msgTime: string = new Date().toISOString();
}

Expand Down
1 change: 1 addition & 0 deletions backend/chatServer/src/event/dto/Question.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class QuestionDto {
roomId: string = '';
nickname: string = '';
color: string = '';
entryTime: string = '';
msg: string = '';
msgTime: string = new Date().toISOString();
msgType: OutgoingMessageType = 'question';
Expand Down
36 changes: 29 additions & 7 deletions backend/chatServer/src/room/room.repository.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Injectable } from '@nestjs/common';
import { Cluster } from 'ioredis';
import { CHATTING_SOCKET_ERROR } from '../event/constants';
import { WsException } from '@nestjs/websockets';
import { QuestionDto } from '../event/dto/Question.dto';
import { ChatException, CHATTING_SOCKET_ERROR } from '../chat/chat.error';

type FORWARDED = string;

@Injectable()
export class RoomRepository {
redisClient!: Cluster;
roomIdPrefix = 'room:';
questionPrefix = 'question';
questionIdPrefix = 'id';
blacklistPrefix = 'blacklist';

injectClient(redisClient: Cluster){
this.redisClient = redisClient;
Expand All @@ -27,6 +29,14 @@ export class RoomRepository {
return `${this.getRoomStringWithPrefix(roomId)}-${this.questionPrefix}-${this.questionIdPrefix}`;
}

private getUserBlacklistStringWithPrefix(address:string){
return `${this.blacklistPrefix}:${address}`;
}

private getUserBlacklistInRoomWithPrefix(roomId:string, address:string){
return `${this.getRoomStringWithPrefix(roomId)}-${this.getUserBlacklistStringWithPrefix(address)}`;
}

private async lindex<T>(key: string, index: number){
const result = await this.redisClient.lindex(key, index);
if(result) return JSON.parse(result) as T;
Expand All @@ -35,12 +45,12 @@ export class RoomRepository {

private async lrange<T>(key: string, start: number, end: number){
const result = await this.redisClient.lrange(key, start, end);
return result.map((r) => JSON.parse(r)) as T;
return result as T;
}

private async getData<T>(key: string) {
const result = await this.redisClient.get(key);
if(result) return JSON.parse(result) as T;
if(result) return typeof result === 'string' ? result as T : JSON.parse(result) as T;
return undefined;
}

Expand All @@ -50,7 +60,7 @@ export class RoomRepository {

async getHost(roomId: string) {
const hostId = await this.redisClient.get(this.getRoomStringWithPrefix(roomId));
if(!hostId) throw new WsException(CHATTING_SOCKET_ERROR.ROOM_EMPTY);
if(!hostId) throw new ChatException(CHATTING_SOCKET_ERROR.ROOM_EMPTY);
return hostId;
}

Expand All @@ -72,7 +82,7 @@ export class RoomRepository {

async markQuestionAsDone(roomId: string, questionId: number): Promise<QuestionDto> {
const question = await this.getQuestion(roomId, questionId);
if(!question) throw new WsException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY);
if(!question) throw new ChatException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY);
question.questionDone = true;
this.redisClient.lset(this.getQuestionStringWithPrefix(roomId), questionId, JSON.stringify(question));
return question;
Expand All @@ -88,7 +98,7 @@ export class RoomRepository {
async getQuestion(roomId: string, questionId: number): Promise<QuestionDto> {
const question = await this.lindex<Omit<QuestionDto, 'questionId'>>(this.getQuestionStringWithPrefix(roomId), questionId);
if(question) return {...question, questionId };
throw new WsException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY);
throw new ChatException(CHATTING_SOCKET_ERROR.QUESTION_EMPTY);
}

async getQuestionId(roomId: string) {
Expand All @@ -102,4 +112,16 @@ export class RoomRepository {
}


async getUserBlacklist(roomId: string, address: string): Promise<FORWARDED[]> {
const userBlacklist = await this.lrange<FORWARDED[]>(this.getUserBlacklistInRoomWithPrefix(roomId, address), 0, -1);
console.log('blacklist', userBlacklist);
if (!userBlacklist) return [];
return userBlacklist;
}

async addUserBlacklistToRoom(roomId: string, address: string, forwarded: string){
console.log(roomId, address, forwarded);
console.log(this.getUserBlacklistInRoomWithPrefix(roomId, address));
return this.redisClient.rpush(this.getUserBlacklistInRoomWithPrefix(roomId, address), forwarded);
}
}
Loading

0 comments on commit 299da5e

Please sign in to comment.