pokemon-showdown/app.js
Cathy J. Fitzpatrick 62c5e5af89 Drop warnings about broken versions of node
These warnings were more important when the latest stable version of
node was broken. Now that the latest stable version has been fixed for
a while, these specific warnings are not necessary.
2013-04-18 21:26:44 -06:00

623 lines
18 KiB
JavaScript

const LOGIN_SERVER_TIMEOUT = 15000;
const LOGIN_SERVER_BATCH_TIME = 1000;
function runNpm(command) {
console.log('Running `npm ' + command + '`...');
var child_process = require('child_process');
var npm = child_process.spawn('npm', [command]);
npm.stdout.on('data', function(data) {
process.stdout.write(data);
});
npm.stderr.on('data', function(data) {
process.stderr.write(data);
});
npm.on('close', function(code) {
if (!code) {
child_process.fork('app.js').disconnect();
}
});
}
try {
require('sugar');
} catch (e) {
runNpm('install');
return;
}
if (!Object.select) {
runNpm('update');
return;
}
fs = require('fs');
if (!fs.existsSync) {
var path = require('path');
fs.existsSync = function(loc) { return path.existsSync(loc) };
}
//request = require('request');
var http = require("http");
var url = require('url');
LoginServer = (function() {
function LoginServer(uri) {
console.log('Creating LoginServer object for ' + uri + '...');
this.uri = uri;
this.requestQueue = [];
LoginServer.loginServers[this.uri] = this;
}
// "static" mapping of URIs to LoginServer objects
LoginServer.loginServers = {};
// "static" flag
LoginServer.disabled = false;
LoginServer.prototype.requestTimer = null;
LoginServer.prototype.requestTimeoutTimer = null;
LoginServer.prototype.requestLog = '';
LoginServer.prototype.lastRequest = 0;
LoginServer.prototype.openRequests = 0;
var getLoginServer = function(action) {
var uri;
if (config.loginservers) {
uri = config.loginservers[action] || config.loginservers[null];
} else {
uri = config.loginserver;
}
if (!uri) {
console.log('ERROR: No login server specified for action: ' + action);
return;
}
return LoginServer.loginServers[uri] || new LoginServer(uri);
};
LoginServer.instantRequest = function(action, data, callback) {
return getLoginServer(action).instantRequest(action, data, callback);
};
LoginServer.request = function(action, data, callback) {
return getLoginServer(action).request(action, data, callback);
};
var parseJSON = function(json) {
if (json[0] === ']') json = json.substr(1);
return JSON.parse(json);
};
LoginServer.prototype.instantRequest = function(action, data, callback) {
if (typeof data === 'function') {
callback = data;
data = null;
}
if (this.openRequests > 5) {
callback(null, null, 'overflow');
return;
}
this.openRequests++;
var dataString = '';
if (data) {
for (var i in data) {
dataString += '&'+i+'='+encodeURIComponent(''+data[i]);
}
}
var req = http.get(url.parse(this.uri+'action.php?act='+action+'&serverid='+config.serverid+'&servertoken='+config.servertoken+'&nocache='+new Date().getTime()+dataString), function(res) {
var buffer = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
buffer += chunk;
});
res.on('end', function() {
var data = null;
try {
var data = parseJSON(buffer);
} catch (e) {}
callback(data, res.statusCode);
this.openRequests--;
});
});
req.on('error', function(error) {
callback(null, null, error);
this.openRequests--;
});
req.end();
};
LoginServer.prototype.request = function(action, data, callback) {
if (typeof data === 'function') {
callback = data;
data = null;
}
if (LoginServer.disabled) {
callback(null, null, 'disabled');
return;
}
if (!data) data = {};
data.act = action;
data.callback = callback;
this.requestQueue.push(data);
this.requestTimerPoke();
};
LoginServer.prototype.requestTimerPoke = function() {
// "poke" the request timer, i.e. make sure it knows it should make
// a request soon
// if we already have it going or the request queue is empty no need to do anything
if (this.openRequests || this.requestTimer || !this.requestQueue.length) return;
this.requestTimer = setTimeout(this.makeRequests.bind(this), LOGIN_SERVER_BATCH_TIME);
};
LoginServer.prototype.makeRequests = function() {
this.requestTimer = null;
var self = this;
var requests = this.requestQueue;
this.requestQueue = [];
if (!requests.length) return;
var requestCallbacks = [];
for (var i=0,len=requests.length; i<len; i++) {
var request = requests[i];
requestCallbacks[i] = request.callback;
delete request.callback;
}
this.requestStart(requests.length);
var postData = 'serverid='+config.serverid+'&servertoken='+config.servertoken+'&nocache='+new Date().getTime()+'&json='+encodeURIComponent(JSON.stringify(requests))+'\n';
var requestOptions = url.parse(this.uri+'action.php');
requestOptions.method = 'post';
requestOptions.headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
};
var req = null;
var reqError = function(error) {
if (self.requestTimeoutTimer) {
clearTimeout(self.requestTimeoutTimer);
self.requestTimeoutTimer = null;
}
req.abort();
for (var i=0,len=requestCallbacks.length; i<len; i++) {
requestCallbacks[i](null, null, error);
}
self.requestEnd();
};
self.requestTimeoutTimer = setTimeout(function() {
reqError('timeout');
}, LOGIN_SERVER_TIMEOUT);
req = http.request(requestOptions, function(res) {
if (self.requestTimeoutTimer) {
clearTimeout(self.requestTimeoutTimer);
self.requestTimeoutTimer = null;
}
var buffer = '';
res.setEncoding('utf8');
res.on('data', function(chunk) {
buffer += chunk;
});
var endReq = function() {
if (self.requestTimeoutTimer) {
clearTimeout(self.requestTimeoutTimer);
self.requestTimeoutTimer = null;
}
//console.log('RESPONSE: '+buffer);
var data = null;
try {
var data = parseJSON(buffer);
} catch (e) {}
for (var i=0,len=requestCallbacks.length; i<len; i++) {
if (data) {
requestCallbacks[i](data[i], res.statusCode);
} else {
requestCallbacks[i](null, res.statusCode, 'corruption');
}
}
self.requestEnd();
}.once();
res.on('end', endReq);
res.on('close', endReq);
self.requestTimeoutTimer = setTimeout(function(){
if (res.connection) res.connection.destroy();
endReq();
}, LOGIN_SERVER_TIMEOUT);
});
req.on('error', reqError);
req.write(postData);
req.end();
};
LoginServer.prototype.requestStart = function(size) {
this.lastRequest = Date.now();
this.requestLog += ' | '+size+' requests: ';
this.openRequests++;
};
LoginServer.prototype.requestEnd = function() {
this.openRequests = 0;
this.requestLog += ''+(Date.now() - this.lastRequest).duration();
this.requestLog = this.requestLog.substr(-1000);
this.requestTimerPoke();
};
LoginServer.prototype.getLog = function() {
return this.requestLog + (this.lastRequest?' ('+(Date.now() - this.lastRequest).duration()+' since last request)':'');
};
return LoginServer;
})();
// Synchronously copy config-example.js over to config.js if it doesn't exist
if (!fs.existsSync('./config/config.js')) {
console.log("config.js doesn't exist - creating one with default settings...");
var BUF_LENGTH, buff, bytesRead, fdr, fdw, pos;
BUF_LENGTH = 64 * 1024;
buff = new Buffer(BUF_LENGTH);
fdr = fs.openSync('./config/config-example.js', 'r');
fdw = fs.openSync('./config/config.js', 'w');
bytesRead = 1;
pos = 0;
while (bytesRead > 0) {
bytesRead = fs.readSync(fdr, buff, 0, BUF_LENGTH, pos);
fs.writeSync(fdw, buff, 0, bytesRead);
pos += bytesRead;
}
fs.closeSync(fdr);
}
config = require('./config/config.js');
if (config.watchconfig) {
fs.watchFile('./config/config.js', function(curr, prev) {
if (curr.mtime <= prev.mtime) return;
try {
delete require.cache[require.resolve('./config/config.js')];
config = require('./config/config.js');
console.log('Reloaded config/config.js');
} catch (e) {}
});
}
if (process.argv[2] && parseInt(process.argv[2])) {
config.port = parseInt(process.argv[2]);
}
if (process.argv[3]) {
config.setuid = process.argv[3];
}
if (config.protocol !== 'io' && config.protocol !== 'eio') config.protocol = 'ws';
var app;
var server;
if (config.protocol === 'io') {
server = require('socket.io').listen(config.port).set('log level', 1);
server.set('transports', ['websocket', 'htmlfile', 'xhr-polling']); // temporary hack until https://github.com/LearnBoost/socket.io/issues/609 is fixed
} else if (config.protocol === 'eio') {
app = require('http').createServer().listen(config.port);
server = require('engine.io').attach(app);
} else {
app = require('http').createServer();
server = require('sockjs').createServer({sockjs_url: "http://cdn.sockjs.org/sockjs-0.3.min.js", log: function(severity, message) {
if (severity === 'error') console.log('ERROR: '+message);
}});
}
/**
* Converts anything to an ID. An ID must have only lowercase alphanumeric
* characters.
* If a string is passed, it will be converted to lowercase and
* non-alphanumeric characters will be stripped.
* If an object with an ID is passed, its ID will be returned.
* Otherwise, an empty string will be returned.
*/
toId = function(text) {
if (text && text.id) text = text.id;
else if (text && text.userid) text = text.userid;
return string(text).toLowerCase().replace(/[^a-z0-9]+/g, '');
};
toUserid = toId;
/**
* Validates a username or Pokemon nickname
*/
var bannedNameStartChars = {'~':1, '&':1, '@':1, '%':1, '+':1, '-':1, '!':1, '?':1, '#':1};
toName = function(name) {
name = string(name).trim();
name = name.replace(/(\||\n|\[|\]|\,)/g, '');
while (bannedNameStartChars[name.substr(0,1)]) {
name = name.substr(1);
}
if (name.length > 18) name = name.substr(0,18);
return name;
};
/**
* Escapes a string for HTML
* If strEscape is true, escapes it for JavaScript, too
*/
sanitize = function(str, strEscape) {
str = (''+(str||''));
str = str.escapeHTML();
if (strEscape) str = str.replace(/'/g, '\\\'');
return str;
};
/**
* Safely ensures the passed variable is a string
* Simply doing ''+str can crash if str.toString crashes or isn't a function
* If we're expecting a string and being given anything that isn't a string
* or a number, it's safe to assume it's an error, and return ''
*/
string = function(str) {
if (typeof str === 'string' || typeof str === 'number') return ''+str;
return '';
}
/**
* Converts any variable to an integer (numbers get floored, non-numbers
* become 0). Then clamps it between min and (optionally) max.
*/
clampIntRange = function(num, min, max) {
if (typeof num !== 'number') num = 0;
num = Math.floor(num);
if (num < min) num = min;
if (typeof max !== 'undefined' && num > max) num = max;
return num;
};
try {
if (config.setuid) {
process.setuid(config.setuid);
console.log("setuid succeeded, we are now running as "+config.setuid);
}
}
catch (err) {
console.log("ERROR: setuid failed: [%s] Call: [%s]", err.message, err.syscall);
process.exit(1);
}
Data = {};
Users = require('./users.js');
Rooms = require('./rooms.js');
delete process.send; // in case we're a child process
Verifier = require('./verifier.js');
parseCommand = require('./chat-commands.js').parseCommand;
Simulator = require('./simulator.js');
lockdown = false;
mutedIps = {};
bannedIps = {};
nameLockedIps = {};
function resolveUser(you, socket) {
if (!you) {
emit(socket, 'connectionError', 'There has been a connection error. Please refresh the page.');
return false;
}
return you.user;
}
emit = function(socket, type, data) {
if (config.protocol === 'io') {
socket.emit(type, data);
} else if (config.protocol === 'eio') {
if (typeof data === 'object') data.type = type;
else data = {type: type, message: data};
socket.send(JSON.stringify(data));
} else {
if (typeof data === 'object') data.type = type;
else data = {type: type, message: data};
socket.write(JSON.stringify(data));
}
};
sendData = function(socket, data) {
if (config.protocol === 'io') {
socket.emit('data', data);
} else if (config.protocol === 'eio') {
socket.send(data);
} else {
socket.write(data);
}
};
function randomString(length) {
var strArr = [];
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i=0;i<length;i++) {
strArr[i] = chars[Math.floor(Math.random()*chars.length)];
}
return strArr.join('');
}
if (config.crashguard) {
// graceful crash - allow current battles to finish before restarting
process.on('uncaughtException', function (err) {
console.log("\n"+err.stack+"\n");
fs.createWriteStream('logs/errors.txt', {'flags': 'a'}).on("open", function(fd) {
this.write("\n"+err.stack+"\n");
this.end();
}).on("error", function (err) {
console.log("\n"+err.stack+"\n");
});
var stack = (""+err.stack).split("\n").slice(0,2).join("<br />");
Rooms.lobby.addRaw('<div class="message-server-crash"><b>THE SERVER HAS CRASHED:</b> '+stack+'<br />Please restart the server.</div>');
Rooms.lobby.addRaw('<div class="message-server-crash">You will not be able to talk in the lobby or start new battles until the server restarts.</div>');
config.modchat = 'crash';
lockdown = true;
});
}
// event functions
var events = {
join: function(data, socket, you) {
if (!data || typeof data.room !== 'string') return;
if (!you) {
you = Users.connectUser(socket, data.room);
return you;
} else {
var youUser = resolveUser(you, socket);
if (!youUser) return;
youUser.joinRoom(data.room, socket);
}
},
chat: function(message, socket, you) {
if (!message || typeof message.room !== 'string' || typeof message.message !== 'string') return;
var youUser = resolveUser(you, socket);
if (!youUser) return;
var room = Rooms.get(message.room, 'lobby');
message.message.split('\n').forEach(function(text){
youUser.chat(text, room, socket);
});
},
leave: function(data, socket, you) {
if (!data || typeof data.room !== 'string') return;
var youUser = resolveUser(you, socket);
if (!youUser) return;
youUser.leaveRoom(Rooms.get(data.room, 'lobby'), socket);
}
};
if (config.protocol === 'io') { // Socket.IO
server.sockets.on('connection', function (socket) {
var you = null;
socket.remoteAddress = socket.handshake.address.address; // for compatibility with SockJS semantics
if (config.proxyip && (config.proxyip === true || config.proxyip.indexOf(socket.remoteAddress) >= 0)) socket.remoteAddress = (socket.headers["x-forwarded-for"]||"").split(",").shift() || socket.remoteAddress; // for proxies
if (bannedIps[socket.remoteAddress]) {
console.log('CONNECT BLOCKED - IP BANNED: '+socket.remoteAddress);
return;
}
console.log('CONNECT: '+socket.remoteAddress+' ['+socket.id+']');
var generator = function(type) {
return function(data) {
console.log(you);
events[type](data, socket, you);
};
};
for (var e in events) {
socket.on(e, (function(type) {
return function(data) {
you = events[type](data, socket, you) || you;
};
})(e));
}
socket.on('disconnect', function() {
var youUser = resolveUser(you, socket);
if (!youUser) return;
youUser.disconnect(socket);
});
});
} else if (config.protocol === 'eio') { // engine.io
server.on('connection', function (socket) {
var you = null;
if (!socket) { // WTF
return;
}
//socket.id = randomString(16); // this sucks
socket.remoteAddress = socket.id;
if (bannedIps[socket.remoteAddress]) {
console.log('CONNECT BLOCKED - IP BANNED: '+socket.remoteAddress);
return;
}
console.log('CONNECT: '+socket.remoteAddress+' ['+socket.id+']');
socket.on('message', function(message) {
var data = null;
if (message.substr(0,1) === '{') {
try {
data = JSON.parse(message);
} catch (e) {}
} else {
var pipeIndex = message.indexOf('|');
if (pipeIndex > 0) data = {
type: 'chat',
room: message.substr(0, pipeIndex),
message: message.substr(pipeIndex+1)
};
}
if (!data) return;
if (events[data.type]) you = events[data.type](data, socket, you) || you;
});
socket.on('close', function() {
var youUser = resolveUser(you, socket);
if (!youUser) return;
youUser.disconnect(socket);
});
});
} else { // SockJS and engine.io
server.on('connection', function (socket) {
var you = null;
if (!socket) { // WTF
return;
}
socket.id = randomString(16); // this sucks
if (config.proxyip && (config.proxyip === true || config.proxyip.indexOf(socket.remoteAddress) >= 0)) socket.remoteAddress = (socket.headers["x-forwarded-for"]||"").split(",").shift() || socket.remoteAddress; // for proxies
if (bannedIps[socket.remoteAddress]) {
console.log('CONNECT BLOCKED - IP BANNED: '+socket.remoteAddress);
return;
}
console.log('CONNECT: '+socket.remoteAddress+' ['+socket.id+']');
socket.on('data', function(message) {
var data = null;
if (message.substr(0,1) === '{') {
try {
data = JSON.parse(message);
} catch (e) {}
} else {
var pipeIndex = message.indexOf('|');
if (pipeIndex >= 0) data = {
type: 'chat',
room: message.substr(0, pipeIndex),
message: message.substr(pipeIndex+1)
};
}
if (!data) return;
if (events[data.type]) you = events[data.type](data, socket, you) || you;
});
socket.on('close', function() {
var youUser = resolveUser(you, socket);
if (!youUser) return;
youUser.disconnect(socket);
});
});
if (config.protocol === 'ws') {
server.installHandlers(app, {});
app.listen(config.port);
}
}
console.log('Server started on port ' + config.port);
console.log('Test your server at http://localhost' +
((config.port !== 8000) ? ('-' + config.port) : '') +
'.psim.us');
// This slow operation is done *after* we start listening for connections
// to the server. Anybody who connects while this require() is running will
// have to wait a couple seconds before they are able to join the server, but
// at least they probably won't receive a connection error message.
Tools = require('./tools.js');
// After loading tools, generate and cache the format list.
rooms.lobby.formatListText = rooms.lobby.getFormatListText();