├── .gitignore ├── Procfile ├── README.md ├── app.js ├── bin └── www ├── models └── Server.js ├── package.json ├── public ├── favico.ico ├── images │ └── icon_x128.png └── stylesheets │ └── style.css ├── routes └── index.js └── views ├── Procfile ├── app.js ├── bin └── www ├── error.jade ├── index.jade ├── layout.jade ├── models └── Server.js ├── package.json ├── public ├── favico.ico ├── images │ └── icon_x128.png └── stylesheets │ └── style.css ├── routes └── index.js └── server.jade /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 31 | node_modules 32 | 33 | .vscode 34 | 35 | run_local 36 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/www -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # master-server 2 | Master server to provide an API for retrieving all registered public OpenRCT2 servers. 3 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var cloudflare = require('cloudflare-express'); 8 | var autoIncrement = require('mongoose-auto-increment'); 9 | 10 | var mongoose = require('mongoose'); 11 | var dbConnectionString = process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://localhost/orctmaster'; 12 | 13 | var options = { 14 | server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }, 15 | replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } } 16 | }; 17 | 18 | mongoose.connect(dbConnectionString, options, function(err) { 19 | if(err) { 20 | console.log('database connection error', err); 21 | } else { 22 | console.log('database connection successful, will store OpenRCT2 server data'); 23 | } 24 | }); 25 | 26 | // make sure we intialized MAI 27 | var connection = mongoose.createConnection(dbConnectionString); 28 | autoIncrement.initialize(connection); 29 | 30 | var routes = require('./routes/index'); 31 | 32 | var returnType = function returnType(req, res, next) { 33 | req.returnJSON = false; 34 | if (!req.accepts('html') && req.accepts('json')) { 35 | req.returnJSON = true; 36 | } 37 | next(); 38 | } 39 | 40 | var app = express(); 41 | 42 | // view engine setup 43 | app.set('views', path.join(__dirname, 'views')); 44 | app.set('view engine', 'jade'); 45 | 46 | app.use(favicon(path.join(__dirname, 'public', 'favico.ico'))); 47 | app.use(logger('dev')); 48 | app.use(bodyParser.json()); 49 | app.use(bodyParser.urlencoded({ extended: false })); 50 | app.use(cookieParser()); 51 | app.use(express.static(path.join(__dirname, 'public'))); 52 | app.use(cloudflare.restore()); 53 | app.enable('trust proxy'); 54 | 55 | app.get('*',returnType); 56 | app.use('/', routes); 57 | 58 | // catch 404 and forward to error handler 59 | app.use(function(req, res, next) { 60 | var err = new Error('Not Found'); 61 | err.status = 404; 62 | next(err); 63 | }); 64 | 65 | // error handlers 66 | 67 | // development error handler 68 | // will print stacktrace 69 | if (app.get('env') === 'development') { 70 | app.use(function(err, req, res, next) { 71 | res.status(err.status || 500); 72 | res.render('error', { 73 | message: err.message, 74 | error: err 75 | }); 76 | }); 77 | } 78 | 79 | // production error handler 80 | // no stacktraces leaked to user 81 | app.use(function(err, req, res, next) { 82 | res.status(err.status || 500); 83 | res.render('error', { 84 | message: err.message, 85 | error: {} 86 | }); 87 | }); 88 | 89 | 90 | module.exports = app; 91 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('OpenRCT2MasterServer:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3001'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /models/Server.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var autoIncrement = require('mongoose-auto-increment'); 3 | 4 | var ServerSchema = new mongoose.Schema({ 5 | key: { type: String, index: true, unique: true}, 6 | ip : { 7 | v4: { type: [String] }, 8 | v6: { type: [String] } 9 | }, 10 | port: { type: Number}, 11 | name: { type: String }, 12 | dedicated: { type: Boolean }, 13 | requiresPassword: { type: Boolean }, 14 | description: { type: String }, 15 | version: { type: String }, 16 | players: { type: Number }, 17 | maxPlayers: { type: Number }, 18 | supportsIPv4: { type: Boolean, default: false}, 19 | supportsIPv6: { type: Boolean, default: false}, 20 | gameInfo: { 21 | mapSize: {type: Number}, 22 | guests: {type: Number}, 23 | day: {type: Number}, 24 | month: {type: Number}, 25 | parkValue: {type: Number}, 26 | cash: {type: Number} 27 | }, 28 | provider: { 29 | name: { type: String }, 30 | email: { type: String }, 31 | website: { type: String } 32 | }, 33 | updated_at: { type: Date, default: Date.now } 34 | }); 35 | 36 | ServerSchema.plugin(autoIncrement.plugin, { model: 'Server', field: 'serverId' }); 37 | 38 | module.exports = mongoose.model('Server', ServerSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenRCT2MasterServer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cloudflare-express": "0.0.1", 11 | "cookie-parser": "~1.3.5", 12 | "debug": "~2.2.0", 13 | "express": "~4.13.1", 14 | "jade": "~1.11.0", 15 | "mongoose": "^4.2.4", 16 | "mongoose-auto-increment": "^5.0.1", 17 | "morgan": "~1.6.1", 18 | "serve-favicon": "~2.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/favico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRCT2/master-server/b324fb7f421cab72ce97eceb9f212a7b3844ea9f/public/favico.ico -------------------------------------------------------------------------------- /public/images/icon_x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRCT2/master-server/b324fb7f421cab72ce97eceb9f212a7b3844ea9f/public/images/icon_x128.png -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | background: #5BA3E7; 5 | 6 | } 7 | 8 | .wrapper { 9 | margin: 0 auto; 10 | width: 960px; 11 | text-align: center; 12 | color: white; 13 | } 14 | 15 | a { 16 | color: #ffffff; 17 | } 18 | 19 | .brand { 20 | width: 128px; 21 | height: 128px; 22 | color: white; 23 | text-shadow: 2px 2px 2px #000, -2px -2px 2px #000, 2px -2px 2px #000, -2px 2px 2px #000; 24 | text-align: center; 25 | line-height: 128px; 26 | background: url("../images/icon_x128.png") center center no-repeat; 27 | display: block; 28 | text-decoration: none; 29 | margin: 0 auto; 30 | } 31 | 32 | .serverList { 33 | width: 100%; 34 | max-width: 100%; 35 | margin-bottom: 20px; 36 | border-spacing: 0px; 37 | border-collapse: collapse; 38 | } 39 | 40 | .serverList > thead > tr > td { 41 | font-weight: bold; 42 | vertical-align: bottom; 43 | border-bottom: 2px solid #DDD; 44 | padding: 8px; 45 | line-height: 1.42857; 46 | } 47 | 48 | .serverList > tbody > tr > td { 49 | padding: 8px; 50 | line-height: 1.42857; 51 | vertical-align: top; 52 | border-top: 1px solid #DDD; 53 | } 54 | 55 | @media screen and (max-width: 960px) { 56 | .wrapper { 57 | margin: 0; 58 | width: 100%; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var net = require('net'); 4 | var Server = require('../models/Server'); 5 | var ipaddr = require('ipaddr.js'); 6 | 7 | var requestGameInfo = function requestGameInfo(req, res, address, port, key) { 8 | var netComGI = new Buffer([0, 4, 0, 0, 0, 9]); 9 | var socket = new net.Socket(); 10 | var hasErrors = false; 11 | var errors; 12 | var data = {}; 13 | socket.setTimeout(5500, function () { 14 | errors = {}; 15 | errors.code = 'TIMEOUT'; 16 | errors.message = 'Socket timeout'; 17 | hasErrors = true; 18 | socket.destroy(); 19 | }) 20 | try { 21 | socket.connect(port, address, function() { 22 | socket.write(netComGI); 23 | }); 24 | } catch (e) { 25 | errors = e; 26 | console.error(e); 27 | hasErrors = true; 28 | } 29 | 30 | socket.on('data', function (bdata) { 31 | var buffData = bdata.toJSON().data; 32 | if (buffData.length > 0) { 33 | var size = ((buffData[0] << 8) | buffData[1]); 34 | var packetType = ((buffData[2] << 24) | (buffData[3] << 16) | (buffData[4] << 8) | (buffData[5])); 35 | if (packetType == 9) { 36 | data = JSON.parse(bdata.toString('utf8', 6, 6 + size - 5)); 37 | socket.destroy(); 38 | } 39 | } 40 | 41 | }); 42 | 43 | socket.on('close', function() { 44 | if (hasErrors) { 45 | if (errors.code == 'ECONNREFUSED' || errors.code == 'EHOSTUNREACH' || errors.code == 'TIMEOUT') { 46 | console.error(errors); 47 | res.json({status: 404, message: 'Unable to reach game server, make sure your ports are open.'}); 48 | } else { 49 | console.error(errors); 50 | res.json({status: 500, message: 'An unknown error has occured. If this state persists, please contact the developers.'}); 51 | } 52 | } else { 53 | updateServer(req, res, address, port, key, data); 54 | } 55 | }); 56 | 57 | socket.on('error', function (err) { 58 | errors = err; 59 | console.error(err); 60 | hasErrors = true; 61 | }) 62 | } 63 | 64 | // update server list and display it 65 | router.get('/', function(req, res, next) { 66 | removeOldServers(req, res); 67 | }); 68 | 69 | // register a new server 70 | router.post('/', function(req, res, next) { 71 | var body = req.body; 72 | var address = req.cf_ip; 73 | var port = body.port; 74 | var key = body.key; 75 | register(req, res, key, address, port); 76 | }); 77 | 78 | // update a server as a result of a heartbeat 79 | router.put('/', function(req, res, next) { 80 | var body = req.body; 81 | var players = body.players; 82 | var token = body.token; 83 | var gameInfo = body.gameInfo; 84 | heartbeat(req, res, token, players, gameInfo); 85 | }); 86 | 87 | var daysInMonth = [31, 30, 31, 30, 31, 31, 30, 31]; 88 | var months = ['March', 'April', 'May', 'June', 'July', 'August', 'September', 'October'] 89 | 90 | var daySuffix = function daySuffix(day) { 91 | var j = day % 10, 92 | k = day % 100; 93 | if (j == 1 && k != 11) { 94 | return day + "st"; 95 | } 96 | if (j == 2 && k != 12) { 97 | return day + "nd"; 98 | } 99 | if (j == 3 && k != 13) { 100 | return day + "rd"; 101 | } 102 | return day + "th"; 103 | } 104 | 105 | var formatNumVals = function formatNumVals(x) { 106 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 107 | } 108 | 109 | router.get('/:id', function(req, res, next) { 110 | var id = parseInt(req.params.id,10); 111 | if (id) { 112 | Server.findOne({serverId: id}, function (err, server) { 113 | if (!err) { 114 | if (server) { 115 | var customGI = {} 116 | var monthNum = Math.floor(server.gameInfo.month % 8); 117 | customGI.month = months[monthNum]; 118 | var dayNum = Math.floor((server.gameInfo.day / 0x10000) * daysInMonth[monthNum]) + 1; 119 | customGI.day = daySuffix(dayNum); 120 | customGI.year = Math.floor(server.gameInfo.month / 8) + 1; 121 | if (server.gameInfo.cash) { 122 | customGI.cash = formatNumVals(Math.floor(server.gameInfo.cash/10)); 123 | } 124 | if (server.gameInfo.parkValue) { 125 | customGI.parkValue = formatNumVals(Math.floor(server.gameInfo.parkValue/10)); 126 | } 127 | if (server.gameInfo.guests) { 128 | customGI.guests = formatNumVals(server.gameInfo.guests); 129 | } 130 | if (req.returnJSON) { 131 | var serverResponse = { 132 | name: server.name, 133 | description: server.description, 134 | requiresPassword: server.requiresPassword, 135 | players: server.players, 136 | maxPlayers: server.maxPlayers, 137 | dedicated: server.dedicated, 138 | gameInfo: server.gameInfo, 139 | customGI: customGI, 140 | provider: server.provider 141 | } 142 | res.json({status: 200, server: serverResponse}); 143 | } else { 144 | server.customGI = customGI; 145 | res.render('server', {server: server}); 146 | } 147 | } else { 148 | var msg = 'Server with the specified ID was not found'; 149 | if (req.returnJSON) { 150 | res.json({status: 500, message: msg}); 151 | } else { 152 | res.render('server', {error: true, message: msg}); 153 | } 154 | } 155 | } else { 156 | var msg = 'Unable to retreive server information from the database'; 157 | if (req.returnJSON) { 158 | res.json({status: 500, message: msg}); 159 | } else { 160 | res.render('server', {error: true, message: msg}); 161 | } 162 | } 163 | }); 164 | } else { 165 | var msg = 'The requested ID is not in the accepted format. Make sure the ID is a numeric value.'; 166 | if (req.returnJSON) { 167 | res.json({status: 400, message: msg}); 168 | } else { 169 | res.render('server', {error: true, message: msg}); 170 | } 171 | } 172 | }); 173 | 174 | var heartbeat = function heartbeat(req, res, token, players, gameInfo) { 175 | Server.findById(token, function (err, server) { 176 | if (!err) { 177 | if (server) { 178 | server.players = players; 179 | server.gameInfo = gameInfo; 180 | server.updated_at = new Date(); 181 | server.save(function (err, serverSave) { 182 | if (!err) { 183 | res.json({status: 200}); 184 | } else { 185 | console.error(err); 186 | res.json({status: 500, message: 'Could not save the server entry'}); 187 | } 188 | }); 189 | } else { 190 | res.json({status: 401, message: 'Invalid token'}); 191 | } 192 | } else { 193 | console.error(err); 194 | res.json({status: 500, message: 'Database failiure'}); 195 | } 196 | }); 197 | } 198 | 199 | var register = function register(req, res, key, address, port) { 200 | Server.findOne({key: key}, function (err, server) { 201 | if (!err) { 202 | if (server) { 203 | if (ipaddr.IPv4.isValid(address)) { 204 | server.ip.v4.push(address); 205 | server.supportsIPv4 = true; 206 | } else if (ipaddr.IPv6.isValid(address)) { 207 | server.ip.v6.push(address); 208 | server.supportsIPv6 = true; 209 | } 210 | server.save(function (saveErr) { 211 | if (!saveErr) { 212 | res.json({status: 200, token: server._id}); 213 | } else { 214 | console.error(saveErr); 215 | res.json({status: 500, message: 'Save failed'}); 216 | } 217 | }); 218 | } else { 219 | requestGameInfo(req, res, address, port, key); 220 | } 221 | } else { 222 | console.error(err); 223 | res.json({status: 500, message: 'Database failiure'}); 224 | } 225 | }); 226 | } 227 | 228 | 229 | var updateServer = function updateServer(req, res, address, port, key, data) { 230 | var server = { 231 | key: key, 232 | ip : { 233 | v4: [], 234 | v6: [] 235 | }, 236 | port: port, 237 | name: data.name, 238 | requiresPassword: data.requiresPassword, 239 | description: data.description, 240 | version: data.version, 241 | players: data.players, 242 | maxPlayers: data.maxPlayers, 243 | supportsIPv4: false, 244 | supportsIPv6: false, 245 | provider: data.provider 246 | } 247 | if (ipaddr.IPv4.isValid(address)) { 248 | server.ip.v4.push(address); 249 | server.supportsIPv4 = true; 250 | } else if (ipaddr.IPv6.isValid(address)) { 251 | server.ip.v6.push(address); 252 | server.supportsIPv6 = true; 253 | } 254 | Server.create(server, function (err, newServer) { 255 | if (!err) { 256 | res.json({status: 200, token: newServer._id}); 257 | } else { 258 | console.error(err); 259 | res.json({status: 500, message: "An error ocured while updating the server record"}); 260 | } 261 | }); 262 | } 263 | 264 | var listServers = function listServers(req, res) { 265 | Server.find().exec(function (err, data) { 266 | if (!err) { 267 | var responseData = [] 268 | data.forEach( function (server) { 269 | var customGI = {} 270 | var monthNum = Math.floor(server.gameInfo.month % 8); 271 | customGI.month = months[monthNum]; 272 | var dayNum = Math.floor((server.gameInfo.day / 0x10000) * daysInMonth[monthNum]) + 1; 273 | customGI.day = daySuffix(dayNum); 274 | customGI.year = Math.floor(server.gameInfo.month / 8) + 1; 275 | if (server.gameInfo.cash) { 276 | customGI.cash = formatNumVals(Math.floor(server.gameInfo.cash/10)); 277 | } 278 | if (server.gameInfo.parkValue) { 279 | customGI.parkValue = formatNumVals(Math.floor(server.gameInfo.parkValue/10)); 280 | } 281 | if (server.gameInfo.guests) { 282 | customGI.guests = formatNumVals(server.gameInfo.guests); 283 | } 284 | var serverData = { 285 | name: server.name, 286 | description: server.description, 287 | version: server.version, 288 | players: server.players, 289 | maxPlayers: server.maxPlayers, 290 | port: server.port, 291 | dedicated: server.dedicated, 292 | ip: server.ip, 293 | requiresPassword: server.requiresPassword, 294 | supportsIPv4: server.supportsIPv4, 295 | supportsIPv6: server.supportsIPv6, 296 | serverId: server.serverId, 297 | gameInfo: server.gameInfo, 298 | customGI: customGI, 299 | provider: server.provider 300 | } 301 | responseData.push(serverData); 302 | }); 303 | if (req.returnJSON) { 304 | res.json({status: 200, servers: responseData}); 305 | } else { 306 | res.render('index', {data:responseData, title: "OpenRCT2 Master Server"}); 307 | } 308 | } else { 309 | console.error(err); 310 | var msg = 'Error fetching the server list. If this error persists, please contact the developers.'; 311 | if (req.returnJSON) { 312 | res.json({status: 500, message: msg}); 313 | } else { 314 | res.render('index', {error: true, message: msg, title: "OpenRCT2 Master Server | Error"}); 315 | } 316 | } 317 | }); 318 | } 319 | 320 | var removeOldServers = function removeOldServers(req, res) { 321 | Server.find().where('updated_at').lt(new Date(new Date() - 1000 * 70)).remove().exec(function(err, data) { 322 | if (req) { 323 | if (!err) { 324 | listServers(req, res); 325 | } else { 326 | console.error(err); 327 | var msg = 'Error cleaning up the server list. If this error persists, please contact the developers.'; 328 | if (req.returnJSON) { 329 | res.json({status: 500, message: msg}); 330 | } else { 331 | res.render('index', {error: true, message: msg, title: "OpenRCT2 Master Server | Error"}) 332 | } 333 | } 334 | } else { 335 | if (!err) { 336 | console.log("Database refreshed on load"); 337 | } else { 338 | console.error(err); 339 | } 340 | } 341 | }); 342 | } 343 | 344 | removeOldServers(); 345 | 346 | module.exports = router; 347 | -------------------------------------------------------------------------------- /views/Procfile: -------------------------------------------------------------------------------- 1 | web: ./bin/www -------------------------------------------------------------------------------- /views/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var favicon = require('serve-favicon'); 4 | var logger = require('morgan'); 5 | var cookieParser = require('cookie-parser'); 6 | var bodyParser = require('body-parser'); 7 | var cloudflare = require('cloudflare-express'); 8 | var autoIncrement = require('mongoose-auto-increment'); 9 | 10 | var mongoose = require('mongoose'); 11 | var dbConnectionString = process.env.MONGOLAB_URI || process.env.MONGODB_URI || 'mongodb://localhost/orctmaster'; 12 | 13 | mongoose.connect(dbConnectionString, function(err) { 14 | if(err) { 15 | console.log('database connection error', err); 16 | } else { 17 | console.log('database connection successful, will store OpenRCT2 server data'); 18 | } 19 | }); 20 | 21 | // make sure we intialized MAI 22 | var connection = mongoose.createConnection(dbConnectionString); 23 | autoIncrement.initialize(connection); 24 | 25 | var routes = require('./routes/index'); 26 | 27 | var returnType = function returnType(req, res, next) { 28 | req.returnJSON = false; 29 | if (!req.accepts('html') && req.accepts('json')) { 30 | req.returnJSON = true; 31 | } 32 | next(); 33 | } 34 | 35 | var app = express(); 36 | 37 | // view engine setup 38 | app.set('views', path.join(__dirname, 'views')); 39 | app.set('view engine', 'jade'); 40 | 41 | app.use(favicon(path.join(__dirname, 'public', 'favico.ico'))); 42 | app.use(logger('dev')); 43 | app.use(bodyParser.json()); 44 | app.use(bodyParser.urlencoded({ extended: false })); 45 | app.use(cookieParser()); 46 | app.use(express.static(path.join(__dirname, 'public'))); 47 | app.use(cloudflare.restore()); 48 | app.enable('trust proxy'); 49 | 50 | app.get('*',returnType); 51 | app.use('/', routes); 52 | 53 | // catch 404 and forward to error handler 54 | app.use(function(req, res, next) { 55 | var err = new Error('Not Found'); 56 | err.status = 404; 57 | next(err); 58 | }); 59 | 60 | // error handlers 61 | 62 | // development error handler 63 | // will print stacktrace 64 | if (app.get('env') === 'development') { 65 | app.use(function(err, req, res, next) { 66 | res.status(err.status || 500); 67 | res.render('error', { 68 | message: err.message, 69 | error: err 70 | }); 71 | }); 72 | } 73 | 74 | // production error handler 75 | // no stacktraces leaked to user 76 | app.use(function(err, req, res, next) { 77 | res.status(err.status || 500); 78 | res.render('error', { 79 | message: err.message, 80 | error: {} 81 | }); 82 | }); 83 | 84 | 85 | module.exports = app; 86 | -------------------------------------------------------------------------------- /views/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('OpenRCT2MasterServer:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3001'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | a.backTo(href='/') Back to server list 5 | h1= message 6 | h2= error.status 7 | pre #{error.stack} 8 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | if error 5 | p Whooops!! 6 | p= message 7 | else 8 | p Public server list 9 | table.serverList 10 | thead 11 | tr 12 | td Name 13 | td Description 14 | td Version 15 | td Password Protected 16 | td Players 17 | td Dedicated server 18 | if data.length > 0 19 | tbody 20 | for server in data 21 | tr 22 | td 23 | a(href="/"+server.serverId)= server.name 24 | td= server.description 25 | td= server.version 26 | td= server.requiresPassword ? "Yes" : "No" 27 | td #{server.players}/#{server.maxPlayers} 28 | td= server.dedicated ? "Yes" : "No" 29 | 30 | if data.length == 0 31 | p No public servers registered at the moment. Check back later. 32 | 33 | 34 | -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | .wrapper 8 | a.brand(href='https://openrct2.io') OpenRCT2 9 | h2 Master Server 10 | block content 11 | -------------------------------------------------------------------------------- /views/models/Server.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var autoIncrement = require('mongoose-auto-increment'); 3 | 4 | var ServerSchema = new mongoose.Schema({ 5 | key: { type: String, index: true, unique: true}, 6 | ip : { 7 | v4: { type: [String] }, 8 | v6: { type: [String] } 9 | }, 10 | port: { type: Number}, 11 | name: { type: String }, 12 | dedicated: { type: Boolean }, 13 | requiresPassword: { type: Boolean }, 14 | description: { type: String }, 15 | version: { type: String }, 16 | players: { type: Number }, 17 | maxPlayers: { type: Number }, 18 | supportsIPv4: { type: Boolean, default: false}, 19 | supportsIPv6: { type: Boolean, default: false}, 20 | gameInfo: { 21 | mapSize: {type: Number}, 22 | guests: {type: Number}, 23 | day: {type: Number}, 24 | month: {type: Number}, 25 | parkValue: {type: Number}, 26 | cash: {type: Number} 27 | }, 28 | provider: { 29 | name: { type: String }, 30 | email: { type: String }, 31 | website: { type: String } 32 | }, 33 | updated_at: { type: Date, default: Date.now } 34 | }); 35 | 36 | ServerSchema.plugin(autoIncrement.plugin, { model: 'Server', field: 'serverId' }); 37 | 38 | module.exports = mongoose.model('Server', ServerSchema); -------------------------------------------------------------------------------- /views/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpenRCT2MasterServer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.13.2", 10 | "cloudflare-express": "0.0.1", 11 | "cookie-parser": "~1.3.5", 12 | "debug": "~2.2.0", 13 | "express": "~4.13.1", 14 | "jade": "~1.11.0", 15 | "mongoose": "^4.2.4", 16 | "mongoose-auto-increment": "^5.0.1", 17 | "morgan": "~1.6.1", 18 | "serve-favicon": "~2.3.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /views/public/favico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRCT2/master-server/b324fb7f421cab72ce97eceb9f212a7b3844ea9f/views/public/favico.ico -------------------------------------------------------------------------------- /views/public/images/icon_x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenRCT2/master-server/b324fb7f421cab72ce97eceb9f212a7b3844ea9f/views/public/images/icon_x128.png -------------------------------------------------------------------------------- /views/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | background: #5BA3E7; 5 | 6 | } 7 | 8 | .wrapper { 9 | margin: 0 auto; 10 | width: 960px; 11 | text-align: center; 12 | color: white; 13 | } 14 | 15 | a { 16 | color: #ffffff; 17 | } 18 | 19 | .brand { 20 | width: 128px; 21 | height: 128px; 22 | color: white; 23 | text-shadow: 2px 2px 2px #000, -2px -2px 2px #000, 2px -2px 2px #000, -2px 2px 2px #000; 24 | text-align: center; 25 | line-height: 128px; 26 | background: url("../images/icon_x128.png") center center no-repeat; 27 | display: block; 28 | text-decoration: none; 29 | margin: 0 auto; 30 | } 31 | 32 | .serverList { 33 | width: 100%; 34 | max-width: 100%; 35 | margin-bottom: 20px; 36 | border-spacing: 0px; 37 | border-collapse: collapse; 38 | } 39 | 40 | .serverList > thead > tr > td { 41 | font-weight: bold; 42 | vertical-align: bottom; 43 | border-bottom: 2px solid #DDD; 44 | padding: 8px; 45 | line-height: 1.42857; 46 | } 47 | 48 | .serverList > tbody > tr > td { 49 | padding: 8px; 50 | line-height: 1.42857; 51 | vertical-align: top; 52 | border-top: 1px solid #DDD; 53 | } 54 | 55 | @media screen and (max-width: 960px) { 56 | .wrapper { 57 | margin: 0; 58 | width: 100%; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /views/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var net = require('net'); 4 | var Server = require('../models/Server'); 5 | var ipaddr = require('ipaddr.js'); 6 | 7 | var requestGameInfo = function requestGameInfo(req, res, address, port, key) { 8 | var netComGI = new Buffer([0, 4, 0, 0, 0, 9]); 9 | var socket = new net.Socket(); 10 | var hasErrors = false; 11 | var errors; 12 | var data = {}; 13 | socket.setTimeout(5500, function () { 14 | errors = {}; 15 | errors.code = 'TIMEOUT'; 16 | errors.message = 'Socket timeout'; 17 | hasErrors = true; 18 | socket.destroy(); 19 | }) 20 | socket.connect(port, address, function() { 21 | socket.write(netComGI); 22 | }); 23 | 24 | socket.on('data', function (bdata) { 25 | var buffData = bdata.toJSON().data; 26 | if (buffData.length > 0) { 27 | var size = ((buffData[0] << 8) | buffData[1]); 28 | var packetType = ((buffData[2] << 24) | (buffData[3] << 16) | (buffData[4] << 8) | (buffData[5])); 29 | if (packetType == 9) { 30 | data = JSON.parse(bdata.toString('utf8', 6, 6 + size - 5)); 31 | socket.destroy(); 32 | } 33 | } 34 | 35 | }); 36 | 37 | socket.on('close', function() { 38 | if (hasErrors) { 39 | if (errors.code == 'ECONNREFUSED' || errors.code == 'EHOSTUNREACH' || errors.code == 'TIMEOUT') { 40 | console.error(errors); 41 | res.json({status: 404, message: 'Unable to reach game server, make sure your ports are open.'}); 42 | } else { 43 | console.error(errors); 44 | res.json({status: 500, message: 'An unknown error has occured. If this state persists, please contact the developers.'}); 45 | } 46 | } else { 47 | updateServer(req, res, address, port, key, data); 48 | } 49 | }); 50 | 51 | socket.on('error', function (err) { 52 | errors = err; 53 | console.error(err); 54 | hasErrors = true; 55 | }) 56 | } 57 | 58 | // update server list and display it 59 | router.get('/', function(req, res, next) { 60 | removeOldServers(req, res); 61 | }); 62 | 63 | // register a new server 64 | router.post('/', function(req, res, next) { 65 | var body = req.body; 66 | var address = req.cf_ip; 67 | var port = body.port; 68 | var key = body.key; 69 | register(req, res, key, address, port); 70 | }); 71 | 72 | // update a server as a result of a heartbeat 73 | router.put('/', function(req, res, next) { 74 | var body = req.body; 75 | var players = body.players; 76 | var token = body.token; 77 | var gameInfo = body.gameInfo; 78 | heartbeat(req, res, token, players, gameInfo); 79 | }); 80 | 81 | var daysInMonth = [31, 30, 31, 30, 31, 31, 30, 31]; 82 | var months = ['March', 'April', 'May', 'June', 'July', 'August', 'September', 'October'] 83 | 84 | var daySuffix = function daySuffix(day) { 85 | var j = day % 10, 86 | k = day % 100; 87 | if (j == 1 && k != 11) { 88 | return day + "st"; 89 | } 90 | if (j == 2 && k != 12) { 91 | return day + "nd"; 92 | } 93 | if (j == 3 && k != 13) { 94 | return day + "rd"; 95 | } 96 | return day + "th"; 97 | } 98 | 99 | var formatNumVals = function formatNumVals(x) { 100 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 101 | } 102 | 103 | router.get('/:id', function(req, res, next) { 104 | var id = parseInt(req.params.id,10); 105 | if (id) { 106 | Server.findOne({serverId: id}, function (err, server) { 107 | if (!err) { 108 | if (server) { 109 | var customGI = {} 110 | var monthNum = Math.floor(server.gameInfo.month % 8); 111 | customGI.month = months[monthNum]; 112 | var dayNum = Math.floor((server.gameInfo.day / 0x10000) * daysInMonth[monthNum]) + 1; 113 | customGI.day = daySuffix(dayNum); 114 | customGI.year = Math.floor(server.gameInfo.month / 8) + 1; 115 | if (server.gameInfo.cash) { 116 | customGI.cash = formatNumVals(Math.floor(server.gameInfo.cash/10)); 117 | } 118 | if (server.gameInfo.parkValue) { 119 | customGI.parkValue = formatNumVals(Math.floor(server.gameInfo.parkValue/10)); 120 | } 121 | if (server.gameInfo.guests) { 122 | customGI.guests = formatNumVals(server.gameInfo.guests); 123 | } 124 | if (req.returnJSON) { 125 | var serverResponse = { 126 | name: server.name, 127 | description: server.description, 128 | requiresPassword: server.requiresPassword, 129 | players: server.players, 130 | maxPlayers: server.maxPlayers, 131 | dedicated: server.dedicated, 132 | gameInfo: server.gameInfo, 133 | customGI: customGI, 134 | provider: server.provider 135 | } 136 | res.json({status: 200, server: serverResponse}); 137 | } else { 138 | server.customGI = customGI; 139 | res.render('server', {server: server}); 140 | } 141 | } else { 142 | var msg = 'Server with the specified ID was not found'; 143 | if (req.returnJSON) { 144 | res.json({status: 500, message: msg}); 145 | } else { 146 | res.render('server', {error: true, message: msg}); 147 | } 148 | } 149 | } else { 150 | var msg = 'Unable to retreive server information from the database'; 151 | if (req.returnJSON) { 152 | res.json({status: 500, message: msg}); 153 | } else { 154 | res.render('server', {error: true, message: msg}); 155 | } 156 | } 157 | }); 158 | } else { 159 | var msg = 'The requested ID is not in the accepted format. Make sure the ID is a numeric value.'; 160 | if (req.returnJSON) { 161 | res.json({status: 400, message: msg}); 162 | } else { 163 | res.render('server', {error: true, message: msg}); 164 | } 165 | } 166 | }); 167 | 168 | var heartbeat = function heartbeat(req, res, token, players, gameInfo) { 169 | Server.findById(token, function (err, server) { 170 | if (!err) { 171 | if (server) { 172 | server.players = players; 173 | server.gameInfo = gameInfo; 174 | server.updated_at = new Date(); 175 | server.save(function (err, serverSave) { 176 | if (!err) { 177 | res.json({status: 200}); 178 | } else { 179 | console.error(err); 180 | res.json({status: 500, message: 'Could not save the server entry'}); 181 | } 182 | }); 183 | } else { 184 | res.json({status: 401, message: 'Invalid token'}); 185 | } 186 | } else { 187 | console.error(err); 188 | res.json({status: 500, message: 'Database failiure'}); 189 | } 190 | }); 191 | } 192 | 193 | var register = function register(req, res, key, address, port) { 194 | Server.findOne({key: key}, function (err, server) { 195 | if (!err) { 196 | if (server) { 197 | if (ipaddr.IPv4.isValid(address)) { 198 | server.ip.v4.push(address); 199 | server.supportsIPv4 = true; 200 | } else if (ipaddr.IPv6.isValid(address)) { 201 | server.ip.v6.push(address); 202 | server.supportsIPv6 = true; 203 | } 204 | server.save(function (saveErr) { 205 | if (!saveErr) { 206 | res.json({status: 200, token: server._id}); 207 | } else { 208 | console.error(saveErr); 209 | res.json({status: 500, message: 'Save failed'}); 210 | } 211 | }); 212 | } else { 213 | requestGameInfo(req, res, address, port, key); 214 | } 215 | } else { 216 | console.error(err); 217 | res.json({status: 500, message: 'Database failiure'}); 218 | } 219 | }); 220 | } 221 | 222 | 223 | var updateServer = function updateServer(req, res, address, port, key, data) { 224 | var server = { 225 | key: key, 226 | ip : { 227 | v4: [], 228 | v6: [] 229 | }, 230 | port: port, 231 | name: data.name, 232 | requiresPassword: data.requiresPassword, 233 | description: data.description, 234 | version: data.version, 235 | players: data.players, 236 | maxPlayers: data.maxPlayers, 237 | supportsIPv4: false, 238 | supportsIPv6: false, 239 | provider: data.provider 240 | } 241 | if (ipaddr.IPv4.isValid(address)) { 242 | server.ip.v4.push(address); 243 | server.supportsIPv4 = true; 244 | } else if (ipaddr.IPv6.isValid(address)) { 245 | server.ip.v6.push(address); 246 | server.supportsIPv6 = true; 247 | } 248 | Server.create(server, function (err, newServer) { 249 | if (!err) { 250 | res.json({status: 200, token: newServer._id}); 251 | } else { 252 | console.error(err); 253 | res.json({status: 500, message: "An error ocured while updating the server record"}); 254 | } 255 | }); 256 | } 257 | 258 | var listServers = function listServers(req, res) { 259 | Server.find().exec(function (err, data) { 260 | if (!err) { 261 | var responseData = [] 262 | data.forEach( function (server) { 263 | var customGI = {} 264 | var monthNum = Math.floor(server.gameInfo.month % 8); 265 | customGI.month = months[monthNum]; 266 | var dayNum = Math.floor((server.gameInfo.day / 0x10000) * daysInMonth[monthNum]) + 1; 267 | customGI.day = daySuffix(dayNum); 268 | customGI.year = Math.floor(server.gameInfo.month / 8) + 1; 269 | if (server.gameInfo.cash) { 270 | customGI.cash = formatNumVals(Math.floor(server.gameInfo.cash/10)); 271 | } 272 | if (server.gameInfo.parkValue) { 273 | customGI.parkValue = formatNumVals(Math.floor(server.gameInfo.parkValue/10)); 274 | } 275 | if (server.gameInfo.guests) { 276 | customGI.guests = formatNumVals(server.gameInfo.guests); 277 | } 278 | var serverData = { 279 | name: server.name, 280 | description: server.description, 281 | version: server.version, 282 | players: server.players, 283 | maxPlayers: server.maxPlayers, 284 | port: server.port, 285 | dedicated: server.dedicated, 286 | ip: server.ip, 287 | requiresPassword: server.requiresPassword, 288 | supportsIPv4: server.supportsIPv4, 289 | supportsIPv6: server.supportsIPv6, 290 | serverId: server.serverId, 291 | gameInfo: server.gameInfo, 292 | customGI: customGI, 293 | provider: server.provider 294 | } 295 | responseData.push(serverData); 296 | }); 297 | if (req.returnJSON) { 298 | res.json({status: 200, servers: responseData}); 299 | } else { 300 | res.render('index', {data:responseData, title: "OpenRCT2 Master Server"}); 301 | } 302 | } else { 303 | console.error(err); 304 | var msg = 'Error fetching the server list. If this error persists, please contact the developers.'; 305 | if (req.returnJSON) { 306 | res.json({status: 500, message: msg}); 307 | } else { 308 | res.render('index', {error: true, message: msg, title: "OpenRCT2 Master Server | Error"}); 309 | } 310 | } 311 | }); 312 | } 313 | 314 | var removeOldServers = function removeOldServers(req, res) { 315 | Server.find().where('updated_at').lt(new Date(new Date() - 1000 * 70)).remove().exec(function(err, data) { 316 | if (req) { 317 | if (!err) { 318 | listServers(req, res); 319 | } else { 320 | console.error(err); 321 | var msg = 'Error cleaning up the server list. If this error persists, please contact the developers.'; 322 | if (req.returnJSON) { 323 | res.json({status: 500, message: msg}); 324 | } else { 325 | res.render('index', {error: true, message: msg, title: "OpenRCT2 Master Server | Error"}) 326 | } 327 | } 328 | } else { 329 | if (!err) { 330 | console.log("Database refreshed on load"); 331 | } else { 332 | console.error(err); 333 | } 334 | } 335 | }); 336 | } 337 | 338 | removeOldServers(); 339 | 340 | module.exports = router; 341 | -------------------------------------------------------------------------------- /views/server.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | a(href='/') Back to server list 5 | if error 6 | p Whooops!! 7 | p= message 8 | else 9 | p Server listing for #{server.name} 10 | p Server Name: #{server.name} 11 | p Server Description: #{server.description} 12 | p Dedicated Server: #{server.dedicated ? "Yes" : "No"} 13 | p Players: #{server.players}/#{server.maxPlayers} 14 | p Password protected: #{server.requiresPassword ? "Yes" : "No"} 15 | p Map size: #{server.gameInfo.mapSize} x #{server.gameInfo.mapSize} 16 | p Game date: #{server.customGI.day}, #{server.customGI.month}, Year #{server.customGI.year} 17 | p Park value: £#{server.customGI.parkValue} 18 | p Cash: #{server.gameInfo.cash != null ? "£"+server.customGI.cash : "Money disabled"} 19 | p Guests: #{server.customGI.guests} 20 | p Provider Name: #{server.provider.name} 21 | p Provider Email: 22 | a(href="mailto:"+server.provider.email) #{server.provider.email} 23 | p Provider Website: 24 | a(href=server.provider.website) #{server.provider.website} 25 | --------------------------------------------------------------------------------