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;
+ }
+}
+?>