diff --git a/WebServers/.gitignore b/WebServers/.gitignore new file mode 100644 index 0000000..81a34ed --- /dev/null +++ b/WebServers/.gitignore @@ -0,0 +1,5 @@ +logs/ +node_modules/ +**/platform_scripts/cmd/*/ +**/platform_scripts/bash/*/ +node.zip \ No newline at end of file diff --git a/WebServers/LICENSE.md b/WebServers/LICENSE.md new file mode 100644 index 0000000..688fa9e --- /dev/null +++ b/WebServers/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2004-2022, Epic Games, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/WebServers/Matchmaker/config.json b/WebServers/Matchmaker/config.json new file mode 100644 index 0000000..9ee3c27 --- /dev/null +++ b/WebServers/Matchmaker/config.json @@ -0,0 +1,6 @@ +{ + "HttpPort": 90, + "UseHTTPS": false, + "MatchmakerPort": 9999, + "LogToFile": true +} \ No newline at end of file diff --git a/WebServers/Matchmaker/matchmaker.js b/WebServers/Matchmaker/matchmaker.js new file mode 100644 index 0000000..a1afca6 --- /dev/null +++ b/WebServers/Matchmaker/matchmaker.js @@ -0,0 +1,295 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +var enableRedirectionLinks = true; +var enableRESTAPI = true; + +const defaultConfig = { + // The port clients connect to the matchmaking service over HTTP + HttpPort: 80, + UseHTTPS: false, + // The matchmaking port the signaling service connects to the matchmaker + MatchmakerPort: 9999, + + // Log to file + LogToFile: true +}; + +// Similar to the Signaling Server (SS) code, load in a config.json file for the MM parameters +const argv = require('yargs').argv; + +var configFile = (typeof argv.configFile != 'undefined') ? argv.configFile.toString() : 'config.json'; +console.log(`configFile ${configFile}`); +const config = require('./modules/config.js').init(configFile, defaultConfig); +console.log("Config: " + JSON.stringify(config, null, '\t')); + +const express = require('express'); +var cors = require('cors'); +const app = express(); +const http = require('http').Server(app); +const fs = require('fs'); +const path = require('path'); +const logging = require('./modules/logging.js'); +logging.RegisterConsoleLogger(); + +if (config.LogToFile) { + logging.RegisterFileLogger('./logs'); +} + +// A list of all the Cirrus server which are connected to the Matchmaker. +var cirrusServers = new Map(); + +// +// Parse command line. +// + +if (typeof argv.HttpPort != 'undefined') { + config.HttpPort = argv.HttpPort; +} +if (typeof argv.MatchmakerPort != 'undefined') { + config.MatchmakerPort = argv.MatchmakerPort; +} + +http.listen(config.HttpPort, () => { + console.log('HTTP listening on *:' + config.HttpPort); +}); + + +if (config.UseHTTPS) { + //HTTPS certificate details + const options = { + key: fs.readFileSync(path.join(__dirname, './certificates/client-key.pem')), + cert: fs.readFileSync(path.join(__dirname, './certificates/client-cert.pem')) + }; + + var https = require('https').Server(options, app); + + //Setup http -> https redirect + console.log('Redirecting http->https'); + app.use(function (req, res, next) { + if (!req.secure) { + if (req.get('Host')) { + var hostAddressParts = req.get('Host').split(':'); + var hostAddress = hostAddressParts[0]; + if (httpsPort != 443) { + hostAddress = `${hostAddress}:${httpsPort}`; + } + return res.redirect(['https://', hostAddress, req.originalUrl].join('')); + } else { + console.error(`unable to get host name from header. Requestor ${req.ip}, url path: '${req.originalUrl}', available headers ${JSON.stringify(req.headers)}`); + return res.status(400).send('Bad Request'); + } + } + next(); + }); + + https.listen(443, function () { + console.log('Https listening on 443'); + }); +} + +// No servers are available so send some simple JavaScript to the client to make +// it retry after a short period of time. +function sendRetryResponse(res) { + res.send(`All ${cirrusServers.size} Cirrus servers are in use. Retrying in 3 seconds. + `); +} + +// Get a Cirrus server if there is one available which has no clients connected. +function getAvailableCirrusServer() { + for (cirrusServer of cirrusServers.values()) { + if (cirrusServer.numConnectedClients === 0 && cirrusServer.ready === true) { + + // Check if we had at least 10 seconds since the last redirect, avoiding the + // chance of redirecting 2+ users to the same SS before they click Play. + // In other words, give the user 10 seconds to click play button the claim the server. + if( cirrusServer.hasOwnProperty('lastRedirect')) { + if( ((Date.now() - cirrusServer.lastRedirect) / 1000) < 10 ) + continue; + } + cirrusServer.lastRedirect = Date.now(); + + return cirrusServer; + } + } + + console.log('WARNING: No empty Cirrus servers are available'); + return undefined; +} + +if(enableRESTAPI) { + // Handle REST signalling server only request. + app.options('/signallingserver', cors()) + app.get('/signallingserver', cors(), (req, res) => { + cirrusServer = getAvailableCirrusServer(); + if (cirrusServer != undefined) { + res.json({ signallingServer: `${cirrusServer.address}:${cirrusServer.port}`}); + console.log(`Returning ${cirrusServer.address}:${cirrusServer.port}`); + } else { + res.json({ signallingServer: '', error: 'No signalling servers available'}); + } + }); +} + +if(enableRedirectionLinks) { + // Handle standard URL. + app.get('/', (req, res) => { + cirrusServer = getAvailableCirrusServer(); + if (cirrusServer != undefined) { + res.redirect(`http://${cirrusServer.address}:${cirrusServer.port}/`); + //console.log(req); + console.log(`Redirect to ${cirrusServer.address}:${cirrusServer.port}`); + } else { + sendRetryResponse(res); + } + }); + + // Handle URL with custom HTML. + app.get('/custom_html/:htmlFilename', (req, res) => { + cirrusServer = getAvailableCirrusServer(); + if (cirrusServer != undefined) { + res.redirect(`http://${cirrusServer.address}:${cirrusServer.port}/custom_html/${req.params.htmlFilename}`); + console.log(`Redirect to ${cirrusServer.address}:${cirrusServer.port}`); + } else { + sendRetryResponse(res); + } + }); +} + +// +// Connection to Cirrus. +// + +const net = require('net'); + +function disconnect(connection) { + console.log(`Ending connection to remote address ${connection.remoteAddress}`); + connection.end(); +} + +const matchmaker = net.createServer((connection) => { + connection.on('data', (data) => { + try { + message = JSON.parse(data); + + if(message) + console.log(`Message TYPE: ${message.type}`); + } catch(e) { + console.log(`ERROR (${e.toString()}): Failed to parse Cirrus information from data: ${data.toString()}`); + disconnect(connection); + return; + } + if (message.type === 'connect') { + // A Cirrus server connects to this Matchmaker server. + cirrusServer = { + address: message.address, + port: message.port, + numConnectedClients: 0, + lastPingReceived: Date.now() + }; + cirrusServer.ready = message.ready === true; + + // Handles disconnects between MM and SS to not add dupes with numConnectedClients = 0 and redirect users to same SS + // Check if player is connected and doing a reconnect. message.playerConnected is a new variable sent from the SS to + // help track whether or not a player is already connected when a 'connect' message is sent (i.e., reconnect). + if(message.playerConnected == true) { + cirrusServer.numConnectedClients = 1; + } + + // Find if we already have a ciruss server address connected to (possibly a reconnect happening) + let server = [...cirrusServers.entries()].find(([key, val]) => val.address === cirrusServer.address && val.port === cirrusServer.port); + + // if a duplicate server with the same address isn't found -- add it to the map as an available server to send users to. + if (!server || server.size <= 0) { + console.log(`Adding connection for ${cirrusServer.address.split(".")[0]} with playerConnected: ${message.playerConnected}`) + cirrusServers.set(connection, cirrusServer); + } else { + console.log(`RECONNECT: cirrus server address ${cirrusServer.address.split(".")[0]} already found--replacing. playerConnected: ${message.playerConnected}`) + var foundServer = cirrusServers.get(server[0]); + + // Make sure to retain the numConnectedClients from the last one before the reconnect to MM + if (foundServer) { + cirrusServers.set(connection, cirrusServer); + console.log(`Replacing server with original with numConn: ${cirrusServer.numConnectedClients}`); + cirrusServers.delete(server[0]); + } else { + cirrusServers.set(connection, cirrusServer); + console.log("Connection not found in Map() -- adding a new one"); + } + } + } else if (message.type === 'streamerConnected') { + // The stream connects to a Cirrus server and so is ready to be used + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServer.ready = true; + console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} ready for use`); + } else { + disconnect(connection); + } + } else if (message.type === 'streamerDisconnected') { + // The stream connects to a Cirrus server and so is ready to be used + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServer.ready = false; + console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} no longer ready for use`); + } else { + disconnect(connection); + } + } else if (message.type === 'clientConnected') { + // A client connects to a Cirrus server. + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServer.numConnectedClients++; + console.log(`Client connected to Cirrus server ${cirrusServer.address}:${cirrusServer.port}`); + } else { + disconnect(connection); + } + } else if (message.type === 'clientDisconnected') { + // A client disconnects from a Cirrus server. + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServer.numConnectedClients--; + console.log(`Client disconnected from Cirrus server ${cirrusServer.address}:${cirrusServer.port}`); + if(cirrusServer.numConnectedClients === 0) { + // this make this server immediately available for a new client + cirrusServer.lastRedirect = 0; + } + } else { + disconnect(connection); + } + } else if (message.type === 'ping') { + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServer.lastPingReceived = Date.now(); + } else { + disconnect(connection); + } + } else { + console.log('ERROR: Unknown data: ' + JSON.stringify(message)); + disconnect(connection); + } + }); + + // A Cirrus server disconnects from this Matchmaker server. + connection.on('error', () => { + cirrusServer = cirrusServers.get(connection); + if(cirrusServer) { + cirrusServers.delete(connection); + console.log(`Cirrus server ${cirrusServer.address}:${cirrusServer.port} disconnected from Matchmaker`); + } else { + console.log(`Disconnected machine that wasn't a registered cirrus server, remote address: ${connection.remoteAddress}`); + } + }); +}); + +matchmaker.listen(config.MatchmakerPort, () => { + console.log('Matchmaker listening on *:' + config.MatchmakerPort); +}); diff --git a/WebServers/Matchmaker/modules/config.js b/WebServers/Matchmaker/modules/config.js new file mode 100644 index 0000000..b599f03 --- /dev/null +++ b/WebServers/Matchmaker/modules/config.js @@ -0,0 +1,49 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +//-- Provides configuration information from file and combines it with default values and command line arguments --// +//-- Hierachy of values: Default Values < Config File < Command Line arguments --// + +const fs = require('fs'); +const path = require('path'); +const argv = require('yargs').argv; + +function initConfig(configFile, defaultConfig){ + defaultConfig = defaultConfig || {}; + + // Using object spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals + let config = {...defaultConfig}; + try{ + let configData = fs.readFileSync(configFile, 'UTF8'); + fileConfig = JSON.parse(configData); + config = {...config, ...fileConfig} + // Update config file with any additional defaults (does not override existing values if default has changed) + fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8'); + } catch(err) { + if (err.code === 'ENOENT') { + console.log("No config file found, writing defaults to log file " + configFile); + fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8'); + } else if (err instanceof SyntaxError) { + console.log(`ERROR: Invalid JSON in ${configFile}, ignoring file config, ${err}`) + } else { + console.log(`ERROR: ${err}`); + } + } + + try{ + //Make a copy of the command line args and remove the unneccessary ones + //The _ value is an array of any elements without a key + let commandLineConfig = {...argv} + delete commandLineConfig._; + delete commandLineConfig.help; + delete commandLineConfig.version; + delete commandLineConfig['$0']; + config = {...config, ...commandLineConfig} + } catch(err) { + console.log(`ERROR: ${err}`); + } + return config; +} + +module.exports = { + init: initConfig +} \ No newline at end of file diff --git a/WebServers/Matchmaker/modules/logging.js b/WebServers/Matchmaker/modules/logging.js new file mode 100644 index 0000000..9482c58 --- /dev/null +++ b/WebServers/Matchmaker/modules/logging.js @@ -0,0 +1,108 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +const fs = require('fs'); +const { Console } = require('console'); + +var loggers=[]; +var logFunctions=[]; +var logColorFunctions=[]; + +console.log = function(msg, ...args) { + logFunctions.forEach((logFunction) => { + logFunction(msg, ...args); + }); +} + +console.logColor = function(color, msg, ...args) { + logColorFunctions.forEach((logColorFunction) => { + logColorFunction(color, msg, ...args); + }); +} + +const AllAttributesOff = '\x1b[0m'; +const BoldOn = '\x1b[1m'; +const Black = '\x1b[30m'; +const Red = '\x1b[31m'; +const Green = '\x1b[32m'; +const Yellow = '\x1b[33m'; +const Blue = '\x1b[34m'; +const Magenta = '\x1b[35m'; +const Cyan = '\x1b[36m'; +const White = '\x1b[37m'; + +/** + * Pad the start of the given number with zeros so it takes up the number of digits. + * e.g. zeroPad(5, 3) = '005' and zeroPad(23, 2) = '23'. + */ +function zeroPad(number, digits) { + let string = number.toString(); + while (string.length < digits) { + string = '0' + string; + } + return string; +} + +/** + * Create a string of the form 'YEAR.MONTH.DATE.HOURS.MINUTES.SECONDS'. + */ +function dateTimeToString() { + let date = new Date(); + return `${date.getFullYear()}.${zeroPad(date.getMonth(), 2)}.${zeroPad(date.getDate(), 2)}.${zeroPad(date.getHours(), 2)}.${zeroPad(date.getMinutes(), 2)}.${zeroPad(date.getSeconds(), 2)}`; +} + +/** + * Create a string of the form 'HOURS.MINUTES.SECONDS.MILLISECONDS'. + */ +function timeToString() { + let date = new Date(); + return `${zeroPad(date.getHours(), 2)}:${zeroPad(date.getMinutes(), 2)}:${zeroPad(date.getSeconds(), 2)}.${zeroPad(date.getMilliseconds(), 3)}`; +} + +function RegisterFileLogger(path) { + if(path == null) + path = './'; + + if (!fs.existsSync(path)) + fs.mkdirSync(path); + + var output = fs.createWriteStream(`./logs/${dateTimeToString()}.log`); + var fileLogger = new Console(output); + logFunctions.push(function(msg, ...args) { + fileLogger.log(`${timeToString()} ${msg}`, ...args); + }); + + logColorFunctions.push(function(color, msg, ...args) { + fileLogger.log(`${timeToString()} ${msg}`, ...args); + }); + loggers.push(fileLogger); +} + +function RegisterConsoleLogger() { + var consoleLogger = new Console(process.stdout, process.stderr) + logFunctions.push(function(msg, ...args) { + consoleLogger.log(`${timeToString()} ${msg}`, ...args); + }); + + logColorFunctions.push(function(color, msg, ...args) { + consoleLogger.log(`${BoldOn}${color}${timeToString()} ${msg}${AllAttributesOff}`, ...args); + }); + loggers.push(consoleLogger); +} + +module.exports = { + //Functions + RegisterFileLogger, + RegisterConsoleLogger, + + //Variables + AllAttributesOff, + BoldOn, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White +} \ No newline at end of file diff --git a/WebServers/Matchmaker/package-lock.json b/WebServers/Matchmaker/package-lock.json new file mode 100644 index 0000000..195ac31 --- /dev/null +++ b/WebServers/Matchmaker/package-lock.json @@ -0,0 +1,1491 @@ +{ + "name": "cirrus-matchmaker", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cirrus-matchmaker", + "version": "0.0.1", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.16.2", + "socket.io": "4.4.1", + "yargs": "17.3.1" + } + }, + "node_modules/@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "node_modules/@types/node": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" + }, + "node_modules/accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "dependencies": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz", + "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "dependencies": { + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==", + "bin": { + "mime": "cli.js" + } + }, + "node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "dependencies": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "dependencies": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body/node_modules/setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "node_modules/socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, + "@types/node": { + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz", + "integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA==" + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "body-parser": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", + "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.1", + "http-errors": "~1.6.2", + "iconv-lite": "0.4.19", + "on-finished": "~2.3.0", + "qs": "6.5.1", + "raw-body": "2.3.2", + "type-is": "~1.6.15" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "requires": { + "ms": "2.1.2" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "engine.io": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.2.tgz", + "integrity": "sha512-v/7eGHxPvO2AWsksyx2PUsQvBafuvqs0jJJQ0FdmJG1b9qIvgSbqDRGwNhfk2XHaTTbTXiC4quRE8Q9nRjsrQQ==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", + "ws": "~8.2.3" + }, + "dependencies": { + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" + } + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.2", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.3", + "qs": "6.5.1", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.1", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + } + } + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", + "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "requires": { + "mime-db": "~1.33.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", + "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.6.0" + } + }, + "qs": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", + "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.2", + "iconv-lite": "0.4.19", + "unpipe": "1.0.0" + }, + "dependencies": { + "depd": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", + "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + }, + "http-errors": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", + "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "requires": { + "depd": "1.1.1", + "inherits": "2.0.3", + "setprototypeof": "1.0.3", + "statuses": ">= 1.3.1 < 2" + } + }, + "setprototypeof": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", + "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==" + } + } +} diff --git a/WebServers/Matchmaker/package.json b/WebServers/Matchmaker/package.json new file mode 100644 index 0000000..df7d59f --- /dev/null +++ b/WebServers/Matchmaker/package.json @@ -0,0 +1,11 @@ +{ + "name": "cirrus-matchmaker", + "version": "0.0.1", + "description": "Cirrus servers connect to the Matchmaker which redirects a browser to the next available Cirrus server", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.16.2", + "socket.io": "4.4.1", + "yargs": "17.3.1" + } +} diff --git a/WebServers/Matchmaker/platform_scripts/bash/common_utils.sh b/WebServers/Matchmaker/platform_scripts/bash/common_utils.sh new file mode 100644 index 0000000..402d44f --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/bash/common_utils.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +function log_msg() { #message + if [ ! -z $VERBOSE ]; then + echo $1 + fi +} + +function print_usage() { + echo " + Usage: + ${0} [--help] [--publicip ] [--turn ] [--stun ] [cirrus options...] + Where: + --help will print this message and stop this script. + --debug will run all scripts with --inspect + --nosudo will run all scripts without \`sudo\` command useful for when run in containers. + --verbose will enable additional logging + --package-manager specify an alternative package manager to apt-get + " + exit 1 +} + +function use_args() { + while(($#)) ; do + case "$1" in + --debug ) IS_DEBUG=1; shift;; + --nosudo ) NO_SUDO=1; shift;; + --verbose ) VERBOSE=1; shift;; + --help ) print_usage;; + * ) echo "Unknown command"; shift;; + esac + done +} + +function call_setup_sh() { + bash "setup.sh" +} + +function start_process() { + if [ ! -z $NO_SUDO ]; then + log_msg "running with sudo removed" + eval $(echo "$@" | sed 's/sudo//g') + else + eval $@ + fi +} + +function get_version() { + local version=$1 + + if command -v $version; then + version=$($@) + fi + + echo $version | sed -E 's/[^0-9.]//g' +} diff --git a/WebServers/Matchmaker/platform_scripts/bash/run.sh b/WebServers/Matchmaker/platform_scripts/bash/run.sh new file mode 100644 index 0000000..180e552 --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/bash/run.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +use_args "$@" +call_setup_sh + +process="${BASH_LOCATION}/node/bin/node matchmaker.js" + +pushd ../.. > /dev/null + +echo "" +echo "Starting Matchmaker use ctrl-c to exit" +echo "-----------------------------------------" +echo "" + +start_process $process + +popd > /dev/null # ../.. + +popd > /dev/null # BASH_SOURCE \ No newline at end of file diff --git a/WebServers/Matchmaker/platform_scripts/bash/setup.sh b/WebServers/Matchmaker/platform_scripts/bash/setup.sh new file mode 100644 index 0000000..3e947f5 --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/bash/setup.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +use_args $@ +# Azure specific fix to allow installing NodeJS from NodeSource +if test -f "/etc/apt/sources.list.d/azure-cli.list"; then + sudo touch /etc/apt/sources.list.d/nodesource.list + sudo touch /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/nodesource.list + sudo chmod 644 /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/azure-cli.list +fi + +function check_version() { #current_version #min_version + #check if same string + if [ -z "$2" ] || [ "$1" = "$2" ]; then + return 0 + fi + + local i current minimum + + IFS="." read -r -a current <<< $1 + IFS="." read -r -a minimum <<< $2 + + # fill empty fields in current with zeros + for ((i=${#current[@]}; i<${#minimum[@]}; i++)) + do + current[i]=0 + done + + for ((i=0; i<${#current[@]}; i++)) + do + if [[ -z ${minimum[i]} ]]; then + # fill empty fields in minimum with zeros + minimum[i]=0 + fi + + if ((10#${current[i]} > 10#${minimum[i]})); then + return 1 + fi + + if ((10#${current[i]} < 10#${minimum[i]})); then + return 2 + fi + done + + # if got this far string is the same once we added missing 0 + return 0 +} + +function check_and_install() { #dep_name #get_version_string #version_min #install_command + local is_installed=0 + + log_msg "Checking for required $1 install" + + local current=$(echo $2 | sed -E 's/[^0-9.]//g') + local minimum=$(echo $3 | sed -E 's/[^0-9.]//g') + + if [ $# -ne 4 ]; then + log_msg "check_and_install expects 4 args (dep_name get_version_string version_min install_command) got $#" + return -1 + fi + + if [ ! -z $current ]; then + log_msg "Current version: $current checking >= $minimum" + check_version "$current" "$minimum" + if [ "$?" -lt 2 ]; then + log_msg "$1 is installed." + return 0 + else + log_msg "Required install of $1 not found installing" + fi + fi + + if [ $is_installed -ne 1 ]; then + echo "$1 installation not found installing..." + + start_process $4 + + if [ $? -ge 1 ]; then + echo "Installation of $1 failed try running `export VERBOSE=1` then run this script again for more details" + exit 1 + fi + + fi +} + +echo "Checking Matchmaker dependencies..." + +# navigate to Matchmaker root +pushd ../.. > /dev/null + +node_version="" +if [[ -f "${BASH_LOCATION}/node/bin/node" ]]; then + node_version=$("${BASH_LOCATION}/node/bin/node" --version) +fi +check_and_install "node" "$node_version" "v16.4.2" "curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz --output node.tar.xz + && tar -xf node.tar.xz + && rm node.tar.xz + && mv node-v*-linux-x64 \"${BASH_LOCATION}/node\"" + +PATH="${BASH_LOCATION}/node/bin:$PATH" +"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install + +popd > /dev/null # Matchmaker + +popd > /dev/null # BASH_SOURCE + +echo "All Matchmaker dependencies up to date." \ No newline at end of file diff --git a/WebServers/Matchmaker/platform_scripts/cmd/run.bat b/WebServers/Matchmaker/platform_scripts/cmd/run.bat new file mode 100644 index 0000000..b94075e --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/cmd/run.bat @@ -0,0 +1,25 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script directory as working directory. +pushd "%~dp0" + +title Matchmaker + +@Rem Run setup to ensure we have node and matchmaker installed. +call setup.bat + +@Rem Move to matchmaker.js directory. +pushd ..\.. + +@Rem Run node server and pass any argument along. +platform_scripts\cmd\node\node.exe matchmaker %* + +@Rem Pop matchmaker.js directory. +popd + +@Rem Pop script directory. +popd + +pause \ No newline at end of file diff --git a/WebServers/Matchmaker/platform_scripts/cmd/setup.bat b/WebServers/Matchmaker/platform_scripts/cmd/setup.bat new file mode 100644 index 0000000..6653bd5 --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/cmd/setup.bat @@ -0,0 +1,17 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Ensure we have NodeJs available for calling. +call setup_node.bat + +@Rem Move to matchmaker.js directory and install its package.json +pushd %~dp0\..\..\ +call platform_scripts\cmd\node\npm install --no-save +popd + +@Rem Pop working directory +popd diff --git a/WebServers/Matchmaker/platform_scripts/cmd/setup_node.bat b/WebServers/Matchmaker/platform_scripts/cmd/setup_node.bat new file mode 100644 index 0000000..cc079e5 --- /dev/null +++ b/WebServers/Matchmaker/platform_scripts/cmd/setup_node.bat @@ -0,0 +1,35 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Name and version of node that we are downloading +SET NodeVersion=v16.4.2 +SET NodeName=node-%NodeVersion%-win-x64 + +@Rem Look for a node directory next to this script +if exist node\ ( + echo Node directory found...skipping install. +) else ( + echo Node directory not found...beginning NodeJS download for Windows. + + @Rem Download nodejs and follow redirects. + curl -L -o ./node.zip "https://nodejs.org/dist/%NodeVersion%/%NodeName%.zip" + + @Rem Unarchive the .zip + tar -xf node.zip + + @Rem Rename the extracted, versioned, directory that contains the NodeJS binaries to simply "node". + ren "%NodeName%\" "node" + + @Rem Delete the downloaded node.zip + del node.zip +) + +@Rem Print node version +echo Node version: & node\node.exe -v + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/README.md b/WebServers/README.md new file mode 100644 index 0000000..0f2c019 --- /dev/null +++ b/WebServers/README.md @@ -0,0 +1,39 @@ +# The new home for the Pixel Streaming servers! +The Pixel Streaming servers and web frontend that was in `Samples/PixelStreaming/WebServers` is now here. + +## Goals + +The goals of this repository are to: + +- Increase the release cadence for the Pixel Streaming servers (to mitigate browser breaking changes sooner). +- Encourage easier contribution of these components by Unreal Engine licensees. +- Facilitate a more standard web release mechanism (e.g. NPM packages or similar... coming soon). +- Grant a permissive license to distribute and modify this code wherever you see fit (MIT licensed). + +## Repository contents + +Reference implementations for the various pieces needed to support a PixelStreaming application: +- SignallingWebServer (Cirrus) +- SFU (Selective Forwarding Unit) +- Matchmaker + +## Versions + +We maintain versions of the servers and frontend that are compatible with existing and in-development version of Unreal Engine. + +:warning: **There are breaking changes between UE versions - so make sure you get the right version**. :warning: + +We maintain the following in branches right now: + +[Master](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/master) (This is our dev branch.) + +[UE5.1](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.1) + +[UE5.0](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE5.0) + +[UE4.27](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.27) + +[UE4.26](https://github.com/EpicGames/PixelStreamingInfrastructure/tree/UE4.26) + +## Legal +© 2004-2022, Epic Games, Inc. Unreal and its logo are Epic’s trademarks or registered trademarks in the US and elsewhere. diff --git a/WebServers/SFU/.dockerignore b/WebServers/SFU/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/WebServers/SFU/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/WebServers/SFU/config.js b/WebServers/SFU/config.js new file mode 100644 index 0000000..1387c88 --- /dev/null +++ b/WebServers/SFU/config.js @@ -0,0 +1,108 @@ +// Parse passed arguments +let passedPublicIP = null; +for(let arg of process.argv){ + if(arg && arg.startsWith("--PublicIP=")){ + let splitArr = arg.split("="); + if(splitArr.length == 2){ + passedPublicIP = splitArr[1]; + console.log("--PublicIP=" + passedPublicIP); + } + } +} + +const config = { + signallingURL: "ws://localhost:8889", + + mediasoup: { + worker: { + rtcMinPort: 40000, + rtcMaxPort: 49999, + logLevel: "debug", + logTags: [ + "info", + "ice", + "dtls", + "rtp", + "srtp", + "rtcp", + "sctp" + // 'rtx', + // 'bwe', + // 'score', + // 'simulcast', + // 'svc' + ], + }, + router: { + mediaCodecs: [ + { + kind: "audio", + mimeType: "audio/opus", + clockRate: 48000, + channels: 2, + }, + { + kind: 'video', + mimeType: 'video/VP8', + clockRate: 90000, + parameters: { + "packetization-mode": 1, + "profile-level-id": "42e01f", + "level-asymmetry-allowed": 1 + } + }, + { + kind: "video", + mimeType: "video/h264", + clockRate: 90000, + parameters: { + "packetization-mode": 1, + "profile-level-id": "42e01f", + "level-asymmetry-allowed": 1 + }, + }, + + ], + }, + + // here you must specify ip addresses to listen on + // some browsers have issues with connecting to ICE on + // localhost so you might have to specify a proper + // private or public ip here. + webRtcTransport: { + listenIps: passedPublicIP != null ? [{ ip: "0.0.0.0", announcedIp: passedPublicIP}] : getLocalListenIps(), + // 100 megabits + initialAvailableOutgoingBitrate: 100_000_000, + }, + }, +} + +function getLocalListenIps() { + const listenIps = [] + if (typeof window === 'undefined') { + const os = require('os') + const networkInterfaces = os.networkInterfaces() + const ips = [] + if (networkInterfaces) { + for (const [key, addresses] of Object.entries(networkInterfaces)) { + addresses.forEach(address => { + if (address.family === 'IPv4') { + listenIps.push({ ip: address.address, announcedIp: null }) + } + /* ignore link-local and other special ipv6 addresses. + * https://www.iana.org/assignments/ipv6-address-space/ipv6-address-space.xhtml + */ + else if (address.family === 'IPv6' && address.address[0] !== 'f') { + listenIps.push({ ip: address.address, announcedIp: null }) + } + }) + } + } + } + if (listenIps.length === 0) { + listenIps.push({ ip: '127.0.0.1', announcedIp: null }) + } + return listenIps +} + +module.exports = config; diff --git a/WebServers/SFU/mediasoup-sdp-bridge/LICENSE b/WebServers/SFU/mediasoup-sdp-bridge/LICENSE new file mode 100644 index 0000000..706f43b --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright © 2020, Iñaki Baz Castillo + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/WebServers/SFU/mediasoup-sdp-bridge/README.md b/WebServers/SFU/mediasoup-sdp-bridge/README.md new file mode 100644 index 0000000..bcf8257 --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/README.md @@ -0,0 +1,182 @@ +# mediasoup-sdp-bridge v3 + +[![][npm-shield-mediasoup-sdp-bridge]][npm-mediasoup-sdp-bridge] +[![][travis-ci-shield-mediasoup-sdp-bridge]][travis-ci-mediasoup-sdp-bridge] + +Node.js library to allow integration of SDP based clients with [mediasoup][mediasoup-website]. + + +## Website and Documentation + +* [mediasoup.org][mediasoup-website] + + +## Support Forum + +* [mediasoup.discourse.group][mediasoup-discourse] + + +## Use-Case Design Proposal + +This section contains a use-case that can serve as usage example to guide design of the internal implementation. + +Within the Node.js server app running mediasoup: + + +### mediasoup receiving media from a remote SDP endpoint + +```typescript +import * as SdpBridge from "mediasoup-sdp-bridge"; +import Signaling from "./my-signaling"; // Our own signaling stuff. + +const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport. + +// Create an SdpEndpoint to handle SDP negotiation with the remote endpoint. +const sdpEndpoint = await SdpBridge.createSdpEndpoint({ + transport: transport, +}); + +// Upon receipt of an SDP Offer from the remote endpoint, apply it. +Signaling.on("sdp-offer", async (sdpOffer: string) => { + // For each media section in the SDP Offer, SdpEndpoint creates a new Producer + // on top of the Transport that was provided. + const producers: Producer[] = await sdpEndpoint.processOffer(sdpOffer); + + // Generate an SDP Answer and reply to the remote endpoint with it. + const sdpAnswer: string = sdpEndpoint.createAnswer(); + + await Signaling.sendAnswer(sdpAnswer); +}); +``` + + +### mediasoup sending media to a remote SDP endpoint + +```typescript +import * as SdpBridge from "mediasoup-sdp-bridge"; +import Signaling from "./my-signaling"; // Our own signaling stuff. + +const transport: Transport = ... // A mediasoup WebRtcTransport or PlainTransport. + +// Create an SdpEndpoint to send media to the remote endpoint. +const sdpEndpoint = await createSdpEndpoint({ + transport: transport, +}); + +// Listen for the "negotiationneeded" event, to send an SDP Offer to the remote +// endpoint. This event is emitted when transport.consume() is called, or when +// a Producer being consumed is closed or paused/resumed. +sdpEndpoint.on("negotiationneeded", () => { + // For each Consumer present in the Transport that was provided, + // SdpEndpoint creates a new media section in the SDP Offer. + const sdpOffer: string = sdpEndpoint.createOffer(); + + // Send the SDP Offer to the remote endpoint. + await Signaling.sendOffer(sdpOffer); +}); + +// Upon receipt of an SDP Answer from the remote endpoint, apply it. +Signaling.on("sdp-answer", async (sdpAnswer: string) => { + await sdpEndpoint.processAnswer(sdpAnswer); +}); + +// Generate remote endpoint's RTP capabilities based on a remote SDP or based +// on handmade capabilities. +const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities( + router.rtpCapabilities, + remoteSdp +); +// or: +const endpointRtpCapabilities = SdpBridge.generateRtpCapabilities( + router.rtpCapabilities, + handmadeRtpCapabilities +); + +// If there were mediasoup Producers already created in the Router, or if a new +// one is created, and we want to consume them in the remote endpoint, tell the +// Transport to consume them. transport.consume() method will trigger the +// "negotiationneeded" event, handled above. +// +// NOTE: By calling consume() method in parallel (without waiting for the +// previous one to complete) we ensure that the "negotiationneeded" event will +// just be emitted once upon completion of all consume() calls, so a single +// SDP Offer/Answer roundtrip will be needed. +transport + .consume({ + producerId: producer1.id, + rtpCapabilities: endpointRtpCapabilities, + }) + .catch((error) => console.error("transport.consume() failed:", error)); + +transport + .consume({ + producerId: producer2.id, + rtpCapabilities: endpointRtpCapabilities, + }) + .catch((error) => console.error("transport.consume() failed:", error)); +``` + + +## Implementation Notes + +### Design limitations + +The initial Use-Case Design Proposal lacks an important detail: it uses an `endpointRtpCapabilities` object, which represents the WebRTC and RTP capabilities of the remote endpoint that will receive media from mediasoup. This *RtpCapabilities* object is assumed to be written either by hand, or obtained from a previous SDP message that somehow might have been obtained from the remote endpoint. It is only *after* having these *RtpCapabilities*, that the SDP Offer/Answer process starts. + +All this, however, goes backwards with the normal flow of the SDP Offer/Answer model. **The remote capabilities should be obtained from the SDP Offer/Answer exchange itself**, not as an unspecified out-of-band mechanism. In theory, how the mediasoup application learns about remote capabilities should come from one of these sources: + +1. An SDP Offer (with *recvonly* or *sendrecv* direction) from a remote endpoint that wants to receive media. + +2. An SDP Answer (with *recvonly* or *sendrecv* direction) from a remote endpoint, in response to an SDP Offer (with *sendonly* or *sendrecv* direction) that the application had previously sent. + +However, in practice both of these options conflict with the current design proposal: + +* (1) is not being considered for now. mediasoup is designed around the assumption that the participant sending media should always be the one starting the connection; thus, the endpoint that will send media is also the one sending the SDP Offer. + +* (2) is the ideal but *it's not possible* with the current design, because the remote capabilities must be already known by the time `sdpEndpoint.createOffer()` is called. + +(Note: Additionally, the *sendrecv* direction is also not considered for now. Both the local application or the remote endpoints are be assumed to be either *sendonly* or *recvonly*.) + + +### Current implementation + +The implementation found in this repo is enough to cover basic usage, but is not complete by any means. Also, it tries to work around the limitations described above, using alternatives that are far from ideal. + +Some notes: + +* Receiving media is the part that works best. It suffices to call `SdpEndpoint.processOffer()`, and this will return one Producer for each media section found in the SDP Offer. + +* Sending media, on the other hand, suffers from the limitation described in (2) above. To work around this, the class `BrowserRtpCapabilities` contains predefined capabilities objects for some of the most common web browsers. These can be used by the application to provide `transport.consume()` with something to work with. + + The obvious drawback to this solution is that the objects in `BrowserRtpCapabilities` must be kept up to date from time to time, in order to accurately represent the actual capabilities of web browsers. To help with this task, there is a handy tool that can be found in the [tools/browser-rtpcapabilities](./tools/browser-rtpcapabilities/) subdirectory. + +* SDP renegotiation is not implemented. The local endpoint cannot make an SDP Re-Offer when the state of the Producers or Consumers changes. + +* There are minor improvements to be done in the implementation. + + - Right now the SdpEndpoint class exports an `SdpEndpoint.addConsumer()` method, which the application uses to provide all Consumers that are created from the corresponding Transport. However, chances are that this is unnecessary: the Transport class already provides an observer event `Transport.observer.on("newconsumer")`, which could be used by the SdpEndpoint to be notified of all new Consumers in its Transport, saving the application the need to provide them explicitly with `addConsumer()`. + + - Some unexpected errors are not handled gracefully, and instead a "BUG" error is logged before the application is forced to exit. These should probably be replaced by a `throw new Error(...)`. + + - Some debug messages are simply commented out to avoid causing too much noise. A proper logging library should be used to allow setting different levels and hiding the less interesting ones. + + - The code hasn't been linted. The default linter rules were left as per the initial commit, but haven't been used yet. For now, the code has just been formatted with the default rules from *Prettier.js*. + + +## Contributors + +* Iñaki Baz Castillo [[website](https://inakibaz.me)|[github](https://github.com/ibc/)] +* Juan Navarro [[github](https://github.com/j1elo)] + + +## License + +[ISC](./LICENSE) + + +[mediasoup-website]: https://mediasoup.org +[mediasoup-discourse]: https://mediasoup.discourse.group +[npm-shield-mediasoup-sdp-bridge]: https://img.shields.io/npm/v/mediasoup-sdp-bridge.svg +[npm-mediasoup-sdp-bridge]: https://npmjs.org/package/mediasoup-sdp-bridge +[travis-ci-shield-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge.svg?branch=master +[travis-ci-mediasoup-sdp-bridge]: https://travis-ci.com/versatica/mediasoup-sdp-bridge diff --git a/WebServers/SFU/mediasoup-sdp-bridge/lib/BrowserRtpCapabilities.js b/WebServers/SFU/mediasoup-sdp-bridge/lib/BrowserRtpCapabilities.js new file mode 100644 index 0000000..efd1a70 --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/lib/BrowserRtpCapabilities.js @@ -0,0 +1,1182 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.safari = exports.chrome = exports.firefox = void 0; +exports.firefox = { + codecs: [ + { + kind: "audio", + mimeType: "audio/opus", + preferredPayloadType: 109, + clockRate: 48000, + channels: 2, + parameters: { + maxplaybackrate: 48000, + stereo: 1, + useinbandfec: 1, + }, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/G722", + preferredPayloadType: 9, + clockRate: 8000, + channels: 1, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMU", + preferredPayloadType: 0, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMA", + preferredPayloadType: 8, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 101, + clockRate: 8000, + channels: 1, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP8", + preferredPayloadType: 120, + clockRate: 90000, + parameters: { + "max-fs": 12288, + "max-fr": 60, + }, + rtcpFeedback: [ + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 124, + clockRate: 90000, + parameters: { + apt: 120, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP9", + preferredPayloadType: 121, + clockRate: 90000, + parameters: { + "max-fs": 12288, + "max-fr": 60, + }, + rtcpFeedback: [ + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 125, + clockRate: 90000, + parameters: { + apt: 121, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 126, + clockRate: 90000, + parameters: { + "profile-level-id": "42e01f", + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + }, + rtcpFeedback: [ + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 127, + clockRate: 90000, + parameters: { + apt: 126, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 97, + clockRate: 90000, + parameters: { + "profile-level-id": "42e01f", + "level-asymmetry-allowed": 1, + }, + rtcpFeedback: [ + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 98, + clockRate: 90000, + parameters: { + apt: 97, + }, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level", + preferredId: 1, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 3, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 3, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + preferredId: 4, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:toffset", + preferredId: 5, + }, + { + kind: "video", + uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + preferredId: 7, + }, + ], +}; +exports.chrome = { + codecs: [ + { + kind: "audio", + mimeType: "audio/opus", + preferredPayloadType: 111, + clockRate: 48000, + channels: 2, + parameters: { + minptime: 10, + useinbandfec: 1, + }, + rtcpFeedback: [ + { + type: "transport-cc", + }, + ], + }, + { + kind: "audio", + mimeType: "audio/ISAC", + preferredPayloadType: 103, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/ISAC", + preferredPayloadType: 104, + clockRate: 32000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/G722", + preferredPayloadType: 9, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMU", + preferredPayloadType: 0, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMA", + preferredPayloadType: 8, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/CN", + preferredPayloadType: 106, + clockRate: 32000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/CN", + preferredPayloadType: 105, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/CN", + preferredPayloadType: 13, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 110, + clockRate: 48000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 112, + clockRate: 32000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 113, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 126, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP8", + preferredPayloadType: 96, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 97, + clockRate: 90000, + parameters: { + apt: 96, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP9", + preferredPayloadType: 98, + clockRate: 90000, + parameters: { + "profile-id": 0, + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 99, + clockRate: 90000, + parameters: { + apt: 98, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP9", + preferredPayloadType: 100, + clockRate: 90000, + parameters: { + "profile-id": 2, + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 101, + clockRate: 90000, + parameters: { + apt: 100, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 102, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + "profile-level-id": "42001f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 121, + clockRate: 90000, + parameters: { + apt: 102, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 127, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 0, + "profile-level-id": "42001f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 120, + clockRate: 90000, + parameters: { + apt: 127, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 125, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + "profile-level-id": "42e01f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 107, + clockRate: 90000, + parameters: { + apt: 125, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 108, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 0, + "profile-level-id": "42e01f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 109, + clockRate: 90000, + parameters: { + apt: 108, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/AV1X", + preferredPayloadType: 35, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 36, + clockRate: 90000, + parameters: { + apt: 35, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/red", + preferredPayloadType: 124, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 119, + clockRate: 90000, + parameters: { + apt: 124, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/ulpfec", + preferredPayloadType: 123, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level", + preferredId: 1, + }, + { + kind: "audio", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + preferredId: 2, + }, + { + kind: "audio", + uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + preferredId: 3, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 4, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + preferredId: 5, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", + preferredId: 6, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:toffset", + preferredId: 14, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + preferredId: 2, + }, + { + kind: "video", + uri: "urn:3gpp:video-orientation", + preferredId: 13, + }, + { + kind: "video", + uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + preferredId: 3, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + preferredId: 12, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type", + preferredId: 11, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/video-timing", + preferredId: 7, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/color-space", + preferredId: 8, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 4, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + preferredId: 5, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", + preferredId: 6, + }, + ], +}; +exports.safari = { + codecs: [ + { + kind: "audio", + mimeType: "audio/opus", + preferredPayloadType: 111, + clockRate: 48000, + channels: 2, + parameters: { + minptime: 10, + useinbandfec: 1, + }, + rtcpFeedback: [ + { + type: "transport-cc", + }, + ], + }, + { + kind: "audio", + mimeType: "audio/ISAC", + preferredPayloadType: 103, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/G722", + preferredPayloadType: 9, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMU", + preferredPayloadType: 0, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/PCMA", + preferredPayloadType: 8, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/CN", + preferredPayloadType: 105, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/CN", + preferredPayloadType: 13, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 110, + clockRate: 48000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 113, + clockRate: 16000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "audio", + mimeType: "audio/telephone-event", + preferredPayloadType: 126, + clockRate: 8000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 96, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + "profile-level-id": "640c1f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 97, + clockRate: 90000, + parameters: { + apt: 96, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 98, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 1, + "profile-level-id": "42e01f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 99, + clockRate: 90000, + parameters: { + apt: 98, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 100, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 0, + "profile-level-id": "640c1f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 101, + clockRate: 90000, + parameters: { + apt: 100, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/H264", + preferredPayloadType: 102, + clockRate: 90000, + parameters: { + "level-asymmetry-allowed": 1, + "packetization-mode": 0, + "profile-level-id": "42e01f", + }, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 127, + clockRate: 90000, + parameters: { + apt: 102, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/VP8", + preferredPayloadType: 104, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [ + { + type: "goog-remb", + }, + { + type: "transport-cc", + }, + { + type: "ccm", + parameter: "fir", + }, + { + type: "nack", + }, + { + type: "nack", + parameter: "pli", + }, + ], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 125, + clockRate: 90000, + parameters: { + apt: 104, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/red", + preferredPayloadType: 106, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/rtx", + preferredPayloadType: 107, + clockRate: 90000, + parameters: { + apt: 106, + }, + rtcpFeedback: [], + }, + { + kind: "video", + mimeType: "video/ulpfec", + preferredPayloadType: 108, + clockRate: 90000, + parameters: {}, + rtcpFeedback: [], + }, + ], + headerExtensions: [ + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:ssrc-audio-level", + preferredId: 1, + }, + { + kind: "audio", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + preferredId: 2, + }, + { + kind: "audio", + uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + preferredId: 3, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 4, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + preferredId: 5, + }, + { + kind: "audio", + uri: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", + preferredId: 6, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:toffset", + preferredId: 14, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + preferredId: 2, + }, + { + kind: "video", + uri: "urn:3gpp:video-orientation", + preferredId: 13, + }, + { + kind: "video", + uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + preferredId: 3, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + preferredId: 12, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/video-content-type", + preferredId: 11, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/video-timing", + preferredId: 7, + }, + { + kind: "video", + uri: "http://www.webrtc.org/experiments/rtp-hdrext/color-space", + preferredId: 8, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:mid", + preferredId: 4, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + preferredId: 5, + }, + { + kind: "video", + uri: "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", + preferredId: 6, + }, + ], +}; +//# sourceMappingURL=BrowserRtpCapabilities.js.map \ No newline at end of file diff --git a/WebServers/SFU/mediasoup-sdp-bridge/lib/SdpUtils.js b/WebServers/SFU/mediasoup-sdp-bridge/lib/SdpUtils.js new file mode 100644 index 0000000..a1feb6f --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/lib/SdpUtils.js @@ -0,0 +1,89 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sdpToSendRtpParameters = exports.sdpToRecvRtpCapabilities = void 0; +const MsRtpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/unifiedPlanUtils")); +const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils")); +const MsOrtc = __importStar(require("mediasoup-client/lib/ortc")); +require("util").inspect.defaultOptions.depth = null; +function sdpToRecvRtpCapabilities(sdpObject, localCaps) { + const caps = MsSdpUtils.extractRtpCapabilities({ + sdpObject, + }); + try { + MsOrtc.validateRtpCapabilities(caps); + } + catch (err) { + console.error("FIXME BUG:", err); + process.exit(1); + } + const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps); + const recvCaps = MsOrtc.getRecvRtpCapabilities(extendedCaps); + { + } + return recvCaps; +} +exports.sdpToRecvRtpCapabilities = sdpToRecvRtpCapabilities; +function sdpToSendRtpParameters(sdpObject, sdpMediaObj, localCaps, kind) { + var _a; + const caps = MsSdpUtils.extractRtpCapabilities({ + sdpObject, + }); + try { + MsOrtc.validateRtpCapabilities(caps); + } + catch (err) { + console.error("FIXME BUG:", err); + process.exit(1); + } + const extendedCaps = MsOrtc.getExtendedRtpCapabilities(caps, localCaps); + const sendParams = MsOrtc.getSendingRemoteRtpParameters(kind, extendedCaps); + // const sdpMediaObj = (sdpObject.media || []).find((m) => m.type === kind) || + // {}; + if ("mid" in sdpMediaObj) { + sendParams.mid = String(sdpMediaObj.mid); + } + else { + sendParams.mid = kind === "audio" ? "0" : "1"; + } + if ("rids" in sdpMediaObj) { + for (const mediaRid of sdpMediaObj.rids) { + (_a = sendParams.encodings) === null || _a === void 0 ? void 0 : _a.push({ rid: mediaRid.id }); + } + } + else { + // sendParams.encodings = MsRtpUtils.getRtpEncodings({ + // sdpObject, + // kind, + // }); + sendParams.encodings = MsRtpUtils.getRtpEncodings({ offerMediaObject: sdpMediaObj }); + } + sendParams.rtcp = { + cname: MsSdpUtils.getCname({ offerMediaObject: sdpMediaObj }), + reducedSize: "rtcpRsize" in sdpMediaObj && sdpMediaObj.rtcpRsize, + mux: "rtcpMux" in sdpMediaObj && sdpMediaObj.rtcpMux, + }; + { + } + return sendParams; +} +exports.sdpToSendRtpParameters = sdpToSendRtpParameters; +//# sourceMappingURL=SdpUtils.js.map \ No newline at end of file diff --git a/WebServers/SFU/mediasoup-sdp-bridge/lib/index.js b/WebServers/SFU/mediasoup-sdp-bridge/lib/index.js new file mode 100644 index 0000000..2725e69 --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/lib/index.js @@ -0,0 +1,198 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateRtpCapabilities2 = exports.generateRtpCapabilities1 = exports.generateRtpCapabilities0 = exports.createSdpEndpoint = exports.SdpEndpoint = void 0; +const MsSdpUtils = __importStar(require("mediasoup-client/lib/handlers/sdp/commonUtils")); +const RemoteSdp_1 = require("mediasoup-client/lib/handlers/sdp/RemoteSdp"); +const SdpTransform = __importStar(require("sdp-transform")); +const uuid_1 = require("uuid"); +const BrowserRtpCapabilities = __importStar(require("./BrowserRtpCapabilities")); +const SdpUtils = __importStar(require("./SdpUtils")); +const MediaSection_1 = require("mediasoup-client/lib/handlers/sdp/MediaSection"); +require("util").inspect.defaultOptions.depth = null; +class SdpEndpoint { + constructor(webRtcTransport, localCaps) { + this.producers = []; + this.producerMedias = []; + this.consumers = []; + this.webRtcTransport = webRtcTransport; + this.transport = webRtcTransport; + this.localCaps = localCaps; + this.sctpMedia = null; + this.consumeData = false; + } + async processOffer(sdpOffer) { + if (this.remoteSdp) { + console.error("[SdpEndpoint.processOffer] ERROR: A remote description was already set"); + return []; + } + this.remoteSdp = sdpOffer; + const remoteSdpObj = SdpTransform.parse(sdpOffer); + await this.webRtcTransport.connect({ + dtlsParameters: MsSdpUtils.extractDtlsParameters({ + sdpObject: remoteSdpObj, + }), + }); + for (const media of remoteSdpObj.media) { + if (media.type == "application") { + this.sctpMedia = media; + console.log("[SdpEndpoint.processOffer] SCTP association received"); + } + else { + if (!("rtp" in media)) { + continue; + } + if (!("direction" in media)) { + continue; + } + if (media.direction !== "sendonly") { + continue; + } + const sendParams = SdpUtils.sdpToSendRtpParameters(remoteSdpObj, media, this.localCaps, media.type); + let producer; + try { + producer = await this.transport.produce({ + kind: media.type, + rtpParameters: sendParams, + paused: false, + }); + } + catch (err) { + console.error("FIXME BUG:", err); + process.exit(1); + } + this.producers.push(producer); + this.producerMedias.push(media); + console.log("[SdpEndpoint.processOffer] mediasoup Producer created, kind: %s, type: %s, paused: %s", producer.kind, producer.type, producer.paused); + } + } + return this.producers; + } + createAnswer() { + if (this.localSdp) { + console.error("[SdpEndpoint.createAnswer] ERROR: A local description was already set"); + return ""; + } + const sdpBuilder = new RemoteSdp_1.RemoteSdp({ + iceParameters: this.webRtcTransport.iceParameters, + iceCandidates: this.webRtcTransport.iceCandidates, + dtlsParameters: this.webRtcTransport.dtlsParameters, + sctpParameters: this.webRtcTransport.sctpParameters, + planB: false, + }); + console.log("[SdpEndpoint.createAnswer] Make 'recvonly' SDP Answer"); + for (let i = 0; i < this.producers.length; i++) { + const sdpMediaObj = this.producerMedias[i]; + const recvParams = this.producers[i].rtpParameters; + sdpBuilder.send({ + offerMediaObject: sdpMediaObj, + reuseMid: undefined, + offerRtpParameters: recvParams, + answerRtpParameters: recvParams, + codecOptions: undefined, + extmapAllowMixed: false, + }); + } + if (this.sctpMedia != null) { + sdpBuilder.sendSctpAssociation({offerMediaObject: this.sctpMedia}); + } + this.localSdp = sdpBuilder.getSdp(); + return this.localSdp; + } + addConsumer(consumer) { + this.consumers.push(consumer); + } + addConsumeData() { + this.consumeData = true; + } + createOffer() { + var _a; + if (this.localSdp) { + console.error("[SdpEndpoint.createOffer] ERROR: A local description was already set"); + return ""; + } + const sdpBuilder = new RemoteSdp_1.RemoteSdp({ + iceParameters: this.webRtcTransport.iceParameters, + iceCandidates: this.webRtcTransport.iceCandidates, + dtlsParameters: this.webRtcTransport.dtlsParameters, + sctpParameters: this.webRtcTransport.sctpParameters, + planB: false, + }); + const sendMsid = uuid_1.v4().substr(0, 8); + console.log("[SdpEndpoint.createOffer] Make 'sendonly' SDP Offer"); + for (let i = 0; i < this.consumers.length; i++) { + const mid = (_a = this.consumers[i].rtpParameters.mid) !== null && _a !== void 0 ? _a : "nomid"; + const kind = this.consumers[i].kind; + const sendParams = this.consumers[i].rtpParameters; + sdpBuilder.receive({ + mid, + kind, + offerRtpParameters: sendParams, + streamId: sendMsid, + trackId: `${sendMsid}-${kind}`, + }); + } + if (this.consumeData) { + sdpBuilder.receiveSctpAssociation(); + } + this.localSdp = sdpBuilder.getSdp(); + return this.localSdp; + } + async processAnswer(sdpAnswer) { + if (this.remoteSdp) { + console.error("[SdpEndpoint.processAnswer] ERROR: A remote description was already set"); + return; + } + this.remoteSdp = sdpAnswer; + const remoteSdpObj = SdpTransform.parse(sdpAnswer); + await this.webRtcTransport.connect({ + dtlsParameters: MsSdpUtils.extractDtlsParameters({ + sdpObject: remoteSdpObj, + }), + }); + { + } + } +} +exports.SdpEndpoint = SdpEndpoint; +function createSdpEndpoint(webRtcTransport, localCaps) { + return new SdpEndpoint(webRtcTransport, localCaps); +} +exports.createSdpEndpoint = createSdpEndpoint; +function generateRtpCapabilities0() { + return BrowserRtpCapabilities.chrome; +} +exports.generateRtpCapabilities0 = generateRtpCapabilities0; +function generateRtpCapabilities1(localCaps, remoteSdp) { + console.error("[SdpEndpoint.generateRtpCapabilities1] BUG: Not implemented"); + process.exit(1); + let caps; + return caps; +} +exports.generateRtpCapabilities1 = generateRtpCapabilities1; +function generateRtpCapabilities2(localCaps, remoteCaps) { + console.error("[SdpEndpoint.generateRtpCapabilities2] BUG: Not implemented"); + process.exit(1); + let caps; + return caps; +} +exports.generateRtpCapabilities2 = generateRtpCapabilities2; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/WebServers/SFU/mediasoup-sdp-bridge/package.json b/WebServers/SFU/mediasoup-sdp-bridge/package.json new file mode 100644 index 0000000..f862047 --- /dev/null +++ b/WebServers/SFU/mediasoup-sdp-bridge/package.json @@ -0,0 +1,27 @@ +{ + "name": "mediasoup-sdp-bridge", + "version": "3.6.5", + "description": "Node.js library to allow integration of SDP based clients with mediasoup", + "contributors": [ + "Iñaki Baz Castillo (https://inakibaz.me)", + "Juan Navarro (https://github.com/j1elo)" + ], + "homepage": "https://mediasoup.org", + "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/versatica/mediasoup-sdp-bridge.git" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + }, + "main": "lib/index.js", + "types": "lib/index.d.ts", + "engines": { + "node": ">=10" + }, + "dependencies": { + "mediasoup-client": "^3.6.41" + } +} diff --git a/WebServers/SFU/package-lock.json b/WebServers/SFU/package-lock.json new file mode 100644 index 0000000..8f97c8a --- /dev/null +++ b/WebServers/SFU/package-lock.json @@ -0,0 +1,371 @@ +{ + "name": "pixelstreaming-sfu", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "pixelstreaming-sfu", + "version": "1.0.0", + "dependencies": { + "mediasoup_prebuilt": "^3.8.4", + "mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge", + "ws": "^7.1.2" + } + }, + "mediasoup-sdp-bridge": { + "version": "3.6.5", + "license": "ISC", + "dependencies": { + "mediasoup-client": "^3.6.41" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "node_modules/@types/node": { + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==" + }, + "node_modules/awaitqueue": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz", + "integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fake-mediastreamtrack": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz", + "integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==", + "dependencies": { + "event-target-shim": "^5.0.1", + "uuid": "^8.1.0" + } + }, + "node_modules/h264-profile-level-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz", + "integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==", + "dependencies": { + "debug": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/mediasoup_prebuilt": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz", + "integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==", + "dependencies": { + "@types/node": "^16.9.1", + "awaitqueue": "^2.3.3", + "debug": "^4.3.2", + "h264-profile-level-id": "^1.0.1", + "netstring": "^0.3.0", + "random-number": "^0.0.9", + "supports-color": "^9.0.2", + "uuid": "^8.3.2" + } + }, + "node_modules/mediasoup-client": { + "version": "3.6.46", + "resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz", + "integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==", + "dependencies": { + "@types/debug": "^4.1.7", + "@types/events": "^3.0.0", + "awaitqueue": "^2.3.3", + "bowser": "^2.11.0", + "debug": "^4.3.2", + "events": "^3.3.0", + "fake-mediastreamtrack": "^1.1.6", + "h264-profile-level-id": "^1.0.1", + "sdp-transform": "^2.14.1", + "supports-color": "^9.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mediasoup" + } + }, + "node_modules/mediasoup-sdp-bridge": { + "resolved": "mediasoup-sdp-bridge", + "link": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/netstring": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz", + "integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI=", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/random-number": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz", + "integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw==" + }, + "node_modules/sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==", + "bin": { + "sdp-verify": "checker.js" + } + }, + "node_modules/supports-color": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz", + "integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "requires": { + "@types/ms": "*" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" + }, + "@types/node": { + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==" + }, + "awaitqueue": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/awaitqueue/-/awaitqueue-2.3.3.tgz", + "integrity": "sha512-RbzQg6VtPUtyErm55iuQLTrBJ2uihy5BKBOEkyBwv67xm5Fn2o/j+Bz+a5BmfSoe2oZ5dcz9Z3fExS8pL+LLhw==" + }, + "bowser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", + "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" + }, + "debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "requires": { + "ms": "2.1.2" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "fake-mediastreamtrack": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/fake-mediastreamtrack/-/fake-mediastreamtrack-1.1.6.tgz", + "integrity": "sha512-lcoO5oPsW57istAsnjvQxNjBEahi18OdUhWfmEewwfPfzNZnji5OXuodQM+VnUPi/1HnQRJ6gBUjbt1TNXrkjQ==", + "requires": { + "event-target-shim": "^5.0.1", + "uuid": "^8.1.0" + } + }, + "h264-profile-level-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/h264-profile-level-id/-/h264-profile-level-id-1.0.1.tgz", + "integrity": "sha512-D3Rln/jKNjKDW5ZTJTK3niSoOGE+pFqPvRHHVgQN3G7umcn/zWGPUo8Q8VpDj16x3hKz++zVviRNRmXu5cpN+Q==", + "requires": { + "debug": "^4.1.1" + } + }, + "mediasoup_prebuilt": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/mediasoup_prebuilt/-/mediasoup_prebuilt-3.8.4.tgz", + "integrity": "sha512-IdPcuT3YTJXNFYAY4JuIy8sZ88qagKPg2dR8d4USR5csTvC+qOq9wAIywO+u2lxLjePHJH+Y8UBM3kKfyU6Uug==", + "requires": { + "@types/node": "^16.9.1", + "awaitqueue": "^2.3.3", + "debug": "^4.3.2", + "h264-profile-level-id": "^1.0.1", + "netstring": "^0.3.0", + "random-number": "^0.0.9", + "supports-color": "^9.0.2", + "uuid": "^8.3.2" + } + }, + "mediasoup-client": { + "version": "3.6.46", + "resolved": "https://registry.npmjs.org/mediasoup-client/-/mediasoup-client-3.6.46.tgz", + "integrity": "sha512-Dv8RxCa1cjSPrKWGf1mnypU5TiQCnrOIy4JpZwwjRQzEtCukCfV1zQabij6BigrtkI+l22ui3fl67Mmm4I0XCA==", + "requires": { + "@types/debug": "^4.1.7", + "@types/events": "^3.0.0", + "awaitqueue": "^2.3.3", + "bowser": "^2.11.0", + "debug": "^4.3.2", + "events": "^3.3.0", + "fake-mediastreamtrack": "^1.1.6", + "h264-profile-level-id": "^1.0.1", + "sdp-transform": "^2.14.1", + "supports-color": "^9.1.0" + } + }, + "mediasoup-sdp-bridge": { + "version": "file:mediasoup-sdp-bridge", + "requires": { + "mediasoup-client": "^3.6.41" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "netstring": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/netstring/-/netstring-0.3.0.tgz", + "integrity": "sha1-ho3FsgxY0/cwVTHUk2jqqr0ZtxI=" + }, + "random-number": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/random-number/-/random-number-0.0.9.tgz", + "integrity": "sha512-ipG3kRCREi/YQpi2A5QGcvDz1KemohovWmH6qGfboVyyGdR2t/7zQz0vFxrfxpbHQgPPdtVlUDaks3aikD1Ljw==" + }, + "sdp-transform": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.14.1.tgz", + "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" + }, + "supports-color": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.1.0.tgz", + "integrity": "sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "ws": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", + "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==", + "requires": {} + } + } +} diff --git a/WebServers/SFU/package.json b/WebServers/SFU/package.json new file mode 100644 index 0000000..754e985 --- /dev/null +++ b/WebServers/SFU/package.json @@ -0,0 +1,23 @@ +{ + "name": "pixelstreaming-sfu", + "version": "1.0.0", + "description": "Reference implementation for a PixelStreaming SFU", + "scripts": { + "start-local": "run-script-os --", + "start-local:windows": ".\\platform_scripts\\cmd\\run.bat", + "start-local:default": "./platform_scripts/bash/run_local.sh", + "start-cloud": "run-script-os --", + "start-cloud:windows": ".\\platform_scripts\\cmd\\run_cloud.bat", + "start-cloud:default": "./platform_scripts/bash/run_cloud.sh", + "start": "run-script-os", + "start:windows": "platform_scripts\\cmd\\node\\node.exe sfu_server.js", + "start:default": "if [ `id -u` -eq 0 ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node sfu_server.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node sfu_server.js\"\nfi\n$process " + + }, + "dependencies": { + "mediasoup-sdp-bridge": "file:mediasoup-sdp-bridge", + "ws": "^7.1.2", + "mediasoup_prebuilt": "^3.8.4", + "run-script-os": "^1.1.6" + } +} diff --git a/WebServers/SFU/platform_scripts/bash/Dockerfile b/WebServers/SFU/platform_scripts/bash/Dockerfile new file mode 100644 index 0000000..7987117 --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/Dockerfile @@ -0,0 +1,25 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +FROM node:latest + +# Make sure Mediasoup requirements are met +RUN apt -y update +RUN apt -y install python3-pip + +# Copy the Selective Forwarding Unit (SFU) to the Docker build context +COPY . /opt/SFU + +# Install the dependencies for the mediasoup server +WORKDIR /opt/SFU +RUN npm update +RUN npm install . + +# Expose TCP port 80 for player WebSocket connections and web server HTTP access +EXPOSE 40000-49999 + +# Expose TCP port 8888 for streamer WebSocket connections +EXPOSE 8889 + +# Set the signalling server as the container's entrypoint +ENTRYPOINT ["/usr/local/bin/node", "/opt/SFU/sfu_server.js"] + diff --git a/WebServers/SFU/platform_scripts/bash/common_utils.sh b/WebServers/SFU/platform_scripts/bash/common_utils.sh new file mode 100644 index 0000000..3dcdb1f --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/common_utils.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +function log_msg() { #message + if [ ! -z $VERBOSE ]; then + echo $1 + fi +} + +function print_usage() { + echo " + Usage: + ${0} [--help] [--publicip ] [sfu options...] + Where: + --help will print this message and stop this script. + --debug will run all scripts with --inspect + --nosudo will run all scripts without \`sudo\` command useful for when run in containers. + --verbose will enable additional logging + --package-manager specify an alternative package manager to apt-get + --publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for + default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the SFU + Other options: stored and passed to the SFU. All parameters printed once the script values are set. + " + exit 1 +} + +function print_parameters() { + echo "" + echo "${0} is running with the following parameters:" + echo "--------------------------------------" + echo "Public IP address : ${publicip}" + echo "SFU command line arguments: ${sfucmd}" + echo "" +} + +function set_start_default_values() { + # publicip and sfucmd are always needed + publicip=$(curl -s https://api.ipify.org) + if [[ -z $publicip ]]; then + publicip="127.0.0.1" + fi + + sfucmd="" +} + +function use_args() { + while(($#)) ; do + case "$1" in + --debug ) IS_DEBUG=1; shift;; + --nosudo ) NO_SUDO=1; shift;; + --verbose ) VERBOSE=1; shift;; + --publicip ) publicip="$2"; shift 2;; + --help ) print_usage;; + * ) echo "Unknown command, adding to SFU command line: $1"; sfucmd+=" $1"; shift;; + esac + done +} + +function call_setup_sh() { + bash "setup.sh" +} + +function start_process() { + if [ ! -z $NO_SUDO ]; then + log_msg "running with sudo removed" + eval $(echo "$@" | sed 's/sudo//g') + else + eval $@ + fi +} + +function get_version() { + local version=$1 + + if command -v $version; then + version=$($@) + fi + + echo $version | sed -E 's/[^0-9.]//g' +} diff --git a/WebServers/SFU/platform_scripts/bash/docker-build.sh b/WebServers/SFU/platform_scripts/bash/docker-build.sh new file mode 100644 index 0000000..546a6bd --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/docker-build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Build docker image for the Selective Forwarding Unit (SFU) + +# When run from SFU/platform_scripts/bash, this uses the SFU directory +# as the build context so the SFU files can be successfully copied into the container image +docker build -t 'mediasoup_sfu:latest' -f ./Dockerfile ../.. + diff --git a/WebServers/SFU/platform_scripts/bash/docker-start.sh b/WebServers/SFU/platform_scripts/bash/docker-start.sh new file mode 100644 index 0000000..4eda05e --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/docker-start.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Start docker container by name using host networking +docker run --name sfu_latest --network host --rm mediasoup_sfu + +# Interactive start example +#docker run --name sfu_latest --network host --rm -it --entrypoint /bin/bash mediasoup_sfu diff --git a/WebServers/SFU/platform_scripts/bash/docker-stop.sh b/WebServers/SFU/platform_scripts/bash/docker-stop.sh new file mode 100644 index 0000000..637eb27 --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/docker-stop.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Stop the docker container +PSID=$(docker ps -a -q --filter="name=sfu_latest") +if [ -z "$PSID" ]; then + echo "Docker SFU is not running, no stopping will be done" + exit 1; +fi +echo "Stopping Mediasoup SFU server ..." +docker stop sfu_latest + diff --git a/WebServers/SFU/platform_scripts/bash/run_cloud.sh b/WebServers/SFU/platform_scripts/bash/run_cloud.sh new file mode 100644 index 0000000..ee24da7 --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/run_cloud.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +set_start_default_values # No server specific defaults +use_args "$@" +call_setup_sh +print_parameters + +process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --" +arguments="--PublicIP=${publicip}" + +# Add arguments passed to script to arguments for executable +arguments+=" ${sfucmd}" + +pushd ../.. > /dev/null + +echo "Running: $process $arguments" +PATH="${BASH_LOCATION}/node/bin:$PATH" +start_process $process $arguments +popd + +popd diff --git a/WebServers/SFU/platform_scripts/bash/run_local.sh b/WebServers/SFU/platform_scripts/bash/run_local.sh new file mode 100644 index 0000000..feb26e0 --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/run_local.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +set_start_default_values # No server specific defaults +use_args "$@" +call_setup_sh + +process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --" + +pushd ../.. > /dev/null + +echo "" +echo "Starting (S)elective (F)orwarding (U)nit use ctrl-c to exit" +echo "-----------------------------------------" +echo "" + +PATH="${BASH_LOCATION}/node/bin:$PATH" +start_process $process + +popd > /dev/null # ../.. + +popd > /dev/null # BASH_SOURCE \ No newline at end of file diff --git a/WebServers/SFU/platform_scripts/bash/setup.sh b/WebServers/SFU/platform_scripts/bash/setup.sh new file mode 100644 index 0000000..06346ac --- /dev/null +++ b/WebServers/SFU/platform_scripts/bash/setup.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +use_args $@ +# Azure specific fix to allow installing NodeJS from NodeSource +if test -f "/etc/apt/sources.list.d/azure-cli.list"; then + sudo touch /etc/apt/sources.list.d/nodesource.list + sudo touch /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/nodesource.list + sudo chmod 644 /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/azure-cli.list +fi + +function check_version() { #current_version #min_version + #check if same string + if [ -z "$2" ] || [ "$1" = "$2" ]; then + return 0 + fi + + local i current minimum + + IFS="." read -r -a current <<< $1 + IFS="." read -r -a minimum <<< $2 + + # fill empty fields in current with zeros + for ((i=${#current[@]}; i<${#minimum[@]}; i++)) + do + current[i]=0 + done + + for ((i=0; i<${#current[@]}; i++)) + do + if [[ -z ${minimum[i]} ]]; then + # fill empty fields in minimum with zeros + minimum[i]=0 + fi + + if ((10#${current[i]} > 10#${minimum[i]})); then + return 1 + fi + + if ((10#${current[i]} < 10#${minimum[i]})); then + return 2 + fi + done + + # if got this far string is the same once we added missing 0 + return 0 +} + +function check_and_install() { #dep_name #get_version_string #version_min #install_command + local is_installed=0 + + log_msg "Checking for required $1 install" + + local current=$(echo $2 | sed -E 's/[^0-9.]//g') + local minimum=$(echo $3 | sed -E 's/[^0-9.]//g') + + if [ $# -ne 4 ]; then + log_msg "check_and_install expects 4 args (dep_name get_version_string version_min install_command) got $#" + return -1 + fi + + if [ ! -z $current ]; then + log_msg "Current version: $current checking >= $minimum" + check_version "$current" "$minimum" + if [ "$?" -lt 2 ]; then + log_msg "$1 is installed." + return 0 + else + log_msg "Required install of $1 not found installing" + fi + fi + + if [ $is_installed -ne 1 ]; then + echo "$1 installation not found installing..." + + start_process $4 + + if [ $? -ge 1 ]; then + echo "Installation of $1 failed try running `export VERBOSE=1` then run this script again for more details" + exit 1 + fi + + fi +} + +echo "Checking Pixel Streaming SFU dependencies." + +# navigate to SFU root +pushd ../.. > /dev/null + +node_version="" +if [[ -f "${BASH_LOCATION}/node/bin/node" ]]; then + node_version=$("${BASH_LOCATION}/node/bin/node" --version) +fi +check_and_install "node" "$node_version" "v16.4.2" "curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz --output node.tar.xz + && tar -xf node.tar.xz + && rm node.tar.xz + && mv node-v*-linux-x64 \"${BASH_LOCATION}/node\"" + +PATH="${BASH_LOCATION}/node/bin:$PATH" +"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install + +popd > /dev/null # SFU + +popd > /dev/null # BASH_SOURCE + +echo "All Pixel Streaming SFU dependencies up to date." \ No newline at end of file diff --git a/WebServers/SFU/platform_scripts/cmd/run_cloud.bat b/WebServers/SFU/platform_scripts/cmd/run_cloud.bat new file mode 100644 index 0000000..6834f75 --- /dev/null +++ b/WebServers/SFU/platform_scripts/cmd/run_cloud.bat @@ -0,0 +1,19 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script directory as working directory. +pushd "%~dp0" + +title SFU + +@Rem Get our public IP if we are running this SFU on the cloud we will need this. +FOR /F "tokens=*" %%g IN ('curl -L -S -s https://api.ipify.org') do (SET PUBLICIP=%%g) + +@Rem Call out run.bat and pass in the Public IP we grabbed earlier. +call run_local.bat --PublicIP=%PUBLICIP% + +@Rem Pop script directory. +popd + +pause \ No newline at end of file diff --git a/WebServers/SFU/platform_scripts/cmd/run_local.bat b/WebServers/SFU/platform_scripts/cmd/run_local.bat new file mode 100644 index 0000000..b182fb1 --- /dev/null +++ b/WebServers/SFU/platform_scripts/cmd/run_local.bat @@ -0,0 +1,25 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script directory as working directory. +pushd "%~dp0" + +title SFU + +@Rem Run setup to ensure we have node and mediasoup installed. +call setup.bat + +@Rem Move to sfu_server.js directory. +pushd ..\.. + +@Rem Run node server and pass any argument along. +platform_scripts\cmd\node\node.exe sfu_server %* + +@Rem Pop sfu_server directory. +popd + +@Rem Pop script directory. +popd + +pause \ No newline at end of file diff --git a/WebServers/SFU/platform_scripts/cmd/setup.bat b/WebServers/SFU/platform_scripts/cmd/setup.bat new file mode 100644 index 0000000..29468f3 --- /dev/null +++ b/WebServers/SFU/platform_scripts/cmd/setup.bat @@ -0,0 +1,17 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Ensure we have NodeJs available for calling. +call setup_node.bat + +@Rem Move to sfu_server.js directory and install its package.json +pushd %~dp0\..\..\ +call platform_scripts\cmd\node\npm install --no-save +popd + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SFU/platform_scripts/cmd/setup_node.bat b/WebServers/SFU/platform_scripts/cmd/setup_node.bat new file mode 100644 index 0000000..cc079e5 --- /dev/null +++ b/WebServers/SFU/platform_scripts/cmd/setup_node.bat @@ -0,0 +1,35 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Name and version of node that we are downloading +SET NodeVersion=v16.4.2 +SET NodeName=node-%NodeVersion%-win-x64 + +@Rem Look for a node directory next to this script +if exist node\ ( + echo Node directory found...skipping install. +) else ( + echo Node directory not found...beginning NodeJS download for Windows. + + @Rem Download nodejs and follow redirects. + curl -L -o ./node.zip "https://nodejs.org/dist/%NodeVersion%/%NodeName%.zip" + + @Rem Unarchive the .zip + tar -xf node.zip + + @Rem Rename the extracted, versioned, directory that contains the NodeJS binaries to simply "node". + ren "%NodeName%\" "node" + + @Rem Delete the downloaded node.zip + del node.zip +) + +@Rem Print node version +echo Node version: & node\node.exe -v + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SFU/sfu_server.js b/WebServers/SFU/sfu_server.js new file mode 100644 index 0000000..20b6557 --- /dev/null +++ b/WebServers/SFU/sfu_server.js @@ -0,0 +1,321 @@ +const config = require('./config'); +const WebSocket = require('ws'); +const mediasoup = require('mediasoup_prebuilt'); +const mediasoupSdp = require('mediasoup-sdp-bridge'); + +let signalServer = null; +let mediasoupRouter; +let streamer = null; +let peers = new Map(); + +function connectSignalling(server) { + console.log("Connecting to Signalling Server at %s", server); + signalServer = new WebSocket(server); + signalServer.addEventListener("open", _ => { console.log(`Connected to signalling server`); }); + signalServer.addEventListener("error", result => { console.log(`Error: ${result.message}`); }); + signalServer.addEventListener("message", result => onSignallingMessage(result.data)); + signalServer.addEventListener("close", result => { + console.log(`Disconnected from signalling server: ${result.code} ${result.reason}`); + console.log("Attempting reconnect to signalling server..."); + setTimeout(()=> { + connectSignalling(server); + }, 2000); + }); +} + +async function onStreamerOffer(sdp) { + console.log("Got offer from streamer"); + + if (streamer != null) { + signalServer.close(1013 /* Try again later */, 'Producer is already connected'); + return; + } + + const transport = await createWebRtcTransport("Streamer"); + const sdpEndpoint = mediasoupSdp.createSdpEndpoint(transport, mediasoupRouter.rtpCapabilities); + const producers = await sdpEndpoint.processOffer(sdp); + const sdpAnswer = sdpEndpoint.createAnswer(); + const answer = { type: "answer", sdp: sdpAnswer }; + + console.log("Sending answer to streamer."); + signalServer.send(JSON.stringify(answer)); + streamer = { transport: transport, producers: producers }; +} + +function getNextStreamerSCTPId() { + if(!streamer){ + throw new TypeError('Cannot generate an SCTP stream id - streamer was null.'); + } + if (!streamer.transport || !streamer.transport.sctpParameters || typeof streamer.transport.sctpParameters.MIS !== 'number') { + throw new TypeError('Streamer was not setup with the following require properties: streamer.transport.sctpParameters.MIS'); + } + const numStreams = streamer.transport.sctpParameters.MIS; + if (!streamer.dataStreamIds){ + streamer.dataStreamIds = Buffer.alloc(numStreams, 0); + } + if (!streamer.nextDataStreamId) { + streamer.nextDataStreamId = 0; + } + + let sctpStreamId; + for (let idx = streamer.nextDataStreamId; idx < streamer.dataStreamIds.length; ++idx) { + sctpStreamId = idx % streamer.dataStreamIds.length; + if (!streamer.dataStreamIds[sctpStreamId]) { + streamer.nextDataStreamId = sctpStreamId + 1; + return sctpStreamId; + } + } + console.error("No available SCTP ids, they are all allocated."); + return -1; +} + +function onStreamerDisconnected() { + console.log("Streamer disconnected"); + disconnectAllPeers(); + + if (streamer != null) { + for (const mediaProducer of streamer.producers) { + mediaProducer.close(); + } + streamer.transport.close(); + streamer = null; + } +} + +async function onPeerConnected(peerId) { + console.log("Player %s joined", peerId); + + if (streamer == null) { + console.log("No streamer connected, ignoring player."); + return; + } + + const transport = await createWebRtcTransport("Peer " + peerId); + const sdpEndpoint = mediasoupSdp.createSdpEndpoint( transport, mediasoupRouter.rtpCapabilities ); + sdpEndpoint.addConsumeData(); // adds the sctp 'application' section to the offer + + // media consumers + let consumers = []; + try { + for (const mediaProducer of streamer.producers) { + const consumer = await transport.consume({ producerId: mediaProducer.id, rtpCapabilities: mediasoupRouter.rtpCapabilities }); + consumer.observer.on("layerschange", function() { console.log("layer changed!", consumer.currentLayers); }); + sdpEndpoint.addConsumer(consumer); + consumers.push(consumer); + } + } catch(err) { + console.error("transport.consume() failed:", err); + return; + } + + const offerSignal = { + type: "offer", + playerId: peerId, + sdp: sdpEndpoint.createOffer(), + sfu: true // indicate we're offering from sfu + }; + + // send offer to peer + signalServer.send(JSON.stringify(offerSignal)); + + const newPeer = { + id: peerId, + transport: transport, + sdpEndpoint: sdpEndpoint, + consumers: consumers + }; + + // add the new peer + peers.set(peerId, newPeer); +} + +async function setupPeerDataChannels(peerId) { + const peer = peers.get(peerId); + if (!peer) { + console.error(`Could not send browser any datachannels for peer=${peerId} because peer was not found.`); + return; + } + + const nextStreamerSCTPStreamId = getNextStreamerSCTPId(); + const nextPeerSCTPStreamId = getNextStreamerSCTPId(); + + console.log(`Attempting streamer SCTP id=${nextStreamerSCTPStreamId}`); + + // streamer data producer (produces data for the peer) + peer.streamerDataProducer = await streamer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextStreamerSCTPStreamId, ordered: true}}); + + console.log(`Attempting peer SCTP id=${nextPeerSCTPStreamId}`); + + // peer data producer (produces data for the streamer) + peer.peerDataProducer = await peer.transport.produceData({label: 'send-datachannel', sctpStreamParameters: {streamId: nextPeerSCTPStreamId, ordered: true}}); + + // peer data consumer (consumes streamer data) + peer.peerDataConsumer = await peer.transport.consumeData({ dataProducerId: peer.streamerDataProducer.id }); + + // streamer data consumer (consumes peer data) + peer.streamerDataConsumer = await streamer.transport.consumeData({ dataProducerId: peer.peerDataProducer.id }); + + const peerSignal = { + type: 'peerDataChannels', + playerId: peerId, + sendStreamId: peer.peerDataProducer.sctpStreamParameters.streamId, + recvStreamId: peer.peerDataConsumer.sctpStreamParameters.streamId + }; + + // Send browser a message with a send/recv data channel SCTP stream id + signalServer.send(JSON.stringify(peerSignal)); + +} + +async function setupStreamerDataChannelsForPeer(peerId) { + + const peer = peers.get(peerId); + if (!peer) { + console.error(`Could not send streamer any datachannels for peer=${peerId} because peer was not found.`); + return; + } + + if(!peer.streamerDataProducer || !peer.streamerDataConsumer){ + console.error(`There was no streamer data producer/consumer setup for peer=${peerId}. Did you make sure to send "dataChannelRequest" first?`); + return; + } + + const streamerSignal = { + type: "streamerDataChannels", + playerId: peerId, + sendStreamId: peer.streamerDataProducer.sctpStreamParameters.streamId, + recvStreamId: peer.streamerDataConsumer.sctpStreamParameters.streamId + }; + + // send streamer a message with a send/recv data channel SCTP stream id + signalServer.send(JSON.stringify(streamerSignal)); +} + +async function onPeerAnswer(peerId, sdp) { + console.log("Got answer from player %s", peerId); + + const consumer = peers.get(peerId); + if (!consumer){ + console.error(`Unable to find player ${peerId}`); + } + else{ + consumer.sdpEndpoint.processAnswer(sdp); + } +} + +function onPeerDisconnected(peerId) { + console.log("Player %s disconnected", peerId); + const peer = peers.get(peerId); + if (peer != null) { + for (consumer of peer.consumers) { + consumer.close(); + } + if (peer.peerDataConsumer) { + peer.peerDataConsumer.close(); + peer.peerDataProducer.close(); + } + if(peer.streamerDataConsumer){ + // Set the streamer sctp id we generated back to zero indicating it can be reused. + if(streamer && streamer.dataStreamIds){ + const allocatedStreamId = peer.streamerDataProducer.sctpStreamParameters.streamId; + const allocatedPeerStreamId = peer.peerDataProducer.sctpStreamParameters.streamId; + streamer.dataStreamIds[allocatedStreamId] = 0; + streamer.dataStreamIds[allocatedPeerStreamId] = 0; + } + peer.streamerDataConsumer.close(); + peer.streamerDataProducer.close(); + } + peer.transport.close(); + } + peers.delete(peerId); +} + +function disconnectAllPeers() { + console.log("Disconnected all players"); + for (const [peerId, peer] of peers) { + onPeerDisconnected(peerId); + } +} + +async function onSignallingMessage(message) { + //console.log(`Got MSG: ${message}`); + const msg = JSON.parse(message); + + if (msg.type == 'offer') { + onStreamerOffer(msg.sdp); + } + else if (msg.type == 'answer') { + onPeerAnswer(msg.playerId, msg.sdp); + } + else if (msg.type == 'playerConnected') { + onPeerConnected(msg.playerId); + } + else if (msg.type == 'playerDisconnected') { + onPeerDisconnected(msg.playerId); + } + else if (msg.type == 'streamerDisconnected') { + onStreamerDisconnected(); + } + else if (msg.type == 'dataChannelRequest') { + setupPeerDataChannels(msg.playerId); + } + else if (msg.type == 'peerDataChannelsReady') { + setupStreamerDataChannelsForPeer(msg.playerId); + } + // todo a new message type for force layer switch (for debugging) + // see: https://mediasoup.org/documentation/v3/mediasoup/api/#consumer-setPreferredLayers + // preferredLayers for debugging to select a particular simulcast layer, looks like { spatialLayer: 2, temporalLayer: 0 } +} + +async function startMediasoup() { + let worker = await mediasoup.createWorker({ + logLevel: config.mediasoup.worker.logLevel, + logTags: config.mediasoup.worker.logTags, + rtcMinPort: config.mediasoup.worker.rtcMinPort, + rtcMaxPort: config.mediasoup.worker.rtcMaxPort, + }); + + worker.on('died', () => { + console.error('mediasoup worker died (this should never happen)'); + process.exit(1); + }); + + const mediaCodecs = config.mediasoup.router.mediaCodecs; + const mediasoupRouter = await worker.createRouter({ mediaCodecs }); + + return mediasoupRouter; +} + +async function createWebRtcTransport(identifier) { + const { + listenIps, + initialAvailableOutgoingBitrate + } = config.mediasoup.webRtcTransport; + + const transport = await mediasoupRouter.createWebRtcTransport({ + listenIps: listenIps, + enableUdp: true, + enableTcp: false, + preferUdp: true, + enableSctp: true, // datachannels + initialAvailableOutgoingBitrate: initialAvailableOutgoingBitrate + }); + + transport.on("icestatechange", (iceState) => { console.log("%s ICE state changed to %s", identifier, iceState); }); + transport.on("iceselectedtuplechange", (iceTuple) => { console.log("%s ICE selected tuple %s", identifier, JSON.stringify(iceTuple)); }); + transport.on("sctpstatechange", (sctpState) => { console.log("%s SCTP state changed to %s", identifier, sctpState); }); + + return transport; +} + +async function main() { + console.log('Starting Mediasoup...'); + console.log("Config = "); + console.log(config); + + mediasoupRouter = await startMediasoup(); + + connectSignalling(config.signallingURL); +} + +main(); diff --git a/WebServers/SignallingWebServer/.dockerignore b/WebServers/SignallingWebServer/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/WebServers/SignallingWebServer/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/WebServers/SignallingWebServer/Public/login.css b/WebServers/SignallingWebServer/Public/login.css new file mode 100644 index 0000000..b22c5d7 --- /dev/null +++ b/WebServers/SignallingWebServer/Public/login.css @@ -0,0 +1,49 @@ +/*Copyright Epic Games, Inc. All Rights Reserved.*/ + +:root { + /*Using colour scheme https://color.adobe.com/TD-Colors---Option-3-color-theme-10394433/*/ + --colour1:#2B3A42; + --colour2:#3F5765; + --colour3:#BDD4DE; + --colour4:#EFEFEF; + --colour5:#FF5035; +} + +form{ + margin: 0px auto; + padding: 1em; + width: 350px; + border-radius: 10px; + border: 1px solid #CCC; + background-color: var(--colour4) +} + +.entry{ + padding: 5px; +} + +label { + display: inline-block; + width: 25%; + text-align: right; +} + +input { + text-indent: 5px; + font-family: verdana,sans-serif; + font-size: 1em; + + width: 65%; + box-sizing: border-box; + border: 1px solid #999; +} + +.button { + margin: 0px auto; + width: 70%; +} + +button { + width: 100%; + font-family: verdana,sans-serif; +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/Public/login.html b/WebServers/SignallingWebServer/Public/login.html new file mode 100644 index 0000000..77bffce --- /dev/null +++ b/WebServers/SignallingWebServer/Public/login.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + Login + + + +
+
+ +
+
+ +
+
+ +
+
+ + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/Public/player.css b/WebServers/SignallingWebServer/Public/player.css new file mode 100644 index 0000000..777921a --- /dev/null +++ b/WebServers/SignallingWebServer/Public/player.css @@ -0,0 +1,559 @@ +/*Copyright Epic Games, Inc. All Rights Reserved.*/ +:root { + /*Using colour scheme https://color.adobe.com/TD-Colors---Option-3-color-theme-10394433/*/ + --colour1:#000000; + --colour2:#FFFFFF; + --colour3:#0585fe; + --colour4:#35b350; + --colour5:#ffab00; + --colour6:#1e1d22; + --colour7:#3c3b40; +} + +body{ + margin: 0px; + background-color: black; + font-family: 'Montserrat', sans-serif; +} + +#playerUI { + width: 100%; + height: 100%; +} + +canvas { + image-rendering: crisp-edges; + position: absolute; +} + +video { + position: absolute; + width: 100%; + height: 100%; +} + +#player{ + width: 100%; + height: 100%; + position: absolute; + background-color: #000; +} + +#videoPlayOverlay{ + position: absolute; + font-size: 1.8em; + width: 100%; + height: 100%; + color: var(--colour2) +} + +/* State for element to be clickable */ +.clickableState{ + align-items: center; + justify-content: center; + display: flex; + cursor: pointer; +} + +/* State for element to show text, this is for informational use*/ +.textDisplayState{ + align-items: center; + justify-content: center; + display: flex; + cursor: pointer; +} + +/* State to hide overlay, WebRTC communication is in progress and or is playing */ +.hiddenState{ + display: none; +} + +#playButton{ + display: inline-block; + height: auto; + z-index: 30; +} + +img#playButton{ + max-width: 241px; + width: 10%; +} + +#freezeFrameOverlay { + background-color: transparent; +} + +.freezeframeBackground { + background-color: #000 !important; +} + +#overlay { + width: 100%; + height: 100%; + z-index: 20; + position: absolute; + color: var(--colour2); + pointer-events: none; + overflow: hidden; +} + +#overlay button { + background-color: var(--colour7); + border: 1px solid var(--colour7); + color: var(--colour2); + position: relative; + width: 3rem; + height: 3rem; + padding: 0.5rem; + text-align: center; +} + +#fullscreen-btn { + padding: 0.6rem !important; +} + +#overlay button:hover { + background-color: var(--colour3); + border: 3px solid var(--colour3); + transition: 0.25s ease; + padding-left: 0.55rem; + padding-top: 0.55rem; +} + +#overlay button:active { + border: 3px solid var(--colour3); + background-color: var(--colour7); + padding-left: 0.55rem; + padding-top: 0.55rem; +} + +#overlay img { + width: 100%; + height: 100%; +} + +.tooltip .tooltiptext { + visibility: hidden; + width: auto; + color: var(--colour2); + text-align: center; + border-radius: 15px; + padding: 0px 10px; + font-family: 'Montserrat', sans-serif; + font-size: 0.75rem; + letter-spacing: 0.75px; + /* Position the tooltip */ + position: absolute; + top: 0; + transform: translateY(25%); + left: 125%; + z-index: 20; +} + +.tooltip:hover .tooltiptext { + visibility: visible; + background-color: var(--colour7); +} + +#connection .tooltiptext { + top: 125%; + transform: translateX(-25%); + left: 0; + z-index: 20; + padding: 5px 10px; +} + +#settings-panel .tooltiptext { + display: block; + top: 125%; + transform: translateX(-50%); + left: 0; + z-index: 20; + padding: 5px 10px; + border: 3px solid var(--colour5); + width: max-content; +} + +#controls { + position: absolute; + top: 2%; + left: 1%; + font-family: 'Michroma', sans-serif; + pointer-events: all; + display: block; +} + +#controls > * { + margin-bottom: 0.5rem; + border-radius: 50%; + display: block; + height: 2rem; + line-height: 1.75rem; + padding: 0.5rem; +} + +#controls #additionalinfo { + text-align: center; + font-family: 'Montserrat', sans-serif; +} + +#unrealengine { + position: absolute; + bottom: 5%; + right: 10%; + font-family: 'Michroma', sans-serif; + pointer-events: all; + width: min-content; +} + +#unrealengine p { + visibility: hidden; + width: 15rem; +} + +#connection { + position: absolute; + bottom: 5%; + left: 10%; + font-family: 'Michroma', sans-serif; + height: 3rem; + width: 3rem; + pointer-events: all; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Old versions of Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome, Edge, Opera and Firefox */ +} + +.panel-wrap { + position: fixed; + top: 0; + bottom: 0; + right: 0; + height: 100%; + min-width: 20vw; + transform: translateX(100%); + transition: .3s ease-out; + pointer-events: all; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + overflow-y: auto; + overflow-x: hidden; + background-color: rgba(30, 29, 34, 0.5) +} + +.panel-wrap-visible { + transform: translateX(0%); +} + +.panel { + color: #eee; + overflow-y: auto; + padding: 1em; +} + +#heading { + display: inline-block; + font-size: 2em; + margin-block-start: 0.67em; + margin-block-end: 0.67em; + margin-inline-start: 0px; + margin-inline-end: 0px; + position: relative; + padding: 0 0 0 2rem; +} + +#close { + margin: 0.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-right: 0.5rem; + font-size: 2em; + float: right; +} + +#close:after { + padding-left: 0.5rem; + display: inline-block; + content: "\00d7"; /* This will render the 'X' */ +} + +#close:hover { + color: var(--colour3); + transition: ease 0.3s; +} + +#content { + margin: 2rem; +} + +.setting { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 0; + margin: 0.5rem 0; +} + +.settings-text{ + margin-right: 2rem; + display: flex; +} + +/*** Toggle Switch styles ***/ +.tgl-switch { + vertical-align: middle; + display: inline-block; + } + + .tgl-switch .tgl { + display:none; + } + + .tgl, .tgl:after, .tgl:before, .tgl *, .tgl *:after, .tgl *:before, .tgl + .tgl-slider { + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + .tgl::-moz-selection, .tgl:after::-moz-selection, .tgl:before::-moz-selection, .tgl *::-moz-selection, .tgl *:after::-moz-selection, .tgl *:before::-moz-selection, .tgl + .tgl-slider::-moz-selection { + background: none; + } + .tgl::selection, .tgl:after::selection, .tgl:before::selection, .tgl *::selection, .tgl *:after::selection, .tgl *:before::selection, .tgl + .tgl-slider::selection { + background: none; + } + + .tgl + .tgl-slider { + outline: 0; + display: block; + width: 40px; + height: 18px; + position: relative; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .tgl + .tgl-slider:after, .tgl + .tgl-slider:before { + position: relative; + display: block; + content: ""; + width: 50%; + height: 100%; + } + .tgl + .tgl-slider:after { + left: 0; + } + .tgl + .tgl-slider:before { + display: none; + } + + .tgl-flat + .tgl-slider { + padding: 2px; + -webkit-transition: all .2s ease; + transition: all .2s ease; + background: var(--colour6); + border: 3px solid var(--colour7); + border-radius: 2em; + } + + .tgl-flat + .tgl-slider:after { + -webkit-transition: all .2s ease; + transition: all .2s ease; + background: var(--colour7); + content: ""; + border-radius: 1em; + } + + .tgl-flat:checked + .tgl-slider { + border: 3px solid var(--colour3); + } + + .tgl-flat:checked + .tgl-slider:after { + left: 50%; + background: var(--colour3); + } + +.subtitle-text { + margin: 0 0 0 1rem; + color: var(--colour5); + position: relative; +} + +.form-group { + padding-top: 4px; + display: grid; + grid-template-columns: 50% 50%; + row-gap: 4px; + padding-right: 10px; + padding-left: 10px; +} + +.form-group label { + color: var(--colour2); + vertical-align: middle; + font-weight: normal; +} + +#stats { + margin-left: 1rem; +} + +#LatencyStats { + margin-left: 1rem; +} + +#hiddenInput { + position: absolute; + left: -10%; /* Although invisible, push off-screen to prevent user interaction. */ + width: 0px; + opacity: 0; +} + +#editTextButton { + position: absolute; + height: 40px; + width: 40px; +} + +.form-group label { + margin-right: 2rem; + min-width: 75%; +} + +input { + text-align: right; +} + +.warning { + box-sizing: border-box; + position: relative; + transform: scale(var(--ggs,1)); + width: 20px; + height: 20px; + border: 2px solid; + border-radius: 40px; + display: none; +} +.warning::after, +.warning::before { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + width: 2px; + background: currentColor; + left: 7px +} +.warning::after { + top: 2px; + height: 8px +} +.warning::before { + height: 2px; + bottom: 2px +} + +/* Flat buttons */ +input[type="button"] { + background-color: transparent; + color: var(--colour2); + font-family: 'Montserrat'; + border: 3px solid var(--colour3); + border-radius: 1rem; + font-size: 0.75rem; + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +input[type="button"]:hover { + background-color: var(--colour3); + transition: ease 0.3s; +} +input[type="button"]:active { + background-color: transparent; +} + +#encoder-params-submit, +#webrtc-params-submit { + text-align: center; +} + +select, +input[type="number"] { + background-color: var(--colour7); + color: var(--colour2); + border: 1px solid var(--colour6); + padding: 0.25rem; + font-family: 'Montserrat'; + border-radius: 0.25rem; +} + +input[type=number]::-webkit-inner-spin-button { + margin-left: 0.5rem; +} + +input[type="number"]:disabled { + padding-right: 0.5rem; + -moz-appearance: textfield; +} + +input[type=number]:disabled::-webkit-inner-spin-button { + display: none; + +} + +#settingsBtn, +#statsBtn { + cursor: pointer; +} + +#streamingVideo { + pointer-events: all; +} + +embed { + border: none; + width: 100%; + height: 100%; +} + +g { + fill: var(--colour2); +} + +object { + pointer-events: none; +} + +#connectionStrength { + fill: var(--colour7); +} + +#minimize { + display: none; +} + +#afkOverlay { + z-index: 999; + background-color: rgba(30, 29, 34, 0.5); + display: inline-block; + height: 100vh; + width: 100vw; + line-height: 100vh; + text-align: center; + overflow: hidden; +} + +#afkOverlay center { + display: inline-block; + line-height: 1.5; + height: 100vh; +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/Public/player.html b/WebServers/SignallingWebServer/Public/player.html new file mode 100644 index 0000000..7121ce5 --- /dev/null +++ b/WebServers/SignallingWebServer/Public/player.html @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + +
+
+

+ + + + + +
+
+ + + + + + + + + + + + + + Not connected +
+
+
+
Settings
+
+
+
+
Enlarge display to fill window
+ +
+
+
Is quality controller?
+ +
+
+
Match viewport resolution
+ +
+
+
Offer To Receive
+ +
+
+
Prefer SFU
+ +
+
+
Use microphone
+ +
+
+
Force mono audio
+ +
+
+
Force TURN
+ +
+
+
Control Scheme
+ +
+ +
+
Hide Browser Cursor
+ +
+
+
Show FPS
+ +
+
+
Request KeyFrame
+ +
+
+
+
Encoder Settings
+
+
+
+ + + + +
+ +
+
+
+ +
+
+
WebRTC Settings
+
+
+
+ + + + + + +
+ +
+
+
+ +
+
+
Stream Settings
+
+
+
+
Player stream
+ +
Player track
+ +
+
+
+
+
+
+
Stream Settings
+
+
+
+
+ +
+
+
+
+
+
+
+
+
Information
+
+
+
+
+
Session Stats
+
+
+
+
+
+
+
+
+
+
Latency Report
+
+ +
+
+
No report yet
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/Public/stresstest.html b/WebServers/SignallingWebServer/Public/stresstest.html new file mode 100644 index 0000000..0bd6ad9 --- /dev/null +++ b/WebServers/SignallingWebServer/Public/stresstest.html @@ -0,0 +1,30 @@ + + + + + + + + + + + +
Total streams: 0
+
+ Max peers: + 5 + +
+
+ Peer creation interval (seconds): + +
+
+ Peer deletion interval (seconds): + +
+
+ + + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/cirrus.js b/WebServers/SignallingWebServer/cirrus.js new file mode 100644 index 0000000..e6d9f6c --- /dev/null +++ b/WebServers/SignallingWebServer/cirrus.js @@ -0,0 +1,936 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +//-- Server side logic. Serves pixel streaming WebRTC-based page, proxies data back to Streamer --// + +var express = require('express'); +var app = express(); + +const fs = require('fs'); +const path = require('path'); +const querystring = require('querystring'); +const bodyParser = require('body-parser'); +const logging = require('./modules/logging.js'); +logging.RegisterConsoleLogger(); + +// Command line argument --configFile needs to be checked before loading the config, all other command line arguments are dealt with through the config object + +const defaultConfig = { + UseFrontend: false, + UseMatchmaker: false, + UseHTTPS: false, + UseAuthentication: false, + LogToFile: true, + LogVerbose: true, + HomepageFile: 'player.html', + AdditionalRoutes: new Map(), + EnableWebserver: true, + MatchmakerAddress: "", + MatchmakerPort: "9999", + PublicIp: "localhost", + HttpPort: 80, + HttpsPort: 443, + StreamerPort: 8888, + SFUPort: 8889, + MaxPlayerCount: -1 +}; + +const argv = require('yargs').argv; +var configFile = (typeof argv.configFile != 'undefined') ? argv.configFile.toString() : path.join(__dirname, 'config.json'); +console.log(`configFile ${configFile}`); +const config = require('./modules/config.js').init(configFile, defaultConfig); + +if (config.LogToFile) { + logging.RegisterFileLogger('./logs'); +} + +console.log("Config: " + JSON.stringify(config, null, '\t')); + +var http = require('http').Server(app); + +if (config.UseHTTPS) { + //HTTPS certificate details + const options = { + key: fs.readFileSync(path.join(__dirname, './certificates/client-key.pem')), + cert: fs.readFileSync(path.join(__dirname, './certificates/client-cert.pem')) + }; + + var https = require('https').Server(options, app); +} + +//If not using authetication then just move on to the next function/middleware +var isAuthenticated = redirectUrl => function (req, res, next) { return next(); } + +if (config.UseAuthentication && config.UseHTTPS) { + var passport = require('passport'); + require('./modules/authentication').init(app); + // Replace the isAuthenticated with the one setup on passport module + isAuthenticated = passport.authenticationMiddleware ? passport.authenticationMiddleware : isAuthenticated +} else if (config.UseAuthentication && !config.UseHTTPS) { + console.error('Trying to use authentication without using HTTPS, this is not allowed and so authentication will NOT be turned on, please turn on HTTPS to turn on authentication'); +} + +const helmet = require('helmet'); +var hsts = require('hsts'); +var net = require('net'); + +var FRONTEND_WEBSERVER = 'https://localhost'; +if (config.UseFrontend) { + var httpPort = 3000; + var httpsPort = 8000; + + //Required for self signed certs otherwise just get an error back when sending request to frontend see https://stackoverflow.com/a/35633993 + process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" + + const httpsClient = require('./modules/httpsClient.js'); + var webRequest = new httpsClient(); +} else { + var httpPort = config.HttpPort; + var httpsPort = config.HttpsPort; +} + +var streamerPort = config.StreamerPort; // port to listen to Streamer connections +var sfuPort = config.SFUPort; + +var matchmakerAddress = '127.0.0.1'; +var matchmakerPort = 9999; +var matchmakerRetryInterval = 5; +var matchmakerKeepAliveInterval = 30; +var maxPlayerCount = -1; + +var gameSessionId; +var userSessionId; +var serverPublicIp; + +// `clientConfig` is send to Streamer and Players +// Example of STUN server setting +// let clientConfig = {peerConnectionOptions: { 'iceServers': [{'urls': ['stun:34.250.222.95:19302']}] }}; +var clientConfig = { type: 'config', peerConnectionOptions: {} }; + +// Parse public server address from command line +// --publicIp +try { + if (typeof config.PublicIp != 'undefined') { + serverPublicIp = config.PublicIp.toString(); + } + + if (typeof config.HttpPort != 'undefined') { + httpPort = config.HttpPort; + } + + if (typeof config.HttpsPort != 'undefined') { + httpsPort = config.HttpsPort; + } + + if (typeof config.StreamerPort != 'undefined') { + streamerPort = config.StreamerPort; + } + + if (typeof config.SFUPort != 'undefined') { + sfuPort = config.SFUPort; + } + + if (typeof config.FrontendUrl != 'undefined') { + FRONTEND_WEBSERVER = config.FrontendUrl; + } + + if (typeof config.peerConnectionOptions != 'undefined') { + clientConfig.peerConnectionOptions = JSON.parse(config.peerConnectionOptions); + console.log(`peerConnectionOptions = ${JSON.stringify(clientConfig.peerConnectionOptions)}`); + } else { + console.log("No peerConnectionConfig") + } + + if (typeof config.MatchmakerAddress != 'undefined') { + matchmakerAddress = config.MatchmakerAddress; + } + + if (typeof config.MatchmakerPort != 'undefined') { + matchmakerPort = config.MatchmakerPort; + } + + if (typeof config.MatchmakerRetryInterval != 'undefined') { + matchmakerRetryInterval = config.MatchmakerRetryInterval; + } + + if (typeof config.MaxPlayerCount != 'undefined') { + maxPlayerCount = config.MaxPlayerCount; + } +} catch (e) { + console.error(e); + process.exit(2); +} + +if (config.UseHTTPS) { + app.use(helmet()); + + app.use(hsts({ + maxAge: 15552000 // 180 days in seconds + })); + + //Setup http -> https redirect + console.log('Redirecting http->https'); + app.use(function (req, res, next) { + if (!req.secure) { + if (req.get('Host')) { + var hostAddressParts = req.get('Host').split(':'); + var hostAddress = hostAddressParts[0]; + if (httpsPort != 443) { + hostAddress = `${hostAddress}:${httpsPort}`; + } + return res.redirect(['https://', hostAddress, req.originalUrl].join('')); + } else { + console.error(`unable to get host name from header. Requestor ${req.ip}, url path: '${req.originalUrl}', available headers ${JSON.stringify(req.headers)}`); + return res.status(400).send('Bad Request'); + } + } + next(); + }); +} + +sendGameSessionData(); + +//Setup the login page if we are using authentication +if(config.UseAuthentication){ + if(config.EnableWebserver) { + app.get('/login', function(req, res){ + res.sendFile(__dirname + '/login.htm'); + }); + } + + // create application/x-www-form-urlencoded parser + var urlencodedParser = bodyParser.urlencoded({ extended: false }) + + //login page form data is posted here + app.post('/login', + urlencodedParser, + passport.authenticate('local', { failureRedirect: '/login' }), + function(req, res){ + //On success try to redirect to the page that they originally tired to get to, default to '/' if no redirect was found + var redirectTo = req.session.redirectTo ? req.session.redirectTo : '/'; + delete req.session.redirectTo; + console.log(`Redirecting to: '${redirectTo}'`); + res.redirect(redirectTo); + } + ); +} + +if(config.EnableWebserver) { + //Setup folders + app.use(express.static(path.join(__dirname, '/Public'))) + app.use('/images', express.static(path.join(__dirname, './images'))) + app.use('/scripts', [isAuthenticated('/login'),express.static(path.join(__dirname, '/scripts'))]); + app.use('/', [isAuthenticated('/login'), express.static(path.join(__dirname, '/custom_html'))]) +} + +try { + for (var property in config.AdditionalRoutes) { + if (config.AdditionalRoutes.hasOwnProperty(property)) { + console.log(`Adding additional routes "${property}" -> "${config.AdditionalRoutes[property]}"`) + app.use(property, [isAuthenticated('/login'), express.static(path.join(__dirname, config.AdditionalRoutes[property]))]); + } + } +} catch (err) { + console.error(`reading config.AdditionalRoutes: ${err}`) +} + +if(config.EnableWebserver) { + + // Request has been sent to site root, send the homepage file + app.get('/', isAuthenticated('/login'), function (req, res) { + homepageFile = (typeof config.HomepageFile != 'undefined' && config.HomepageFile != '') ? config.HomepageFile.toString() : defaultConfig.HomepageFile; + + let pathsToTry = [ path.join(__dirname, homepageFile), path.join(__dirname, '/Public', homepageFile), path.join(__dirname, '/custom_html', homepageFile), homepageFile ]; + + // Try a few paths, see if any resolve to a homepage file the user has set + for(let pathToTry of pathsToTry){ + if(fs.existsSync(pathToTry)){ + // Send the file for browser to display it + res.sendFile(pathToTry); + return; + } + } + + // Catch file doesn't exist, and send back 404 if not + console.error('Unable to locate file ' + homepageFile) + res.status(404).send('Unable to locate file ' + homepageFile); + return; + }); +} + +//Setup http and https servers +http.listen(httpPort, function () { + console.logColor(logging.Green, 'Http listening on *: ' + httpPort); +}); + +if (config.UseHTTPS) { + https.listen(httpsPort, function () { + console.logColor(logging.Green, 'Https listening on *: ' + httpsPort); + }); +} + +console.logColor(logging.Cyan, `Running Cirrus - The Pixel Streaming reference implementation signalling server for Unreal Engine 5.1.`); + +let nextPlayerId = 100; // reserve some player ids +const SFUPlayerId = "1"; // sfu is a special kind of player + +let streamer = null; // WebSocket connected to Streamer +let sfu = null; // WebSocket connected to SFU +let players = new Map(); // playerId <-> player, where player is either a web-browser or a native webrtc player + +function sfuIsConnected() { + return sfu && sfu.readyState == 1; +} + +function logIncoming(sourceName, msgType, msg) { + if (config.LogVerbose) + console.logColor(logging.Blue, "\x1b[37m-> %s\x1b[34m: %s", sourceName, msg); + else + console.logColor(logging.Blue, "\x1b[37m-> %s\x1b[34m: %s", sourceName, msgType); +} + +function logOutgoing(destName, msgType, msg) { + if (config.LogVerbose) + console.logColor(logging.Green, "\x1b[37m<- %s\x1b[32m: %s", destName, msg); + else + console.logColor(logging.Green, "\x1b[37m<- %s\x1b[32m: %s", destName, msgType); +} + +// normal peer to peer signalling goes to streamer. SFU streaming signalling goes to the sfu +function sendMessageToController(msg, skipSFU, skipStreamer = false) { + const rawMsg = JSON.stringify(msg); + if (sfu && sfu.readyState == 1 && !skipSFU) { + logOutgoing("SFU", msg.type, rawMsg); + sfu.send(rawMsg); + } + if (streamer && streamer.readyState == 1 && !skipStreamer) { + logOutgoing("Streamer", msg.type, rawMsg); + streamer.send(rawMsg); + } + + if (!sfu && !streamer) { + console.error("sendMessageToController: No streamer or SFU connected!\nMSG: %s", rawMsg); + } +} + +function sendMessageToPlayer(playerId, msg) { + let player = players.get(playerId); + if (!player) { + console.log(`dropped message ${msg.type} as the player ${playerId} is not found`); + return; + } + const playerName = playerId == SFUPlayerId ? "SFU" : `player ${playerId}`; + const rawMsg = JSON.stringify(msg); + logOutgoing(playerName, msg.type, rawMsg); + player.ws.send(rawMsg); +} + +let WebSocket = require('ws'); +const { URL } = require('url'); + +console.logColor(logging.Green, `WebSocket listening for Streamer connections on :${streamerPort}`) +let streamerServer = new WebSocket.Server({ port: streamerPort, backlog: 1 }); +streamerServer.on('connection', function (ws, req) { + + // Check if we have an already existing connection to a streamer, if so, deny a new streamer connecting. + if(streamer != null){ + /* We send a 1008 because that a "policy violation", which similar enough to what is happening here. */ + ws.close(1008, 'Cirrus supports only 1 streamer being connected, already one connected, so dropping this new connection.'); + console.logColor(logging.Yellow, `Dropping new streamer connection, we already have a connected streamer`); + return; + } + + console.logColor(logging.Green, `Streamer connected: ${req.connection.remoteAddress}`); + sendStreamerConnectedToMatchmaker(); + + ws.on('message', (msgRaw) => { + + var msg; + try { + msg = JSON.parse(msgRaw); + } catch(err) { + console.error(`cannot parse Streamer message: ${msgRaw}\nError: ${err}`); + streamer.close(1008, 'Cannot parse'); + return; + } + + logIncoming("Streamer", msg.type, msgRaw); + + try { + // just send pings back to sender + if (msg.type == 'ping') { + const rawMsg = JSON.stringify({ type: "pong", time: msg.time}); + logOutgoing("Streamer", msg.type, rawMsg); + ws.send(rawMsg); + return; + } + + // Convert incoming playerId to a string if it is an integer, if needed. (We support receiving it as an int or string). + let playerId = msg.playerId; + if (playerId && typeof playerId === 'number') + { + playerId = playerId.toString(); + } + delete msg.playerId; // no need to send it to the player + + if (msg.type == 'offer') { + sendMessageToPlayer(playerId, msg); + } else if (msg.type == 'answer') { + sendMessageToPlayer(playerId, msg); + } else if (msg.type == 'iceCandidate') { + sendMessageToPlayer(playerId, msg); + } else if (msg.type == 'disconnectPlayer') { + let player = players.get(playerId); + if (player) { + player.ws.close(1011 /* internal error */, msg.reason); + } + } else { + console.error(`unsupported Streamer message type: ${msg.type}`); + streamer.close(1008, 'Unsupported message type'); + } + } catch(err) { + console.error(`ERROR: ws.on message error: ${err.message}`); + } + }); + + function onStreamerDisconnected() { + sendStreamerDisconnectedToMatchmaker(); + disconnectAllPlayers(); + if (sfuIsConnected()) { + const msg = { type: "streamerDisconnected" }; + sfu.send(JSON.stringify(msg)); + } + streamer = null; + } + + ws.on('close', function(code, reason) { + console.error(`streamer disconnected: ${code} - ${reason}`); + onStreamerDisconnected(); + }); + + ws.on('error', function(error) { + console.error(`streamer connection error: ${error}`); + onStreamerDisconnected(); + try { + ws.close(1006 /* abnormal closure */, error); + } catch(err) { + console.error(`ERROR: ws.on error: ${err.message}`); + } + }); + + streamer = ws; + + streamer.send(JSON.stringify(clientConfig)); + + if (sfuIsConnected()) { + const msg = { type: "playerConnected", playerId: SFUPlayerId, dataChannel: true, sfu: true }; + streamer.send(JSON.stringify(msg)); + } +}); + +console.logColor(logging.Green, `WebSocket listening for SFU connections on :${sfuPort}`); +let sfuServer = new WebSocket.Server({ port: sfuPort}); +sfuServer.on('connection', function (ws, req) { + // reject if we already have an sfu + if (sfuIsConnected()) { + ws.close(1013, 'Already have SFU'); + return; + } + + players.set(SFUPlayerId, { ws: ws, id: SFUPlayerId }); + + ws.on('message', (msgRaw) => { + var msg; + try { + msg = JSON.parse(msgRaw); + } catch (err) { + console.error(`cannot parse SFU message: ${msgRaw}\nError: ${err}`); + ws.close(1008, 'Cannot parse'); + return; + } + + logIncoming("SFU", msg.type, msgRaw); + + if (msg.type == 'offer') { + // offers from the sfu are for players + const playerId = msg.playerId; + delete msg.playerId; + sendMessageToPlayer(playerId, msg); + } + else if (msg.type == 'answer') { + // answers from the sfu are for the streamer + msg.playerId = SFUPlayerId; + const rawMsg = JSON.stringify(msg); + logOutgoing("Streamer", msg.type, rawMsg); + streamer.send(rawMsg); + } + else if (msg.type == 'streamerDataChannels') { + // sfu is asking streamer to open a data channel for a connected peer + msg.sfuId = SFUPlayerId; + const rawMsg = JSON.stringify(msg); + logOutgoing("Streamer", msg.type, rawMsg); + streamer.send(rawMsg); + } + else if (msg.type == 'peerDataChannels') { + // sfu is telling a peer what stream id to use for a data channel + const playerId = msg.playerId; + delete msg.playerId; + sendMessageToPlayer(playerId, msg); + // remember the player has a data channel + const player = players.get(playerId); + player.datachannel = true; + } + }); + + ws.on('close', function(code, reason) { + console.error(`SFU disconnected: ${code} - ${reason}`); + sfu = null; + disconnectSFUPlayer(); + }); + + ws.on('error', function(error) { + console.error(`SFU connection error: ${error}`); + sfu = null; + disconnectSFUPlayer(); + try { + ws.close(1006 /* abnormal closure */, error); + } catch(err) { + console.error(`ERROR: ws.on error: ${err.message}`); + } + }); + + sfu = ws; + console.logColor(logging.Green, `SFU (${req.connection.remoteAddress}) connected `); + + if (streamer && streamer.readyState == 1) { + const msg = { type: "playerConnected", playerId: SFUPlayerId, dataChannel: true, sfu: true }; + streamer.send(JSON.stringify(msg)); + } +}); + +let playerCount = 0; + +console.logColor(logging.Green, `WebSocket listening for Players connections on :${httpPort}`); +let playerServer = new WebSocket.Server({ server: config.UseHTTPS ? https : http}); +playerServer.on('connection', function (ws, req) { + // Reject connection if streamer is not connected + if (!streamer || streamer.readyState != 1 /* OPEN */) { + ws.close(1013 /* Try again later */, 'Streamer is not connected'); + return; + } + + var url = require('url'); + const parsedUrl = url.parse(req.url); + const urlParams = new URLSearchParams(parsedUrl.search); + const preferSFU = urlParams.has('preferSFU') && urlParams.get('preferSFU') !== 'false'; + const skipSFU = !preferSFU; + const skipStreamer = preferSFU && sfu; + + if(preferSFU && !sfu) { + ws.send(JSON.stringify({ type: "warning", warning: "Even though ?preferSFU was specified, there is currently no SFU connected." })); + } + + if(playerCount + 1 > maxPlayerCount && maxPlayerCount !== -1) + { + console.logColor(logging.Red, `new connection would exceed number of allowed concurrent connections. Max: ${maxPlayerCount}, Current ${playerCount}`); + ws.close(1013, `too many connections. max: ${maxPlayerCount}, current: ${playerCount}`); + return; + } + + ++playerCount; + let playerId = (++nextPlayerId).toString(); + console.logColor(logging.Green, `player ${playerId} (${req.connection.remoteAddress}) connected`); + players.set(playerId, { ws: ws, id: playerId }); + + function sendPlayersCount() { + let playerCountMsg = JSON.stringify({ type: 'playerCount', count: players.size }); + for (let p of players.values()) { + p.ws.send(playerCountMsg); + } + } + + ws.on('message', (msgRaw) =>{ + + var msg; + try { + msg = JSON.parse(msgRaw); + } catch (err) { + console.error(`cannot parse player ${playerId} message: ${msgRaw}\nError: ${err}`); + ws.close(1008, 'Cannot parse'); + return; + } + + if(!msg || !msg.type) + { + console.error(`Cannot parse message ${msgRaw}`); + return; + } + + logIncoming(`player ${playerId}`, msg.type, msgRaw); + + if (msg.type == 'offer') { + msg.playerId = playerId; + sendMessageToController(msg, skipSFU); + } else if (msg.type == 'answer') { + msg.playerId = playerId; + sendMessageToController(msg, skipSFU, skipStreamer); + } else if (msg.type == 'iceCandidate') { + msg.playerId = playerId; + sendMessageToController(msg, skipSFU, skipStreamer); + } else if (msg.type == 'stats') { + console.log(`player ${playerId}: stats\n${msg.data}`); + } else if (msg.type == "dataChannelRequest") { + msg.playerId = playerId; + sendMessageToController(msg, skipSFU, true); + } else if (msg.type == "peerDataChannelsReady") { + msg.playerId = playerId; + sendMessageToController(msg, skipSFU, true); + } + else { + console.error(`player ${playerId}: unsupported message type: ${msg.type}`); + ws.close(1008, 'Unsupported message type'); + return; + } + }); + + function onPlayerDisconnected() { + try { + --playerCount; + const player = players.get(playerId); + if (player.datachannel) { + // have to notify the streamer that the datachannel can be closed + sendMessageToController({ type: 'playerDisconnected', playerId: playerId }, true, false); + } + players.delete(playerId); + sendMessageToController({ type: 'playerDisconnected', playerId: playerId }, skipSFU); + sendPlayerDisconnectedToFrontend(); + sendPlayerDisconnectedToMatchmaker(); + sendPlayersCount(); + } catch(err) { + console.logColor(logging.Red, `ERROR:: onPlayerDisconnected error: ${err.message}`); + } + } + + ws.on('close', function(code, reason) { + console.logColor(logging.Yellow, `player ${playerId} connection closed: ${code} - ${reason}`); + onPlayerDisconnected(); + }); + + ws.on('error', function(error) { + console.error(`player ${playerId} connection error: ${error}`); + ws.close(1006 /* abnormal closure */, error); + onPlayerDisconnected(); + + console.logColor(logging.Red, `Trying to reconnect...`); + reconnect(); + }); + + sendPlayerConnectedToFrontend(); + sendPlayerConnectedToMatchmaker(); + + ws.send(JSON.stringify(clientConfig)); + + sendMessageToController({ type: "playerConnected", playerId: playerId, dataChannel: true, sfu: false }, skipSFU, skipStreamer); + sendPlayersCount(); +}); + +function disconnectAllPlayers(code, reason) { + console.log("killing all players"); + let clone = new Map(players); + for (let player of clone.values()) { + if (player.id != SFUPlayerId) { // dont dc the sfu + player.ws.close(code, reason); + } + } +} + +function disconnectSFUPlayer() { + console.log("disconnecting SFU from streamer"); + if(players.has(SFUPlayerId)) { + players.get(SFUPlayerId).ws.close(4000, "SFU Disconnected"); + players.delete(SFUPlayerId); + } + sendMessageToController({ type: 'playerDisconnected', playerId: SFUPlayerId }, true, false); +} + +/** + * Function that handles the connection to the matchmaker. + */ + +if (config.UseMatchmaker) { + var matchmaker = new net.Socket(); + + matchmaker.on('connect', function() { + console.log(`Cirrus connected to Matchmaker ${matchmakerAddress}:${matchmakerPort}`); + + // message.playerConnected is a new variable sent from the SS to help track whether or not a player + // is already connected when a 'connect' message is sent (i.e., reconnect). This happens when the MM + // and the SS get disconnected unexpectedly (was happening often at scale for some reason). + var playerConnected = false; + + // Set the playerConnected flag to tell the MM if there is already a player active (i.e., don't send a new one here) + if( players && players.size > 0) { + playerConnected = true; + } + + // Add the new playerConnected flag to the message body to the MM + message = { + type: 'connect', + address: typeof serverPublicIp === 'undefined' ? '127.0.0.1' : serverPublicIp, + port: httpPort, + ready: streamer && streamer.readyState === 1, + playerConnected: playerConnected + }; + + matchmaker.write(JSON.stringify(message)); + }); + + matchmaker.on('error', (err) => { + console.log(`Matchmaker connection error ${JSON.stringify(err)}`); + }); + + matchmaker.on('end', () => { + console.log('Matchmaker connection ended'); + }); + + matchmaker.on('close', (hadError) => { + console.logColor(logging.Blue, 'Setting Keep Alive to true'); + matchmaker.setKeepAlive(true, 60000); // Keeps it alive for 60 seconds + + console.log(`Matchmaker connection closed (hadError=${hadError})`); + + reconnect(); + }); + + // Attempt to connect to the Matchmaker + function connect() { + matchmaker.connect(matchmakerPort, matchmakerAddress); + } + + // Try to reconnect to the Matchmaker after a given period of time + function reconnect() { + console.log(`Try reconnect to Matchmaker in ${matchmakerRetryInterval} seconds`) + setTimeout(function() { + connect(); + }, matchmakerRetryInterval * 1000); + } + + function registerMMKeepAlive() { + setInterval(function() { + message = { + type: 'ping' + }; + matchmaker.write(JSON.stringify(message)); + }, matchmakerKeepAliveInterval * 1000); + } + + connect(); + registerMMKeepAlive(); +} + +//Keep trying to send gameSessionId in case the server isn't ready yet +function sendGameSessionData() { + //If we are not using the frontend web server don't try and make requests to it + if (!config.UseFrontend) + return; + webRequest.get(`${FRONTEND_WEBSERVER}/server/requestSessionId`, + function (response, body) { + if (response.statusCode === 200) { + gameSessionId = body; + console.log('SessionId: ' + gameSessionId); + } + else { + console.error('Status code: ' + response.statusCode); + console.error(body); + } + }, + function (err) { + //Repeatedly try in cases where the connection timed out or never connected + if (err.code === "ECONNRESET") { + //timeout + sendGameSessionData(); + } else if (err.code === 'ECONNREFUSED') { + console.error('Frontend server not running, unable to setup game session'); + } else { + console.error(err); + } + }); +} + +function sendUserSessionData(serverPort) { + //If we are not using the frontend web server don't try and make requests to it + if (!config.UseFrontend) + return; + webRequest.get(`${FRONTEND_WEBSERVER}/server/requestUserSessionId?gameSessionId=${gameSessionId}&serverPort=${serverPort}&appName=${querystring.escape(clientConfig.AppName)}&appDescription=${querystring.escape(clientConfig.AppDescription)}${(typeof serverPublicIp === 'undefined' ? '' : '&serverHost=' + serverPublicIp)}`, + function (response, body) { + if (response.statusCode === 410) { + sendUserSessionData(serverPort); + } else if (response.statusCode === 200) { + userSessionId = body; + console.log('UserSessionId: ' + userSessionId); + } else { + console.error('Status code: ' + response.statusCode); + console.error(body); + } + }, + function (err) { + //Repeatedly try in cases where the connection timed out or never connected + if (err.code === "ECONNRESET") { + //timeout + sendUserSessionData(serverPort); + } else if (err.code === 'ECONNREFUSED') { + console.error('Frontend server not running, unable to setup user session'); + } else { + console.error(err); + } + }); +} + +function sendServerDisconnect() { + //If we are not using the frontend web server don't try and make requests to it + if (!config.UseFrontend) + return; + try { + webRequest.get(`${FRONTEND_WEBSERVER}/server/serverDisconnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`, + function (response, body) { + if (response.statusCode === 200) { + console.log('serverDisconnected acknowledged by Frontend'); + } else { + console.error('Status code: ' + response.statusCode); + console.error(body); + } + }, + function (err) { + //Repeatedly try in cases where the connection timed out or never connected + if (err.code === "ECONNRESET") { + //timeout + sendServerDisconnect(); + } else if (err.code === 'ECONNREFUSED') { + console.error('Frontend server not running, unable to setup user session'); + } else { + console.error(err); + } + }); + } catch(err) { + console.logColor(logging.Red, `ERROR::: sendServerDisconnect error: ${err.message}`); + } +} + +function sendPlayerConnectedToFrontend() { + //If we are not using the frontend web server don't try and make requests to it + if (!config.UseFrontend) + return; + try { + webRequest.get(`${FRONTEND_WEBSERVER}/server/clientConnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`, + function (response, body) { + if (response.statusCode === 200) { + console.log('clientConnected acknowledged by Frontend'); + } else { + console.error('Status code: ' + response.statusCode); + console.error(body); + } + }, + function (err) { + //Repeatedly try in cases where the connection timed out or never connected + if (err.code === "ECONNRESET") { + //timeout + sendPlayerConnectedToFrontend(); + } else if (err.code === 'ECONNREFUSED') { + console.error('Frontend server not running, unable to setup game session'); + } else { + console.error(err); + } + }); + } catch(err) { + console.logColor(logging.Red, `ERROR::: sendPlayerConnectedToFrontend error: ${err.message}`); + } +} + +function sendPlayerDisconnectedToFrontend() { + //If we are not using the frontend web server don't try and make requests to it + if (!config.UseFrontend) + return; + try { + webRequest.get(`${FRONTEND_WEBSERVER}/server/clientDisconnected?gameSessionId=${gameSessionId}&appName=${querystring.escape(clientConfig.AppName)}`, + function (response, body) { + if (response.statusCode === 200) { + console.log('clientDisconnected acknowledged by Frontend'); + } + else { + console.error('Status code: ' + response.statusCode); + console.error(body); + } + }, + function (err) { + //Repeatedly try in cases where the connection timed out or never connected + if (err.code === "ECONNRESET") { + //timeout + sendPlayerDisconnectedToFrontend(); + } else if (err.code === 'ECONNREFUSED') { + console.error('Frontend server not running, unable to setup game session'); + } else { + console.error(err); + } + }); + } catch(err) { + console.logColor(logging.Red, `ERROR::: sendPlayerDisconnectedToFrontend error: ${err.message}`); + } +} + +function sendStreamerConnectedToMatchmaker() { + if (!config.UseMatchmaker) + return; + try { + message = { + type: 'streamerConnected' + }; + matchmaker.write(JSON.stringify(message)); + } catch (err) { + console.logColor(logging.Red, `ERROR sending streamerConnected: ${err.message}`); + } +} + +function sendStreamerDisconnectedToMatchmaker() { + if (!config.UseMatchmaker) + { + return; + } + + try { + message = { + type: 'streamerDisconnected' + }; + matchmaker.write(JSON.stringify(message)); + } catch (err) { + console.logColor(logging.Red, `ERROR sending streamerDisconnected: ${err.message}`); + } +} + +// The Matchmaker will not re-direct clients to this Cirrus server if any client +// is connected. +function sendPlayerConnectedToMatchmaker() { + if (!config.UseMatchmaker) + return; + try { + message = { + type: 'clientConnected' + }; + matchmaker.write(JSON.stringify(message)); + } catch (err) { + console.logColor(logging.Red, `ERROR sending clientConnected: ${err.message}`); + } +} + +// The Matchmaker is interested when nobody is connected to a Cirrus server +// because then it can re-direct clients to this re-cycled Cirrus server. +function sendPlayerDisconnectedToMatchmaker() { + if (!config.UseMatchmaker) + return; + try { + message = { + type: 'clientDisconnected' + }; + matchmaker.write(JSON.stringify(message)); + } catch (err) { + console.logColor(logging.Red, `ERROR sending clientDisconnected: ${err.message}`); + } +} diff --git a/WebServers/SignallingWebServer/config.json b/WebServers/SignallingWebServer/config.json new file mode 100644 index 0000000..073d0c2 --- /dev/null +++ b/WebServers/SignallingWebServer/config.json @@ -0,0 +1,19 @@ +{ + "UseFrontend": false, + "UseMatchmaker": false, + "UseHTTPS": false, + "UseAuthentication": false, + "LogToFile": true, + "LogVerbose": true, + "HomepageFile": "player.html", + "AdditionalRoutes": {}, + "EnableWebserver": true, + "MatchmakerAddress": "", + "MatchmakerPort": "9999", + "PublicIp": "localhost", + "HttpPort": 9888, + "HttpsPort": 443, + "StreamerPort": 8888, + "SFUPort": 8889, + "MaxPlayerCount": -1 +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/images/Info.svg b/WebServers/SignallingWebServer/images/Info.svg new file mode 100644 index 0000000..afe2365 --- /dev/null +++ b/WebServers/SignallingWebServer/images/Info.svg @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/images/Maximize.svg b/WebServers/SignallingWebServer/images/Maximize.svg new file mode 100644 index 0000000..ae7952e --- /dev/null +++ b/WebServers/SignallingWebServer/images/Maximize.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/images/Minimize.svg b/WebServers/SignallingWebServer/images/Minimize.svg new file mode 100644 index 0000000..3777ee6 --- /dev/null +++ b/WebServers/SignallingWebServer/images/Minimize.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/images/Play.png b/WebServers/SignallingWebServer/images/Play.png new file mode 100644 index 0000000..0146c79 Binary files /dev/null and b/WebServers/SignallingWebServer/images/Play.png differ diff --git a/WebServers/SignallingWebServer/images/Settings.svg b/WebServers/SignallingWebServer/images/Settings.svg new file mode 100644 index 0000000..8df5c54 --- /dev/null +++ b/WebServers/SignallingWebServer/images/Settings.svg @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/WebServers/SignallingWebServer/images/favicon-16x16.png b/WebServers/SignallingWebServer/images/favicon-16x16.png new file mode 100644 index 0000000..90dfb93 Binary files /dev/null and b/WebServers/SignallingWebServer/images/favicon-16x16.png differ diff --git a/WebServers/SignallingWebServer/images/favicon-32x32.png b/WebServers/SignallingWebServer/images/favicon-32x32.png new file mode 100644 index 0000000..87bd731 Binary files /dev/null and b/WebServers/SignallingWebServer/images/favicon-32x32.png differ diff --git a/WebServers/SignallingWebServer/images/favicon-96x96.png b/WebServers/SignallingWebServer/images/favicon-96x96.png new file mode 100644 index 0000000..ab2064a Binary files /dev/null and b/WebServers/SignallingWebServer/images/favicon-96x96.png differ diff --git a/WebServers/SignallingWebServer/images/favicon.ico b/WebServers/SignallingWebServer/images/favicon.ico new file mode 100644 index 0000000..0b46e98 Binary files /dev/null and b/WebServers/SignallingWebServer/images/favicon.ico differ diff --git a/WebServers/SignallingWebServer/modules/authentication/db/index.js b/WebServers/SignallingWebServer/modules/authentication/db/index.js new file mode 100644 index 0000000..5fd0f68 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/authentication/db/index.js @@ -0,0 +1,2 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +exports.users = require('./users'); diff --git a/WebServers/SignallingWebServer/modules/authentication/db/store_password.js b/WebServers/SignallingWebServer/modules/authentication/db/store_password.js new file mode 100644 index 0000000..f16145b --- /dev/null +++ b/WebServers/SignallingWebServer/modules/authentication/db/store_password.js @@ -0,0 +1,80 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// +// Usage: npm run store_password -- --username --password +// or from ./modules/authentication/db dir: node store_password.js --username --password +// +// --usersFile is an optional parameter that can be used to specify a different location for the users database file +// use this if running the command from a different working dir. The default location is './users.json' +// e.g. If running from the SignallingWebServer dir use: --usersFile ./modules/authentication/db/users.json + +const argv = require('yargs').argv; +const fs = require('fs'); +const bcrypt = require('bcryptjs'); + +var username, password; +var usersFile = './users.json' + +const STORE_PLAINTEXT_PASSWORD = false; + +try { + if(typeof argv.username != 'undefined'){ + username = argv.username.toString(); + } + + if(typeof argv.password != 'undefined'){ + password = argv.password; + } + + if(typeof argv.usersFile != 'undefined'){ + usersFile = argv.usersFile; + } +} catch (e) { + console.error(e); + process.exit(2); +} + +if(username && password){ + let existingAccounts = []; + if (fs.existsSync(usersFile)) { + console.log(`File '${usersFile}' exists, reading file`) + var content = fs.readFileSync(usersFile, 'utf8'); + try{ + existingAccounts = JSON.parse(content); + } + catch(e){ + console.error(`Existing file '${usersFile}', has invalid JSON: ${e}`); + } + } + + var existingUser = existingAccounts.find( u => u.username == username) + if(existingUser){ + console.log(`User '${username}', already exists, updating password`) + existingUser.passwordHash = generatePasswordHash(password) + if(STORE_PLAINTEXT_PASSWORD) + existingUser.password = password; + else if (existingUser.password) + delete existingUser.password; + + } else { + console.log(`Adding new user '${username}'`) + let newUser = { + id: existingAccounts.length + 1, + username: username, + passwordHash: generatePasswordHash(password) + } + if(STORE_PLAINTEXT_PASSWORD) + newUser.password = password; + + existingAccounts.push(newUser); + } + + console.log(`Writing updated users to '${usersFile}'`); + var newContent = JSON.stringify(existingAccounts); + fs.writeFileSync(usersFile, newContent); +} else { + console.log(`Please pass in both username (${username}) and password (${password}) please`); +} + +function generatePasswordHash(pass){ + return bcrypt.hashSync(pass, 12) +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/modules/authentication/db/users.js b/WebServers/SignallingWebServer/modules/authentication/db/users.js new file mode 100644 index 0000000..586ccb9 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/authentication/db/users.js @@ -0,0 +1,35 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +const fs = require('fs'); +const path = require('path'); + +// Read in users from file +let records = []; +let usersFile = path.join(__dirname, './users.json'); +if (fs.existsSync(usersFile)) { + console.log(`Reading users from '${usersFile}'`) + var content = fs.readFileSync(usersFile, 'utf8'); + try { + records = JSON.parse(content); + } catch(e) { + console.log(`ERROR: Failed to parse users from file '${usersFile}'`) + } +} + +exports.findById = function(id, cb) { + var idx = id - 1; + if (records[idx]) { + cb(null, records[idx]); + } else { + cb(new Error('User ' + id + ' does not exist')); + } +} + +exports.findByUsername = function(username, cb) { + for (var i = 0, len = records.length; i < len; i++) { + var record = records[i]; + if (record.username === username) { + return cb(null, record); + } + } + return cb(null, null); +} diff --git a/WebServers/SignallingWebServer/modules/authentication/index.js b/WebServers/SignallingWebServer/modules/authentication/index.js new file mode 100644 index 0000000..d19bf66 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/authentication/index.js @@ -0,0 +1,4 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +module.exports = { + init: require('./init') +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/modules/authentication/init.js b/WebServers/SignallingWebServer/modules/authentication/init.js new file mode 100644 index 0000000..ae34f43 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/authentication/init.js @@ -0,0 +1,109 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +// Adapted from +// * https://blog.risingstack.com/node-hero-node-js-authentication-passport-js/ +// * https://github.com/RisingStack/nodehero-authentication/tree/master/app +// * https://github.com/passport/express-4.x-local-example + + +const passport = require('passport'); +const session = require('express-session'); +const bcrypt = require('bcryptjs'); +const LocalStrategy = require('passport-local').Strategy; +const path = require('path'); +const fs = require('fs'); +var db = require('./db'); + +function initPassport (app) { + + // Generate session secret if it doesn't already exist and save it to file for use next time + let config = {}; + let configPath = path.join(__dirname, './config.json'); + if (fs.existsSync(configPath)) { + let content = fs.readFileSync(configPath, 'utf8'); + try { + config = JSON.parse(content); + } catch (e) { + console.log(`Error with config file '${configPath}': ${e}`); + } + } + + if(!config.sessionSecret){ + config.sessionSecret = bcrypt.genSaltSync(12); + let content = JSON.stringify(config); + fs.writeFileSync(configPath, content); + } + + // Setup session id settings + app.use(session({ + secret: config.sessionSecret, + resave: false, + saveUninitialized: false, + cookie: { + secure: true, + maxAge: 24 * 60 * 60 * 1000 /* 1 day */ + //maxAge: 5 * 1000 /* 5 seconds */ + } + })); + + app.use(passport.initialize()); + app.use(passport.session()); + + passport.serializeUser(function(user, cb) { + cb(null, user.id); + }); + + passport.deserializeUser(function(id, cb) { + db.users.findById(id, function (err, user) { + if (err) { return cb(err); } + cb(null, user); + }); + }); + + console.log('Setting up auth'); + passport.use(new LocalStrategy( + (username, password, callback) => { + db.users.findByUsername(username, (err, user) => { + if (err) { + console.log(`Unable to login '${username}', error ${err}`); + return callback(err); + } + + // User not found + if (!user) { + console.log(`User '${username}' not found`); + return callback(null, false); + } + + // Always use hashed passwords and fixed time comparison + bcrypt.compare(password, user.passwordHash, (err, isValid) => { + if (err) { + console.log(`Error comparing password for user '${username}': ${err}`); + return callback(err); + } + if (!isValid) { + console.log(`Password incorrect for user '${username}'`) + return callback(null, false); + } + + console.log(`User '${username}' logged in`); + return callback(null, user); + }); + }) + } + )); + + passport.authenticationMiddleware = function authenticationMiddleware (redirectUrl) { + return function (req, res, next) { + if (req.isAuthenticated()) { + return next(); + } + + // Set redirectTo property so that user can be redirected back there after logging in + //console.log(`Original request path '${req.originalUrl}'`); + req.session.redirectTo = req.originalUrl; + res.redirect(redirectUrl); + } + } +} + +module.exports = initPassport; \ No newline at end of file diff --git a/WebServers/SignallingWebServer/modules/config.js b/WebServers/SignallingWebServer/modules/config.js new file mode 100644 index 0000000..e4f33ca --- /dev/null +++ b/WebServers/SignallingWebServer/modules/config.js @@ -0,0 +1,56 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +//-- Provides configuration information from file and combines it with default values and command line arguments --// +//-- Hierachy of values: Default Values < Config File < Command Line arguments --// + +const fs = require('fs'); +const path = require('path'); +const argv = require('yargs').argv; + +function initConfig(configFile, defaultConfig){ + defaultConfig = defaultConfig || {}; + + // Using object spread syntax: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax#Spread_in_object_literals + let config = {...defaultConfig}; + try { + let configData = fs.readFileSync(configFile, 'UTF8'); + fileConfig = JSON.parse(configData); + config = {...config, ...fileConfig} + + try { + accessSync('configFile', constants.W_OK); + // Update config file with any additional defaults (does not override existing values if default has changed) + fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8'); + } catch (err) { + console.log("Config file is readonly, skipping writing config..."); + } + + } catch(err) { + if (err.code === 'ENOENT') { + console.log("No config file found, writing defaults to log file " + configFile); + fs.writeFileSync(configFile, JSON.stringify(config, null, '\t'), 'UTF8'); + } else if (err instanceof SyntaxError) { + console.log(`ERROR: Invalid JSON in ${configFile}, ignoring file config, ${err}`) + } else { + console.log(`ERROR: ${err}`); + } + } + + try { + //Make a copy of the command line args and remove the unneccessary ones + //The _ value is an array of any elements without a key + let commandLineConfig = {...argv} + delete commandLineConfig._; + delete commandLineConfig.help; + delete commandLineConfig.version; + delete commandLineConfig['$0']; + config = {...config, ...commandLineConfig} + } catch(err) { + console.log(`ERROR: ${err}`); + } + return config; +} + +module.exports = { + init: initConfig +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/modules/httpsClient.js b/WebServers/SignallingWebServer/modules/httpsClient.js new file mode 100644 index 0000000..4b1fe00 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/httpsClient.js @@ -0,0 +1,95 @@ +// Copyright Epic Games, Inc. All Rights Reserved. +var querystring = require('querystring') +const https = require('https'); +const assert = require('assert'); + +function cleanUrl(aUrl){ + let url = aUrl; + if(url.startsWith("https://")) + url = url.substring("https://".length); + + return url +} + +function createOptions(requestType, url){ + let index = url.indexOf('/'); + + let urlParts = url.split('/', 2) + + return { + hostname: (index === -1) ? url.substring(0) : url.substring(0, index), + port: 443, + path: (index === -1) ? '' : url.substring(index), + method: requestType, + timeout: 30000, + }; +} + +function makeHttpsCall(options, aCallback, aError){ + //console.log(JSON.stringify(options)); + const req = https.request(options, function(response){ + let data = ''; + + //console.log('statusCode:', response.statusCode); + //console.log('headers:', response.headers); + + // A chunk of data has been received. + response.on('data', (chunk) => { + data += chunk; + }); + + // The whole response has been received. Print out the result. + response.on('end', () => { + if(typeof aCallback != "undefined") + aCallback(response, data); + }); + }); + + req.on('timeout', function () { + console.log("Request timed out. " + (options.timeout / 1000) + " seconds expired"); + + // Source: https://github.com/nodejs/node/blob/master/test/parallel/test-http-client-timeout-option.js#L27 + req.destroy(); + }); + + req.on("error", (err) => { + if(typeof aError != "undefined") { + aError(err); + } else { + console.log("Error: " + err.message); + } + }); + + return req; +} + +module.exports = class HttpClient { + get(aUrl, aCallback, aError) { + let url = cleanUrl(aUrl); + + let options = createOptions('GET', url); + + const req = makeHttpsCall(options, aCallback, aError); + + req.end(); + } + + post(aUrl, body, aCallback, aError) { + let url = cleanUrl(aUrl); + + let options = createOptions('POST', url); + + let postBody = querystring.stringify(body); + + //Add extra options for POST request type + options.headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': postBody.length + }; + + const req = makeHttpsCall(options, aCallback, aError); + + req.write(postBody); + req.end(); + } +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/modules/logging.js b/WebServers/SignallingWebServer/modules/logging.js new file mode 100644 index 0000000..9482c58 --- /dev/null +++ b/WebServers/SignallingWebServer/modules/logging.js @@ -0,0 +1,108 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +const fs = require('fs'); +const { Console } = require('console'); + +var loggers=[]; +var logFunctions=[]; +var logColorFunctions=[]; + +console.log = function(msg, ...args) { + logFunctions.forEach((logFunction) => { + logFunction(msg, ...args); + }); +} + +console.logColor = function(color, msg, ...args) { + logColorFunctions.forEach((logColorFunction) => { + logColorFunction(color, msg, ...args); + }); +} + +const AllAttributesOff = '\x1b[0m'; +const BoldOn = '\x1b[1m'; +const Black = '\x1b[30m'; +const Red = '\x1b[31m'; +const Green = '\x1b[32m'; +const Yellow = '\x1b[33m'; +const Blue = '\x1b[34m'; +const Magenta = '\x1b[35m'; +const Cyan = '\x1b[36m'; +const White = '\x1b[37m'; + +/** + * Pad the start of the given number with zeros so it takes up the number of digits. + * e.g. zeroPad(5, 3) = '005' and zeroPad(23, 2) = '23'. + */ +function zeroPad(number, digits) { + let string = number.toString(); + while (string.length < digits) { + string = '0' + string; + } + return string; +} + +/** + * Create a string of the form 'YEAR.MONTH.DATE.HOURS.MINUTES.SECONDS'. + */ +function dateTimeToString() { + let date = new Date(); + return `${date.getFullYear()}.${zeroPad(date.getMonth(), 2)}.${zeroPad(date.getDate(), 2)}.${zeroPad(date.getHours(), 2)}.${zeroPad(date.getMinutes(), 2)}.${zeroPad(date.getSeconds(), 2)}`; +} + +/** + * Create a string of the form 'HOURS.MINUTES.SECONDS.MILLISECONDS'. + */ +function timeToString() { + let date = new Date(); + return `${zeroPad(date.getHours(), 2)}:${zeroPad(date.getMinutes(), 2)}:${zeroPad(date.getSeconds(), 2)}.${zeroPad(date.getMilliseconds(), 3)}`; +} + +function RegisterFileLogger(path) { + if(path == null) + path = './'; + + if (!fs.existsSync(path)) + fs.mkdirSync(path); + + var output = fs.createWriteStream(`./logs/${dateTimeToString()}.log`); + var fileLogger = new Console(output); + logFunctions.push(function(msg, ...args) { + fileLogger.log(`${timeToString()} ${msg}`, ...args); + }); + + logColorFunctions.push(function(color, msg, ...args) { + fileLogger.log(`${timeToString()} ${msg}`, ...args); + }); + loggers.push(fileLogger); +} + +function RegisterConsoleLogger() { + var consoleLogger = new Console(process.stdout, process.stderr) + logFunctions.push(function(msg, ...args) { + consoleLogger.log(`${timeToString()} ${msg}`, ...args); + }); + + logColorFunctions.push(function(color, msg, ...args) { + consoleLogger.log(`${BoldOn}${color}${timeToString()} ${msg}${AllAttributesOff}`, ...args); + }); + loggers.push(consoleLogger); +} + +module.exports = { + //Functions + RegisterFileLogger, + RegisterConsoleLogger, + + //Variables + AllAttributesOff, + BoldOn, + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White +} \ No newline at end of file diff --git a/WebServers/SignallingWebServer/package-lock.json b/WebServers/SignallingWebServer/package-lock.json new file mode 100644 index 0000000..0e8e9c3 --- /dev/null +++ b/WebServers/SignallingWebServer/package-lock.json @@ -0,0 +1,1798 @@ +{ + "name": "cirrus-webserver", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "cirrus-webserver", + "version": "0.0.1", + "dependencies": { + "bcryptjs": "^2.4.3", + "express": "^4.16.2", + "express-session": "^1.15.6", + "helmet": "^3.21.3", + "passport": "^0.4.0", + "passport-local": "^1.0.0", + "ws": "^7.1.2", + "y18n": "^5.0.5", + "yargs": "^15.3.0" + } + }, + "node_modules/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dependencies": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, + "node_modules/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dependencies": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bowser": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", + "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==" + }, + "node_modules/bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-security-policy-builder": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", + "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "node_modules/dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "node_modules/dont-sniff-mimetype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", + "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dependencies": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express-session": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "dependencies": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express-session/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/feature-policy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", + "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/helmet": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.23.3.tgz", + "integrity": "sha512-U3MeYdzPJQhtvqAVBPntVgAvNSOJyagwZwyKsFdyRa8TV3pOKVFljalPOCxbw5Wwf2kncGhmP0qHjyazIdNdSA==", + "dependencies": { + "depd": "2.0.0", + "dont-sniff-mimetype": "1.1.0", + "feature-policy": "0.3.0", + "helmet-crossdomain": "0.4.0", + "helmet-csp": "2.10.0", + "hide-powered-by": "1.1.0", + "hpkp": "2.0.0", + "hsts": "2.2.0", + "nocache": "2.1.0", + "referrer-policy": "1.2.0", + "x-xss-protection": "1.3.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet-crossdomain": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", + "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet-csp": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz", + "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==", + "dependencies": { + "bowser": "2.9.0", + "camelize": "1.0.0", + "content-security-policy-builder": "2.1.0", + "dasherize": "2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/helmet/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/hide-powered-by": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", + "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "node_modules/hsts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", + "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", + "dependencies": { + "depd": "2.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/hsts/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "dependencies": { + "mime-db": "1.49.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node_modules/negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nocache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", + "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "dependencies": { + "passport-strategy": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dependencies": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/referrer-policy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", + "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "node_modules/serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "node_modules/setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dependencies": { + "ansi-regex": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/x-xss-protection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", + "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + } + }, + "dependencies": { + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bowser": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", + "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "camelize": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", + "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-security-policy-builder": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", + "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "dasherize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", + "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dont-sniff-mimetype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", + "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "express-session": { + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.2.tgz", + "integrity": "sha512-mPcYcLA0lvh7D4Oqr5aNJFMtBMKPLl++OKKxkHzZ0U0oDq1rpKBnkR5f5vCHR26VeArlTOEF9td4x5IjICksRQ==", + "requires": { + "cookie": "0.4.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "feature-policy": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz", + "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "helmet": { + "version": "3.23.3", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.23.3.tgz", + "integrity": "sha512-U3MeYdzPJQhtvqAVBPntVgAvNSOJyagwZwyKsFdyRa8TV3pOKVFljalPOCxbw5Wwf2kncGhmP0qHjyazIdNdSA==", + "requires": { + "depd": "2.0.0", + "dont-sniff-mimetype": "1.1.0", + "feature-policy": "0.3.0", + "helmet-crossdomain": "0.4.0", + "helmet-csp": "2.10.0", + "hide-powered-by": "1.1.0", + "hpkp": "2.0.0", + "hsts": "2.2.0", + "nocache": "2.1.0", + "referrer-policy": "1.2.0", + "x-xss-protection": "1.3.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "helmet-crossdomain": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", + "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" + }, + "helmet-csp": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz", + "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==", + "requires": { + "bowser": "2.9.0", + "camelize": "1.0.0", + "content-security-policy-builder": "2.1.0", + "dasherize": "2.0.0" + } + }, + "hide-powered-by": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", + "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" + }, + "hpkp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", + "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" + }, + "hsts": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", + "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", + "requires": { + "depd": "2.0.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", + "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==" + }, + "mime-types": { + "version": "2.1.32", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", + "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "requires": { + "mime-db": "1.49.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "nocache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", + "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "referrer-policy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", + "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==" + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "requires": { + "random-bytes": "~1.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "ws": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", + "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "requires": {} + }, + "x-xss-protection": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", + "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "dependencies": { + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + } + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } +} diff --git a/WebServers/SignallingWebServer/package.json b/WebServers/SignallingWebServer/package.json new file mode 100644 index 0000000..59b6532 --- /dev/null +++ b/WebServers/SignallingWebServer/package.json @@ -0,0 +1,34 @@ +{ + "name": "cirrus-webserver", + "version": "0.0.1", + "description": "cirrus web server", + "scripts": { + "store_password": "run-script-os", + "store_password:default": "./platform_scripts/bash/node/bin/node ./modules/authentication/db/store_password.js --usersFile=./modules/authentication/db/users.json", + "store_password:windows": "platform_scripts\\cmd\\node\\node.exe ./modules/authentication/db/store_password.js --usersFile=./modules/authentication/db/users.json", + "start-local": "run-script-os --", + "start-local:default": "./platform_scripts/bash/Start_Local.sh", + "start-local:windows": ".\\platform_scripts\\cmd\\Start_Local.bat", + "start-signalling-server": "run-script-os --", + "start-signalling-server:default": "./platform_scripts/bash/Start_SignallingServer.sh", + "start-signalling-server:windows": ".\\platform_scripts\\cmd\\Start_SignallingServer.bat", + "start-with-turn-signalling-server": "run-script-os --", + "start-with-turn-signalling-server:default": "./platform_scripts/bash/Start_WithTurn_SignallingServer.sh", + "start-wiht-turn-signalling-server:windows": ".\\platform_scripts\\cmd\\Start_WithTurn_SignallingServer.bat", + "start": "run-script-os", + "start:default": "if [ `id -u` -eq 0 ] || [ ! -z $NO_SUDO ]\nthen\n export process=\"./platform_scripts/bash/node/bin/node cirrus.js\"\nelse\n export process=\"sudo ./platform_scripts/bash/node/bin/node cirrus.js\"\nfi\n$process ", + "start:windows": "platform_scripts\\cmd\\node\\node.exe cirrus.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "express": "^4.16.2", + "express-session": "^1.15.6", + "helmet": "^3.21.3", + "passport": "^0.4.0", + "passport-local": "^1.0.0", + "run-script-os": "^1.1.6", + "ws": "^7.1.2", + "y18n": "^5.0.5", + "yargs": "^15.3.0" + } +} diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/Dockerfile b/WebServers/SignallingWebServer/platform_scripts/bash/Dockerfile new file mode 100644 index 0000000..48b3d23 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/Dockerfile @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +FROM node:latest + +# Copy the signalling server source code to the Docker build context +COPY . /opt/SignallingWebServer + +# Install the dependencies for the signalling server +WORKDIR /opt/SignallingWebServer +RUN npm install . + +# Expose TCP port 80 for player WebSocket connections and web server HTTP access +EXPOSE 80 + +# Expose TCP port 8888 for streamer WebSocket connections +EXPOSE 8888 +EXPOSE 8888/udp + +# Expose port for SFU connections +EXPOSE 8889 + +# Google stun +EXPOSE 19302 + +# Matchmaker +EXPOSE 9999 + +# Turn coturn +EXPOSE 3478 +EXPOSE 3479 + +# Set the signalling server as the container's entrypoint +ENTRYPOINT ["/usr/local/bin/node", "/opt/SignallingWebServer/cirrus.js"] + diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/README.txt b/WebServers/SignallingWebServer/platform_scripts/bash/README.txt new file mode 100644 index 0000000..1797c97 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/README.txt @@ -0,0 +1,12 @@ +How to use files in this directory: +- Make sure that all of your dependencies are installed. Use ./setup.sh what will install whatever is missing as long as you are on a supported operating system. Please note that setup.sh is called from every script designed to run + +- Run a local instance of the Cirrus server by using the ./run_local.sh script + +- Use the following scripts to run locally or in your cloud instance: + - Start_SignallingServer.sh - Start only the Signalling (STUN) server + - Start_TURNServer.sh - Start only the TURN server + - Start_WithTURN_SignallingServer.sh - Start a TURN server and the Cirrus server together + +- Please note that scripts intended to run need to be executable: $ chmod +x *.sh will do that job. +- The local/cloud Start_*.sh shell scripts can be invoked with the --help command line option to see how those can be configured. The following options can be supplied: --publicip, --turn, --stun. Please read the --help diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/Start_SignallingServer.sh b/WebServers/SignallingWebServer/platform_scripts/bash/Start_SignallingServer.sh new file mode 100644 index 0000000..af182b2 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/Start_SignallingServer.sh @@ -0,0 +1,33 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +set_start_default_values "n" "y" # Set STUN server defaults only +use_args "$@" +call_setup_sh +print_parameters + +peerconnectionoptions='{\"iceServers\":[{\"urls\":[\"stun:${stunserver}\"]}]}' + +process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --" +arguments="" + +if [ ! -z $IS_DEBUG ]; then + arguments+=" --inspect" +fi + +arguments+=" --peerConnectionOptions=\"${peerconnectionoptions}\" --PublicIp=${publicip}" +# Add arguments passed to script to arguments for executable +arguments+=" ${cirruscmd}" + +pushd ../.. +echo "Running: $process $arguments" +PATH="${BASH_LOCATION}/node/bin:$PATH" +start_process $process $arguments +popd + +popd \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/Start_TURNServer.sh b/WebServers/SignallingWebServer/platform_scripts/bash/Start_TURNServer.sh new file mode 100644 index 0000000..74c5f74 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/Start_TURNServer.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source turn_user_pwd.sh +source common_utils.sh + +set_start_default_values "y" "n" # TURN server defaults only +use_args "$@" +call_setup_sh +print_parameters + +localip=$(hostname -I | awk '{print $1}') +echo "Private IP: $localip" + +turnport="${turnserver##*:}" +if [ -z "${turnport}" ]; then + turnport=3478 +fi +echo "TURN port: ${turnport}" +echo "" + + +# Hmm, plain text +realm="PixelStreaming" +process="turnserver" +arguments="-p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -n -u ${turnusername}:${turnpassword}" + +# Add arguments passed to script to arguments for executable +arguments+=" ${cirruscmd}" + +pushd ../.. >/dev/null +echo "Running: $process $arguments" +# pause +start_process $process $arguments & +popd >/dev/null # ../.. + +popd >/dev/null # BASH_SOURCE diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/Start_WithTURN_SignallingServer.sh b/WebServers/SignallingWebServer/platform_scripts/bash/Start_WithTURN_SignallingServer.sh new file mode 100644 index 0000000..40f83de --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/Start_WithTURN_SignallingServer.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +set_start_default_values "y" "y" # Set both TURN and STUN server defaults +use_args "$@" +call_setup_sh +print_parameters + +bash Start_TURNServer.sh --turn "${turnserver}" + +peerconnectionoptions='{\"iceServers\":[{\"urls\":[\"stun:$stunserver\",\"turn:$turnserver\"],\"username\":\"PixelStreamingUser\",\"credential\":\"AnotherTURNintheroad\"}]}' + +process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --" +arguments="" + +if [ ! -z $IS_DEBUG ]; then + arguments+=" --inspect" +fi + +arguments+=" --peerConnectionOptions=\"$peerconnectionoptions\" --PublicIp=$publicip" +# Add arguments passed to script to arguments for executable +arguments+=" ${cirruscmd}" + +pushd ../.. +echo "Running: $process $arguments" +PATH="${BASH_LOCATION}/node/bin:$PATH" +start_process $process $arguments +popd + +popd diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/common_utils.sh b/WebServers/SignallingWebServer/platform_scripts/bash/common_utils.sh new file mode 100644 index 0000000..8a6cc57 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/common_utils.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +function log_msg() { #message + if [ ! -z $VERBOSE ]; then + echo $1 + fi +} + +function print_usage() { + echo " + Usage: + ${0} [--help] [--publicip ] [--turn ] [--stun ] [cirrus options...] + Where: + --help will print this message and stop this script. + --debug will run all scripts with --inspect + --nosudo will run all scripts without \`sudo\` command useful for when run in containers. + --verbose will enable additional logging + --package-manager specify an alternative package manager to apt-get + --publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for + default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the Cirrus server and the default IP address of the TURN server + --turn defines what TURN server to be used, syntax: --turn 127.0.0.1:19303 + default value: as above, IP address downloaded from https://api.ipify.org; in case if download failure it is set to 127.0.0.1 + --stun defined what STUN server to be used, syntax: --stun stun.l.google.com:19302 + default value as above + Other options: stored and passed to the Cirrus server. All parameters printed once the script values are set. + Command line options might be omitted to run with defaults and it is a good practice to omit specific ones when just starting the TURN or the STUN server alone, not the whole set of scripts. + " + exit 1 +} + +function print_parameters() { + echo "" + echo "${0} is running with the following parameters:" + echo "--------------------------------------" + if [[ -n "${stunserver}" ]]; then echo "STUN server : ${stunserver}" ; fi + if [[ -n "${turnserver}" ]]; then echo "TURN server : ${turnserver}" ; fi + echo "Public IP address : ${publicip}" + echo "Cirrus server command line arguments: ${cirruscmd}" + echo "" +} + +function set_start_default_values() { + # publicip and cirruscmd are always needed + publicip=$(curl -s https://api.ipify.org) + if [[ -z $publicip ]]; then + publicip="127.0.0.1" + fi + cirruscmd="" + + if [ "$1" = "y" ]; then + turnserver="${publicip}:19303" + fi + + if [ "$2" = "y" ]; then + stunserver="stun.l.google.com:19302" + fi +} + +function use_args() { + while(($#)) ; do + case "$1" in + --debug ) IS_DEBUG=1; shift;; + --nosudo ) NO_SUDO=1; shift;; + --verbose ) VERBOSE=1; shift;; + --stun ) stunserver="$2"; shift 2;; + --turn ) turnserver="$2"; shift 2;; + --publicip ) publicip="$2"; turnserver="${publicip}:19303"; shift 2;; + --help ) print_usage;; + * ) echo "Unknown command, adding to cirrus command line: $1"; cirruscmd+=" $1"; shift;; + esac + done +} + +function call_setup_sh() { + bash "setup.sh" +} + +function start_process() { + if [ ! -z $NO_SUDO ]; then + log_msg "running with sudo removed" + eval $(echo "$@" | sed 's/sudo//g') + else + eval $@ + fi +} + +function get_version() { + local version=$1 + + if command -v $version; then + version=$($@) + fi + + echo $version | sed -E 's/[^0-9.]//g' +} diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-build-cirrus.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-build-cirrus.sh new file mode 100644 index 0000000..6c9d1c9 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-build-cirrus.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# When run from SignallingWebServer/platform_scripts/bash, this uses the SignallingWebServer directory +# as the build context so the Cirrus files can be successfully copied into the container image +docker build --network=host -t 'cirrus-webserver:latest' -f ./Dockerfile ../.. + diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-local.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-local.sh new file mode 100644 index 0000000..959ca43 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-local.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Start docker container by name using host networking +docker run --name cirrus_latest --network host --rm cirrus-webserver + +# Interactive start example +#docker run --name cirrus_latest --network host --rm -it --entrypoint /bin/bash cirrus-webserver diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-with-turn.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-with-turn.sh new file mode 100644 index 0000000..e109803 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus-with-turn.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Suppress printing of directory stack +pushd () { + command pushd "$@" > /dev/null +} +popd () { + command popd "$@" > /dev/null +} + +# Stop both stun and turn +pushd "$(dirname ${BASH_SOURCE[0]})" +./docker-start-cirrus.sh --with-turn & +popd diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus.sh new file mode 100644 index 0000000..7bc6c0a --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-cirrus.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +source turn_user_pwd.sh + +USETURN="false" + +for arg do + shift + [ "${arg}" = "--with-turn" ] && USETURN="true" && continue + set -- "$@" "${arg}" +done + +# Get stun server data for passing to the container +source common_utils.sh +if [ "${USETURN}" = "true" ]; then + set_start_default_values "y" "y" # Both TURN and STUN server defaults +else + set_start_default_values "n" "y" # Only STUN server defaults +fi +use_args "$@" + +# Start docker container by name using host networking +if [ "${USETURN}" = "true" ]; then + peerConnectionOptions="{\""iceServers\"":[{\""urls\"":[\""stun:"${stunserver}"\"",\""turn":"${turnserver}\""],\""username\"":\""${turnusername}\"",\""credential\"":\""${turnpassword}\""}]}" +else + peerConnectionOptions="{\""iceServers\"":[{\""urls\"":[\""stun:"${stunserver}"\""]}]}" +fi + +docker run --name cirrus_latest --network host --rm --entrypoint /usr/local/bin/node cirrus-webserver /opt/SignallingWebServer/cirrus.js --peerConnectionOptions="${peerConnectionOptions}" --publicIp="${publicip}" + diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-turn.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-turn.sh new file mode 100644 index 0000000..c0fad21 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-start-turn.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Get stun server data for passing to the container +source common_utils.sh +set_start_default_values "n" "y" # Only STUN server defaults +use_args "$@" + +localip=$(hostname -I | awk '{print $1}') +echo "Private IP: $localip" + +turnport="${turnserver##*:}" +if [ -z "${turnport}" ]; then + turnport=3478 +fi +echo "TURN port: ${turnport}" +echo "" + +turnusername="PixelStreamingUser" +turnpassword="AnotherTURNintheroad" +realm="PixelStreaming" +process="turnserver" +arguments="-p ${turnport} -r $realm -X $publicip -E $localip -L $localip --no-cli --no-tls --no-dtls --pidfile /var/run/turnserver.pid -f -a -v -n -u ${turnusername}:${turnpassword}" + +# Add arguments passed to script to arguments for executable +arguments+=" ${cirruscmd}" + +# Start docker container by name using host networking +echo "Running: ${process} ${arguments}" + +# Get the docker image +docker pull coturn/coturn + +# Start the TURN server +#docker run --name coturn_latest --network host -it --entrypoint /bin/bash coturn/coturn +#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "sudo mkdir -p /var/run" coturn/coturn "" +#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "/bin/ls" coturn/coturn "/var/" + +docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "${process}" coturn/coturn "${arguments}" + +#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "/bin/bash" coturn/coturn "ls -latr /var/run/" +#docker run --name coturn_latest --network host --rm -a stdin -a stdout -a stderr --entrypoint "sudo chown ubuntu:ubuntu /var/run/turnserver.pid | sudo chmod +x /var/run/turnserver.pid | ${process}" coturn/coturn "${arguments}" diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-all.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-all.sh new file mode 100644 index 0000000..15a3e86 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-all.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Suppress printing of directory stack +pushd () { + command pushd "$@" > /dev/null +} +popd () { + command popd "$@" > /dev/null +} + +# Stop both stun and turn +pushd "$(dirname ${BASH_SOURCE[0]})" +./docker-stop-cirrus.sh +./docker-stop-turn.sh +popd diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-cirrus.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-cirrus.sh new file mode 100644 index 0000000..4f93ac1 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-cirrus.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Stop the docker container +PSID=$(docker ps -a -q --filter="name=cirrus_latest") +if [ -z "$PSID" ]; then + echo "Docker stun is not running, no stopping will be done" + exit 1; +fi +echo "Stopping stun server ..." +docker stop cirrus_latest + diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-turn.sh b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-turn.sh new file mode 100644 index 0000000..a928bf5 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/docker-stop-turn.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Stop the docker container +PSID=$(docker ps -a -q --filter="name=coturn_latest") +if [ -z "$PSID" ]; then + echo "Docker turn is not running, no stopping will be done" + exit 1; +fi +echo "Stopping turn server..." +docker stop coturn_latest + diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/run_local.sh b/WebServers/SignallingWebServer/platform_scripts/bash/run_local.sh new file mode 100644 index 0000000..dff8e9d --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/run_local.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +set_start_default_values "n" "n" # No server specific defaults +use_args "$@" +call_setup_sh +print_parameters + +process="${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js run start:default --" +arguments="" + +if [ ! -z $IS_DEBUG ]; then + arguments+=" --inspect" +fi + +arguments+=" --publicIp=${publicip}" +arguments+=" ${cirruscmd}" + +pushd ../.. > /dev/null + +echo "" +echo "Starting Cirrus server use ctrl-c to exit" +echo "-----------------------------------------" +echo "" + +PATH="${BASH_LOCATION}/node/bin:$PATH" +start_process $process $arguments + +popd > /dev/null # ../.. + +popd > /dev/null # BASH_SOURCE \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/setup.sh b/WebServers/SignallingWebServer/platform_scripts/bash/setup.sh new file mode 100644 index 0000000..85e4449 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/setup.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. +BASH_LOCATION=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${BASH_LOCATION}" > /dev/null + +source common_utils.sh + +use_args $@ +# Azure specific fix to allow installing NodeJS from NodeSource +if test -f "/etc/apt/sources.list.d/azure-cli.list"; then + sudo touch /etc/apt/sources.list.d/nodesource.list + sudo touch /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/nodesource.list + sudo chmod 644 /usr/share/keyrings/nodesource.gpg + sudo chmod 644 /etc/apt/sources.list.d/azure-cli.list +fi + +function check_version() { #current_version #min_version + #check if same string + if [ -z "$2" ] || [ "$1" = "$2" ]; then + return 0 + fi + + local i current minimum + + IFS="." read -r -a current <<< $1 + IFS="." read -r -a minimum <<< $2 + + # fill empty fields in current with zeros + for ((i=${#current[@]}; i<${#minimum[@]}; i++)) + do + current[i]=0 + done + + for ((i=0; i<${#current[@]}; i++)) + do + if [[ -z ${minimum[i]} ]]; then + # fill empty fields in minimum with zeros + minimum[i]=0 + fi + + if ((10#${current[i]} > 10#${minimum[i]})); then + return 1 + fi + + if ((10#${current[i]} < 10#${minimum[i]})); then + return 2 + fi + done + + # if got this far string is the same once we added missing 0 + return 0 +} + +function check_and_install() { #dep_name #get_version_string #version_min #install_command + local is_installed=0 + + log_msg "Checking for required $1 install" + + local current=$(echo $2 | sed -E 's/[^0-9.]//g') + local minimum=$(echo $3 | sed -E 's/[^0-9.]//g') + + if [ $# -ne 4 ]; then + log_msg "check_and_install expects 4 args (dep_name get_version_string version_min install_command) got $#" + return -1 + fi + + if [ ! -z $current ]; then + log_msg "Current version: $current checking >= $minimum" + check_version "$current" "$minimum" + if [ "$?" -lt 2 ]; then + log_msg "$1 is installed." + return 0 + else + log_msg "Required install of $1 not found installing" + fi + fi + + if [ $is_installed -ne 1 ]; then + echo "$1 installation not found installing..." + + start_process $4 + + if [ $? -ge 1 ]; then + echo "Installation of $1 failed try running `export VERBOSE=1` then run this script again for more details" + fi + + fi +} + +echo "Checking Pixel Streaming Server dependencies." + +# navigate to SignallingWebServer root +pushd ../.. > /dev/null + +node_version="" +if [[ -f "${BASH_LOCATION}/node/bin/node" ]]; then + node_version=$("${BASH_LOCATION}/node/bin/node" --version) +fi +check_and_install "node" "$node_version" "v16.4.2" "curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz --output node.tar.xz + && tar -xf node.tar.xz + && rm node.tar.xz + && mv node-v*-linux-x64 \"${BASH_LOCATION}/node\"" + +PATH="${BASH_LOCATION}/node/bin:$PATH" +"${BASH_LOCATION}/node/lib/node_modules/npm/bin/npm-cli.js" install + +popd > /dev/null # SignallingWebServer + +popd > /dev/null # BASH_SOURCE + +#command #dep_name #get_version_string #version_min #install command +coturn_version=$(if command -v turnserver &> /dev/null; then echo 1; else echo 0; fi) +if [ $coturn_version -eq 0 ]; then + if ! command -v apt-get &> /dev/null; then + echo "Setup for the scripts is designed for use with distros that use the apt-get package manager" \ + "if you are seeing this message you will have to update \"${BASH_LOCATION}/setup.sh\" with\n" \ + "a package manger and the equivalent packages for your distribution. Please follow the\n" \ + "instructions found at https://pkgs.org/search/?q=coturn to install Coturn for your specific distribution" + exit 1 + else + if [ `id -u` -eq 0 ]; then + check_and_install "coturn" "$coturn_version" "1" "apt-get install -y coturn" + else + check_and_install "coturn" "$coturn_version" "1" "sudo apt-get install -y coturn" + fi + fi +fi diff --git a/WebServers/SignallingWebServer/platform_scripts/bash/turn_user_pwd.sh b/WebServers/SignallingWebServer/platform_scripts/bash/turn_user_pwd.sh new file mode 100644 index 0000000..77687f2 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/bash/turn_user_pwd.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright Epic Games, Inc. All Rights Reserved. + +# Plain text TURN setup +turnusername="PixelStreamingUser" +turnpassword="AnotherTURNintheroad" + diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/README.txt b/WebServers/SignallingWebServer/platform_scripts/cmd/README.txt new file mode 100644 index 0000000..0ad362f --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/README.txt @@ -0,0 +1,13 @@ +How to use files in this directory: +- Files with .ps1 extension can be run with PowerShell[.exe] in Windows. Powershell needs to be started as Administrator to run setup.ps1 so it can run installation / installation check steps +- Make sure that all of your dependencies are installed. Use .\setup.ps1 what will install whatever is missing as long as you are on a supported operating system + +- Run a local instance of the Cirrus server by using the .\run_local.ps1 script + +- Use the following scripts to run locally or in your cloud instance: + - Start_SignallingServer.ps1 - Start only the Signalling (STUN) server + - Start_TURNServer.ps1 - Start only the TURN server + - Start_WithTURN_SignallingServer.ps1 - Start a TURN server and the Cirrus server together +- The Start_Common.ps1 file contains shared functions for other Start_*.ps1 scripts and it is not supposed to run alone + +- The local/cloud Start_*.ps1 powershell scripts can be invoked with the --help command line option to see how those can be configured. The following options can be supplied: --publicip, --turn, --stun. Please read the --help diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/Start_Common.ps1 b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_Common.ps1 new file mode 100644 index 0000000..9fa923c --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_Common.ps1 @@ -0,0 +1,77 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +# Do setup as a common task, it is smart and will not reinstall if not required. +Start-Process -FilePath "$PSScriptRoot\setup.bat" -Wait -NoNewWindow + +$global:ScriptName = $MyInvocation.MyCommand.Name +$global:PublicIP = $null +$global:StunServer = $null +$global:TurnServer = $null +$global:CirrusCmd = $null + +function print_usage { + echo " + Usage (in MS Windows Power Shell): + $global:ScriptName [--help] [--publicip ] [--turn ] [--stun ] [cirrus options...] + Where: + --help will print this message and stop this script. + --publicip is used to define public ip address (using default port) for turn server, syntax: --publicip ; it is used for + default value: Retrieved from 'curl https://api.ipify.org' or if unsuccessful then set to 127.0.0.1. It is the IP address of the Cirrus server and the default IP address of the TURN server + --turn defines what TURN server to be used, syntax: --turn 127.0.0.1:19303 + default value: as above, IP address downloaded from https://api.ipify.org; in case if download failure it is set to 127.0.0.1 + --stun defined what STUN server to be used, syntax: --stun stun.l.google.com:19302 + default value as above + Other options: stored and passed to the Cirrus server. All parameters printed once the script values are set. + Command line options might be omitted to run with defaults and it is a good practice to omit specific ones when just starting the TURN or the STUN server alone, not the whole set of scripts. + " + exit 1 +} + +function print_parameters { + echo "" + echo "$scriptname is running with the following parameters:" + echo "--------------------------------------" + if ($global:StunServer -ne $null) { echo "STUN server : $global:StunServer" } + if ($global:TurnServer -ne $null) { echo "TURN server : $global:TurnServer" } + echo "Public IP address : $global:PublicIP" + echo "Cirrus server command line arguments: $global:CirrusCmd" + echo "" +} + +function set_start_default_values($SetTurnServerVar, $SetStunServerVar) { + # publicip and cirruscmd are always needed + $global:PublicIP = Invoke-WebRequest -Uri "https://api.ipify.org" -UseBasicParsing + if ($global:PublicIP -eq $null -Or $global:PublicIP.length -eq 0) { + $global:PublicIP = "127.0.0.1" + } else { + $global:PublicIP = ($global:PublicIP).Content + } + $global:cirruscmd = "" + + if ($SetTurnServerVar -eq "y") { + $global:TurnServer = $global:PublicIP + ":19303" + } + if ($SetStunServerVar -eq "y") { + $global:StunServer = "stun.l.google.com:19302" + } +} + +function use_args($arg) { + $CmdArgs = $arg -split (" ") + while($CmdArgs.count -gt 0) { + $Cmd, $CmdArgs = $CmdArgs + if ($Cmd -eq "--stun") { + $global:StunServer, $CmdArgs = $CmdArgs + } elseif ($Cmd -eq "--turn") { + $global:TurnServer, $CmdArgs = $CmdArgs + } elseif ($Cmd -eq "--publicip") { + $global:PublicIP, $CmdArgs = $CmdArgs + $global:TurnServer = $global:publicip + ":19303" + } elseif ($Cmd -eq "--help") { + print_usage + } else { + echo "Unknown command, adding to cirrus command line: $Cmd" + $global:CirrusCmd += " $Cmd" + } + } +} diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/Start_SignallingServer.ps1 b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_SignallingServer.ps1 new file mode 100644 index 0000000..e687a23 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_SignallingServer.ps1 @@ -0,0 +1,19 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +. "$PSScriptRoot\Start_Common.ps1" + +set_start_default_values "n" "y" # Set both TURN and STUN server defaults +use_args($args) +print_parameters + +$peerConnectionOptions = "{ \""iceServers\"": [{\""urls\"": [\""stun:" + $global:StunServer + "\""]}] }" + +$ProcessExe = "platform_scripts\cmd\node\node.exe" +$Arguments = @("cirrus", "--peerConnectionOptions=""$peerConnectionOptions""", "--PublicIp=$global:PublicIp") +# Add arguments passed to script to Arguments for executable +$Arguments += $global:CirrusCmd + +Push-Location $PSScriptRoot\..\..\ +Write-Output "Running: $ProcessExe $Arguments" +Start-Process -FilePath $ProcessExe -ArgumentList "$Arguments" -Wait -NoNewWindow +Pop-Location diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/Start_TURNServer.ps1 b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_TURNServer.ps1 new file mode 100644 index 0000000..17a0e82 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_TURNServer.ps1 @@ -0,0 +1,38 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +. "$PSScriptRoot\Start_Common.ps1" + +set_start_default_values "y" "n" # Set both TURN and STUN server defaults +use_args($args) +print_parameters +#$LocalIp = Invoke-WebRequest -Uri "http://169.254.169.254/latest/meta-data/local-ipv4" +$LocalIP = (Test-Connection -ComputerName (hostname) -Count 1 | Select IPV4Address).IPV4Address.IPAddressToString + +Write-Output "Private IP: $LocalIp" + +$TurnPort="19303" +$Pos = $global:TurnServer.LastIndexOf(":") +if ($Pos -ne -1) { + $TurnPort = $global:TurnServer.Substring($Pos+ 1) +} +echo "TURN port: ${turnport}" +echo "" + +Push-Location $PSScriptRoot + +$TurnUsername = "PixelStreamingUser" +$TurnPassword = "AnotherTURNintheroad" +$Realm = "PixelStreaming" +$ProcessExe = ".\turnserver.exe" +$Arguments = "-p $TurnPort -r $Realm -X $PublicIP -E $LocalIP -L $LocalIP --no-cli --no-tls --no-dtls --pidfile `"C:\coturn.pid`" -f -a -v -n -u $TurnUsername`:$TurnPassword" + +# Add arguments passed to script to Arguments for executable +$Arguments += $args + +Push-Location $PSScriptRoot\coturn\ +Write-Output "Running: $ProcessExe $Arguments" +# pause +Start-Process -FilePath $ProcessExe -ArgumentList $Arguments -NoNewWindow +Pop-Location + +Pop-Location \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/Start_WithTURN_SignallingServer.ps1 b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_WithTURN_SignallingServer.ps1 new file mode 100644 index 0000000..392f902 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/Start_WithTURN_SignallingServer.ps1 @@ -0,0 +1,25 @@ +# Copyright Epic Games, Inc. All Rights Reserved. + +. "$PSScriptRoot\Start_Common.ps1" + +set_start_default_values "y" "y" # Set both TURN and STUN server defaults +use_args($args) +print_parameters + +Push-Location $PSScriptRoot + +Start-Process -FilePath "PowerShell" -ArgumentList ".\Start_TURNServer.ps1" -WorkingDirectory "$PSScriptRoot" + +$peerConnectionOptions = "{ \""iceServers\"": [{\""urls\"": [\""stun:" + $global:StunServer + "\"",\""turn:" + $global:TurnServer + "\""], \""username\"": \""PixelStreamingUser\"", \""credential\"": \""AnotherTURNintheroad\""}] }" + +$ProcessExe = "platform_scripts\cmd\node\node.exe" +$Arguments = @("cirrus", "--peerConnectionOptions=""$peerConnectionOptions""", "--PublicIp=$global:PublicIp") +# Add arguments passed to script to Arguments for executable +$Arguments += $args + +Push-Location $PSScriptRoot\..\..\ +Write-Output "Running: $ProcessExe $Arguments" +Start-Process -FilePath $ProcessExe -ArgumentList $Arguments -Wait -NoNewWindow +Pop-Location + +Pop-Location \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/build.bat b/WebServers/SignallingWebServer/platform_scripts/cmd/build.bat new file mode 100644 index 0000000..35a3574 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/build.bat @@ -0,0 +1,39 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script directory as working directory. +pushd "%~dp0" + +title Building Cirrus.exe + +@Rem Run setup to ensure we have node and cirrus installed. +call setup.bat + +@Rem Look for a `nexe` directory next to this script +if exist nexe\ ( + echo nexe directory found...skipping install. +) else ( + echo nexe directory not found...beginning nexe install. + + @Rem Make `nexe directory` + mkdir nexe + + @Rem npm init and install nexe + pushd nexe + call ..\node\npm init -y + call ..\node\npm install nexe --save + popd +) + +@Rem Move to cirrus directory. +pushd ..\.. + +@Rem Build cirrus.exe using `nexe` using node 14.5.0 (as that is one of the latest prebuilts node versions in the nexe repo) +call platform_scripts\cmd\node\npx nexe cirrus.js --target "x64-14.15.3" -r "Public/*" -r "scripts/*" -r "images/*" -r "config.json" + +@Rem Pop cirrus directory. +popd ..\.. + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/run_local.bat b/WebServers/SignallingWebServer/platform_scripts/cmd/run_local.bat new file mode 100644 index 0000000..d9cba4d --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/run_local.bat @@ -0,0 +1,25 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script directory as working directory. +pushd "%~dp0" + +title Cirrus + +@Rem Run setup to ensure we have node and cirrus installed. +call setup.bat + +@Rem Move to cirrus directory. +pushd ..\.. + +@Rem Run node server and pass any argument along. +platform_scripts\cmd\node\node.exe cirrus %* + +@Rem Pop cirrus directory. +popd + +@Rem Pop script directory. +popd + +pause \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/setup.bat b/WebServers/SignallingWebServer/platform_scripts/cmd/setup.bat new file mode 100644 index 0000000..3b6dcbd --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/setup.bat @@ -0,0 +1,20 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Ensure we have NodeJs available for calling. +call setup_node.bat + +@Rem Ensure we have CoTURN available for calling. +call setup_coturn.bat + +@Rem Move to cirrus.js directory and install its package.json +pushd %~dp0\..\..\ +call platform_scripts\cmd\node\npm install --no-save +popd + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/setup_coturn.bat b/WebServers/SignallingWebServer/platform_scripts/cmd/setup_coturn.bat new file mode 100644 index 0000000..fd9f2e7 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/setup_coturn.bat @@ -0,0 +1,25 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Look for CoTURN directory next to this script +if exist coturn\ ( + echo CoTURN directory found...skipping install. +) else ( + echo CoTURN directory not found...beginning CoTURN download for Windows. + + @Rem Download nodejs and follow redirects. + curl -L -o ./turnserver.zip "https://github.com/mcottontensor/coturn/releases/download/v4.5.2-windows/turnserver.zip" + + @Rem Unarchive the .zip to a directory called "turnserver" + mkdir coturn & tar -xf turnserver.zip -C coturn + + @Rem Delete the downloaded turnserver.zip + del turnserver.zip +) + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SignallingWebServer/platform_scripts/cmd/setup_node.bat b/WebServers/SignallingWebServer/platform_scripts/cmd/setup_node.bat new file mode 100644 index 0000000..cc079e5 --- /dev/null +++ b/WebServers/SignallingWebServer/platform_scripts/cmd/setup_node.bat @@ -0,0 +1,35 @@ +@Rem Copyright Epic Games, Inc. All Rights Reserved. + +@echo off + +@Rem Set script location as working directory for commands. +pushd "%~dp0" + +@Rem Name and version of node that we are downloading +SET NodeVersion=v16.4.2 +SET NodeName=node-%NodeVersion%-win-x64 + +@Rem Look for a node directory next to this script +if exist node\ ( + echo Node directory found...skipping install. +) else ( + echo Node directory not found...beginning NodeJS download for Windows. + + @Rem Download nodejs and follow redirects. + curl -L -o ./node.zip "https://nodejs.org/dist/%NodeVersion%/%NodeName%.zip" + + @Rem Unarchive the .zip + tar -xf node.zip + + @Rem Rename the extracted, versioned, directory that contains the NodeJS binaries to simply "node". + ren "%NodeName%\" "node" + + @Rem Delete the downloaded node.zip + del node.zip +) + +@Rem Print node version +echo Node version: & node\node.exe -v + +@Rem Pop working directory +popd \ No newline at end of file diff --git a/WebServers/SignallingWebServer/scripts/app.js b/WebServers/SignallingWebServer/scripts/app.js new file mode 100644 index 0000000..cd5ed76 --- /dev/null +++ b/WebServers/SignallingWebServer/scripts/app.js @@ -0,0 +1,2756 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +/** + * Class definitions + * TODO: Move these to seperate files once we introduce a bundler + */ +class TwoWayMap { + constructor(map = {}) { + this.map = map; + this.reverseMap = new Map(); + for(const key in map) { + const value = map[key]; + this.reverseMap[value] = key; + } + } + + getFromKey(key) { return this.map[key]; } + getFromValue(value) { return this.reverseMap[value]; } + + add(key, value) { + this.map[key] = value; + this.reverseMap[value] = key; + } + + remove(key, value) { + delete this.map[key]; + delete this.reverseMap[value]; + } +} + +/** + * Frontend logic + */ +// Window events for a gamepad connecting +let haveEvents = 'GamepadEvent' in window; +let haveWebkitEvents = 'WebKitGamepadEvent' in window; +let controllers = {}; +let rAF = window.mozRequestAnimationFrame || + window.webkitRequestAnimationFrame || + window.requestAnimationFrame; + +let webRtcPlayerObj = null; +let print_stats = false; +let print_inputs = false; +let connect_on_load = false; +let ws; +const WS_OPEN_STATE = 1; + +let inputController = null; +let autoPlayAudio = true; +let qualityController = false; +let qualityControlOwnershipCheckBox; +let matchViewportResolution; +let VideoEncoderQP = "N/A"; +// TODO: Remove this - workaround because of bug causing UE to crash when switching resolutions too quickly +let lastTimeResized = new Date().getTime(); +let resizeTimeout; + +let responseEventListeners = new Map(); + +let freezeFrameOverlay = null; +let shouldShowPlayOverlay = true; + +let isFullscreen = false; +let isMuted = false; +// A freeze frame is a still JPEG image shown instead of the video. +let freezeFrame = { + receiving: false, + size: 0, + jpeg: undefined, + height: 0, + width: 0, + valid: false +}; + +let file = { + mimetype: "", + extension: "", + receiving: false, + size: 0, + data: [], + valid: false, + timestampStart: undefined +}; + +// Optionally detect if the user is not interacting (AFK) and disconnect them. +let afk = { + enabled: false, // Set to true to enable the AFK system. + warnTimeout: 120, // The time to elapse before warning the user they are inactive. + closeTimeout: 10, // The time after the warning when we disconnect the user. + + active: false, // Whether the AFK system is currently looking for inactivity. + overlay: undefined, // The UI overlay warning the user that they are inactive. + warnTimer: undefined, // The timer which waits to show the inactivity warning overlay. + countdown: 0, // The inactivity warning overlay has a countdown to show time until disconnect. + countdownTimer: undefined, // The timer used to tick the seconds shown on the inactivity warning overlay. +} + +// If the user focuses on a UE input widget then we show them a button to open +// the on-screen keyboard. JavaScript security means we can only show the +// on-screen keyboard in response to a user interaction. +let editTextButton = undefined; + +// A hidden input text box which is used only for focusing and opening the +// on-screen keyboard. +let hiddenInput = undefined; + +let MaxByteValue = 255; +// The delay between the showing/unshowing of a freeze frame and when the stream will stop/start +// eg showing freeze frame -> delay -> stop stream OR show stream -> delay -> unshow freeze frame +freezeFrameDelay = 50; // ms + +let activeKeys = []; + +let toStreamerMessages = new TwoWayMap(); +let fromStreamerMessages = new TwoWayMap(); + +const MessageDirection = { + // A message sent to the streamer. eg Key presses + // ie player -> streamer + ToStreamer: 0, + + // A message recevied from the streamer. eg Freeze frames + // ie streamer -> player + FromStreamer: 1 +}; + +let toStreamerHandlers = new Map(); // toStreamerHandlers[message](args..) +let fromStreamerHandlers = new Map(); // fromStreamerHandlers[message](args..) +function populateDefaultProtocol() { + /* + * Control Messages. Range = 0..49. + */ + toStreamerMessages.add("IFrameRequest", { + "id": 0, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("RequestQualityControl", { + "id": 1, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("FpsRequest", { + "id": 2, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("AverageBitrateRequest", { + "id": 3, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("StartStreaming", { + "id": 4, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("StopStreaming", { + "id": 5, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("LatencyTest", { + "id": 6, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("RequestInitialSettings", { + "id": 7, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("TestEcho", { + "id": 8, + "byteLength": 0, + "structure": [] + }); + /* + * Input Messages. Range = 50..89. + */ + // Generic Input Messages. Range = 50..59. + toStreamerMessages.add("UIInteraction", { + "id": 50, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("Command", { + "id": 51, + "byteLength": 0, + "structure": [] + }); + // Keyboard Input Message. Range = 60..69. + toStreamerMessages.add("KeyDown", { + "id": 60, + "byteLength": 2, + // keyCode isRepeat + "structure": ["uint8", "uint8"] + }); + toStreamerMessages.add("KeyUp", { + "id": 61, + "byteLength": 1, + // keyCode + "structure": ["uint8"] + }); + toStreamerMessages.add("KeyPress", { + "id": 62, + "byteLength": 2, + // charcode + "structure": ["uint16"] + }); + // Mouse Input Messages. Range = 70..79. + toStreamerMessages.add("MouseEnter", { + "id": 70, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("MouseLeave", { + "id": 71, + "byteLength": 0, + "structure": [] + }); + toStreamerMessages.add("MouseDown", { + "id": 72, + "byteLength": 5, + // button x y + "structure": ["uint8", "uint16", "uint16"] + }); + toStreamerMessages.add("MouseUp", { + "id": 73, + "byteLength": 5, + // button x y + "structure": ["uint8", "uint16", "uint16"] + }); + toStreamerMessages.add("MouseMove", { + "id": 74, + "byteLength": 8, + // x y deltaX deltaY + "structure": ["uint16", "uint16", "int16", "int16"] + }); + toStreamerMessages.add("MouseWheel", { + "id": 75, + "byteLength": 6, + // delta x y + "structure": ["int16", "uint16", "uint16"] + }); + toStreamerMessages.add("MouseDouble", { + "id": 76, + "byteLength": 5, + // button x y + "structure": ["uint8", "uint16", "uint16"] + }); + // Touch Input Messages. Range = 80..89. + toStreamerMessages.add("TouchStart", { + "id": 80, + "byteLength": 8, + // numtouches(1) x y idx force valid + "structure": ["uint8", "uint16", "uint16", "uint8", "uint8", "uint8"] + }); + toStreamerMessages.add("TouchEnd", { + "id": 81, + "byteLength": 8, + // numtouches(1) x y idx force valid + "structure": ["uint8", "uint16", "uint16", "uint8", "uint8", "uint8"] + }); + toStreamerMessages.add("TouchMove", { + "id": 82, + "byteLength": 8, + // numtouches(1) x y idx force valid + "structure": ["uint8", "uint16", "uint16", "uint8", "uint8", "uint8"] + }); + // Gamepad Input Messages. Range = 90..99 + toStreamerMessages.add("GamepadButtonPressed", { + "id": 90, + "byteLength": 3, + // ctrlerId button isRepeat + "structure": ["uint8", "uint8", "uint8"] + }); + toStreamerMessages.add("GamepadButtonReleased", { + "id": 91, + "byteLength": 3, + // ctrlerId button isRepeat(0) + "structure": ["uint8", "uint8", "uint8"] + }); + toStreamerMessages.add("GamepadAnalog", { + "id": 92, + "byteLength": 10, + // ctrlerId button analogValue + "structure": ["uint8", "uint8", "double"] + }); + + fromStreamerMessages.add("QualityControlOwnership", 0); + fromStreamerMessages.add("Response", 1); + fromStreamerMessages.add("Command", 2); + fromStreamerMessages.add("FreezeFrame", 3); + fromStreamerMessages.add("UnfreezeFrame", 4); + fromStreamerMessages.add("VideoEncoderAvgQP", 5); + fromStreamerMessages.add("LatencyTest", 6); + fromStreamerMessages.add("InitialSettings", 7); + fromStreamerMessages.add("FileExtension", 8); + fromStreamerMessages.add("FileMimeType", 9); + fromStreamerMessages.add("FileContents", 10); + fromStreamerMessages.add("TestEcho", 11); + fromStreamerMessages.add("InputControlOwnership", 12); + fromStreamerMessages.add("Protocol", 255); +} + +function registerMessageHandlers() { + registerMessageHandler(MessageDirection.FromStreamer, "QualityControlOwnership", onQualityControlOwnership); + registerMessageHandler(MessageDirection.FromStreamer, "Response", onResponse); + registerMessageHandler(MessageDirection.FromStreamer, "Command", onCommand); + registerMessageHandler(MessageDirection.FromStreamer, "FreezeFrame", onFreezeFrameMessage); + registerMessageHandler(MessageDirection.FromStreamer, "UnfreezeFrame", invalidateFreezeFrameOverlay); + registerMessageHandler(MessageDirection.FromStreamer, "VideoEncoderAvgQP", onVideoEncoderAvgQP); + registerMessageHandler(MessageDirection.FromStreamer, "LatencyTest", onLatencyTestMessage); + registerMessageHandler(MessageDirection.FromStreamer, "InitialSettings", onInitialSettings); + registerMessageHandler(MessageDirection.FromStreamer, "FileExtension", onFileExtension); + registerMessageHandler(MessageDirection.FromStreamer, "FileMimeType", onFileMimeType); + registerMessageHandler(MessageDirection.FromStreamer, "FileContents", onFileContents); + registerMessageHandler(MessageDirection.FromStreamer, "TestEcho", () => {/* Do nothing */ }); + registerMessageHandler(MessageDirection.FromStreamer, "InputControlOwnership", onInputControlOwnership); + registerMessageHandler(MessageDirection.FromStreamer, "Protocol", onProtocolMessage); + + registerMessageHandler(MessageDirection.ToStreamer, "IFrameRequest", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "RequestQualityControl", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "FpsRequest", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "AverageBitrateRequest", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "StartStreaming", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "StopStreaming", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "LatencyTest", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "RequestInitialSettings", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "TestEcho", () => { /* Do nothing */}); + registerMessageHandler(MessageDirection.ToStreamer, "UIInteraction", emitUIInteraction); + registerMessageHandler(MessageDirection.ToStreamer, "Command", emitCommand); + registerMessageHandler(MessageDirection.ToStreamer, "KeyDown", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "KeyUp", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "KeyPress", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseEnter", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseLeave", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseDown", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseUp", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseMove", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseWheel", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "MouseDouble", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "TouchStart", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "TouchEnd", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "TouchMove", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "GamepadButtonPressed", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "GamepadButtonReleased", sendMessageToStreamer); + registerMessageHandler(MessageDirection.ToStreamer, "GamepadAnalog", sendMessageToStreamer); +} + +function registerMessageHandler(messageDirection, messageType, messageHandler) { + switch (messageDirection) { + case MessageDirection.ToStreamer: + toStreamerHandlers[messageType] = messageHandler; + break; + case MessageDirection.FromStreamer: + fromStreamerHandlers[messageType] = messageHandler; + break; + default: + console.log(`Unknown message direction ${messageDirection}`); + } +} + +function onQualityControlOwnership(data) { + let view = new Uint8Array(data); + let ownership = view[1] === 0 ? false : true; + console.log("Received quality controller message, will control quality: " + ownership); + qualityController = ownership; + // If we own the quality control, we can't relinquish it. We only lose + // quality control when another peer asks for it + if (qualityControlOwnershipCheckBox !== null) { + qualityControlOwnershipCheckBox.disabled = ownership; + qualityControlOwnershipCheckBox.checked = ownership; + } +} + +function onResponse(data) { + let response = new TextDecoder("utf-16").decode(data.slice(1)); + for (let listener of responseEventListeners.values()) { + listener(response); + } +} + +function onCommand(data) { + let commandAsString = new TextDecoder("utf-16").decode(data.slice(1)); + console.log(commandAsString); + let command = JSON.parse(commandAsString); + if (command.command === 'onScreenKeyboard') { + showOnScreenKeyboard(command); + } +} + +function onFreezeFrameMessage(data) { + let view = new Uint8Array(data); + processFreezeFrameMessage(view); +} + +function onVideoEncoderAvgQP(data) { + VideoEncoderQP = new TextDecoder("utf-16").decode(data.slice(1)); +} + +function onLatencyTestMessage(data) { + let latencyTimingsAsString = new TextDecoder("utf-16").decode(data.slice(1)); + console.log("Got latency timings from UE."); + console.log(latencyTimingsAsString); + let latencyTimingsFromUE = JSON.parse(latencyTimingsAsString); + if (webRtcPlayerObj) { + webRtcPlayerObj.latencyTestTimings.SetUETimings(latencyTimingsFromUE); + } +} + +function onInitialSettings(data) { + let settingsString = new TextDecoder("utf-16").decode(data.slice(1)); + let settingsJSON = JSON.parse(settingsString); + + if (settingsJSON.PixelStreaming) { + let allowConsoleCommands = settingsJSON.PixelStreaming.AllowPixelStreamingCommands; + if (allowConsoleCommands === false) { + console.warn("-AllowPixelStreamingCommands=false, sending arbitray console commands from browser to UE is disabled."); + } + let disableLatencyTest = settingsJSON.PixelStreaming.DisableLatencyTest; + if (disableLatencyTest) { + document.getElementById("test-latency-button").disabled = true; + document.getElementById("test-latency-button").title = "Disabled by -PixelStreamingDisableLatencyTester=true"; + console.warn("-PixelStreamingDisableLatencyTester=true, requesting latency report from the the browser to UE is disabled."); + } + } + if (settingsJSON.Encoder) { + document.getElementById('encoder-min-qp-text').value = settingsJSON.Encoder.MinQP; + document.getElementById('encoder-max-qp-text').value = settingsJSON.Encoder.MaxQP; + } + if (settingsJSON.WebRTC) { + document.getElementById("webrtc-fps-text").value = settingsJSON.WebRTC.FPS; + // reminder bitrates are sent in bps but displayed in kbps + document.getElementById("webrtc-min-bitrate-text").value = settingsJSON.WebRTC.MinBitrate / 1000; + document.getElementById("webrtc-max-bitrate-text").value = settingsJSON.WebRTC.MaxBitrate / 1000; + } +} + +function onFileExtension(data) { + let view = new Uint8Array(data); + processFileExtension(view); +} + +function onFileMimeType(data) { + let view = new Uint8Array(data); + processFileMimeType(view); +} + +function onFileContents(data) { + let view = new Uint8Array(data); + processFileContents(view); +} + +function onInputControlOwnership(data) { + let view = new Uint8Array(data); + let ownership = view[1] === 0 ? false : true; + console.log("Received input controller message - will your input control the stream: " + ownership); + inputController = ownership; +} + +function onProtocolMessage(data) { + try { + let protocolString = new TextDecoder("utf-16").decode(data.slice(1)); + let protocolJSON = JSON.parse(protocolString); + if (!protocolJSON.hasOwnProperty("Direction")) { + throw new Error('Malformed protocol received. Ensure the protocol message contains a direction'); + } + let direction = protocolJSON.Direction; + delete protocolJSON.Direction; + console.log(`Received new ${ direction == MessageDirection.FromStreamer ? "FromStreamer" : "ToStreamer" } protocol. Updating existing protocol...`); + Object.keys(protocolJSON).forEach((messageType) => { + let message = protocolJSON[messageType]; + switch (direction) { + case MessageDirection.ToStreamer: + // Check that the message contains all the relevant params + if (!message.hasOwnProperty("id") || !message.hasOwnProperty("byteLength")) { + console.error(`ToStreamer->${messageType} protocol definition was malformed as it didn't contain at least an id and a byteLength\n + Definition was: ${JSON.stringify(message, null, 2)}`); + // return in a forEach is equivalent to a continue in a normal for loop + return; + } + if(message.byteLength > 0 && !message.hasOwnProperty("structure")) { + // If we specify a bytelength, will must have a corresponding structure + console.error(`ToStreamer->${messageType} protocol definition was malformed as it specified a byteLength but no accompanying structure`); + // return in a forEach is equivalent to a continue in a normal for loop + return; + } + + if (toStreamerHandlers[messageType]) { + // If we've registered a handler for this message type we can add it to our supported messages. ie registerMessageHandler(...) + toStreamerMessages.add(messageType, message); + } else { + console.error(`There was no registered handler for "${messageType}" - try adding one using registerMessageHandler(MessageDirection.ToStreamer, "${messageType}", myHandler)`); + } + break; + case MessageDirection.FromStreamer: + // Check that the message contains all the relevant params + if (!message.hasOwnProperty("id")) { + console.error(`FromStreamer->${messageType} protocol definition was malformed as it didn't contain at least an id\n + Definition was: ${JSON.stringify(message, null, 2)}`); + // return in a forEach is equivalent to a continue in a normal for loop + return; + } + if (fromStreamerHandlers[messageType]) { + // If we've registered a handler for this message type. ie registerMessageHandler(...) + fromStreamerMessages.add(messageType, message.id); + } else { + console.error(`There was no registered handler for "${message}" - try adding one using registerMessageHandler(MessageDirection.FromStreamer, "${messageType}", myHandler)`); + } + break; + default: + throw new Error(`Unknown direction: ${direction}`); + } + }); + + // Once the protocol has been received, we can send our control messages + requestInitialSettings(); + requestQualityControl(); + } catch (e) { + console.log(e); + } +} + +// https://w3c.github.io/gamepad/#remapping +const gamepadLayout = { + // Buttons + RightClusterBottomButton: 0, + RightClusterRightButton: 1, + RightClusterLeftButton: 2, + RightClusterTopButton: 3, + LeftShoulder: 4, + RightShoulder: 5, + LeftTrigger: 6, + RightTrigger: 7, + SelectOrBack: 8, + StartOrForward: 9, + LeftAnalogPress: 10, + RightAnalogPress: 11, + LeftClusterTopButton: 12, + LeftClusterBottomButton: 13, + LeftClusterLeftButton: 14, + LeftClusterRightButton: 15, + CentreButton: 16, + // Axes + LeftStickHorizontal: 0, + LeftStickVertical: 1, + RightStickHorizontal: 2, + RightStickVertical: 3 +}; + +function scanGamepads() { + let gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []); + for (let i = 0; i < gamepads.length; i++) { + if (gamepads[i] && (gamepads[i].index in controllers)) { + controllers[gamepads[i].index].currentState = gamepads[i]; + } + } +} + +function updateStatus() { + scanGamepads(); + // Iterate over multiple controllers in the case the mutiple gamepads are connected + for (let j in controllers) { + let controller = controllers[j]; + let currentState = controller.currentState; + let prevState = controller.prevState; + // Iterate over buttons + for (let i = 0; i < currentState.buttons.length; i++) { + let currButton = currentState.buttons[i]; + let prevButton = prevState.buttons[i]; + if (currButton.pressed) { + // press + if (i == gamepadLayout.LeftTrigger) { + // UEs left analog has a button index of 5 + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, 5, currButton.value]); + } else if (i == gamepadLayout.RightTrigger) { + // UEs right analog has a button index of 6 + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, 6, currButton.value]); + } else { + toStreamerHandlers.GamepadButtonPressed("GamepadButtonPressed", [j, i, prevButton.pressed]); + } + } else if (!currButton.pressed && prevButton.pressed) { + // release + if (i == gamepadLayout.LeftTrigger) { + // UEs left analog has a button index of 5 + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, 5, 0]); + } else if (i == gamepadLayout.RightTrigger) { + // UEs right analog has a button index of 6 + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, 6, 0]); + } else { + toStreamerHandlers.GamepadButtonReleased("GamepadButtonReleased", [j, i]); + } + } + } + // Iterate over gamepad axes (we will increment in lots of 2 as there is 2 axes per stick) + for (let i = 0; i < currentState.axes.length; i += 2) { + // Horizontal axes are even numbered + let x = parseFloat(currentState.axes[i].toFixed(4)); + + // Vertical axes are odd numbered + // https://w3c.github.io/gamepad/#remapping Gamepad browser side standard mapping has positive down, negative up. This is downright disgusting. So we fix it. + let y = -parseFloat(currentState.axes[i + 1].toFixed(4)); + + // UE's analog axes follow the same order as the browsers, but start at index 1 so we will offset as such + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, i + 1, x]); // Horizontal axes, only offset by 1 + toStreamerHandlers.GamepadAnalog("GamepadAnalog", [j, i + 2, y]); // Vertical axes, offset by two (1 to match UEs axes convention and then another 1 for the vertical axes) + } + controllers[j].prevState = currentState; + } + rAF(updateStatus); +} + +function gamepadConnectHandler(e) { + console.log("Gamepad connect handler"); + gamepad = e.gamepad; + controllers[gamepad.index] = {}; + controllers[gamepad.index].currentState = gamepad; + controllers[gamepad.index].prevState = gamepad; + console.log("Gamepad: " + gamepad.id + " connected"); + rAF(updateStatus); +} + +function gamepadDisconnectHandler(e) { + console.log("Gamepad disconnect handler"); + console.log("Gamepad: " + e.gamepad.id + " disconnected"); + delete controllers[e.gamepad.index]; +} + + +function fullscreen() { + // if already full screen; exit + // else go fullscreen + if ( + document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement || + document.msFullscreenElement + ) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else { + let element; + //HTML elements controls + if(!(document.fullscreenEnabled || document.webkitFullscreenEnabled)) { + // Chrome and FireFox on iOS can only fullscreen a