PixelStreamingInfrastructure/Matchmaker/matchmaker.js

304 lines
10 KiB
JavaScript

// 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,
EnableWebserver: 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');
});
}
let htmlDirectory = 'html/sample'
if(config.EnableWebserver) {
// Setup folders
if (fs.existsSync('html/custom')) {
app.use(express.static(path.join(__dirname, '/html/custom')))
htmlDirectory = 'html/custom'
} else {
app.use(express.static(path.join(__dirname, '/html/sample')))
}
}
// 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) {
// find check if a custom template should be used or the sample one
let html = fs.readFileSync(`${htmlDirectory}/queue/queue.html`, { encoding: 'utf8' })
html = html.replace(/\$\{cirrusServers\.size\}/gm, cirrusServers.size)
res.setHeader('content-type', 'text/html')
res.send(html)
}
// 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);
});