1
1
import http from "http" ;
2
2
import express from "express" ;
3
3
import { WebSocket , WebSocketServer } from "ws" ;
4
+ import jsonschema from "jsonschema" ;
4
5
5
6
import database from "./database.js" ;
6
7
import defaultRoomState from "./defaultRoom.js" ;
8
+ import { protocolSchema } from "../shared/schema.js" ;
9
+ import { ProtocolMessage } from "../shared/generated-schema.js" ;
7
10
8
11
const bindIpAddress = "127.0.0.1" ;
9
12
@@ -111,25 +114,25 @@ function handleNewSocket(socket: WebSocket) {
111
114
let user : UserInRoom | null = null ;
112
115
113
116
socket . on ( "message" , function ( data , isBinary ) {
114
- if ( isBinary ) throw new Error ( "no" ) ;
115
- const msg = data . toString ( ) ;
117
+ try {
118
+ if ( isBinary ) throwShenanigan ( "received binary message" ) ;
119
+ let msg = data . toString ( ) ;
120
+ handleMessage ( msg ) ;
121
+ } catch ( e ) {
122
+ if ( ! ( e instanceof Shenanigan ) ) {
123
+ console . log ( e ) ;
124
+ }
125
+ disconnect ( ) ;
126
+ }
127
+ } ) ;
116
128
129
+ function handleMessage ( msg : string ) {
117
130
if ( clientState === ClientState . DISCONNECTING ) return ;
118
- console . log ( msg ) ;
119
- let allowedCommands = ( function ( ) {
120
- switch ( clientState ) {
121
- case ClientState . WAITING_FOR_LOGIN :
122
- return [ "joinRoom" ] ;
123
- case ClientState . PLAY :
124
- return [ "makeAMove" , "changeMyName" , "changeMyRole" ] ;
125
- default : programmerError ( ) ;
126
- }
127
- } ) ( ) ;
128
- let message = parseAndValidateMessage ( msg , allowedCommands ) ;
129
- if ( message == null ) return ;
131
+ let message = parseAndValidateMessage ( msg ) ;
130
132
131
133
switch ( message . cmd ) {
132
134
case "joinRoom" : {
135
+ if ( clientState !== ClientState . WAITING_FOR_LOGIN ) throwShenanigan ( "not now kid" ) ;
133
136
let roomCode = message . args . roomCode ;
134
137
if ( roomCode === "new" ) {
135
138
room = newRoom ( ) ;
@@ -176,6 +179,7 @@ function handleNewSocket(socket: WebSocket) {
176
179
break ;
177
180
}
178
181
case "makeAMove" : {
182
+ if ( clientState !== ClientState . PLAY ) throwShenanigan ( "not now kid" ) ;
179
183
if ( ! ( user != null && room != null ) ) programmerError ( ) ;
180
184
let msg = JSON . stringify ( message ) ;
181
185
for ( let otherId in room . usersById ) {
@@ -186,6 +190,7 @@ function handleNewSocket(socket: WebSocket) {
186
190
break ;
187
191
}
188
192
case "changeMyName" : {
193
+ if ( clientState !== ClientState . PLAY ) throwShenanigan ( "not now kid" ) ;
189
194
if ( ! ( user != null && room != null ) ) programmerError ( ) ;
190
195
let newName = message . args ;
191
196
user . userName = newName ;
@@ -199,6 +204,7 @@ function handleNewSocket(socket: WebSocket) {
199
204
break ;
200
205
}
201
206
case "changeMyRole" : {
207
+ if ( clientState !== ClientState . PLAY ) throwShenanigan ( "not now kid" ) ;
202
208
if ( ! ( user != null && room != null ) ) programmerError ( ) ;
203
209
let newRole = message . args ;
204
210
user . role = newRole ;
@@ -211,10 +217,14 @@ function handleNewSocket(socket: WebSocket) {
211
217
}
212
218
break ;
213
219
}
214
- default : throw new Error ( "TODO: handle command: " + message . cmd ) ;
220
+ default : throw new Error ( "TODO: handle command: " + msg ) ;
215
221
}
216
- } ) ;
222
+ }
217
223
224
+ function throwShenanigan ( ...msgs : any [ ] ) : never {
225
+ console . log ( "error handling client message:" , ...msgs ) ;
226
+ throw new Shenanigan ( ) ;
227
+ }
218
228
function disconnect ( ) {
219
229
// we initiate a disconnect
220
230
socket . close ( ) ;
@@ -260,74 +270,28 @@ function handleNewSocket(socket: WebSocket) {
260
270
socket . send ( msg ) ;
261
271
}
262
272
263
- function parseAndValidateMessage ( msg : string , allowedCommands : string [ ] ) {
273
+ function parseAndValidateMessage ( msg : string ) : ProtocolMessage {
274
+ // json.
264
275
let message ;
265
276
try {
266
277
message = JSON . parse ( msg ) ;
267
278
} catch ( e ) {
268
- return failValidation ( e ) ;
269
- }
270
- // TODO: rethink validation so that it only protects the server, not other clients
271
- if ( typeof message != "object" ) return failValidation ( "JSON root data type expected to be object" ) ;
272
- // ignore all unexpected fields.
273
- message = {
274
- cmd : message . cmd ,
275
- args : message . args ,
276
- } ;
277
- if ( allowedCommands . indexOf ( message . cmd ) === - 1 ) return failValidation ( "invalid command" , message . cmd ) ;
278
- switch ( message . cmd ) {
279
- case "createRoom" : {
280
- if ( message . args != null ) return failValidation ( "expected no args. got:" , message . args ) ;
281
- delete message . args ;
282
- break ;
283
- }
284
- case "joinRoom" : {
285
- message . args = {
286
- roomCode : message . args . roomCode ,
287
- } ;
288
- if ( typeof message . args . roomCode !== "string" ) return failValidation ( "expected string:" , message . args . roomCode ) ;
289
- // although the room code might be bogus, that's a reasonable mistake, not a malfunction.
290
- break ;
291
- }
292
- case "makeAMove" : {
293
- let move = message . args ;
294
- if ( ! Array . isArray ( move ) ) return failValidation ( "expected args to be an array" ) ;
295
- break ;
296
- }
297
- case "changeMyName" : {
298
- let newName = message . args ;
299
- if ( typeof newName !== "string" ) return failValidation ( "expected string:" , newName ) ;
300
- if ( newName . length > 16 ) newName = newName . substring ( 0 , 16 ) ;
301
- if ( newName . length === 0 ) return failValidation ( "new name is empty string" ) ;
302
- message . args = newName ;
303
- break ;
304
- }
305
- case "changeMyRole" : {
306
- let newRole = message . args ;
307
- if ( typeof newRole !== "string" ) return failValidation ( "expected string:" , newRole ) ;
308
- message . args = newRole ;
309
- break ;
310
- }
311
- default : throw new Error ( "TODO: handle command: " + message . cmd ) ;
279
+ throwShenanigan ( msg + "\n" , e ) ;
312
280
}
313
281
314
- // seems legit
315
- return message ;
282
+ // json schema.
283
+ let res = jsonschema . validate ( message , protocolSchema , {
284
+ required : true ,
285
+ nestedErrors : true ,
286
+ } ) ;
287
+ if ( ! res . valid ) throwShenanigan ( "client message fails validation:" , message , res . toString ( ) + "---" ) ;
316
288
317
- function failValidation ( blurb : string | any , offendingValue ?: any ) {
318
- if ( arguments . length >= 2 ) {
319
- if ( typeof offendingValue === "string" ) {
320
- // make whitespace easier to see
321
- offendingValue = JSON . stringify ( offendingValue ) ;
322
- }
323
- console . log ( "message failed validation:" , blurb , offendingValue ) ;
324
- } else {
325
- console . log ( "message failed validation:" , blurb ) ;
326
- }
327
- return null ;
328
- }
289
+ console . log ( msg ) ;
290
+ return message ;
329
291
}
330
292
}
293
+ class Shenanigan extends Error { }
294
+
331
295
332
296
const roomCodeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;
333
297
function generateRoomCode ( ) {
0 commit comments