2020-04-04 16:27:18 +02:00
/* eslint-disable no-process-env */
2016-02-28 20:23:41 +01:00
/ * *
2020-04-02 19:07:03 +02:00
* Copyright ( C ) 2016 - 2020 Raphaël Jakse < raphael . trivabble @ jakse . fr >
2016-02-28 20:23:41 +01:00
*
* @ licstart
* This file is part of Trivabble .
*
* Trivabble is free software : you can redistribute it and / or modify it
* under the terms of the GNU Affero General Public License ( GNU AGPL )
* as published by the Free Software Foundation , either version 3 of
* the License , or ( at your option ) any later version .
*
* Trivabble is distributed in the hope that it will be useful , but
* WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with Trivabble . If not , see < http : //www.gnu.org/licenses/>.
* @ licend
*
* @ source : https : //trivabble.1s.fr/
* @ source : https : //gitlab.com/raphj/trivabble/
* /
2020-04-12 20:14:16 +02:00
/*eslint strict: [2, "global"]*/
2020-04-04 16:27:18 +02:00
"use strict" ;
const port = parseInt ( process . env . TRIVABBLE _PORT || "3000" ) ;
2016-02-28 20:23:41 +01:00
const SAVE _TIMEOUT = 5000 ;
2020-04-04 16:19:57 +02:00
const KEEP _ALIVE = 30000 ;
2020-04-29 08:15:32 +02:00
const GAMES _BACKUP = process . env . TRIVABBLE _GAMES _BACKUP || "games.backup.json" ;
2020-05-16 10:02:18 +02:00
const DEFAULT _BOARD _LANG = process . env . TRIVABBLE _DEFAULT _BOARD _LANG || "fr" ;
2016-02-28 20:23:41 +01:00
2020-05-07 01:08:59 +02:00
const VERSION = 202005070100 ;
2020-04-28 19:27:37 +02:00
2020-04-04 16:57:44 +02:00
function envTrue ( name ) {
return ( process . env [ name ] || "" ) . toLowerCase ( ) === "true" ;
}
2020-04-05 12:34:30 +02:00
const DEV _ENABLE _SERVING _FILES = envTrue ( "DEV_ENABLE_SERVING_FILES" ) ;
const DEBUG _LOG = DEV _ENABLE _SERVING _FILES || envTrue ( "DEBUG_LOG" ) ;
2020-04-04 16:27:18 +02:00
if ( DEV _ENABLE _SERVING _FILES ) {
console . log ( "DEV_ENABLE_SERVING_FILES: Serving files in the current directory. Please never do this on a production server, this is for development purposes only." ) ;
}
2020-04-05 15:49:48 +02:00
const debuglog = DEBUG _LOG ? console . log . bind ( console ) : ( ) => null ;
2020-04-04 16:57:44 +02:00
2020-05-16 10:02:18 +02:00
const http = require ( "http" ) ;
const path = require ( "path" ) ;
const fs = require ( "fs" ) ;
2020-04-12 20:14:39 +02:00
const crypto = require ( "crypto" ) ;
const REQUEST _TYPE _LONG _POLLING = 1 ;
const REQUEST _TYPE _SSE = 2 ;
const REQUEST _TYPE _WEBSOCKET = 3 ;
2016-02-28 20:23:41 +01:00
2020-05-07 00:53:37 +02:00
/* eslint no-sync: ["error", { allowAtRootLevel: true }] */
2020-04-28 19:29:36 +02:00
2020-05-04 00:28:33 +02:00
/* Manage multi language board */
2020-05-16 10:02:18 +02:00
const boardTilesPerLang = { } ;
const availableBoardLangs = { } ;
for ( const lang of fs . readdirSync ( path . join ( _ _dirname , "lang" ) ) ) {
const data = require ( path . join ( _ _dirname , "lang" , lang ) ) ; // eslint-disable-line global-require
boardTilesPerLang [ data . code ] = data ;
availableBoardLangs [ data . code ] = data . name ;
2020-05-06 23:28:09 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
const games = { } ;
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
let saveTimeout = null ;
let running = true ;
2020-04-05 21:05:48 +02:00
const server = http . createServer ( handleRequest ) ;
2020-04-12 20:14:22 +02:00
server . setTimeout ( 0 ) ; // The default in node 13 is 0. Earlier versions have 120.
2016-02-28 20:23:41 +01:00
2020-04-05 22:00:07 +02:00
function saveGames ( callback ) {
2020-04-29 08:15:32 +02:00
fs . writeFile ( GAMES _BACKUP , JSON . stringify ( games ) , function ( err ) {
2016-02-28 20:23:41 +01:00
if ( err ) {
2020-04-04 16:27:18 +02:00
console . error ( "ERROR: Cannot save games!" ) ;
2020-04-05 12:51:47 +02:00
}
2020-04-05 22:00:07 +02:00
if ( typeof callback === "function" ) {
return callback ( err ) ;
2016-02-28 20:23:41 +01:00
}
} ) ;
2020-04-05 12:34:30 +02:00
saveTimeout = null ;
2016-02-28 20:23:41 +01:00
}
2020-04-15 21:53:32 +02:00
function shuffleInPlace ( a ) {
// https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array
for ( let i = a . length - 1 ; i > 0 ; i -- ) {
const j = Math . floor ( Math . random ( ) * ( i + 1 ) ) ;
const x = a [ i ] ;
a [ i ] = a [ j ] ;
a [ j ] = x ;
}
}
2016-02-28 20:23:41 +01:00
function Game ( ) {
this . init ( ) ;
this . listeningPlayers = [ ] ;
this . pendingEvents = [ ] ;
}
2020-04-12 20:14:39 +02:00
function writeMessage ( responseAndType , data , terminate ) {
2020-04-13 21:18:40 +02:00
if ( ! responseAndType [ 0 ] ) {
return ;
}
2020-04-12 20:14:39 +02:00
if ( responseAndType [ 1 ] === REQUEST _TYPE _WEBSOCKET ) {
webSocketWrite ( responseAndType [ 0 ] , data ) ;
} else {
responseAndType [ 0 ] [ terminate ? "end" : "write" ] (
responseAndType [ 1 ] === REQUEST _TYPE _SSE
? "data:" + data + "\n\n"
: data . length + data
) ;
}
2020-04-04 16:19:57 +02:00
2020-04-05 12:51:47 +02:00
if ( terminate ) {
2020-04-12 20:14:39 +02:00
stopKeepAlive ( responseAndType ) ;
if ( responseAndType [ 1 ] === REQUEST _TYPE _WEBSOCKET && responseAndType [ 0 ] && ! responseAndType [ 0 ] . isDestroyed ) {
responseAndType [ 0 ] . end ( webSocketCloseBuffer ) ;
responseAndType [ 0 ] = null ;
}
2020-04-05 12:51:47 +02:00
} else {
2020-04-12 20:14:39 +02:00
keepAlive ( responseAndType ) ;
2020-04-04 16:19:57 +02:00
}
}
2020-04-05 12:51:47 +02:00
function stop ( ) {
2020-04-05 22:00:07 +02:00
console . log ( "Saving games a first time" ) ;
if ( saveTimeout ) {
clearTimeout ( saveTimeout ) ;
saveTimeout = null ;
}
2020-04-05 12:51:47 +02:00
2020-04-05 22:00:07 +02:00
saveGames ( function ( ) {
running = false ;
console . log ( "Closing connections and saving the game state..." ) ;
2020-04-05 12:51:47 +02:00
2020-04-05 22:00:07 +02:00
let listeningPlayerCount = 0 ;
let gamesCount = 0 ;
for ( const gameID of Object . keys ( games ) ) {
const game = games [ gameID ] ;
for ( const listeningPlayer of game . listeningPlayers ) {
2020-04-12 20:14:39 +02:00
writeMessage ( listeningPlayer , '{"stopping": 2000}' , true ) ;
2020-04-05 22:00:07 +02:00
}
2020-04-05 18:13:13 +02:00
2020-04-05 22:00:07 +02:00
if ( game . listeningPlayers . length ) {
listeningPlayerCount += game . listeningPlayers . length ;
gamesCount ++ ;
}
2020-04-05 18:13:13 +02:00
}
2020-04-05 12:51:47 +02:00
2020-04-13 21:18:40 +02:00
console . log (
"Stopped" , gamesCount , "game" + ( gamesCount === 1 ? "" : "s" ) ,
"and" , listeningPlayerCount , "player connection" + ( listeningPlayerCount === 1 ? "" : "s" ) +
"."
) ;
2020-04-05 22:00:07 +02:00
server . close ( saveGames ) ;
} ) ;
2020-04-05 12:51:47 +02:00
}
2020-04-12 20:14:39 +02:00
const webSocketPingBuffer = Buffer . from ( [
0b10001001 , 0b00000000
] ) ;
const webSocketCloseBuffer = Buffer . from ( [
0b10001000 , 0b00000000
] ) ;
function stopKeepAlive ( responseAndType ) {
if ( responseAndType . keepAliveTimeout ) {
clearTimeout ( responseAndType . keepAliveTimeout ) ;
responseAndType . keepAliveTimeout = 0 ;
2020-04-04 16:19:57 +02:00
}
}
2020-04-12 20:14:39 +02:00
function keepAlive ( responseAndType ) {
stopKeepAlive ( responseAndType ) ;
responseAndType . keepAliveTimeout = setTimeout ( function keepAliveTimeout ( ) {
responseAndType [ 0 ] . write (
responseAndType [ 1 ] === REQUEST _TYPE _WEBSOCKET
? webSocketPingBuffer
: (
responseAndType [ 1 ] === REQUEST _TYPE _SSE
? ":\n\n"
: "2[]"
)
2020-04-04 18:08:28 +02:00
) ;
2020-04-04 16:19:57 +02:00
} , KEEP _ALIVE ) ;
2016-02-28 20:23:41 +01:00
}
function newBoard ( ) {
2020-04-05 15:49:48 +02:00
const res = new Array ( 15 * 15 ) ;
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
for ( let i = 0 ; i < 15 * 15 ; i ++ ) {
2016-02-28 20:23:41 +01:00
res [ i ] = "" ;
}
return res ;
}
2020-05-04 23:27:36 +02:00
Game . prototype . init = function ( lang ) {
2016-02-28 20:23:41 +01:00
this . board = newBoard ( ) ;
2020-05-16 10:02:18 +02:00
this . lang = lang || DEFAULT _BOARD _LANG ;
this . bag = boardTilesPerLang [ this . lang ] . bag . slice ( ) ;
this . letterValues = boardTilesPerLang [ this . lang ] . letterValues ;
2016-02-28 20:23:41 +01:00
this . racks = { } ;
this . scores = { } ;
2020-04-04 16:51:45 +02:00
this . lastUpdated = new Date ( ) ;
2020-04-19 10:46:53 +02:00
this . currentPlayer = "" ;
2020-04-15 21:53:32 +02:00
shuffleInPlace ( this . bag ) ;
2020-04-04 16:27:18 +02:00
} ;
2016-02-28 20:23:41 +01:00
Game . prototype . toJSON = function ( ) {
return {
board : this . board ,
2020-05-04 23:27:36 +02:00
lang : this . lang ,
2016-02-28 20:23:41 +01:00
bag : this . bag ,
2020-05-12 07:50:29 +02:00
letterValues : this . letterValues ,
2016-02-28 20:23:41 +01:00
racks : this . racks ,
2020-04-04 16:51:45 +02:00
scores : this . scores ,
2020-04-19 10:46:53 +02:00
lastUpdated : this . lastUpdated . toISOString ( ) ,
currentPlayer : this . currentPlayer
2016-02-28 20:23:41 +01:00
} ;
} ;
Game . fromJSON = function ( obj ) {
2020-04-05 15:49:48 +02:00
const game = new Game ( ) ;
2016-02-28 20:23:41 +01:00
game . board = obj . board || newBoard ( ) ;
2020-05-16 10:02:18 +02:00
game . lang = obj . lang || DEFAULT _BOARD _LANG ;
game . bag = boardTilesPerLang [ game . lang ] . bag . slice ( ) ;
game . letterValues = boardTilesPerLang [ game . lang ] . letterValues ;
2016-02-28 20:23:41 +01:00
game . racks = obj . racks || { } ;
2020-04-04 16:27:18 +02:00
game . scores = obj . scores || { } ;
2020-04-04 16:51:45 +02:00
game . lastUpdated = obj . lastUpdated ? new Date ( obj . lastUpdated ) : new Date ( ) ;
2020-04-19 10:46:53 +02:00
game . currentPlayer = obj . currentPlayer || "" ;
2016-02-28 20:23:41 +01:00
return game ;
} ;
Game . prototype . getPlayerRack = function ( player ) {
2020-04-05 15:49:48 +02:00
const playerID = "#" + player ;
2016-02-28 20:23:41 +01:00
return ( this . racks [ playerID ] || ( this . racks [ playerID ] = [ ] ) ) ;
} ;
Game . prototype . getPlayerScore = function ( player ) {
2020-04-05 15:49:48 +02:00
const playerID = "#" + player ;
2016-02-28 20:23:41 +01:00
return ( this . scores [ playerID ] || ( this . scores [ playerID ] = 0 ) ) ;
} ;
Game . prototype . setPlayerScore = function ( player , score ) {
2020-04-05 15:49:48 +02:00
const playerID = "#" + player ;
2016-02-28 20:23:41 +01:00
2020-04-04 16:27:18 +02:00
if ( ! Object . prototype . hasOwnProperty . call ( this . racks , playerID ) || typeof score !== "number" ) {
2016-02-28 20:23:41 +01:00
return ;
}
this . scores [ playerID ] = score ;
2020-04-12 20:14:35 +02:00
this . pendingEvents . push ( {
players : [ {
player : player ,
score : score
} ]
} ) ;
2016-02-28 20:23:41 +01:00
} ;
2020-04-19 10:46:53 +02:00
Game . prototype . setCurrentPlayer = function ( player ) {
this . currentPlayer = player ;
this . pendingEvents . push ( { currentPlayer : player } ) ;
} ;
2016-02-28 20:23:41 +01:00
Game . prototype . playerJoined = function ( playerName ) {
if ( playerName ) {
2020-04-04 16:27:18 +02:00
this . getPlayerRack ( playerName ) ; // Create the player's rack
2016-02-28 20:23:41 +01:00
}
2020-04-05 15:49:48 +02:00
const players = [ ] ;
2016-02-28 20:23:41 +01:00
2020-05-16 10:02:18 +02:00
for ( let player of Object . keys ( this . racks ) ) {
player = player . slice ( 1 ) ; // '#'
players . push ( {
player : player ,
score : this . getPlayerScore ( player ) ,
rackCount : countTiles ( this . getPlayerRack ( player ) )
} ) ;
2016-02-28 20:23:41 +01:00
}
2020-04-12 20:14:18 +02:00
this . pendingEvents . push ( { players : players } ) ;
2016-02-28 20:23:41 +01:00
} ;
2020-04-12 20:14:39 +02:00
Game . prototype . addListeningPlayer = function ( playerName , responseAndType ) {
2020-04-05 15:49:48 +02:00
const that = this ;
2016-02-28 20:23:41 +01:00
2020-04-12 20:14:39 +02:00
that . listeningPlayers . push ( responseAndType ) ;
keepAlive ( responseAndType ) ;
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
let closed = false ;
2020-04-05 10:30:04 +02:00
2020-05-16 10:02:18 +02:00
function close ( ) {
2020-04-05 10:30:04 +02:00
if ( closed ) {
return ;
}
closed = true ;
2020-04-12 20:14:39 +02:00
stopKeepAlive ( responseAndType ) ;
2020-04-13 21:18:40 +02:00
responseAndType [ 0 ] = null ;
2020-04-05 10:30:04 +02:00
2020-04-12 20:14:39 +02:00
const index = that . listeningPlayers . indexOf ( responseAndType ) ;
2016-02-28 20:23:41 +01:00
if ( index !== - 1 ) {
that . listeningPlayers [ index ] = that . listeningPlayers [ that . listeningPlayers . length - 1 ] ;
that . listeningPlayers . pop ( ) ;
}
2020-04-05 10:30:04 +02:00
}
2020-04-12 20:14:39 +02:00
responseAndType [ 0 ] . on ( "error" , close ) ;
responseAndType [ 0 ] . on ( "close" , close ) ;
responseAndType [ 0 ] . on ( "finish" , close ) ;
responseAndType [ 0 ] . on ( "prefinish" , close ) ;
2016-02-28 20:23:41 +01:00
this . playerJoined ( playerName ) ;
this . commit ( ) ;
} ;
Game . prototype . commit = function ( ) {
2020-04-12 20:14:35 +02:00
const msg = JSON . stringify ( this . pendingEvents ) ;
2016-02-28 20:23:41 +01:00
this . pendingEvents = [ ] ;
2020-04-05 15:49:48 +02:00
for ( let i = 0 ; i < this . listeningPlayers . length ; i ++ ) {
2016-02-28 20:23:41 +01:00
while ( i < this . listeningPlayers . length && ! this . listeningPlayers [ i ] ) {
this . listeningPlayers [ i ] = this . listeningPlayers [ this . listeningPlayers . length - 1 ] ;
2020-04-04 16:27:18 +02:00
this . listeningPlayers . pop ( ) ;
2016-02-28 20:23:41 +01:00
}
if ( this . listeningPlayers [ i ] ) {
2020-04-12 20:14:39 +02:00
writeMessage ( this . listeningPlayers [ i ] , msg ) ;
2016-02-28 20:23:41 +01:00
}
}
2020-04-05 12:34:30 +02:00
if ( saveTimeout === null ) {
saveTimeout = setTimeout ( saveGames , SAVE _TIMEOUT ) ;
2016-02-28 20:23:41 +01:00
}
} ;
Game . prototype . bagPopLetter = function ( player ) {
2020-04-15 21:53:32 +02:00
if ( this . bag . length ) {
const letter = this . bag . pop ( ) ;
2020-04-04 16:27:18 +02:00
this . pendingEvents . push ( {
player : player ,
action : "popBag" ,
2020-04-15 21:53:32 +02:00
remainingLetters : this . bag . length
2020-04-04 16:27:18 +02:00
} ) ;
2016-02-28 20:23:41 +01:00
return letter ;
}
return "" ;
} ;
Game . prototype . getCell = function ( index ) {
return this . board [ index ] ;
2020-04-04 16:27:18 +02:00
} ;
2016-02-28 20:23:41 +01:00
Game . prototype . setCell = function ( index , letter , player ) {
this . board [ index ] = letter ;
2020-04-12 20:14:18 +02:00
this . pendingEvents . push ( {
2020-04-28 19:29:36 +02:00
player : player ,
action : "setCell" ,
indexTo : index ,
letter : letter
2020-04-12 20:14:18 +02:00
} ) ;
2016-02-28 20:23:41 +01:00
} ;
Game . prototype . bagPushLetter = function ( letter , player ) {
if ( letter ) {
2020-04-15 21:53:32 +02:00
this . bag . push ( letter ) ;
shuffleInPlace ( this . bag ) ;
2016-02-28 20:23:41 +01:00
2020-04-12 20:14:18 +02:00
this . pendingEvents . push ( {
player : player ,
action : "pushBag" ,
2020-04-15 21:53:32 +02:00
remainingLetters : this . bag . length
2020-04-12 20:14:18 +02:00
} ) ;
2016-02-28 20:23:41 +01:00
}
} ;
Game . prototype . reset = function ( player ) {
2020-05-04 23:27:36 +02:00
this . init ( this . lang ) ;
2020-04-04 16:27:18 +02:00
this . pendingEvents . push ( {
player : player ,
action : "reset" ,
board : this . board ,
2020-04-15 21:53:32 +02:00
remainingLetters : this . bag . length ,
2020-04-12 20:14:18 +02:00
rack : [ ]
2020-04-04 16:27:18 +02:00
} ) ;
2016-02-28 20:23:41 +01:00
this . playerJoined ( ) ;
} ;
function newGameId ( ) {
2020-04-05 15:49:48 +02:00
let number ;
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
let k = 10000 ;
let retries = 0 ;
2016-02-28 20:23:41 +01:00
do {
number = Math . floor ( Math . random ( ) * k ) . toString ( ) ;
if ( retries > 10 ) {
retries = 0 ;
k *= 10 ;
} else {
retries ++ ;
}
} while ( games [ number ] ) ;
return number . toString ( ) ;
}
function countTiles ( rack ) {
2020-04-05 15:49:48 +02:00
let count = 0 ;
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
for ( let i = 0 ; i < rack . length ; i ++ ) {
2016-02-28 20:23:41 +01:00
if ( rack [ i ] ) {
count ++ ;
}
}
return count ;
}
2020-04-12 20:14:36 +02:00
function joinGame ( gameNumber ) {
2020-04-05 10:35:53 +02:00
if ( ! gameNumber ) {
gameNumber = newGameId ( ) ;
}
2016-02-28 20:23:41 +01:00
2020-04-05 12:32:31 +02:00
const game = games [ gameNumber ] || ( games [ gameNumber ] = new Game ( ) ) ;
2016-02-28 20:23:41 +01:00
2020-04-05 10:35:53 +02:00
return { gameNumber , game } ;
}
2020-04-28 19:25:16 +02:00
function reply ( message , response , cmdNumber , r ) {
response . write (
JSON . stringify ( { ... r , id : message . id || message . cmds [ cmdNumber ] . id } )
) ;
}
2020-04-25 23:16:34 +02:00
function handleCommand ( cmdNumber , message , response ) {
const { gameNumber , game } = joinGame ( message . gameNumber ) ;
2020-04-05 10:35:53 +02:00
2020-04-04 16:51:45 +02:00
game . lastUpdated = new Date ( ) ;
2020-04-25 23:16:34 +02:00
const playerName = message . playerName ;
2020-04-05 15:49:48 +02:00
let rack = null ;
2020-04-25 23:16:34 +02:00
const cmd = message . cmds [ cmdNumber ] ;
2016-02-28 20:23:41 +01:00
switch ( cmd . cmd ) {
2020-04-12 20:14:18 +02:00
case "sync" : // DEPRECATED. Here for old clients.
2020-04-05 15:49:48 +02:00
case "joinGame" : {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , {
error : 0 ,
gameNumber : gameNumber ,
playerName : playerName ,
2020-05-04 23:27:36 +02:00
boardLang : game . lang ,
2020-05-16 10:02:18 +02:00
availableBoardLangs : availableBoardLangs ,
2020-04-28 19:25:16 +02:00
currentPlayer : game . currentPlayer ,
rack : game . getPlayerRack ( playerName ) ,
board : game . board ,
remainingLetters : game . bag . length ,
2020-05-05 08:30:39 +02:00
letterletterValues : game . letterValues ,
2020-04-28 19:27:37 +02:00
version : VERSION
2020-04-28 19:25:16 +02:00
} ) ;
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
2020-04-04 16:27:18 +02:00
2020-04-05 15:49:48 +02:00
case "hello" : {
2020-04-04 17:19:28 +02:00
game . playerJoined ( playerName ) ;
2020-05-04 23:27:36 +02:00
reply ( message , response , cmdNumber , { error : 0 , boardLang : game . lang , version : VERSION } ) ;
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
case "score" : {
2020-04-04 17:19:28 +02:00
game . setPlayerScore ( cmd . player , cmd . score ) ;
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 0 } ) ;
2020-04-19 10:46:53 +02:00
break ;
}
case "currentPlayer" : {
game . setCurrentPlayer ( cmd . player ) ;
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 0 } ) ;
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
case "moveLetter" : {
let letter = "" ;
2016-02-28 20:23:41 +01:00
2020-04-22 23:26:45 +02:00
// This case can fail in various ways. Instead altering the state
// of the game immediately, we store the operations to run them
// at the very end, if nothing failed.
const operations = [ ] ;
2020-04-04 17:19:28 +02:00
switch ( cmd . from ) {
case "rack" :
rack = game . getPlayerRack ( playerName ) ;
2016-02-28 20:23:41 +01:00
2020-04-22 23:26:45 +02:00
if ( cmd . indexFrom > 6 || cmd . indexFrom < 0 ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Wrong indexFrom" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
}
2020-04-04 17:19:28 +02:00
letter = rack [ cmd . indexFrom ] ;
2020-05-01 18:51:13 +02:00
if ( ! letter ) {
reply ( message , response , cmdNumber , { error : 1 , reason : "Moving from an empty location" } ) ;
return false ;
}
2020-04-27 19:57:45 +02:00
operations . push ( ( ) => {
rack [ cmd . indexFrom ] = "" ;
} ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
case "board" :
2020-04-22 23:26:45 +02:00
if ( cmd . indexFrom < 0 || cmd . indexFrom >= 15 * 15 ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Wrong indexFrom" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-04 17:19:28 +02:00
}
2020-04-22 23:26:45 +02:00
letter = game . getCell ( cmd . indexFrom ) ;
operations . push ( game . setCell . bind ( game , cmd . indexFrom , "" , playerName ) ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
case "bag" :
2020-04-22 23:26:45 +02:00
if ( ! game . bag . length ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Empty bag" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
}
2020-04-27 19:57:45 +02:00
operations . push ( ( ) => {
letter = game . bagPopLetter ( playerName ) ;
} ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
default :
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Moving letter from an unknown place" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-04 17:19:28 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
switch ( cmd . to ) {
case "rack" :
2020-04-22 23:26:45 +02:00
if ( cmd . indexTo < 0 || cmd . indexTo > 6 ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Wrong indexTo" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-04 17:19:28 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
rack = rack || game . getPlayerRack ( playerName ) ;
2020-04-22 23:26:45 +02:00
if ( rack [ cmd . indexTo ] ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Moving a tile to a non-empty location" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
}
2020-04-27 19:57:45 +02:00
operations . push ( ( ) => {
rack [ cmd . indexTo ] = letter ;
} ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
case "board" :
2020-04-22 23:26:45 +02:00
if ( cmd . indexTo < 0 || cmd . indexTo >= 15 * 15 ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Wrong indexTo" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-04 17:19:28 +02:00
}
2020-04-22 23:26:45 +02:00
operations . push ( game . setCell . bind ( game , cmd . indexTo , letter , playerName ) ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
case "bag" :
2020-04-22 23:26:45 +02:00
operations . push ( game . bagPushLetter . bind ( game , letter , playerName ) ) ;
2020-04-04 17:19:28 +02:00
break ;
2016-02-28 20:23:41 +01:00
2020-04-04 17:19:28 +02:00
default :
response . write ( "{\"error\":1, \"reason\":\"Moving letter to an unknown place\"}" ) ;
2020-04-22 23:26:45 +02:00
return false ;
}
for ( const operation of operations ) {
operation ( ) ;
2020-04-04 17:19:28 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , {
error : 0 ,
rack : (
( cmd . from === "bag" && cmd . to === "rack" )
? rack
: undefined // eslint-disable-line no-undefined
) ,
remainingLetters : game . bag . length
} ) ;
2020-04-04 17:19:28 +02:00
if ( rack ) {
game . pendingEvents . push ( {
players : [ {
player : playerName ,
rackCount : countTiles ( rack )
2020-04-12 20:14:18 +02:00
} ]
2020-04-04 17:19:28 +02:00
} ) ;
}
2016-02-28 20:23:41 +01:00
break ;
2020-04-05 15:49:48 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-05 15:49:48 +02:00
case "setRack" : {
2020-04-22 23:26:45 +02:00
if ( cmd . rack . length > 7 ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , {
error : 1 ,
rack : rack ,
reason : "the new rack is not at the right size"
} ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-04 17:19:28 +02:00
}
2020-04-22 23:26:45 +02:00
rack = game . getPlayerRack ( playerName ) ;
2020-04-25 23:16:34 +02:00
const oldRackSorted = rack . filter ( ( l ) => l ) ;
2020-04-22 23:26:45 +02:00
oldRackSorted . sort ( ) ;
2020-04-25 23:16:34 +02:00
const newRackSorted = cmd . rack . filter ( ( l ) => l ) ;
2020-04-22 23:26:45 +02:00
newRackSorted . sort ( ) ;
for ( let i = 0 ; i < 7 ; i ++ ) {
if ( ( oldRackSorted [ i ] !== newRackSorted [ i ] ) && ( oldRackSorted [ i ] || newRackSorted [ i ] ) ) {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , {
error : 1 ,
rack : rack ,
reason : "the new rack doesn't contain the same number of letters"
} ) ;
2020-04-22 23:26:45 +02:00
return false ;
2016-02-28 20:23:41 +01:00
}
2020-04-04 16:27:18 +02:00
}
2016-02-28 20:23:41 +01:00
2020-04-22 23:26:45 +02:00
for ( let i = 0 ; i < 7 ; i ++ ) {
2020-04-04 17:19:28 +02:00
rack [ i ] = cmd . rack [ i ] ;
}
2020-04-04 16:27:18 +02:00
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 0 } ) ;
2020-04-04 16:27:18 +02:00
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
case "resetGame" : {
2020-04-04 17:19:28 +02:00
game . reset ( ) ;
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 0 } ) ;
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
2020-04-04 16:27:18 +02:00
2020-05-04 00:28:33 +02:00
case "changeBoard" : {
2020-05-16 10:02:18 +02:00
game . lang = cmd . lang || DEFAULT _BOARD _LANG ;
2020-05-04 00:28:33 +02:00
game . reset ( ) ;
2020-05-12 07:50:29 +02:00
reply ( message , response , cmdNumber , { error : 0 , boardLang : game . lang , letterValues : game . letterValues } ) ;
2020-05-04 00:28:33 +02:00
break ;
}
2020-04-05 15:49:48 +02:00
case "msg" : {
2020-04-04 17:19:28 +02:00
game . pendingEvents . push ( {
msg : {
sender : playerName ,
2020-04-12 20:14:36 +02:00
content : cmd . msg ,
specialMsg : cmd . specialMsg
2020-04-12 20:14:18 +02:00
}
2020-04-04 17:19:28 +02:00
} ) ;
2020-04-12 20:14:36 +02:00
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 0 } ) ;
2020-04-04 17:19:28 +02:00
break ;
2020-04-05 15:49:48 +02:00
}
2020-04-04 17:19:28 +02:00
2020-04-05 15:49:48 +02:00
default : {
2020-04-28 19:25:16 +02:00
reply ( message , response , cmdNumber , { error : 1 , reason : "Unknown command" } ) ;
2020-04-22 23:26:45 +02:00
return false ;
2020-04-05 15:49:48 +02:00
}
2016-02-28 20:23:41 +01:00
}
2020-04-22 23:26:45 +02:00
return true ;
2016-02-28 20:23:41 +01:00
}
2020-04-25 23:16:34 +02:00
function handleCommands ( message , responseAndType ) {
if ( ! message . cmds || ! message . cmds . length ) {
const { gameNumber , game } = joinGame ( message . gameNumber ) ;
2016-02-28 20:23:41 +01:00
2020-04-12 20:14:39 +02:00
writeMessage ( responseAndType ,
2020-04-04 16:27:18 +02:00
JSON . stringify ( {
2020-05-16 10:02:18 +02:00
playerName : message . playerName ,
currentPlayer : game . currentPlayer ,
gameNumber : gameNumber ,
boardLang : game . lang ,
availableBoardLangs : availableBoardLangs ,
letterValues : game . letterValues ,
rack : game . getPlayerRack ( message . playerName ) ,
board : game . board ,
remainingLetters : game . bag . length ,
version : VERSION
2020-04-04 16:27:18 +02:00
} )
2016-02-28 20:23:41 +01:00
) ;
2020-04-25 23:16:34 +02:00
game . addListeningPlayer ( message . playerName , responseAndType ) ;
2016-02-28 20:23:41 +01:00
return ;
}
2020-04-25 23:16:34 +02:00
let wsMessage = "" ;
2020-04-12 20:14:39 +02:00
const response = (
responseAndType [ 1 ] === REQUEST _TYPE _WEBSOCKET
? {
write : function ( s ) {
2020-04-25 23:16:34 +02:00
wsMessage += s ;
2020-04-12 20:14:39 +02:00
} ,
end : function ( s ) {
if ( s ) {
2020-04-25 23:16:34 +02:00
wsMessage += s ;
2020-04-12 20:14:39 +02:00
}
2020-04-25 23:16:34 +02:00
webSocketWrite ( responseAndType [ 0 ] , wsMessage ) ;
2020-04-12 20:14:39 +02:00
}
}
: responseAndType [ 0 ]
) ;
2020-04-04 16:17:23 +02:00
2016-02-28 20:23:41 +01:00
response . write ( "[" ) ;
2020-04-25 23:16:34 +02:00
for ( let i = 0 ; i < message . cmds . length ; i ++ ) {
2016-02-28 20:23:41 +01:00
if ( i ) {
response . write ( "," ) ;
}
2020-04-22 23:26:45 +02:00
2020-04-25 23:16:34 +02:00
if ( ! handleCommand ( i , message , response ) ) {
2020-04-22 23:26:45 +02:00
break ;
}
2016-02-28 20:23:41 +01:00
}
response . end ( "]" ) ;
2020-04-25 23:16:34 +02:00
if ( games [ message . gameNumber ] ) {
games [ message . gameNumber ] . commit ( ) ;
2016-02-28 20:23:41 +01:00
}
}
2020-04-12 20:14:39 +02:00
// Thx https://medium.com/hackernoon/implementing-a-websocket-server-with-node-js-d9b78ec5ffa8
function webSocketWrite ( socket , data ) {
/* eslint-disable capitalized-comments */
// Copy the data into a buffer
data = Buffer . from ( data ) ;
const byteLength = data . length ;
// Note: we're not supporting > 65535 byte payloads at this stage
const lengthByteCount = byteLength < 126 ? 0 : 2 ;
const payloadLength = lengthByteCount === 0 ? byteLength : 126 ;
const buffer = Buffer . alloc ( 2 + lengthByteCount + byteLength ) ;
// Write out the first byte, using opcode `1` to indicate that the message
// payload contains text data
buffer . writeUInt8 ( 0b10000001 , 0 ) ;
buffer . writeUInt8 ( payloadLength , 1 ) ;
// Write the length of the payload to the second byte
if ( lengthByteCount === 2 ) {
buffer . writeUInt16BE ( byteLength , 2 ) ;
}
// Write the data to the data buffer
data . copy ( buffer , 2 + lengthByteCount ) ;
socket . write ( buffer ) ;
}
function webSocketGetMessage ( buffer ) {
/* eslint-disable no-bitwise, capitalized-comments */
let dataAfter = "" ;
let finalOffset = - 1 ;
if ( buffer . length < 2 ) {
return [ "" , 0 ] ;
}
const firstByte = buffer . readUInt8 ( 0 ) ;
const isFinalFrame = Boolean ( ( firstByte >>> 7 ) & 0x1 ) ;
// const [reserved1, reserved2, reserved3] = [
// Boolean((firstByte >>> 6) & 0x1),
// Boolean((firstByte >>> 5) & 0x1),
// Boolean((firstByte >>> 4) & 0x1)
// ];
const opCode = firstByte & 0xF ;
// We can return null to signify that this is a connection termination frame
if ( opCode === 0x8 ) {
return null ;
}
const secondByte = buffer . readUInt8 ( 1 ) ;
const isMasked = ( secondByte >>> 7 ) & 0x1 ;
// Keep track of our current position as we advance through the buffer
let currentOffset = 2 ;
let payloadLength = secondByte & 0x7F ;
if ( payloadLength > 125 ) {
if ( payloadLength === 126 ) {
payloadLength = buffer . readUInt16BE ( currentOffset ) ;
currentOffset += 2 ;
} else {
// 127
// If this has a value, the frame size is ridiculously huge!
// const leftPart = buffer.readUInt32BE(currentOffset);
// const rightPart = buffer.readUInt32BE(currentOffset += 4);
// Honestly, if the frame length requires 64 bits, you're probably doing it wrong.
// In Node.js you'll require the BigInt type, or a special library to handle this.
throw new Error ( "Large websocket payloads not currently implemented" ) ;
}
}
if ( payloadLength + currentOffset > buffer . length ) {
return [ "" , 0 ] ;
}
if ( ! isFinalFrame ) {
const message = webSocketGetMessage ( buffer . slice ( payloadLength + currentOffset ) ) ;
if ( message ) {
if ( message [ 1 ] ) {
dataAfter = message [ 0 ] ;
finalOffset = message [ 1 ] ;
} else {
return [ "" , 0 ] ;
}
} else if ( message === null ) {
// ??!?
return null ;
}
}
let maskingKey ;
if ( isMasked ) {
maskingKey = buffer . readUInt32BE ( currentOffset ) ;
currentOffset += 4 ;
}
// Allocate somewhere to store the final message data
const data = Buffer . alloc ( payloadLength ) ;
// Only unmask the data if the masking bit was set to 1
if ( isMasked ) {
// Loop through the source buffer one byte at a time, keeping track of which
// byte in the masking key to use in the next XOR calculation
for ( let i = 0 , j = 0 ; i < payloadLength ; ++ i , j = i % 4 ) {
// Extract the correct byte mask from the masking key
const shift = ( j === 3 ) ? 0 : ( 3 - j ) << 3 ;
const mask = ( shift === 0 ? maskingKey : ( maskingKey >>> shift ) ) & 0xFF ;
// Read a byte from the source buffer
const source = buffer . readUInt8 ( currentOffset ++ ) ;
// XOR the source byte and write the result to the data buffer
data . writeUInt8 ( mask ^ source , i ) ;
}
} else {
// Not masked - we can just read the data as-is
buffer . copy ( data , 0 , currentOffset ) ;
}
return [
opCode === 0x1
? ( data . toString ( "utf8" ) + dataAfter )
: "" ,
finalOffset === - 1
? currentOffset
: finalOffset
] ;
}
2016-02-28 20:23:41 +01:00
function handleRequest ( request , response ) {
2020-04-05 15:49:48 +02:00
let post = "" ;
2020-04-12 20:14:39 +02:00
const responseAndType = [ response , REQUEST _TYPE _LONG _POLLING ] ;
let upgradedToWebSocket = false ;
2020-04-05 10:30:04 +02:00
2020-04-05 12:34:30 +02:00
response . on ( "error" , function connectionError ( e ) {
2020-04-05 10:30:04 +02:00
console . error ( "An error occurred while trying to write on a socket" , e ) ;
2020-04-12 20:14:39 +02:00
stopKeepAlive ( responseAndType ) ;
2020-04-05 10:30:04 +02:00
} ) ;
2016-02-28 20:23:41 +01:00
2020-04-04 16:27:18 +02:00
// Thx http://stackoverflow.com/questions/4295782/how-do-you-extract-post-data-in-node-js
2016-02-28 20:23:41 +01:00
request . on ( "data" , function ( data ) {
2020-04-12 20:14:39 +02:00
if ( upgradedToWebSocket ) {
return ;
}
2016-02-28 20:23:41 +01:00
post += data ;
// Too much POST data, kill the connection!
if ( post . length > 1e6 ) {
request . connection . destroy ( ) ;
}
} ) ;
2020-04-12 20:14:39 +02:00
request . on ( "upgrade" , function ( ) {
upgradedToWebSocket = true ;
} ) ;
request . on ( "error" , function ( error ) {
console . error ( "Error while handling this request" , request , error ) ;
} ) ;
2016-02-28 20:23:41 +01:00
request . on ( "end" , function ( ) {
2020-04-12 20:14:39 +02:00
if ( upgradedToWebSocket ) {
return ;
}
2020-04-05 12:51:47 +02:00
if ( ! running ) {
response . statusCode = 503 ;
response . statusMessage = "Server is stopping" ;
response . end ( "Server is stopping" ) ;
return ;
}
2020-04-04 16:27:18 +02:00
if ( DEV _ENABLE _SERVING _FILES && ! request . url . startsWith ( "/:" ) ) {
if ( request . url === "/" ) {
request . url = "/index.html" ;
}
2020-04-04 16:57:44 +02:00
debuglog ( "Serving " + request . url ) ;
2020-04-04 16:27:18 +02:00
2020-05-16 10:02:18 +02:00
const requestedPath = path . join ( _ _dirname , ".." , "public" , request . url ) ;
fs . exists ( requestedPath , function ( exists ) {
2020-04-04 16:27:18 +02:00
if ( exists ) {
2020-05-16 10:02:18 +02:00
fs . readFile ( requestedPath , function ( err , contents ) {
2020-04-04 16:27:18 +02:00
if ( err ) {
response . statusCode = 500 ;
response . setHeader ( "Content-Type" , "text/plain; charset=utf-8" ) ;
response . end ( err ) ;
}
2020-04-05 15:49:48 +02:00
let mime = "application/xhtml+xml; charset=utf-8" ;
const mimes = {
2020-04-04 16:27:18 +02:00
".mp3" : "audio/mpeg" ,
".ogg" : "audio/ogg" ,
".js" : "application/javascript; charset=utf-8" ,
".css" : "text/css; charset=utf-8" ,
".svg" : "image/svg+xml"
} ;
2020-04-05 15:49:48 +02:00
for ( const i in mimes ) {
2020-04-12 20:14:35 +02:00
if ( Object . prototype . hasOwnProperty . call ( mimes , i ) && request . url . endsWith ( i ) ) {
2020-04-04 16:27:18 +02:00
mime = mimes [ i ] ;
2020-04-12 20:14:37 +02:00
if ( i === ".js" ) {
contents = contents . toString ( ) . replace (
/(?<before>^|\s|\()(?:const|let)(?<after>\s)/gu ,
"$1var$2"
) ;
}
2020-04-04 16:27:18 +02:00
break ;
}
}
response . setHeader ( "Content-Type" , mime ) ;
response . end ( contents ) ;
} ) ;
} else {
response . statusCode = 404 ;
response . end ( "404" ) ;
}
} ) ;
return ;
}
2020-04-13 10:25:30 +02:00
const sseMatch = request . url . match ( /\/(?::[^/]+\/)?sse\/(?<data>[\s\S]+)$/u ) ;
2020-04-04 16:17:23 +02:00
2020-04-12 20:14:39 +02:00
if ( sseMatch ) {
2020-04-04 16:17:23 +02:00
response . setHeader ( "Content-Type" , "text/event-stream" ) ;
2020-04-12 20:14:39 +02:00
post = decodeURIComponent ( sseMatch . groups . data ) ;
responseAndType [ 1 ] = REQUEST _TYPE _SSE ;
2020-04-04 16:17:23 +02:00
} else {
response . setHeader ( "Content-Type" , "text/plain" ) ;
response . setHeader ( "Transfer-Encoding" , "chunked" ) ;
}
2020-04-12 20:14:39 +02:00
debuglog ( "REQ:" , request . url , post , responseAndType [ 1 ] ) ;
2020-04-05 21:05:48 +02:00
try {
post = post && JSON . parse ( post ) ;
} catch ( e ) {
response . statusCode = 400 ;
response . setHeader ( "application/json" ) ;
response . end ( '{"error":1, "reason": "Unable to parse your JSON data"}' ) ;
return ;
}
2020-04-12 20:14:39 +02:00
handleCommands ( post , responseAndType ) ;
2016-02-28 20:23:41 +01:00
} ) ;
}
2020-04-29 08:15:32 +02:00
fs . readFile ( GAMES _BACKUP , function ( err , data ) {
2016-02-28 20:23:41 +01:00
try {
if ( err ) {
console . error ( "WARNING: Could not restore previous backup of the games" ) ;
} else {
2020-04-05 15:49:48 +02:00
const backup = JSON . parse ( data ) ;
2016-02-28 20:23:41 +01:00
2020-05-16 10:02:18 +02:00
for ( const gameNumber of Object . keys ( backup ) ) {
games [ gameNumber ] = Game . fromJSON ( backup [ gameNumber ] ) ;
2016-02-28 20:23:41 +01:00
}
}
} catch ( e ) {
console . error ( "WARNING: Could not restore previous backup of the games: file is broken:" ) ;
console . error ( "WARNING: " , e ) ;
}
2020-04-12 20:14:39 +02:00
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
server . on ( "upgrade" , function upgradeToWebsocket ( request , socket ) {
2020-04-13 21:18:40 +02:00
const responseAndType = [ socket , REQUEST _TYPE _WEBSOCKET ] ;
function closeSocket ( ) {
responseAndType [ 0 ] = null ;
}
2020-04-12 20:14:39 +02:00
socket . on ( "error" , function ( e ) {
2020-04-13 21:18:40 +02:00
console . error ( "Socket error" , e . toString ( ) ) ;
closeSocket ( ) ;
2020-04-12 20:14:39 +02:00
} ) ;
2020-04-13 21:18:40 +02:00
socket . on ( "close" , closeSocket ) ;
2020-04-12 20:14:39 +02:00
2020-04-13 10:25:30 +02:00
const webSocketMatch = request . url . match ( /\/(?::[^/]+\/)?ws\/(?<data>[\s\S]+)$/u ) ;
2020-04-12 20:14:39 +02:00
let decodedData ;
try {
decodedData = decodeURIComponent ( webSocketMatch . groups . data ) ;
} catch ( e ) {
socket . end ( "HTTP/1.1 400 Bad Request" ) ;
console . error ( "WS: FAILED - Could not decode the given data " + webSocketMatch . groups . data ) ;
return ;
}
2020-04-25 23:16:34 +02:00
let message ;
2020-04-12 20:14:39 +02:00
if ( webSocketMatch ) {
try {
2020-04-25 23:16:34 +02:00
message = JSON . parse ( decodedData ) ;
2020-04-12 20:14:39 +02:00
} catch ( e ) {
socket . end ( "HTTP/1.1 400 Bad Request" ) ;
console . error ( "WS: FAILED - Could not parse the given data " + decodedData ) ;
return ;
}
} else {
socket . end ( "HTTP/1.1 400 Bad Request" ) ;
return ;
}
2020-04-16 09:43:30 +02:00
if ( request . headers . upgrade . toLowerCase ( ) !== "websocket" ) {
2020-04-12 20:14:39 +02:00
socket . end ( "HTTP/1.1 400 Bad Request" ) ;
console . error ( "WS: FAILED - HTTP header 'Upgrade' is not 'websocket' " + decodedData ) ;
return ;
}
socket . write (
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n"
) ;
const webSocketKey = request . headers [ "sec-websocket-key" ] ;
if ( webSocketKey ) {
socket . write (
"Sec-WebSocket-Accept: " + (
crypto
. createHash ( "sha1" )
. update ( webSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" , "binary" )
. digest ( "base64" )
) + "\r\n"
) ;
}
socket . write ( "\r\n" ) ;
debuglog ( "WS: established " + decodedData ) ;
let receivedBytes = null ;
socket . on ( "data" , function webSocketData ( buffer ) {
if ( receivedBytes ) {
buffer = Buffer . concat ( [ receivedBytes , buffer ] ) ;
receivedBytes = null ;
}
while ( true ) {
2020-04-25 23:16:34 +02:00
const wsMessage = webSocketGetMessage ( buffer ) ;
if ( wsMessage && wsMessage [ 1 ] ) {
if ( wsMessage [ 0 ] ) {
debuglog ( "WS message: " + wsMessage [ 0 ] ) ;
2020-04-12 20:14:39 +02:00
try {
2020-04-25 23:16:34 +02:00
message . cmds = JSON . parse ( wsMessage [ 0 ] ) ;
2020-04-12 20:14:39 +02:00
} catch ( e ) {
writeMessage ( responseAndType , '{"error":1, "reason": "Unable to parse your JSON data"}' ) ;
return ;
}
2020-04-25 23:16:34 +02:00
handleCommands ( message , responseAndType ) ;
message . cmds = null ;
2020-04-12 20:14:39 +02:00
}
2020-04-25 23:16:34 +02:00
} else if ( wsMessage === null ) {
2020-04-12 20:14:39 +02:00
if ( responseAndType [ 0 ] && ! socket . isDestroyed ) {
responseAndType [ 0 ] = null ;
socket . end ( webSocketCloseBuffer ) ;
}
return ;
}
2020-04-25 23:16:34 +02:00
if ( wsMessage [ 1 ] === buffer . length ) {
2020-04-12 20:14:39 +02:00
break ;
}
2020-04-25 23:16:34 +02:00
if ( wsMessage [ 1 ] === 0 ) {
2020-04-12 20:14:39 +02:00
receivedBytes = buffer ;
return ;
}
2020-04-25 23:16:34 +02:00
buffer = buffer . slice ( wsMessage [ 1 ] ) ;
2020-04-12 20:14:39 +02:00
}
} ) ;
2020-04-25 23:16:34 +02:00
handleCommands ( message , responseAndType ) ;
2020-04-12 20:14:39 +02:00
} ) ;
2020-04-05 21:05:48 +02:00
server . on ( "error" , function ( error ) {
console . error ( "An error happened in the HTTP server" , error ) ;
} ) ;
2020-05-16 10:02:18 +02:00
server . listen ( port , function ( ) {
2016-02-28 20:23:41 +01:00
console . log ( "Server listening on: http://localhost:%s" , port ) ;
} ) ;
} ) ;
2020-04-05 12:51:47 +02:00
process . once ( "SIGINT" , function ( ) {
console . log ( "SIGINT received..." ) ;
stop ( ) ;
} ) ;
process . once ( "SIGTERM" , function ( ) {
console . log ( "SIGTERM received..." ) ;
stop ( ) ;
} ) ;
2020-04-05 21:05:48 +02:00
process . on ( "uncaughtException" , function ( err ) {
console . log ( err ) ;
} ) ;