강동현

런타임에 메세지 검증

1 +import {
2 + Boolean,
3 + Number,
4 + String,
5 + Literal,
6 + Array,
7 + Tuple,
8 + Record,
9 + Union,
10 + Static,
11 +} from "runtypes";
12 +
13 +export const UserDataRecord = Record({
14 + username: String,
15 +});
16 +
17 +export type UserData = Static<typeof UserDataRecord>;
18 +
1 /** 19 /**
2 * 방 리스트에서 사용됩니다. 20 * 방 리스트에서 사용됩니다.
3 */ 21 */
4 -export interface RoomDescription { 22 +export const RoomDescriptionRecord = Record({
5 - uuid: string; 23 + uuid: String,
6 - name: string; 24 + name: String,
7 - currentUsers: number; 25 + currentUsers: Number,
8 - maxUsers: number; 26 + maxUsers: Number,
9 -} 27 +});
28 +
29 +export type RoomDescription = Static<typeof RoomDescriptionRecord>;
10 30
11 /** 31 /**
12 * 방에 접속했을 때 사용됩니다. 32 * 방에 접속했을 때 사용됩니다.
13 */ 33 */
14 -export interface RoomInfo { 34 +export const RoomInfoRecord = Record({
15 - uuid: string; 35 + uuid: String,
16 - name: string; 36 + name: String,
17 - maxUsers: number; 37 + maxUsers: Number,
18 - users: UserData[]; 38 + users: Array(UserDataRecord),
19 -} 39 +});
20 40
21 -export interface UserData { 41 +export type RoomInfo = Static<typeof RoomInfoRecord>;
22 - username: string;
23 -}
24 42
25 export type Role = "drawer" | "guesser" | "winner" | "spectator"; 43 export type Role = "drawer" | "guesser" | "winner" | "spectator";
......
1 -import { Role, RoomDescription, RoomInfo } from "./dataType"; 1 +import {
2 + Boolean,
3 + Number,
4 + String,
5 + Literal,
6 + Array,
7 + Tuple,
8 + Record,
9 + Union,
10 + Static,
11 +} from "runtypes";
12 +import {
13 + Role,
14 + RoomDescription,
15 + RoomDescriptionRecord,
16 + RoomInfo,
17 + RoomInfoRecord,
18 +} from "./dataType";
2 19
3 // 서버로 들어오는 메세지 타입을 정의합니다. 20 // 서버로 들어오는 메세지 타입을 정의합니다.
4 // 'result' 속성은 서버 요청 결과에만 포함되는 특별한 속성입니다. 21 // 'result' 속성은 서버 요청 결과에만 포함되는 특별한 속성입니다.
5 -interface ServerInboundMessageMap { 22 +export class ServerInboundMessageRecordMap {
6 // 로그인을 시도합니다. 23 // 로그인을 시도합니다.
7 - login: { 24 + login = Record({ username: String });
8 - username: string;
9 - };
10 25
11 // 방 목록을 요청합니다. 26 // 방 목록을 요청합니다.
12 - roomList: { 27 + roomList = Record({
13 - result: RoomDescription[]; 28 + result: Array(RoomDescriptionRecord),
14 - }; 29 + });
15 30
16 // 방에 접속합니다. 31 // 방에 접속합니다.
17 - joinRoom: { 32 + joinRoom = Record({
18 - uuid: string; 33 + uuid: String,
19 - result: RoomInfo; 34 + result: RoomInfoRecord,
20 - }; 35 + });
21 36
22 // 방에서 나갑니다. 37 // 방에서 나갑니다.
23 - leaveRoom: {}; 38 + leaveRoom = Record({});
24 39
25 // 채팅을 보냅니다. 40 // 채팅을 보냅니다.
26 - chat: { 41 + chat = Record({
27 - message: string; 42 + message: String,
28 - }; 43 + });
29 44
30 // drawer가 단어를 선택합니다. 45 // drawer가 단어를 선택합니다.
31 - chooseWord: { 46 + chooseWord = Record({
32 - word: string; 47 + word: String,
33 - }; 48 + });
34 49
35 // 브러시 정보를 변경합니다. 50 // 브러시 정보를 변경합니다.
36 - setBrush: { 51 + setBrush = Record({
37 - size: number; 52 + size: Number,
38 - color: string; 53 + color: String,
39 - drawing: boolean; 54 + drawing: Boolean,
40 - }; 55 + });
41 56
42 // 브러시를 이동합니다. 57 // 브러시를 이동합니다.
43 - moveBrush: { 58 + moveBrush = Record({
44 - x: number; 59 + x: Number,
45 - y: number; 60 + y: Number,
46 - }; 61 + });
47 } 62 }
48 63
64 +type ServerInboundMessageMap = {
65 + [Key in keyof ServerInboundMessageRecordMap]: Static<
66 + ServerInboundMessageRecordMap[Key]
67 + >;
68 +};
69 +
49 // 서버에서 나가는 메세지 타입을 정의합니다. 70 // 서버에서 나가는 메세지 타입을 정의합니다.
50 interface ServerOutboundMessageMap { 71 interface ServerOutboundMessageMap {
51 // 방에 접속 중인 유저 목록이 업데이트 되었습니다. 72 // 방에 접속 중인 유저 목록이 업데이트 되었습니다.
......
...@@ -3,5 +3,7 @@ ...@@ -3,5 +3,7 @@
3 "version": "1.0.0", 3 "version": "1.0.0",
4 "main": "index.js", 4 "main": "index.js",
5 "license": "MIT", 5 "license": "MIT",
6 - "dependencies": {} 6 + "dependencies": {
7 + "runtypes": "^6.3.0"
8 + }
7 } 9 }
......
...@@ -2,3 +2,7 @@ ...@@ -2,3 +2,7 @@
2 # yarn lockfile v1 2 # yarn lockfile v1
3 3
4 4
5 +runtypes@^6.3.0:
6 + version "6.3.0"
7 + resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-6.3.0.tgz#bd88392c21f471bd45591d5eabaa4644ca7cdf3c"
8 + integrity sha512-FTNUs13CIrCTjReBOaeY/8EY1LYIQVkkwyE9z5MCjZe9uew9/8TRbWF1PcTczgTFfGBjkjUKeedFWU2O3ExjPg==
......
...@@ -10,12 +10,15 @@ import { ...@@ -10,12 +10,15 @@ import {
10 import { Room } from "../room/Room"; 10 import { Room } from "../room/Room";
11 import { RoomManager } from "../room/RoomManager"; 11 import { RoomManager } from "../room/RoomManager";
12 import { User } from "../user/User"; 12 import { User } from "../user/User";
13 +import { MessageValidator } from "./MessageValidator";
13 import { SocketWrapper } from "./SocketWrapper"; 14 import { SocketWrapper } from "./SocketWrapper";
14 15
15 export class Connection { 16 export class Connection {
16 public readonly socket: SocketWrapper; 17 public readonly socket: SocketWrapper;
17 public readonly roomManager: RoomManager; 18 public readonly roomManager: RoomManager;
18 19
20 + static readonly validator: MessageValidator = new MessageValidator();
21 +
19 public user?: User; 22 public user?: User;
20 23
21 constructor(socket: SocketWrapper, roomManager: RoomManager) { 24 constructor(socket: SocketWrapper, roomManager: RoomManager) {
...@@ -38,6 +41,10 @@ export class Connection { ...@@ -38,6 +41,10 @@ export class Connection {
38 const type = raw.type as ServerInboundMessageKey; 41 const type = raw.type as ServerInboundMessageKey;
39 const message = raw.message; 42 const message = raw.message;
40 43
44 + if (!Connection.validator.validate(type, message)) {
45 + return { ok: false };
46 + }
47 +
41 // 유저 정보가 없으므로 로그인은 따로 핸들링 48 // 유저 정보가 없으므로 로그인은 따로 핸들링
42 if (type === "login") { 49 if (type === "login") {
43 return this.handleLogin(message); 50 return this.handleLogin(message);
......
1 +import {
2 + ServerInboundMessageKey,
3 + ServerInboundMessageRecordMap,
4 +} from "../../common";
5 +import { Record } from "runtypes";
6 +
7 +export class MessageValidator {
8 + private readonly messageRecordMap: Map<
9 + ServerInboundMessageKey,
10 + Record<any, boolean>
11 + > = new Map();
12 + constructor() {
13 + const messageRecordMapContainsResult = new ServerInboundMessageRecordMap();
14 + for (const key in messageRecordMapContainsResult) {
15 + const recordContainsResult =
16 + messageRecordMapContainsResult[
17 + key as keyof ServerInboundMessageRecordMap
18 + ];
19 + //@ts-ignore because some of entries don't have result property.
20 + this.messageRecordMap.set(key, recordContainsResult.omit("result"));
21 + }
22 + }
23 +
24 + public validate(key: ServerInboundMessageKey, message: any) {
25 + const messageRecord = this.messageRecordMap.get(key);
26 + if (messageRecord) {
27 + return messageRecord.validate(message).success;
28 + }
29 + return false;
30 + }
31 +}
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
13 "mocha": "^8.4.0", 13 "mocha": "^8.4.0",
14 "mocha-steps": "^1.3.0", 14 "mocha-steps": "^1.3.0",
15 "nodemon": "^2.0.7", 15 "nodemon": "^2.0.7",
16 + "runtypes": "^6.3.0",
16 "socket.io": "^4.1.2", 17 "socket.io": "^4.1.2",
17 "socket.io-client": "^4.1.2", 18 "socket.io-client": "^4.1.2",
18 "ts-node": "^9.1.1", 19 "ts-node": "^9.1.1",
......
...@@ -1400,6 +1400,11 @@ responselike@^1.0.2: ...@@ -1400,6 +1400,11 @@ responselike@^1.0.2:
1400 dependencies: 1400 dependencies:
1401 lowercase-keys "^1.0.0" 1401 lowercase-keys "^1.0.0"
1402 1402
1403 +runtypes@^6.3.0:
1404 + version "6.3.0"
1405 + resolved "https://registry.yarnpkg.com/runtypes/-/runtypes-6.3.0.tgz#bd88392c21f471bd45591d5eabaa4644ca7cdf3c"
1406 + integrity sha512-FTNUs13CIrCTjReBOaeY/8EY1LYIQVkkwyE9z5MCjZe9uew9/8TRbWF1PcTczgTFfGBjkjUKeedFWU2O3ExjPg==
1407 +
1403 safe-buffer@5.1.2: 1408 safe-buffer@5.1.2:
1404 version "5.1.2" 1409 version "5.1.2"
1405 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 1410 resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
......