getGuest(); // see if we're logged in $scookie = $_POST['sid'] ?? $_COOKIE['sid'] ?? null; if (!$scookie) { // nope, not logged in return; } $sid = ''; $session = 0; $scsplit = explode(',', $scookie); if (count($scsplit) === 3) { $this->cookiename = $scsplit[0]; $session = intval($scsplit[1]); $sid = $scsplit[2]; $this->sid = $sid; } if (!$session) { return; } $res = $psdb->query( "SELECT sid, timeout, `{$psdb->prefix}users`.* " . "FROM `{$psdb->prefix}sessions`, `{$psdb->prefix}users` " . "WHERE `session` = ? " . "AND `{$psdb->prefix}sessions`.`userid` = `{$psdb->prefix}users`.`userid` " . "LIMIT 1", [$session]); if (!$res) { // query problem? $this->killCookie(); return; } $sess = $psdb->fetch_assoc($res); if (!$sess || !password_verify($sid, $sess['sid'])) { // invalid session ID $this->killCookie(); return; } if (intval($sess['timeout'])<$ctime) { // session expired $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']; $curuser['password'] = null; $curuser['nonce'] = null; $curuser['passwordhash'] = null; $this->scookie = $scookie; $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): string { 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 [ 'username' => $username, 'userid' => $userid, 'group' => 0, 'loggedin' => false, 'ip' => $this->getIp(), ]; } function qsid() { return $this->scookie ? '?sid='.$this->scookie : ''; } function asid() { return $this->scookie ? '&sid='.$this->scookie : ''; } function hasid() { return $this->scookie ? '&sid='.$this->scookie : ''; } function fsid() { return $this->scookie ? '' : ''; } /** * New SID and password hashing functions. */ function mksid(string $osid) { if (function_exists('psconfig_mksid')) { return psconfig_mksid($osid); } return substr(base64_encode(random_bytes(18)), 0, 24); } function sidHash(string $sid) { global $psconfig; return password_hash($sid, PASSWORD_DEFAULT, ['cost' => $psconfig['sid_cost']]); } function passwordNeedsRehash(string $hash) { global $psconfig; return password_needs_rehash($hash, PASSWORD_DEFAULT, ['cost' => $psconfig['password_cost']]); } function passwordHash(string $pass) { global $psconfig; return password_hash($pass, PASSWORD_DEFAULT, ['cost' => $psconfig['password_cost']]); } public function passwordVerify(string $name, string $pass) { global $psdb; $userid = $this->userid($name); $res = $psdb->query( "SELECT `password`, `nonce`, `passwordhash` " . "FROM `{$psdb->prefix}users` " . "WHERE `userid` = ? " . "LIMIT 1", [$userid] ); if (!$res) return false; $user = $psdb->fetch_assoc($res); return $this->passwordVerifyInner($userid, $pass, $user); } private function passwordVerifyInner(string $userid, string $pass, $user) { global $psdb, $psconfig; // throttle $ip = $this->getIp(); $res = $psdb->query( "SELECT `count`, `time` FROM `{$psdb->prefix}loginthrottle` WHERE `ip` = ? LIMIT 1", [$ip] ); $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 = ?, lastuserid = ?, `time` = ? WHERE ip = ?", [$loginthrottle['count'], $userid, time(), $ip]); return false; } else if ($loginthrottle['time'] + 24 * 60 * 60 < time()) { $loginthrottle = [ 'count' => 0, 'time' => time(), ]; } } if (substr(@$user['email'], -1) === '@') { // Forgive me, gods, for I have hardcoded way more than I really should have $valResult = shell_exec("cd /var/www/html/play.pokemonshowdown.com && node lib/validate-token.js " . escapeshellarg($pass)); $payload = json_decode($valResult, true); if (!$payload) return false; if (strpos($payload['aud'], $psconfig['gapi_clientid']) === false) return false; if ($payload['email'] === substr($user['email'], 0, -1)) { return true; } return false; } $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 = ?, lastuserid = ?, `time` = ? WHERE ip = ?", [$loginthrottle['count'], $userid, time(), $ip]); } else { $psdb->query("INSERT INTO `{$psdb->prefix}loginthrottle` (ip, count, lastuserid, `time`) VALUES (?, 1, ?, ?)", [$ip, $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`=?, `password`=NULL, `nonce`=NULL WHERE `userid`=?", [$hash, $userid] ); } } return true; } function login(string $name, string $pass, $timeout = false, $debug = false) { global $psdb, $curuser, $psconfig; $ctime = time(); $this->logout(); $userid = $this->userid($name); $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$userid]); 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 two weeks $timeout = 2*7*24*60*60; } $timeout += $ctime; $this->sid = $this->mksid($this->sid); $nsidhash = $this->sidHash($this->sid); $res = $psdb->query( "INSERT INTO `{$psdb->prefix}sessions` (`userid`,`sid`,`time`,`timeout`,`ip`) VALUES (?,?,?,?,?)", [$user['userid'], $nsidhash, $ctime, $timeout, $this->getIp()] ); if (!$res) die; $this->session = $psdb->insert_id(); $this->scookie = $name . ',' . $this->session . ',' . $this->sid; $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->scookie, ['expires' => time() + (363)*24*60*60, 'path' => '/', 'domain' => $psconfig['routes']['root'], 'secure' => true, 'httponly' => true, 'samesite' => 'None']); $encodedcookie = rawurlencode($this->scookie); header("Set-Cookie: sid=$encodedcookie; Max-Age=31363200; Domain={$psconfig['routes']['root']}; Path=/; Secure; SameSite=None"); return $curuser; } function updateCookie() { global $psconfig; if (!$this->sid) { $this->sid = $this->mksid($this->sid); $this->scookie = ',,' . $this->sid; // setcookie('sid', $this->scookie, ['expires' => time() + (363)*24*60*60, 'path' => '/', 'domain' => $psconfig['routes']['root'], 'secure' => true, 'httponly' => true, 'samesite' => 'None']); $encodedcookie = rawurlencode($this->scookie); header("Set-Cookie: sid=$encodedcookie; Max-Age=31363200; Domain={$psconfig['routes']['root']}; Path=/; Secure; SameSite=None"); } } function killCookie() { global $psconfig; if ($this->sid) { $this->scookie = ',,' . $this->sid; // setcookie('sid', $this->scookie, ['expires' => time() + (363)*24*60*60, 'path' => '/', 'domain' => $psconfig['routes']['root'], 'secure' => true, 'httponly' => true, 'samesite' => 'None']); $encodedcookie = rawurlencode($this->scookie); header("Set-Cookie: sid=$encodedcookie; Max-Age=31363200; Domain={$psconfig['routes']['root']}; Path=/; Secure; SameSite=None"); } else { // setcookie('sid', '', ['expires' => time() - 60*60*24*2, 'path' => '/', 'domain' => $psconfig['routes']['root'], 'secure' => true, 'httponly' => true, 'samesite' => 'None']); header("Set-Cookie: sid=; Max-Age=0; Domain={$psconfig['routes']['root']}; Path=/; Secure; SameSite=None"); } } 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` = ? LIMIT 1", [$this->session]); $this->sid = ''; $this->scookie = ''; $this->session = 0; $this->killCookie(); return $curuser; } function createPasswordResetToken(string $name, $timeout=false) { global $psdb, $curuser; $ctime = time(); $userid = $this->userid($name); $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$userid]); 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 (?, ?, ?, ?, ?)", [$user['userid'], $curuser['userid'], time(), $this->getIp(), $modlogentry] ); // magical character string... $token = bin2hex(random_bytes(15)); $res = $psdb->query( "INSERT INTO `{$psdb->prefix}sessions` (`userid`,`sid`,`time`,`timeout`,`ip`) VALUES (?, ?, ?, ?, ?)", [$user['userid'], $token, $ctime, $timeout, $this->getIp()] ); if (!$res) die($psdb->error()); } return $token; } function validatePasswordResetToken(string $token) { global $psdb, $psconfig; if (strlen($token) !== ($psconfig['sid_length'] * 2)) return false; $res = $psdb->query("SELECT * FROM `{$psdb->prefix}sessions` WHERE `sid` = ? LIMIT 1", [$token]); $session = $psdb->fetch_assoc($res); if (!$session) return false; if (intval($session['timeout'])userid($userid); if (!$userid || (($userid === $curuser['userid']) && !empty($user['loggedin']))) { return $curuser; } $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$userid]); if (!$res) // query failed for weird reason return false; $user = $psdb->fetch_assoc($res); if (!$user) { return false; } // unset these values to avoid them being leaked accidentally $user['outdatedpassword'] = !!$user['password']; unset($user['password']); unset($user['nonce']); unset($user['passwordhash']); return $user; } function getGroupName($user = null) { global $ntbb_cache; $user = $this->getUser($user); return @$ntbb_cache['groups'][$user['group']]['name']; } function getGroupSymbol($user = null) { global $ntbb_cache; $user = $this->getUser($user); return @$ntbb_cache['groups'][$user['group']]['symbol']; } function getUserData(string $username) { $userdata = $this->getUser($username); if ($userdata) return $userdata; $userdata = $this->getGuest($username); return $userdata; } function getAssertion(string $userid, string $serverhostname, array $user = null, $challengekeyid = -1, $challenge = '', $challengeprefix = '') { global $psdb, $curuser, $psconfig; if (substr($userid, 0, 5) === 'guest') { return ';;Your username cannot start with \'guest\'.'; } else if (strlen($userid) > 18) { return ';;Your username must be less than 19 characters long.'; } else if (!$this->isUseridAllowed($userid)) { return ';;Your username contains disallowed text.'; } $data = ''; if (!$user) { $user = $curuser; } $ip = $this->getIp(); $forceUsertype = false; if (in_array($ip, $psconfig['autolock_ip']) || in_array($user['ip'], $psconfig['autolock_ip'])) $forceUsertype = '5'; if (($user['userid'] === $userid) && !empty($user['loggedin'])) { // already logged in $usertype = '2'; if (in_array($userid, $psconfig['sysops'], true)) { $usertype = '3'; } else { // check autoconfirmed if ($forceUsertype) { $usertype = $forceUsertype; } else if (intval(@$user['banstate']) <= -10) { $usertype = '4'; } else if (@$user['banstate'] >= 100) { return ';;Your username is no longer available.'; } else if (@$user['banstate'] >= 40) { if ($serverhostname === 'sim3.psim.us') { $usertype = '40'; } else { $usertype = '2'; } } else if (@$user['banstate'] >= 30) { $usertype = '6'; } else if (@$user['banstate'] >= 20) { $usertype = '5'; } else if (@$user['banstate'] == 0) { if (@$user['registertime'] && time() - $user['registertime'] > 7*24*60*60) { $res = $psdb->query("SELECT formatid FROM ntbb_ladder WHERE userid = ? AND w != 0 LIMIT 1", [$userid]); if ($psdb->fetch_assoc($res)) { $usertype = '4'; $psdb->query("UPDATE ntbb_users SET banstate = -10 WHERE userid = ? LIMIT 1", [$userid]); } } } } if (!@$user['logintime'] || time() - $user['logintime'] > 24*60*60) { $psdb->query( "UPDATE ntbb_users SET logintime = ?, loginip = ? WHERE userid = ? LIMIT 1", [time(), $ip, $userid] ); } $data = $user['userid'].',' . $usertype . ','.time().','.$serverhostname; } else { if ((strlen($userid) < 1) || ctype_digit($userid)) { return ';;Your username must contain at least one letter.'; } $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$userid]); if (!$res) { // query failed for weird reason return ';;The login server is under heavy load, please try logging in again later'; } else if ($row = $psdb->fetch_assoc($res)) { // Username exists, but the user isn't logged in: require authentication. if ($row['banstate'] >= 100) { return ';;Your username is no longer available.'; } if ($row['password'] && $row['nonce']) { return ';;Your username is no longer available.'; } if (substr($row['email'], -1) === '@') return ';;@gmail'; return ';'; } else { // Unregistered username. $usertype = '1'; if ($forceUsertype) $usertype = $forceUsertype; $data = $userid.','.$usertype.','.time().','.$serverhostname; $this->updateCookie(); } } $splitChallenge = explode(';', $challenge); if (count($splitChallenge) == 1) { $splitChallenge = explode('|', $challenge); } if (count($splitChallenge) == 1) { $splitChallenge = explode('%7C', $challenge); } $challengetoken = @$_REQUEST['challengetoken']; if (count($splitChallenge) > 1) { $challengekeyid = intval($splitChallenge[0]); $challenge = $splitChallenge[1]; if (@$splitChallenge[2] && !$challengetoken) $challengetoken = $splitChallenge[2]; $_REQUEST['challengetoken'] = $challengetoken; } if ($challengekeyid < 1) { return ';;This server is requesting an invalid login key. This probably means that either you are not connected to a server, or the server is set up incorrectly.'; } else if ($challengekeyid < 2) { // Compromised keys - no longer supported. return ';;This server is using login key ' . $challengekeyid . ', which is no longer supported. Please tell the server operator to update their config.js file.'; } else if (empty($psconfig['privatekeys'][$challengekeyid])) { // Bogus key id. return ';;Unknown key ID'; } else if (!preg_match('/^[0-9a-f]*$/', $challenge)) { // Bogus challenge. return ';;Corrupt challenge'; } else { // Include the challenge in the assertion. $data = $challengeprefix . $challenge . ',' . $data; } if (strpos($challengeprefix . $challenge, ',') !== false) { trigger_error("challenge contains comma? ".$challengeprefix." // ".$challenge, E_USER_ERROR); } if (function_exists('psconfig_validate_assertion')) { psconfig_validate_assertion($data, $serverhostname, $user); } $sig = ''; openssl_sign($data, $sig, openssl_get_privatekey($psconfig['privatekeys'][$challengekeyid])); return $data.';'.bin2hex($sig); } function modifyUser($user, array $changes) { global $psdb, $curuser; $userid = $user; if (is_array($user)) $userid = $user['userid']; $res = $psdb->query("SELECT * FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$userid]); if (!$res) // query failed for weird reason return false; $user = $psdb->fetch_assoc($res); if (!$user['userid']) return false; if (@$changes['password']) { $modlogentry = "Password changed from: {$user['passwordhash']}"; $psdb->query( "INSERT INTO `{$psdb->prefix}usermodlog` (`userid`,`actorid`,`date`,`ip`,`entry`) VALUES (?,?,?,?,?)", [$user['userid'], $curuser['userid'], time(), $this->getIp(), $modlogentry] ); $passwordhash = $this->passwordHash($changes['password']); $psdb->query( "UPDATE `{$psdb->prefix}users` SET `passwordhash`=?, `password`=NULL, `nonce`=NULL WHERE `userid` = ?", [$passwordhash, $user['userid']] ); if ($psdb->error()) { return false; } $psdb->query("DELETE FROM `{$psdb->prefix}sessions` WHERE `userid` = ?", [$user['userid']]); if ($curuser['userid'] === $userid) { $this->login($userid, $changes['password']); } } if (!empty($changes['group'])) { $group = intval($changes['group']); $psdb->query("UPDATE `{$psdb->prefix}users` SET `group` = $group WHERE `userid` = ?", [$user['userid']]); if ($psdb->error()) { return false; } } if (!empty($changes['username'])) { $newUsername = $changes['username']; if (strlen($newUsername) > 18) return false; $newUserid = $this->userid($newUsername); if ($userid !== $newUserid) return false; $psdb->query( "UPDATE `{$psdb->prefix}users` SET `username` = ? WHERE `userid` = ?", [$newUsername, $userid] ); if ($psdb->error()) { return false; } } if (!empty($changes['userdata'])) { $psdb->query( "UPDATE `{$psdb->prefix}users` SET `userdata` = ? WHERE `userid` = ?", [$changes['userdata'], $user['userid']] ); if ($psdb->error()) { return false; } $user['userdata'] = $changes['userdata']; } return true; } function getRecentRegistrationCount($ip = '', $timeperiod = 7200 /* 2 hours */) { global $psdb; if ($ip === '') { $ip = $this->getIp(); } $timestamp = time() - $timeperiod; $res = $psdb->query( "SELECT COUNT(*) AS `registrationcount` FROM `{$psdb->prefix}users` WHERE `ip` = ? AND `registertime` > ?", [$ip, $timestamp] ); if (!$res) { return false; } $user = $psdb->fetch_assoc($res); if ($user === NULL) { // Should be impossible. return 0; } return $user['registrationcount']; } function addUser(array $user, string $password) { global $psdb, $curuser; $ctime = time(); $user['userid'] = $this->userid($user['username']); $user['passwordhash'] = $this->passwordHash($password); if (!$this->isUseridAllowed($user['userid'])) { return false; } $res = $psdb->query("SELECT `userid` FROM `{$psdb->prefix}users` WHERE `userid` = ? LIMIT 1", [$user['userid']]); if ($row = $psdb->fetch_assoc($res)) { return false; } $psdb->query( "INSERT INTO `{$psdb->prefix}users` (`userid`,`username`,`passwordhash`,`email`,`registertime`,`ip`) VALUES (?, ?, ?, ?, ?, ?)", [$user['userid'], $user['username'], $user['passwordhash'], @$user['email'], $ctime, $this->getIp()] ); if ($psdb->error()) { return false; } $user['usernum'] = $psdb->insert_id(); $this->login($user['username'], $password); if (!$curuser['loggedin'] || $curuser['userid'] !== $user['userid']) return false; return $curuser; } function wordfilter(string $text) { $text = str_ireplace('lolicon', '*', $text); $text = str_ireplace('roricon', '*', $text); return $text; } function isUseridAllowed(string $userid) { if (strpos($userid, 'nigger') !== false) return false; if (strpos($userid, 'nigga') !== false) return false; if (strpos($userid, 'faggot') !== false) return false; if (strpos($userid, 'lolicon') !== false) return false; if (strpos($userid, 'roricon') !== false) return false; if (strpos($userid, 'lazyafrican') !== false) return false; return true; } function isSysop(array $user = null) { global $curuser, $psconfig; if (!$user) $user = $curuser; return in_array($user['userid'], $psconfig['sysops'], true); } function isAdmin(array $user = null) { global $curuser; if (!$user) $user = $curuser; return ($user['group'] ?? 0) == 2; } /** unlike isAdmin, includes Smogon senior staff */ function isLeader(array $user = null) { global $curuser; if (!$user) $user = $curuser; return ($user['group'] ?? 0) == 2 || ($user['group'] ?? 0) == 6; } } $users = new NTBBSession();