강동현

런타임에 메세지 검증

import {
Boolean,
Number,
String,
Literal,
Array,
Tuple,
Record,
Union,
Static,
} from "runtypes";
export const UserDataRecord = Record({
username: String,
});
export type UserData = Static<typeof UserDataRecord>;
/**
* 방 리스트에서 사용됩니다.
*/
export interface RoomDescription {
uuid: string;
name: string;
currentUsers: number;
maxUsers: number;
}
export const RoomDescriptionRecord = Record({
uuid: String,
name: String,
currentUsers: Number,
maxUsers: Number,
});
export type RoomDescription = Static<typeof RoomDescriptionRecord>;
/**
* 방에 접속했을 때 사용됩니다.
*/
export interface RoomInfo {
uuid: string;
name: string;
maxUsers: number;
users: UserData[];
}
export const RoomInfoRecord = Record({
uuid: String,
name: String,
maxUsers: Number,
users: Array(UserDataRecord),
});
export interface UserData {
username: string;
}
export type RoomInfo = Static<typeof RoomInfoRecord>;
export type Role = "drawer" | "guesser" | "winner" | "spectator";
......
import { Role, RoomDescription, RoomInfo } from "./dataType";
import {
Boolean,
Number,
String,
Literal,
Array,
Tuple,
Record,
Union,
Static,
} from "runtypes";
import {
Role,
RoomDescription,
RoomDescriptionRecord,
RoomInfo,
RoomInfoRecord,
} from "./dataType";
// 서버로 들어오는 메세지 타입을 정의합니다.
// 'result' 속성은 서버 요청 결과에만 포함되는 특별한 속성입니다.
interface ServerInboundMessageMap {
export class ServerInboundMessageRecordMap {
// 로그인을 시도합니다.
login: {
username: string;
};
login = Record({ username: String });
// 방 목록을 요청합니다.
roomList: {
result: RoomDescription[];
};
roomList = Record({
result: Array(RoomDescriptionRecord),
});
// 방에 접속합니다.
joinRoom: {
uuid: string;
result: RoomInfo;
};
joinRoom = Record({
uuid: String,
result: RoomInfoRecord,
});
// 방에서 나갑니다.
leaveRoom: {};
leaveRoom = Record({});
// 채팅을 보냅니다.
chat: {
message: string;
};
chat = Record({
message: String,
});
// drawer가 단어를 선택합니다.
chooseWord: {
word: string;
};
chooseWord = Record({
word: String,
});
// 브러시 정보를 변경합니다.
setBrush: {
size: number;
color: string;
drawing: boolean;
};
setBrush = Record({
size: Number,
color: String,
drawing: Boolean,
});
// 브러시를 이동합니다.
moveBrush: {
x: number;
y: number;
};
moveBrush = Record({
x: Number,
y: Number,
});
}
type ServerInboundMessageMap = {
[Key in keyof ServerInboundMessageRecordMap]: Static<
ServerInboundMessageRecordMap[Key]
>;
};
// 서버에서 나가는 메세지 타입을 정의합니다.
interface ServerOutboundMessageMap {
// 방에 접속 중인 유저 목록이 업데이트 되었습니다.
......
......@@ -3,5 +3,7 @@
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {}
"dependencies": {
"runtypes": "^6.3.0"
}
}
......
......@@ -2,3 +2,7 @@
# yarn lockfile v1
runtypes@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-6.3.0.tgz#bd88392c21f471bd45591d5eabaa4644ca7cdf3c"
integrity sha512-FTNUs13CIrCTjReBOaeY/8EY1LYIQVkkwyE9z5MCjZe9uew9/8TRbWF1PcTczgTFfGBjkjUKeedFWU2O3ExjPg==
......
......@@ -10,12 +10,15 @@ import {
import { Room } from "../room/Room";
import { RoomManager } from "../room/RoomManager";
import { User } from "../user/User";
import { MessageValidator } from "./MessageValidator";
import { SocketWrapper } from "./SocketWrapper";
export class Connection {
public readonly socket: SocketWrapper;
public readonly roomManager: RoomManager;
static readonly validator: MessageValidator = new MessageValidator();
public user?: User;
constructor(socket: SocketWrapper, roomManager: RoomManager) {
......@@ -38,6 +41,10 @@ export class Connection {
const type = raw.type as ServerInboundMessageKey;
const message = raw.message;
if (!Connection.validator.validate(type, message)) {
return { ok: false };
}
// 유저 정보가 없으므로 로그인은 따로 핸들링
if (type === "login") {
return this.handleLogin(message);
......
import {
ServerInboundMessageKey,
ServerInboundMessageRecordMap,
} from "../../common";
import { Record } from "runtypes";
export class MessageValidator {
private readonly messageRecordMap: Map<
ServerInboundMessageKey,
Record<any, boolean>
> = new Map();
constructor() {
const messageRecordMapContainsResult = new ServerInboundMessageRecordMap();
for (const key in messageRecordMapContainsResult) {
const recordContainsResult =
messageRecordMapContainsResult[
key as keyof ServerInboundMessageRecordMap
];
//@ts-ignore because some of entries don't have result property.
this.messageRecordMap.set(key, recordContainsResult.omit("result"));
}
}
public validate(key: ServerInboundMessageKey, message: any) {
const messageRecord = this.messageRecordMap.get(key);
if (messageRecord) {
return messageRecord.validate(message).success;
}
return false;
}
}
......@@ -13,6 +13,7 @@
"mocha": "^8.4.0",
"mocha-steps": "^1.3.0",
"nodemon": "^2.0.7",
"runtypes": "^6.3.0",
"socket.io": "^4.1.2",
"socket.io-client": "^4.1.2",
"ts-node": "^9.1.1",
......
......@@ -1400,6 +1400,11 @@ responselike@^1.0.2:
dependencies:
lowercase-keys "^1.0.0"
runtypes@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-6.3.0.tgz#bd88392c21f471bd45591d5eabaa4644ca7cdf3c"
integrity sha512-FTNUs13CIrCTjReBOaeY/8EY1LYIQVkkwyE9z5MCjZe9uew9/8TRbWF1PcTczgTFfGBjkjUKeedFWU2O3ExjPg==
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
......