From 240be183676a629f9e7ee3e6d458a3cf23fb71af Mon Sep 17 00:00:00 2001 From: slawkens Date: Mon, 16 May 2022 20:31:19 +0200 Subject: [PATCH] Update login.php for latest TFS 1.x and otservbr Works in both. Thanks for Znote for rfc6238 lib. --- login.php | 277 +++++++++++++++++++++++--------------- system/libs/rfc6238.php | 285 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 453 insertions(+), 109 deletions(-) create mode 100644 system/libs/rfc6238.php diff --git a/login.php b/login.php index 0a641404..8fb7c9bb 100644 --- a/login.php +++ b/login.php @@ -1,45 +1,47 @@ getAttribute('startdate'); - return date_create("{$date}")->format('U'); + if ($table1) { + if ($date) { + if ($table2) { + $date = $table1->getAttribute('startdate'); + return date_create("{$date}")->format('U'); + } else { + $date = $table1->getAttribute('enddate'); + return date_create("{$date}")->format('U'); + } } else { - $date = $table1->getAttribute('enddate'); - return date_create("{$date}")->format('U'); - } - } else { - foreach($table1 as $attr) { - if ($attr) { - return $attr->getAttribute($table2); + foreach($table1 as $attr) { + if ($attr) { + return $attr->getAttribute($table2); + } } } } -} - return; + return 'error'; } -$request = file_get_contents('php://input'); -$result = json_decode($request); -$action = isset($result->type) ? $result->type : ''; +$request = json_decode(file_get_contents('php://input')); +$action = $request->type ?? ''; + +/** @var OTS_Base_DB $db */ +/** @var array $config */ switch ($action) { case 'cacheinfo': @@ -51,35 +53,32 @@ switch ($action) { 'gamingyoutubestreams' => 0, 'gamingyoutubeviewer' => 0 ])); - break; - - case 'eventschedule': - $eventlist = []; - $file_path = config('server_path') . 'data/XML/events.xml'; - if (!file_exists($file_path)) { - die(json_encode([])); - break; - } - $xml = new DOMDocument; - $xml->load($file_path); - $tmplist = []; - $tableevent = $xml->getElementsByTagName('event'); - foreach ($tableevent as $event) { - if ($event) { $tmplist = [ - 'colorlight' => parseEvent($event->getElementsByTagName('colors'), false, 'colorlight'), - 'colordark' => parseEvent($event->getElementsByTagName('colors'), false, 'colordark'), - 'description' => parseEvent($event->getElementsByTagName('description'), false, 'description'), - 'displaypriority' => intval(parseEvent($event->getElementsByTagName('details'), false, 'displaypriority')), - 'enddate' => intval(parseEvent($event, true, false)), - 'isseasonal' => getBoolean(intval(parseEvent($event->getElementsByTagName('details'), false, 'isseasonal'))), - 'name' => $event->getAttribute('name'), - 'startdate' => intval(parseEvent($event, true, true)), - 'specialevent' => intval(parseEvent($event->getElementsByTagName('details'), false, 'specialevent')) - ]; - $eventlist[] = $tmplist; } } - die(json_encode(['eventlist' => $eventlist, 'lastupdatetimestamp' => time()])); - break; + case 'eventschedule': + $eventlist = []; + $file_path = config('server_path') . 'data/XML/events.xml'; + if (!file_exists($file_path)) { + die(json_encode([])); + } + $xml = new DOMDocument; + $xml->load($file_path); + $tmplist = []; + $tableevent = $xml->getElementsByTagName('event'); + + foreach ($tableevent as $event) { + if ($event) { $tmplist = [ + 'colorlight' => parseEvent($event->getElementsByTagName('colors'), false, 'colorlight'), + 'colordark' => parseEvent($event->getElementsByTagName('colors'), false, 'colordark'), + 'description' => parseEvent($event->getElementsByTagName('description'), false, 'description'), + 'displaypriority' => intval(parseEvent($event->getElementsByTagName('details'), false, 'displaypriority')), + 'enddate' => intval(parseEvent($event, true, false)), + 'isseasonal' => getBoolean(intval(parseEvent($event->getElementsByTagName('details'), false, 'isseasonal'))), + 'name' => $event->getAttribute('name'), + 'startdate' => intval(parseEvent($event, true, true)), + 'specialevent' => intval(parseEvent($event->getElementsByTagName('details'), false, 'specialevent')) + ]; + $eventlist[] = $tmplist; } } + die(json_encode(['eventlist' => $eventlist, 'lastupdatetimestamp' => time()])); case 'boostedcreature': $boostDB = $db->query("select * from " . $db->tableName('boosted_creature'))->fetchAll(); @@ -92,9 +91,9 @@ switch ($action) { break; case 'login': - + $port = $config['lua']['gameProtocolPort']; - + // default world info $world = [ 'id' => 0, @@ -115,75 +114,136 @@ switch ($action) { ]; $characters = []; - $account = null; - - // common columns - $columns = 'name, level, sex, vocation, looktype, lookhead, lookbody, looklegs, lookfeet, lookaddons, lastlogin, isreward, istutorial'; - $account = new OTS_Account(); - $account->findByEmail($result->email); + + $inputEmail = $request->email ?? false; + $inputAccountName = $request->accountname ?? false; + $inputToken = $request->token ?? false; + + if ($inputEmail != false) { // login by email + $account->findByEmail($request->email); + } + else if($inputAccountName != false) { // login by account name + $account->find($inputAccountName); + } + $config_salt_enabled = fieldExist('salt', 'accounts'); - $current_password = encrypt(($config_salt_enabled ? $account->getCustomField('salt') : '') . $result->password); + $current_password = encrypt(($config_salt_enabled ? $account->getCustomField('salt') : '') . $request->password); if (!$account->isLoaded() || $account->getPassword() != $current_password) { - sendError('Email or password is not correct.'); + sendError(($inputEmail != false ? 'Email' : 'Account name') . ' or password is not correct.'); + } + + //log_append('test.log', var_export($account->getCustomField('secret'), true)); + $accountHasSecret = false; + if (fieldExist('secret', 'accounts')) { + $accountSecret = $account->getCustomField('secret'); + if ($accountSecret != null && $accountSecret != '') { + $accountHasSecret = true; + if ($inputToken === false) { + sendError('Submit a valid two-factor authentication token.', 6); + } else { + require_once LIBS . 'rfc6238.php'; + if (TokenAuth6238::verify($accountSecret, $inputToken) !== true) { + sendError('Two-factor authentication failed, token is wrong.', 6); + } + } + } + } + + // common columns + $columns = 'id, name, level, sex, vocation, looktype, lookhead, lookbody, looklegs, lookfeet, lookaddons'; + + if (fieldExist('isreward', 'accounts')) { + $columns .= ', isreward'; + } + + if (fieldExist('istutorial', 'accounts')) { + $columns .= ', istutorial'; } $players = $db->query("select {$columns} from players where account_id = " . $account->getId() . " AND deletion = 0"); if($players && $players->rowCount() > 0) { $players = $players->fetchAll(); + + $highestLevelId = 0; + $highestLevel = 0; foreach ($players as $player) { - $characters[] = create_char($player); + if ($player['level'] >= $highestLevel) { + $highestLevel = $player['level']; + $highestLevelId = $player['id']; + } + } + + foreach ($players as $player) { + $characters[] = create_char($player, $highestLevelId); } } - - $save = false; - $timeNow = time(); - $query = $db->query("select `premdays`, `lastday` from `accounts` where `id` = " . $account->getId()); - if($query->rowCount() > 0) { + + if (fieldExist('premdays', 'accounts') && fieldExist('lastday', 'accounts')) { + $save = false; + $timeNow = time(); + $query = $db->query("select `premdays`, `lastday` from `accounts` where `id` = " . $account->getId()); + if ($query->rowCount() > 0) { $query = $query->fetch(); $premDays = (int)$query['premdays']; $lastDay = (int)$query['lastday']; $lastLogin = $lastDay; - } - else { - sendError("Error while fetching your account data. Please contact admin."); - } - if($premDays != 0 && $premDays != PHP_INT_MAX ) { - if($lastDay == 0) { - $lastDay = $timeNow; - $save = true; } else { - $days = (int)(($timeNow - $lastDay) / 86400); - if($days > 0) { - if($days >= $premDays) { - $premDays = 0; - $lastDay = 0; - } else { - $premDays -= $days; - $reminder = (int)(($timeNow - $lastDay) % 86400); - $lastDay = $timeNow - $reminder; - } - - $save = true; - } + sendError("Error while fetching your account data. Please contact admin."); + } + if ($premDays != 0 && $premDays != PHP_INT_MAX) { + if ($lastDay == 0) { + $lastDay = $timeNow; + $save = true; + } else { + $days = (int)(($timeNow - $lastDay) / 86400); + if ($days > 0) { + if ($days >= $premDays) { + $premDays = 0; + $lastDay = 0; + } else { + $premDays -= $days; + $reminder = ($timeNow - $lastDay) % 86400; + $lastDay = $timeNow - $reminder; + } + + $save = true; + } + } + } else if ($lastDay != 0) { + $lastDay = 0; + $save = true; + } + if ($save) { + $db->query("update `accounts` set `premdays` = " . $premDays . ", `lastday` = " . $lastDay . " where `id` = " . $account->getId()); } - } else if ($lastDay != 0) { - $lastDay = 0; - $save = true; } - if($save) { - $db->query("update `accounts` set `premdays` = " . $premDays . ", `lastday` = " . $lastDay . " where `id` = " . $account->getId()); - } - $premiumAccount = $premDays > 0; - $timePremium = time() + ($premDays * 86400); $worlds = [$world]; $playdata = compact('worlds', 'characters'); + + $sessionKey = ($inputEmail !== false) ? $inputEmail : $inputAccountName; // email or account name + $sessionKey .= "\n" . $request->password; // password + if (!fieldExist('istutorial', 'players')) { + $sessionKey .= "\n"; + } + $sessionKey .= ($accountHasSecret && strlen($accountSecret) > 5) ? $inputToken : ''; + + // this is workaround to distinguish between TFS 1.x and otservbr + // TFS 1.x requires the number in session key + // otservbr requires just login and password + // so we check for istutorial field which is present in otservbr, and not in TFS + if (!fieldExist('istutorial', 'players')) { + $sessionKey .= "\n".floor(time() / 30); + } + + log_append('slaw.log', $sessionKey); + $session = [ - 'sessionkey' => "$result->email\n$result->password", - 'lastlogintime' => (!$account) ? 0 : $account->getLastLogin(), - 'ispremium' => ($config['lua']['freePremium']) ? true : $account->isPremium(), + 'sessionkey' => $sessionKey, + 'lastlogintime' => 0, + 'ispremium' => $config['lua']['freePremium'] || $account->isPremium(), 'premiumuntil' => ($account->getPremDays()) > 0 ? (time() + ($account->getPremDays() * 86400)) : 0, 'status' => 'active', // active, frozen or suspended 'returnernotification' => false, @@ -195,20 +255,19 @@ switch ($action) { 'emailcoderequest' => false ]; die(json_encode(compact('session', 'playdata'))); - break; - + default: sendError("Unrecognized event {$action}."); break; } -function create_char($player) { +function create_char($player, $highestLevelId) { global $config; return [ 'worldid' => 0, 'name' => $player['name'], 'ismale' => intval($player['sex']) === 1, - 'tutorial' => (bool)$player['istutorial'], + 'tutorial' => isset($player['istutorial']) && $player['istutorial'], 'level' => intval($player['level']), 'vocation' => $config['vocations'][$player['vocation']], 'outfitid' => intval($player['looktype']), @@ -217,10 +276,10 @@ function create_char($player) { 'legscolor' => intval($player['looklegs']), 'detailcolor' => intval($player['lookfeet']), 'addonsflags' => intval($player['lookaddons']), - 'ishidden' => 0, + 'ishidden' => isset($player['deletion']) && (int)$player['deletion'] === 1, 'istournamentparticipant' => false, - 'ismaincharacter' => true, - 'dailyrewardstate' => intval($player['isreward']), + 'ismaincharacter' => $highestLevelId == $player['id'], + 'dailyrewardstate' => isset($player['isreward']) ? intval($player['isreward']) : 0, 'remainingdailytournamentplaytime' => 0 ]; } diff --git a/system/libs/rfc6238.php b/system/libs/rfc6238.php new file mode 100644 index 00000000..579f08f2 --- /dev/null +++ b/system/libs/rfc6238.php @@ -0,0 +1,285 @@ +'0', 'B'=>'1', 'C'=>'2', 'D'=>'3', 'E'=>'4', 'F'=>'5', 'G'=>'6', 'H'=>'7', + 'I'=>'8', 'J'=>'9', 'K'=>'10', 'L'=>'11', 'M'=>'12', 'N'=>'13', 'O'=>'14', 'P'=>'15', + 'Q'=>'16', 'R'=>'17', 'S'=>'18', 'T'=>'19', 'U'=>'20', 'V'=>'21', 'W'=>'22', 'X'=>'23', + 'Y'=>'24', 'Z'=>'25', '2'=>'26', '3'=>'27', '4'=>'28', '5'=>'29', '6'=>'30', '7'=>'31' + ); + + /** + * Use padding false when encoding for urls + * + * @return base32 encoded string + * @author Bryan Ruiz + **/ + public static function encode($input, $padding = true) { + if(empty($input)) return ""; + + $input = str_split($input); + $binaryString = ""; + + for($i = 0; $i < count($input); $i++) { + $binaryString .= str_pad(base_convert(ord($input[$i]), 10, 2), 8, '0', STR_PAD_LEFT); + } + + $fiveBitBinaryArray = str_split($binaryString, 5); + $base32 = ""; + $i=0; + + while($i < count($fiveBitBinaryArray)) { + $base32 .= self::$map[base_convert(str_pad($fiveBitBinaryArray[$i], 5,'0'), 2, 10)]; + $i++; + } + + if($padding && ($x = strlen($binaryString) % 40) != 0) { + if($x == 8) $base32 .= str_repeat(self::$map[32], 6); + else if($x == 16) $base32 .= str_repeat(self::$map[32], 4); + else if($x == 24) $base32 .= str_repeat(self::$map[32], 3); + else if($x == 32) $base32 .= self::$map[32]; + } + + return $base32; + } + + public static function decode($input) { + if(empty($input)) return; + + $paddingCharCount = substr_count($input, self::$map[32]); + $allowedValues = array(6,4,3,1,0); + + if(!in_array($paddingCharCount, $allowedValues)) return false; + + for($i=0; $i<4; $i++){ + if($paddingCharCount == $allowedValues[$i] && + substr($input, -($allowedValues[$i])) != str_repeat(self::$map[32], $allowedValues[$i])) return false; + } + + $input = str_replace('=','', $input); + $input = str_split($input); + $binaryString = ""; + + for($i=0; $i < count($input); $i = $i+8) { + $x = ""; + + if(!in_array($input[$i], self::$map)) return false; + + for($j=0; $j < 8; $j++) { + $x .= str_pad(base_convert(@self::$flippedMap[@$input[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT); + } + + $eightBits = str_split($x, 8); + + for($z = 0; $z < count($eightBits); $z++) { + $binaryString .= ( ($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48 ) ? $y:""; + } + } + + return $binaryString; + } +} + +// http://www.faqs.org/rfcs/rfc6238.html +// https://github.com/Voronenko/PHPOTP/blob/08cda9cb9c30b7242cf0b3a9100a6244a2874927/code/rfc6238.php +// Local changes: http -> https, consistent indentation, 200x200 -> 300x300 QR image size, PHP end tag +class TokenAuth6238 { + + /** + * verify + * + * @param string $secretkey Secret clue (base 32). + * @return bool True if success, false if failure + */ + public static function verify($secretkey, $code, $rangein30s = 3) { + $key = base32static::decode($secretkey); + $unixtimestamp = time()/30; + + for($i=-($rangein30s); $i<=$rangein30s; $i++) { + $checktime = (int)($unixtimestamp+$i); + $thiskey = self::oath_hotp($key, $checktime); + + if ((int)$code == self::oath_truncate($thiskey,6)) { + return true; + } + + } + return false; + } + + + public static function getTokenCode($secretkey,$rangein30s = 3) { + $result = ""; + $key = base32static::decode($secretkey); + $unixtimestamp = time()/30; + + for($i=-($rangein30s); $i<=$rangein30s; $i++) { + $checktime = (int)($unixtimestamp+$i); + $thiskey = self::oath_hotp($key, $checktime); + $result = $result." # ".self::oath_truncate($thiskey,6); + } + + return $result; + } + + public static function getTokenCodeDebug($secretkey,$rangein30s = 3) { + $result = ""; + print "
SecretKey: $secretkey
"; + + $key = base32static::decode($secretkey); + print "Key(base 32 decode): $key
"; + + $unixtimestamp = time()/30; + print "UnixTimeStamp (time()/30): $unixtimestamp
"; + + for($i=-($rangein30s); $i<=$rangein30s; $i++) { + $checktime = (int)($unixtimestamp+$i); + print "Calculating oath_hotp from (int)(unixtimestamp +- 30sec offset): $checktime basing on secret key
"; + + $thiskey = self::oath_hotp($key, $checktime, true); + print "======================================================
"; + print "CheckTime: $checktime oath_hotp:".$thiskey."
"; + + $result = $result." # ".self::oath_truncate($thiskey,6,true); + } + + return $result; + } + + public static function getBarCodeUrl($username, $domain, $secretkey, $issuer) { + $url = "https://chart.apis.google.com/chart"; + $url = $url."?chs=300x300&chld=M|0&cht=qr&chl=otpauth://totp/"; + $url = $url.$username . "@" . $domain . "%3Fsecret%3D" . $secretkey . '%26issuer%3D' . rawurlencode($issuer); + return $url; + } + + public static function generateRandomClue($length = 16) { + $b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM"; + $s = ""; + + for ($i = 0; $i < $length; $i++) + $s .= $b32[rand(0,31)]; + + return $s; + } + + private static function hotp_tobytestream($key) { + $result = array(); + $last = strlen($key); + for ($i = 0; $i < $last; $i = $i + 2) { + $x = $key[$i] + $key[$i + 1]; + $x = strtoupper($x); + $x = hexdec($x); + $result = $result.chr($x); + } + + return $result; + } + + private static function oath_hotp ($key, $counter, $debug=false) { + $result = ""; + $orgcounter = $counter; + $cur_counter = array(0,0,0,0,0,0,0,0); + + if ($debug) { + print "Packing counter $counter (".dechex($counter).")into binary string - pay attention to hex representation of key and binary representation
"; + } + + for($i=7;$i>=0;$i--) { // C for unsigned char, * for repeating to the end of the input data + $cur_counter[$i] = pack ('C*', $counter); + + if ($debug) { + print $cur_counter[$i]."(".dechex(ord($cur_counter[$i])).")"." from $counter
"; + } + + $counter = $counter >> 8; + } + + if ($debug) { + foreach ($cur_counter as $char) { + print ord($char) . " "; + } + + print "
"; + } + + $binary = implode($cur_counter); + + // Pad to 8 characters + str_pad($binary, 8, chr(0), STR_PAD_LEFT); + + if ($debug) { + print "Prior to HMAC calculation pad with zero on the left until 8 characters.
"; + print "Calculate sha1 HMAC(Hash-based Message Authentication Code http://en.wikipedia.org/wiki/HMAC).
"; + print "hash_hmac ('sha1', $binary, $key)
"; + } + + $result = hash_hmac ('sha1', $binary, $key); + + if ($debug) { + print "Result: $result
"; + } + + return $result; + } + + private static function oath_truncate($hash, $length = 6, $debug=false) { + $result=""; + + // Convert to dec + if($debug) { + print "converting hex hash into characters
"; + } + + $hashcharacters = str_split($hash,2); + + if($debug) { + print_r($hashcharacters); + print "
and convert to decimals:
"; + } + + for ($j=0; $j"; + print "offset:".$offset; + } + + $result = ( + (($hmac_result[$offset+0] & 0x7f) << 24 ) | + (($hmac_result[$offset+1] & 0xff) << 16 ) | + (($hmac_result[$offset+2] & 0xff) << 8 ) | + ($hmac_result[$offset+3] & 0xff) + ) % pow(10,$length); + + return $result; + } +} +?>