diff --git a/README.md b/README.md
index 478fd00f4..6500136d6 100644
--- a/README.md
+++ b/README.md
@@ -42,19 +42,16 @@ Everything else can be tested, though.
Warning
------------------------------------------------------------------------
-This repository is not "batteries included". It does NOT include everything
-necessary to run a full Pokémon Showdown client.
-
-In particular, it doesn't include a login/authentication server, nor does it
-include the database abstraction library used by the ladder library (although
-it's similar enough to `mysqli` that you can use that with minimal changes).
+This repository is not "batteries included". It does NOT include instructions
+to run a full Pokémon Showdown client, and we will not provide them. Please
+do not ask for help on this; you will be turned away.
It also doesn't include several resource files (namely, the `/audio/` and
`/sprites/` directories) for size reasons.
In other words, this repository is incomplete and NOT intended for people
who wish to serve their own Pokémon Showdown client (you can, but it'll
-require you to rewrite some things). Rather, it's intended for people who
+require you figure it out yourself). Rather, it's intended for people who
wish to contribute and submit pull requests to Pokémon Showdown's client.
License
diff --git a/action.php b/action.php
index fee0ab2a4..d47e543d0 100644
--- a/action.php
+++ b/action.php
@@ -24,7 +24,7 @@ if (preg_match('/^http\\:\\/\\/[a-z0-9]+\\.psim\\.us\\//', @$_SERVER['HTTP_REFER
}
// header("X-Debug: " . @$_SERVER['HTTP_REFERER']);
-include_once '../pokemonshowdown.com/lib/ntbb-session.lib.php';
+include_once 'lib/ntbb-session.lib.php';
include_once '../pokemonshowdown.com/config/servers.inc.php';
include_once 'lib/dispatcher.lib.php';
diff --git a/config/config-example.inc.php b/config/config-example.inc.php
new file mode 100644
index 000000000..ed291e0e3
--- /dev/null
+++ b/config/config-example.inc.php
@@ -0,0 +1,46 @@
+ ['zarel'],
+
+// password and SID hashing settings
+
+ 'password_cost' => 12,
+ 'sid_length' => 15,
+ 'sid_cost' => 4,
+
+// database
+
+ 'server' => 'localhost',
+ 'username' => '',
+ 'password' => '',
+ 'database' => '',
+ 'prefix' => 'ps_',
+ 'charset' => 'utf8',
+
+// CORS requests
+
+ 'cors' => [
+ '/^http:\/\/smogon\.com$/' => 'smogon.com_',
+ '/^http:\/\/www\.smogon\.com$/' => 'www.smogon.com_',
+ '/^http:\/\/logs\.psim\.us$/' => 'logs.psim.us_',
+ '/^http:\/\/logs\.psim\.us:8080$/' => 'logs.psim.us_',
+ '/^http:\/\/[a-z0-9]+\.psim\.us$/' => '',
+ '/^http:\/\/play\.pokemonshowdown\.com$/' => '',
+ ],
+
+// key signing for SSO
+
+ 'privatekeys' => [
+
+1 => '-----BEGIN PRIVATE KEY-----
+[insert RSA private key]
+-----END PRIVATE KEY-----
+',
+
+ ]
+
+];
diff --git a/config/servers-example.inc.php b/config/servers-example.inc.php
new file mode 100644
index 000000000..453b925ad
--- /dev/null
+++ b/config/servers-example.inc.php
@@ -0,0 +1,11 @@
+
+ array (
+ 'name' => 'Smogon University',
+ 'id' => 'showdown',
+ 'server' => 'sim.psim.us',
+ 'port' => 8000,
+ ),
+);
diff --git a/lib/ntbb-database.lib.php b/lib/ntbb-database.lib.php
new file mode 100644
index 000000000..4a18a9e74
--- /dev/null
+++ b/lib/ntbb-database.lib.php
@@ -0,0 +1,65 @@
+server = $server;
+ $this->username = $username;
+ $this->password = $password;
+ $this->database = $database;
+ $this->prefix = $prefix;
+ $this->charset = $charset;
+ }
+
+ function connect() {
+ if (!$this->db) {
+ $this->db = mysqli_connect($this->server, $this->username, $this->password, $this->database);
+ if ($this->charset) {
+ mysqli_set_charset($this->db, $this->charset);
+ }
+ }
+ }
+ function query($query) {
+ $this->connect();
+ //$this->queries[] = $query;
+ return mysqli_query($this->db, $query);
+ }
+ function fetch_assoc($resource) {
+ return mysqli_fetch_assoc($resource);
+ }
+ function fetch($resource) {
+ return mysqli_fetch_assoc($resource);
+ }
+ function escape($data) {
+ $this->connect();
+ return mysqli_real_escape_string($this->db, $data);
+ }
+ function error() {
+ if ($this->db) {
+ return mysqli_error($this->db);
+ }
+ }
+ function insert_id() {
+ if ($this->db) {
+ return mysqli_insert_id($this->db);
+ }
+ }
+}
+
+$psdb = new NTBBDatabase($psconfig['server'],
+ $psconfig['username'],
+ $psconfig['password'],
+ $psconfig['database'],
+ $psconfig['prefix'],
+ $psconfig['charset']);
diff --git a/lib/ntbb-ladder.lib.php b/lib/ntbb-ladder.lib.php
old mode 100755
new mode 100644
index ab0da2b5c..f888b0c31
--- a/lib/ntbb-ladder.lib.php
+++ b/lib/ntbb-ladder.lib.php
@@ -4,7 +4,7 @@ error_reporting(E_ALL);
// An implementation of the Glicko2 rating system.
-@include_once dirname(__FILE__).'/../../pokemonshowdown.com/lib/ntbb-database.lib.php';
+@include_once dirname(__FILE__).'/ntbb-database.lib.php';
// connect to the ladder database (if we aren't already connected)
if (empty($ladderdb)) {
diff --git a/lib/ntbb-session.lib.php b/lib/ntbb-session.lib.php
new file mode 100644
index 000000000..1da9cd2cf
--- /dev/null
+++ b/lib/ntbb-session.lib.php
@@ -0,0 +1,640 @@
+getGuest();
+
+ // see if we're logged in
+ $osid = @$_COOKIE['sid'];
+ if (!$osid) {
+ // nope, not logged in
+ return;
+ }
+ $osidsplit = explode('x', $osid, 2);
+ if (count($osidsplit) !== 2) {
+ // malformed `sid` cookie
+ $this->killCookie();
+ return;
+ }
+ $session = intval($osidsplit[0]);
+ $res = $psdb->query(
+ "SELECT sid, timeout, `{$psdb->prefix}users`.* " .
+ "FROM `{$psdb->prefix}sessions`, `{$psdb->prefix}users` " .
+ "WHERE `session` = $session " .
+ "AND `{$psdb->prefix}sessions`.`userid` = `{$psdb->prefix}users`.`userid` " .
+ "LIMIT 1");
+ if (!$res) {
+ // query problem?
+ $this->killCookie();
+ return;
+ }
+ $sess = $psdb->fetch_assoc($res);
+ if (!$sess || !password_verify(base64_decode($osidsplit[1]), $sess['sid'])) {
+ // invalid session ID
+ $this->killCookie();
+ return;
+ }
+ if (intval($sess['timeout'])<$ctime) {
+ // session expired
+ // delete all sessions that will expire within 30 minutes
+ $ctime += 60 * 30;
+ $psdb->query("DELETE FROM `{$psdb->prefix}sessions` WHERE `timeout` < $ctime");
+ $this->killCookie();
+ return;
+ }
+
+ // okay, legit session ID - you're logged in now.
+ $curuser = $sess;
+ $curuser['loggedin'] = true;
+ // unset these values to avoid them being leaked accidentally
+ $curuser['outdatedpassword'] = !!$curuser['password'];
+ unset($curuser['password']);
+ unset($curuser['nonce']);
+ unset($curuser['passwordhash']);
+
+ $this->sid = $osid;
+ $this->session = $session;
+ }
+
+ // taken from http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5
+ function cidr_match($ip, $range) {
+ list($subnet, $bits) = explode('/', $range);
+ $ip = ip2long($ip);
+ $subnet = ip2long($subnet);
+ $mask = -1 << (32 - $bits);
+ $subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
+ return ($ip & $mask) == $subnet;
+ }
+
+ function getIp() {
+ $ip = $_SERVER['REMOTE_ADDR'];
+ foreach ($this->trustedproxies as &$proxyip) {
+ if ($this->cidr_match($ip, $proxyip)) {
+ $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
+ $ip = array_pop($parts);
+ break;
+ }
+ }
+ return $ip;
+ }
+
+ function userid($username) {
+ if (!$username) $username = '';
+ $username = strtr($username, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz");
+ return preg_replace('/[^A-Za-z0-9]+/','',$username);
+ }
+ function getGuest($username='') {
+ $userid = $this->userid($username);
+ if (!$userid)
+ {
+ $username = 'Guest';
+ $userid = 'guest';
+ }
+ return array(
+ 'username' => $username,
+ 'userid' => $userid,
+ 'group' => 0,
+ 'loggedin' => false
+ );
+ }
+
+ function qsid() { return $this->sid ? '?sid='.$this->sid : ''; }
+ function asid() { return $this->sid ? '&sid='.$this->sid : ''; }
+ function hasid() { return $this->sid ? '&sid='.$this->sid : ''; }
+ function fsid() { return $this->sid ? '' : ''; }
+
+ /**
+ * New SID and password hashing functions.
+ */
+ function mksid() {
+ global $psconfig;
+ if (!function_exists('mcrypt_create_iv')) {
+ error_log('mcrypt_create_iv is not defined!');
+ die;
+ }
+ return mcrypt_create_iv($psconfig['sid_length'], MCRYPT_DEV_URANDOM);
+ }
+ function sidHash($sid) {
+ global $psconfig;
+ return password_hash($sid, PASSWORD_DEFAULT, array('cost' => $psconfig['sid_cost']));
+ }
+ function passwordNeedsRehash($hash) {
+ global $psconfig;
+ return password_needs_rehash($hash, PASSWORD_DEFAULT,
+ array('cost' => $psconfig['password_cost'])
+ );
+ }
+ function passwordHash($pass) {
+ global $psconfig;
+ return password_hash($pass, PASSWORD_DEFAULT,
+ array('cost' => $psconfig['password_cost'])
+ );
+ }
+
+ public function passwordVerify($name, $pass) {
+ global $psdb;
+
+ $userid = $this->userid($name);
+
+ $res = $psdb->query(
+ "SELECT `password`, `nonce`, `passwordhash` " .
+ "FROM `{$psdb->prefix}users` " .
+ "WHERE `userid` = '".$psdb->escape($userid)."' " .
+ "LIMIT 1"
+ );
+ if (!$res) return false;
+
+ $user = $psdb->fetch_assoc($res);
+ return $this->passwordVerifyInner($userid, $pass, $user);
+ }
+
+ private function passwordVerifyInner($userid, $pass, $user) {
+ global $psdb;
+
+ // throttle
+ $ip = $this->getIp();
+ $res = $psdb->query(
+ "SELECT `count`, `time` " .
+ "FROM `{$psdb->prefix}loginthrottle` " .
+ "WHERE `ip` = '".$ip."' " .
+ "LIMIT 1"
+ );
+ $loginthrottle = null;
+ if ($res) $loginthrottle = $psdb->fetch_assoc($res);
+ if ($loginthrottle) {
+ if ($loginthrottle['count'] > 500) {
+ $loginthrottle['count']++;
+ $psdb->query("UPDATE `{$psdb->prefix}loginthrottle` SET count = {$loginthrottle['count']}, lastuserid = '".$userid."', `time` = '".time()."' WHERE ip = '".$ip."'");
+ return false;
+ } else if ($loginthrottle['time'] + 24 * 60 * 60 < time()) {
+ $loginthrottle = [
+ 'count' => 0,
+ 'time' => time(),
+ ];
+ }
+ }
+
+ $rehash = false;
+ if ($user['passwordhash']) {
+ // new password hashes
+ if (!password_verify($pass, $user['passwordhash'])) {
+ // wrong password
+ if ($loginthrottle) {
+ $loginthrottle['count']++;
+ $psdb->query("UPDATE `{$psdb->prefix}loginthrottle` SET count = {$loginthrottle['count']}, lastuserid = '".$userid."', `time` = '".time()."' WHERE ip = '".$ip."'");
+ } else {
+ $psdb->query("INSERT INTO `{$psdb->prefix}loginthrottle` (ip, count, lastuserid, `time`) VALUES ('".$ip."', 1, '".$userid."', '".time()."')");
+ }
+ return false;
+ }
+ $rehash = $this->passwordNeedsRehash($user['passwordhash']);
+ } else if ($user['password'] && $user['nonce']) {
+ // original ntbb-session password hashes
+ return false;
+ } else {
+ // error
+ return false;
+ }
+ if ($rehash) {
+ // create a new password hash for the user
+ $hash = $this->passwordHash($pass);
+ if ($hash) {
+ $psdb->query("UPDATE `{$psdb->prefix}users` SET `passwordhash`='" . $psdb->escape($hash) . "', `password`=NULL, `nonce`=NULL WHERE `userid`='" . $psdb->escape($userid) . "'");
+ }
+ }
+ return true;
+ }
+
+ function login($name, $pass, $timeout = false, $debug = false) {
+ global $psdb, $curuser;
+ $ctime = time();
+
+ $this->logout();
+ $userid = $this->userid($name);
+ $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = '".$psdb->escape($userid)."' LIMIT 1");
+ if (!$res) {
+ if ($debug) error_log('no such user');
+ return $curuser;
+ }
+ $user = $psdb->fetch_assoc($res);
+ if ($user['banstate'] >= 100) {
+ return $curuser;
+ }
+ if (!$this->passwordVerifyInner($userid, $pass, $user)) {
+ if ($debug) error_log('wrong password');
+ return $curuser;
+ }
+ if (!$timeout) {
+ // expire in a week and 30 minutes
+ $timeout = (14*24*60+30)*60;
+ }
+ $timeout += $ctime;
+
+ $nsid = $this->mksid();
+ $nsidhash = $this->sidHash($nsid);
+ $res = $psdb->query("INSERT INTO `{$psdb->prefix}sessions` (`userid`,`sid`,`time`,`timeout`,`ip`) VALUES ('".$psdb->escape($user['userid'])."', '" . $psdb->escape($nsidhash) . "',$ctime,$timeout,'".$psdb->escape($this->getIp())."')");
+ if (!$res) die;
+
+ $this->session = $psdb->insert_id();
+ $this->sid = $this->session . 'x' . base64_encode($nsid);
+
+ $curuser = $user;
+ $curuser['loggedin'] = true;
+ // unset these values to avoid them being leaked accidentally
+ $curuser['outdatedpassword'] = !!$curuser['password'];
+ unset($curuser['password']);
+ unset($curuser['nonce']);
+ unset($curuser['passwordhash']);
+
+ setcookie('sid', $this->sid, $timeout, '/', $this->cookiedomain, false, true);
+
+ return $curuser;
+ }
+
+ function killCookie() {
+ setcookie('sid', '', time()-60*60*24*2,
+ '/', $this->cookiedomain, false, true);
+ }
+
+ function csrfData() {
+ echo '';
+ return '';
+ }
+
+ function csrfCheck() {
+ if (empty($_POST['csrf'])) return false;
+ $csrf = $_POST['csrf'];
+ if ($csrf === @$_COOKIE['sid']) return true;
+ return false;
+ }
+
+ function logout() {
+ global $psdb,$curuser;
+
+ if (!$this->session) return $curuser;
+
+ $curuser = $this->getGuest();
+ $psdb->query("DELETE FROM `{$psdb->prefix}sessions` WHERE `session` = '{$this->session}' LIMIT 1");
+ $this->sid = '';
+ $this->session = 0;
+
+ $this->killCookie();
+
+ return $curuser;
+ }
+
+ function createPasswordResetToken($name, $timeout=false) {
+ global $psdb, $curuser;
+ $ctime = time();
+
+ $userid = $this->userid($name);
+ $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = '".$psdb->escape($userid)."' LIMIT 1");
+ if (!$res) // user doesn't exist
+ return false;
+ $user = $psdb->fetch_assoc($res);
+ if (!$timeout) {
+ $timeout = 7*24*60*60;
+ }
+ $timeout += $ctime;
+ {
+ $modlogentry = "Password reset token generated";
+ $psdb->query("INSERT INTO `{$psdb->prefix}usermodlog` (`userid`,`actorid`,`date`,`ip`,`entry`) VALUES ('".$psdb->escape($user['userid'])."','".$psdb->escape($curuser['userid'])."','" . $psdb->escape(time()) . "','".$psdb->escape($this->getIp())."','".$psdb->escape($modlogentry)."')");
+
+ // magical character string...
+ $nsid = bin2hex($this->mksid());
+ $res = $psdb->query("INSERT INTO `{$psdb->prefix}sessions` (`userid`,`sid`,`time`,`timeout`,`ip`) VALUES ('".$psdb->escape($user['userid'])."', '$nsid',$ctime,$timeout,'".$psdb->escape($this->getIp())."')");
+ if (!$res) die($psdb->error());
+ }
+
+ return $nsid;
+ }
+
+ function validatePasswordResetToken($token) {
+ global $psdb, $psconfig;
+ if (strlen($token) !== ($psconfig['sid_length'] * 2)) return false;
+ $res = $psdb->query("SELECT * FROM `{$psdb->prefix}sessions` WHERE `sid` = '".$psdb->escape($token)."' LIMIT 1");
+ $session = $psdb->fetch_assoc($res);
+ if (!$session) return false;
+
+ if (intval($session['timeout'])