├── .babelrc ├── .gitignore ├── .gitpod.yml ├── README.md ├── assertion-analyser.js ├── package-lock.json ├── package.json ├── public ├── Collectible.mjs ├── Player.mjs ├── game.mjs └── style.css ├── routes └── fcctesting.js ├── sample.env ├── server.js ├── test-runner.js ├── tests ├── 1_unit-tests.js └── 2_functional-tests.js └── views └── index.html /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .glitch-assets 3 | node_modules/ -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-node-lts 2 | 3 | ports: 4 | - port: 3000 5 | onOpen: open-preview 6 | visibility: public 7 | 8 | tasks: 9 | - init: npm install 10 | command: npm run start 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Secure Real Time Multiplayer Game 2 | 3 | This is the boilerplate for the Secure Real Time Multiplayer Game project. Instructions for building your project can be found at https://www.freecodecamp.org/learn/information-security/information-security-projects/secure-real-time-multiplayer-game 4 | -------------------------------------------------------------------------------- /assertion-analyser.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | function objParser(str, init) { 29 | // finds objects, arrays, strings, and function arguments 30 | // between parens, because they may contain ',' 31 | var openSym = ['[', '{', '"', "'", '(']; 32 | var closeSym = [']', '}', '"', "'", ')']; 33 | var type; 34 | for(var i = (init || 0); i < str.length; i++ ) { 35 | type = openSym.indexOf(str[i]); 36 | if( type !== -1) break; 37 | } 38 | if (type === -1) return null; 39 | var open = openSym[type]; 40 | var close = closeSym[type]; 41 | var count = 1; 42 | for(var k = i+1; k < str.length; k++) { 43 | if(open === '"' || open === "'") { 44 | if(str[k] === close) count--; 45 | if(str[k] === '\\') k++; 46 | } else { 47 | if(str[k] === open) count++; 48 | if(str[k] === close) count--; 49 | } 50 | if(count === 0) break; 51 | } 52 | if(count !== 0) return null; 53 | var obj = str.slice(i, k+1); 54 | return { 55 | start : i, 56 | end: k, 57 | obj: obj 58 | }; 59 | } 60 | 61 | function replacer(str) { 62 | // replace objects with a symbol ( __#n) 63 | var obj; 64 | var cnt = 0; 65 | var data = []; 66 | while(obj = objParser(str)) { 67 | data[cnt] = obj.obj; 68 | str = str.substring(0, obj.start) + '__#' + cnt++ + str.substring(obj.end+1) 69 | } 70 | return { 71 | str : str, 72 | dictionary : data 73 | } 74 | } 75 | 76 | function splitter(str) { 77 | // split on commas, then restore the objects 78 | var strObj = replacer(str); 79 | var args = strObj.str.split(','); 80 | args = args.map(function(a){ 81 | var m = a.match(/__#(\d+)/); 82 | while (m) { 83 | a = a.replace(/__#(\d+)/, strObj.dictionary[m[1]]); 84 | m = a.match(/__#(\d+)/); 85 | } 86 | return a.trim(); 87 | }) 88 | return args; 89 | } 90 | 91 | function assertionAnalyser(body) { 92 | 93 | // already filtered in the test runner 94 | // // remove comments 95 | // body = body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 96 | // // get test function body 97 | // body = body.match(/\{\s*([\s\S]*)\}\s*$/)[1]; 98 | 99 | if(!body) return "invalid assertion"; 100 | // replace assertions bodies, so that they cannot 101 | // contain the word 'assertion' 102 | 103 | var body = body.match(/(?:browser\s*\.\s*)?assert\s*\.\s*\w*\([\s\S]*\)/)[0]; 104 | var s = replacer(body); 105 | // split on 'assertion' 106 | var splittedAssertions = s.str.split('assert'); 107 | var assertions = splittedAssertions.slice(1); 108 | // match the METHODS 109 | 110 | var assertionBodies = []; 111 | var methods = assertions.map(function(a, i){ 112 | var m = a.match(/^\s*\.\s*(\w+)__#(\d+)/); 113 | assertionBodies.push(parseInt(m[2])); 114 | var pre = splittedAssertions[i].match(/browser\s*\.\s*/) ? 'browser.' : ''; 115 | return pre + m[1]; 116 | }); 117 | if(methods.some(function(m){ return !m })) return "invalid assertion"; 118 | // remove parens from the assertions bodies 119 | var bodies = assertionBodies.map(function(b){ 120 | return s.dictionary[b].slice(1,-1).trim(); 121 | }); 122 | assertions = methods.map(function(m, i) { 123 | return { 124 | method: m, 125 | args: splitter(bodies[i]) //replace objects, split on ',' ,then restore objects 126 | } 127 | }) 128 | return assertions; 129 | } 130 | 131 | module.exports = assertionAnalyser; 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-real-time-multiplayer-game", 3 | "version": "1.0.0", 4 | "description": "Information Security 5: Secure Real-Time Multiplayer Game", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "PORT=3005 mocha --require @babel/register --recursive --exit --ui tdd tests/" 9 | }, 10 | "dependencies": { 11 | "@babel/core": "^7.7.5", 12 | "@babel/preset-env": "^7.7.6", 13 | "@babel/register": "^7.7.4", 14 | "body-parser": "^1.19.0", 15 | "chai": "^4.2.0", 16 | "chai-http": "^4.3.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "helmet": "^3.21.3", 21 | "jsdom": "^16.2.0", 22 | "mocha": "^7.1.0", 23 | "socket.io": "^2.3.0", 24 | "socket.io-client": "^2.3.0" 25 | }, 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /public/Collectible.mjs: -------------------------------------------------------------------------------- 1 | class Collectible { 2 | constructor({x, y, value, id}) { 3 | 4 | } 5 | 6 | } 7 | 8 | /* 9 | Note: Attempt to export this for use 10 | in server.js 11 | */ 12 | try { 13 | module.exports = Collectible; 14 | } catch(e) {} 15 | 16 | export default Collectible; 17 | -------------------------------------------------------------------------------- /public/Player.mjs: -------------------------------------------------------------------------------- 1 | class Player { 2 | constructor({x, y, score, id}) { 3 | 4 | } 5 | 6 | movePlayer(dir, speed) { 7 | 8 | } 9 | 10 | collision(item) { 11 | 12 | } 13 | 14 | calculateRank(arr) { 15 | 16 | } 17 | } 18 | 19 | export default Player; 20 | -------------------------------------------------------------------------------- /public/game.mjs: -------------------------------------------------------------------------------- 1 | import Player from './Player.mjs'; 2 | import Collectible from './Collectible.mjs'; 3 | 4 | const socket = io(); 5 | const canvas = document.getElementById('game-window'); 6 | const context = canvas.getContext('2d'); 7 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 20px; 3 | } 4 | 5 | code { 6 | background: #d0d0d5; 7 | } 8 | 9 | canvas { 10 | margin: auto; 11 | } 12 | 13 | .container { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | @media (min-width: 800px) { 19 | .container { 20 | flex-direction: row; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /routes/fcctesting.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | 'use strict'; 29 | 30 | const cors = require('cors'); 31 | const fs = require('fs'); 32 | const runner = require('../test-runner'); 33 | 34 | module.exports = function (app) { 35 | 36 | app.route('/_api/server.js') 37 | .get(function(req, res, next) { 38 | console.log('requested'); 39 | fs.readFile(__dirname + '/server.js', function(err, data) { 40 | if(err) return next(err); 41 | res.send(data.toString()); 42 | }); 43 | }); 44 | app.route('/_api/routes/api.js') 45 | .get(function(req, res, next) { 46 | console.log('requested'); 47 | fs.readFile(__dirname + '/routes/api.js', function(err, data) { 48 | if(err) return next(err); 49 | res.type('txt').send(data.toString()); 50 | }); 51 | }); 52 | app.route('/_api/controllers/convertHandler.js') 53 | .get(function(req, res, next) { 54 | console.log('requested'); 55 | fs.readFile(__dirname + '/controllers/convertHandler.js', function(err, data) { 56 | if(err) return next(err); 57 | res.type('txt').send(data.toString()); 58 | }); 59 | }); 60 | 61 | app.route('/_api/public/Collectible.mjs') 62 | .get(function (req, res, next) { 63 | console.log('requested'); 64 | fs.readFile(__dirname + '/public/Collectible.mjs', function (err, data) { 65 | if (err) return next(err); 66 | res.type('txt').send(data.toString()); 67 | }); 68 | }); 69 | 70 | app.route('/_api/public/Player.mjs') 71 | .get(function (req, res, next) { 72 | console.log('requested'); 73 | fs.readFile(__dirname + '/public/Player.mjs', function (err, data) { 74 | if (err) return next(err); 75 | res.type('txt').send(data.toString()); 76 | }); 77 | }); 78 | 79 | var error; 80 | app.get('/_api/get-tests', cors(), function(req, res, next){ 81 | console.log(error); 82 | if(!error && process.env.NODE_ENV === 'test') return next(); 83 | res.json({status: 'unavailable'}); 84 | }, 85 | function(req, res, next){ 86 | if(!runner.report) return next(); 87 | res.json(testFilter(runner.report, req.query.type, req.query.n)); 88 | }, 89 | function(req, res){ 90 | runner.on('done', function(report){ 91 | process.nextTick(() => res.json(testFilter(runner.report, req.query.type, req.query.n))); 92 | }); 93 | }); 94 | app.get('/_api/app-info', function(req, res) { 95 | var hs = Object.keys(res._headers) 96 | .filter(h => !h.match(/^access-control-\w+/)); 97 | var hObj = {}; 98 | hs.forEach(h => {hObj[h] = res._headers[h]}); 99 | delete res._headers['strict-transport-security']; 100 | res.json({headers: hObj}); 101 | }); 102 | 103 | }; 104 | 105 | function testFilter(tests, type, n) { 106 | var out; 107 | switch (type) { 108 | case 'unit' : 109 | out = tests.filter(t => t.context.match('Unit Tests')); 110 | break; 111 | case 'functional': 112 | out = tests.filter(t => t.context.match('Functional Tests') && !t.title.match('#example')); 113 | break; 114 | default: 115 | out = tests; 116 | } 117 | if(n !== undefined) { 118 | return out[n] || out; 119 | } 120 | return out; 121 | } -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # Environment Config 2 | 3 | # store your secrets and config variables in here 4 | # only invited collaborators will be able to see your .env values 5 | 6 | # note: .env is a shell file so there can't be spaces around '=' 7 | 8 | PORT=3000 9 | # NODE_ENV=test -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const expect = require('chai'); 5 | const socket = require('socket.io'); 6 | const cors = require('cors'); 7 | 8 | const fccTestingRoutes = require('./routes/fcctesting.js'); 9 | const runner = require('./test-runner.js'); 10 | 11 | const app = express(); 12 | 13 | app.use('/public', express.static(process.cwd() + '/public')); 14 | app.use('/assets', express.static(process.cwd() + '/assets')); 15 | 16 | app.use(bodyParser.json()); 17 | app.use(bodyParser.urlencoded({ extended: true })); 18 | 19 | //For FCC testing purposes and enables user to connect from outside the hosting platform 20 | app.use(cors({origin: '*'})); 21 | 22 | // Index page (static HTML) 23 | app.route('/') 24 | .get(function (req, res) { 25 | res.sendFile(process.cwd() + '/views/index.html'); 26 | }); 27 | 28 | //For FCC testing purposes 29 | fccTestingRoutes(app); 30 | 31 | // 404 Not Found Middleware 32 | app.use(function(req, res, next) { 33 | res.status(404) 34 | .type('text') 35 | .send('Not Found'); 36 | }); 37 | 38 | const portNum = process.env.PORT || 3000; 39 | 40 | // Set up server and tests 41 | const server = app.listen(portNum, () => { 42 | console.log(`Listening on port ${portNum}`); 43 | if (process.env.NODE_ENV==='test') { 44 | console.log('Running Tests...'); 45 | setTimeout(function () { 46 | try { 47 | runner.run(); 48 | } catch (error) { 49 | console.log('Tests are not valid:'); 50 | console.error(error); 51 | } 52 | }, 1500); 53 | } 54 | }); 55 | 56 | module.exports = app; // For testing 57 | -------------------------------------------------------------------------------- /test-runner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * 4 | * 5 | * 6 | * 7 | * 8 | * 9 | * 10 | * 11 | * 12 | * 13 | * DO NOT EDIT THIS FILE 14 | * For FCC testing purposes! 15 | * 16 | * 17 | * 18 | * 19 | * 20 | * 21 | * 22 | * 23 | * 24 | * 25 | * 26 | */ 27 | 28 | var analyser = require('./assertion-analyser'); 29 | var EventEmitter = require('events').EventEmitter; 30 | 31 | var Mocha = require('mocha'), 32 | fs = require('fs'), 33 | path = require('path'); 34 | require("@babel/register"); 35 | 36 | var mocha = new Mocha(); 37 | var testDir = './tests' 38 | 39 | 40 | // Add each .js file to the mocha instance 41 | fs.readdirSync(testDir).filter(function(file){ 42 | // Only keep the .js files 43 | return file.substr(-3) === '.js'; 44 | 45 | }).forEach(function(file){ 46 | mocha.addFile( 47 | path.join(testDir, file) 48 | ); 49 | }); 50 | 51 | var emitter = new EventEmitter(); 52 | emitter.run = function() { 53 | 54 | var tests = []; 55 | var context = ""; 56 | var separator = ' -> '; 57 | // Run the tests. 58 | try { 59 | var runner = mocha.ui('tdd').run() 60 | .on('test end', function(test) { 61 | // remove comments 62 | var body = test.body.replace(/\/\/.*\n|\/\*.*\*\//g, ''); 63 | // collapse spaces 64 | body = body.replace(/\s+/g,' '); 65 | var obj = { 66 | title: test.title, 67 | context: context.slice(0, -separator.length), 68 | state: test.state, 69 | // body: body, 70 | assertions: analyser(body) 71 | }; 72 | tests.push(obj); 73 | }) 74 | .on('end', function() { 75 | emitter.report = tests; 76 | emitter.emit('done', tests) 77 | }) 78 | .on('suite', function(s) { 79 | context += (s.title + separator); 80 | 81 | }) 82 | .on('suite end', function(s) { 83 | context = context.slice(0, -(s.title.length + separator.length)) 84 | }) 85 | } catch(e) { 86 | throw(e); 87 | } 88 | }; 89 | 90 | module.exports = emitter; 91 | 92 | /* 93 | * Mocha.runner Events: 94 | * can be used to build a better custom report 95 | * 96 | * - `start` execution started 97 | * - `end` execution complete 98 | * - `suite` (suite) test suite execution started 99 | * - `suite end` (suite) all tests (and sub-suites) have finished 100 | * - `test` (test) test execution started 101 | * - `test end` (test) test completed 102 | * - `hook` (hook) hook execution started 103 | * - `hook end` (hook) hook complete 104 | * - `pass` (test) test passed 105 | * - `fail` (test, err) test failed 106 | * - `pending` (test) test pending 107 | */ -------------------------------------------------------------------------------- /tests/1_unit-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * To run the tests on Repl.it, set `NODE_ENV` to `test` 4 | * without quotes in the `.env` file. 5 | * To run the tests in the console, open the terminal 6 | * with [Ctrl + `] (backtick) and run the command `npm run test`. 7 | * 8 | */ 9 | 10 | import Player from '../public/Player.mjs'; 11 | import Collectible from '../public/Collectible.mjs'; 12 | const chai = require('chai'); 13 | const assert = chai.assert; 14 | const { JSDOM } = require('jsdom'); 15 | 16 | suite('Unit Tests', () => { 17 | suiteSetup(() => { 18 | // Mock the DOM for testing and load Solver 19 | return JSDOM.fromFile('./views/index.html') 20 | .then((dom) => { 21 | 22 | global.window = dom.window; 23 | global.document = dom.window.document; 24 | }); 25 | }); 26 | 27 | suite('Collectible class', () => { 28 | test('Collectible class generates a collectible item object.', done => { 29 | const testItem = new Collectible({ x: 100, y: 100, id: Date.now() }); 30 | 31 | assert.isObject(testItem); 32 | done(); 33 | }); 34 | 35 | test('Collectible item object contains x and y coordinates and a unique id.', done => { 36 | const testItem = new Collectible({ x: 100, y: 100, id: Date.now() }); 37 | 38 | assert.typeOf(testItem.x, 'Number'); 39 | assert.typeOf(testItem.y, 'Number'); 40 | assert.exists(testItem.id); 41 | done(); 42 | }); 43 | }); 44 | 45 | suite('Player class', () => { 46 | test('Player class generates a player object.', done => { 47 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 48 | 49 | assert.isObject(testPlayer); 50 | done(); 51 | }); 52 | 53 | test('Player object contains a score, x and y coordinates, and a unique id.', done => { 54 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 55 | 56 | assert.typeOf(testPlayer.x, 'Number'); 57 | assert.typeOf(testPlayer.y, 'Number'); 58 | assert.typeOf(testPlayer.score, 'Number'); 59 | assert.exists(testPlayer.id); 60 | done(); 61 | }); 62 | 63 | test("movePlayer(str, num) adjusts a player's position.", done => { 64 | // Note: Only testing movement along the x axis in case 65 | // the game is a 2D platformer 66 | const testPlayer = new Player({ x: 100, y: 100, score: 0, id: Date.now() }); 67 | testPlayer.movePlayer('right', 5); 68 | const testPos1 = { x: testPlayer.x, y: testPlayer.y } 69 | const expectedPos1 = { x: 105, y: 100 } 70 | 71 | testPlayer.movePlayer('left', 10); 72 | const testPos2 = { x: testPlayer.x, y: testPlayer.y } 73 | const expectedPos2 = { x: 95, y: 100 } 74 | 75 | assert.deepEqual(testPos1, expectedPos1); 76 | assert.deepEqual(testPos2, expectedPos2); 77 | done(); 78 | }); 79 | 80 | test("collision(obj) returns true when a player's avatar collides with a collectible item object.", done => { 81 | const testPlayer = new Player({ x: 100, y: 100, id: Date.now() }); 82 | const testItem = new Collectible({ x: 100, y: 100, value: 1, id: Date.now() }); 83 | 84 | assert.isTrue(testPlayer.collision(testItem)); 85 | done(); 86 | }); 87 | 88 | test("calculateRank(arr) returns the player's rank string.", done => { 89 | const testPlayer1 = new Player({ x: 100, y: 100, id: 1 }); 90 | const testPlayer2 = new Player({ x: 150, y: 150, id: 2 }); 91 | testPlayer1.score = 5; 92 | testPlayer2.score = 3; 93 | const testArr = [ testPlayer1, testPlayer2 ]; 94 | 95 | // Account for possible space 96 | assert.match(testPlayer1.calculateRank(testArr), /Rank\: 1\s?\/\s?2/); 97 | assert.match(testPlayer2.calculateRank(testArr), /Rank\: 2\s?\/\s?2/); 98 | done(); 99 | }); 100 | }); 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /tests/2_functional-tests.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * To run the tests on Repl.it, set `NODE_ENV` to `test` 4 | * without quotes in the `.env` file. 5 | * To run the tests in the console, open the terminal 6 | * with [Ctrl + `] (backtick) and run the command `npm run test`. 7 | * 8 | */ 9 | 10 | const chai = require('chai'); 11 | const assert = chai.assert; 12 | const chaiHttp = require('chai-http'); 13 | const server = require('../server'); 14 | 15 | chai.use(chaiHttp); 16 | 17 | suite('Functional Tests', () => { 18 | 19 | suite('Headers test', () => { 20 | test("Prevent the client from trying to guess / sniff the MIME type.", done => { 21 | chai.request(server) 22 | .get('/') 23 | .end((err, res) => { 24 | assert.deepStrictEqual(res.header['x-content-type-options'], 'nosniff'); 25 | done(); 26 | }); 27 | }); 28 | 29 | test("Prevent cross-site scripting (XSS) attacks.", done => { 30 | chai.request(server) 31 | .get('/') 32 | .end((err, res) => { 33 | assert.deepStrictEqual(res.header['x-xss-protection'], '1; mode=block'); 34 | done(); 35 | }); 36 | }); 37 | 38 | test("Nothing from the website is cached in the client.", done => { 39 | chai.request(server) 40 | .get('/') 41 | .end((err, res) => { 42 | assert.deepStrictEqual(res.header['surrogate-control'], 'no-store'); 43 | assert.deepStrictEqual(res.header['cache-control'], 'no-store, no-cache, must-revalidate, proxy-revalidate'); 44 | assert.deepStrictEqual(res.header['pragma'], 'no-cache'); 45 | assert.deepStrictEqual(res.header['expires'], '0'); 46 | done(); 47 | }); 48 | }); 49 | 50 | test("The headers say that the site is powered by 'PHP 7.4.3'.", done => { 51 | chai.request(server) 52 | .get('/') 53 | .end((err, res) => { 54 | assert.deepStrictEqual(res.header['x-powered-by'], 'PHP 7.4.3'); 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | }); 61 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Secure Real-Time Multiplayer Game 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Secure Real Time Multiplayer Game

19 |
20 |
21 |
22 | 28 | 29 |
30 | 31 | 32 | 33 | --------------------------------------------------------------------------------