Last active
July 21, 2019 09:01
-
-
Save esterTion/fe184d4e51c2421310bdeba26046f139 to your computer and use it in GitHub Desktop.
Arcaea Api Proxy w/ data recording
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// https://gist.github.com/esterTion/fe184d4e51c2421310bdeba26046f139 | |
$uri = $_SERVER['REQUEST_URI']; | |
$req = file_get_contents("php://input"); | |
if (empty($_SERVER['HTTP_AUTHORIZATION']) || trim($_SERVER['HTTP_AUTHORIZATION']) === 'Bearer') { | |
if (substr($uri, 0, 21) === '/5/compose/aggregate?' && $_SERVER['HTTP_APPVERSION'] === '2.0.2') { | |
header('Content-Type: application/json'); | |
echo file_get_contents(__DIR__.'/empty_arc_response_2.0.2.json'); | |
exit; | |
} | |
header('Content-Type: application/json', true, 400); | |
echo '{"code":"InvalidHeader","message":"BasicAuth content is invalid."}'; | |
exit; | |
} | |
define('API_VER', '6'); | |
if (substr($uri, 0, 21) === '/'.API_VER.'/score/token/world?') { | |
chdir(__DIR__); | |
$token_db = new PDO('sqlite:arc-token.db'); | |
$auth = $_SERVER['HTTP_AUTHORIZATION']; | |
$song = $_GET['song_id']; | |
$diffi = $_GET['difficulty']; | |
if (empty($auth) || empty($song) || empty($auth)) exit; | |
$stmt = $token_db->prepare('SELECT token FROM saved_token WHERE auth_token=? AND song=? AND difficulty=?'); | |
$stmt->execute([substr($auth, 7), $song, $diffi]); | |
$saved = $stmt->fetchAll(); | |
if (!empty($saved)) { | |
header('Content-Type: application/json'); | |
echo json_encode([ | |
'success'=>true, | |
'value' => [ | |
'max_stamina_ts' => (time() + 2 * 3600 - 30) * 1000, | |
'stamina' => 2, | |
'token' => $saved[0]['token'] | |
] | |
]); | |
exit; | |
} | |
} | |
$headers = []; | |
$headersMap = []; | |
foreach ($_SERVER as $key=>$val) { | |
if (substr($key, 0, 5) === 'HTTP_' && $key !== 'HTTP_HOST' && $key !== 'HTTP_ACCEPT_ENCODING') { | |
$key = str_replace('_', '-', substr($key, 5)); | |
$headersMap[strtolower($key)] = $val; | |
$headers[] = $key .': '.$val; | |
} | |
} | |
$curl = curl_init(); | |
curl_setopt_array($curl, [ | |
CURLOPT_URL => 'http://127.0.0.1:6160'.$uri, | |
CURLOPT_HEADER => 1, | |
CURLOPT_HTTPHEADER => $headers, | |
CURLOPT_ENCODING => 'deflate,gzip', | |
CURLOPT_RETURNTRANSFER => 1 | |
]); | |
if ($_SERVER['REQUEST_METHOD'] !== 'GET' ) { | |
curl_setopt_array($curl, [ | |
CURLOPT_POST => 1, | |
CURLOPT_POSTFIELDS => $req, | |
CURLOPT_CUSTOMREQUEST => $_SERVER['REQUEST_METHOD'] | |
]); | |
} | |
$res = curl_exec($curl); | |
$headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); | |
$respHeaders = explode("\r\n", substr($res, 0, $headerSize)); | |
$res = substr($res, $headerSize); | |
$respHeaders[] = 'Request-URI: '.$uri; | |
array_map('header', $respHeaders); | |
header_remove('Content-Encoding'); | |
header_remove('Content-Length'); | |
if (substr($uri, 0, 25) == '/'.API_VER.'/serve/download/me/song') { | |
$res = str_replace('static-lb.lowiro.com', 'arc-src.estertion.win/dl', $res); | |
//$res = preg_replace('("checksum":"([a-f0-9]+)","url":"[^"]+)', '$0&hash=$1', $res); | |
$res = json_decode($res, true); | |
foreach ($res['value'] as &$song) { | |
if (isset($song['audio'])) { | |
$song['audio']['url'] .= '&hash='.$song['audio']['checksum']; | |
} | |
if (isset($song['chart'])) { | |
foreach ($song['chart'] as &$chart) { | |
$chart['url'] .= '&hash='.$chart['checksum']; | |
} | |
} | |
} | |
$res = json_encode($res, JSON_UNESCAPED_SLASHES+JSON_UNESCAPED_UNICODE+JSON_FORCE_OBJECT); | |
} else if (substr($uri, 0, 21) === '/'.API_VER.'/compose/aggregate?') { | |
$res = str_replace('"is_aprilfools":false', '"is_aprilfools":true', $res); | |
} | |
echo $res; | |
fastcgi_finish_request(); | |
$recordDb = new PDO('sqlite:../response-record.db'); | |
$recordDb->prepare('INSERT INTO arc (time,path,method,request,response) VALUES (?,?,?,CAST(? AS BLOB),CAST(? AS BLOB))')->execute([ | |
time(), | |
$uri, | |
$_SERVER['REQUEST_METHOD'], | |
$req, | |
gzencode($res, 9) | |
]); | |
$res = json_decode($res, true); | |
if (substr($uri, 0, 21) === '/'.API_VER.'/compose/aggregate?') { | |
require_once '/data/home/web/task/mysql.php'; | |
$mysqli->select_db('arc'); | |
$mysqli->begin_transaction(); | |
$charas = []; | |
$charasInfoStmt = $mysqli->prepare('INSERT IGNORE INTO chara_info (id,char_type,name,skill_id,skill_unlock_level,uncap_cores) VALUES (?,?,?,?,?,?)'); | |
foreach ($res['value'][0]['value']['character_stats'] as $char) { | |
$charas[] = '('.implode(',', [ | |
$char['character_id'], | |
$char['level'], | |
$char['level_exp'], | |
$char['frag'], | |
$char['prog'], | |
$char['overdrive'] | |
]).')'; | |
$charasInfoStmt->bind_param( | |
'iissis', | |
$char['character_id'], | |
$char['char_type'], | |
$char['name'], | |
$char['skill_id'], | |
$char['skill_unlock_level'], | |
implode(', ', array_map(function($i){return $i['core_type'].'*'.$i['amount'];},$char['uncap_cores'])) | |
); | |
$charasInfoStmt->execute(); | |
} | |
if (!empty($charas)) { | |
$mysqli->query('REPLACE INTO chara_stats (id,lv,lv_exp,frag,prog,overdrive) VALUES '.implode(',', $charas)); | |
} | |
$recents = []; | |
$ratings = []; | |
foreach ($res['value'][0]['value']['recent_score'] as $score) { | |
$recents[] = '('.implode(',', [ | |
'"'.$score['song_id'].'"', | |
$score['difficulty'], | |
$score['score'], | |
$score['rating'], | |
floor($score['time_played']/1000) | |
]).')'; | |
/*$ratings[] = '('.implode(',', [ | |
'"'.$score['song_id'].'"', | |
$score['difficulty'], | |
get_constant_from_score_and_rating($score['score'], $score['rating']), | |
$score['score'], | |
$score['rating'] | |
]).')';*/ | |
} | |
foreach ($res['value'][0]['value']['friends'] as &$friend) { | |
foreach ($friend['recent_score'] as $score) { | |
$ratings[] = '('.implode(',', [ | |
'"'.$score['song_id'].'"', | |
$score['difficulty'], | |
get_constant_from_score_and_rating($score['score'], $score['rating']), | |
$score['score'], | |
$score['rating'] | |
]).')'; | |
} | |
} | |
if (isset($res['value'][0]['value']['user_id'])) { | |
$token_db = new PDO('sqlite:arc-token.db'); | |
$token_db->prepare('REPLACE INTO auth_token_map (auth_token,uid,ucode,name,last_login) VALUES (?,?,?,?,?)')->execute([ | |
substr($headersMap['authorization'], 7), | |
$res['value'][0]['value']['user_id'], | |
$res['value'][0]['value']['user_code'], | |
$res['value'][0]['value']['name'], | |
date('Y/m/d H:i:s') | |
]); | |
$token_db->prepare('REPLACE INTO player_info (id,data) VALUES (?,CAST(? AS BLOB))')->execute([ | |
$res['value'][0]['value']['user_id'], | |
brotli_compress(json_encode($res['value'][0]['value']), 9) | |
]); | |
} | |
/* | |
if (!empty($recents)) { | |
$mysqli->query('INSERT IGNORE INTO song_rating (id,difficulty,score,rating,ts) VALUES '.implode(',', $recents)); | |
} | |
*/ | |
if (!empty($ratings)) { | |
$mysqli->query('INSERT IGNORE INTO chart_constants (id,difficulty,constant,source_score,source_rating) VALUES '.implode(',', $ratings)); | |
} | |
$mysqli->commit(); | |
if (isset($res['value'][5]['value']['current_map'])) { | |
$token_db->prepare('REPLACE INTO player_map_info (id,data) VALUES (?,CAST(? AS BLOB))')->execute([ | |
$res['value'][5]['value']['user_id'], | |
brotli_compress(json_encode($res['value'][5]['value']['maps']), 9) | |
]); | |
} | |
} else if ($uri === '/'.API_VER.'/score/song') { | |
require_once '/data/home/web/task/mysql.php'; | |
$mysqli->select_db('arc'); | |
$req_param = []; | |
array_map(function ($i) use(&$req_param) {$val = explode('=', $i); $req_param[$val[0]] = urldecode($val[1]);}, explode('&', $req)); | |
chdir(__DIR__); | |
$token_db = new PDO('sqlite:arc-token.db'); | |
$uid_row = execQuery($token_db, 'select uid from auth_token_map where auth_token="'.substr($headersMap['authorization'], 7).'"'); | |
$uid = empty($uid_row) ? 0 : $uid_row[0]['uid']; | |
$mysqli->query('INSERT INTO recent_scores (song_id,difficulty,score,ts,rating_after,uid,chara,`exp`,progress) VALUES ('.implode(',', [ | |
'"'.$req_param['song_id'].'"', | |
$req_param['difficulty'], | |
$req_param['score'], | |
time(), | |
$res['value']['user_rating'], | |
$uid, | |
isset($res['value']['exp']) ? $res['value']['char_stats']['character_id'] : 'NULL', | |
isset($res['value']['exp']) ? $res['value']['exp'] : 'NULL', | |
isset($res['value']['exp']) ? $res['value']['base_progress'] : 'NULL', | |
]).')'); | |
if (isset($res['value']['char_stats'])) { | |
$mysqli->query('INSERT IGNORE INTO chara_stats (id,lv,frag,prog,overdrive) VALUES ('.implode(',', [ | |
$res['value']['char_stats']['character_id'], | |
$res['value']['level'], | |
$res['value']['char_stats']['frag'], | |
$res['value']['char_stats']['prog'], | |
$res['value']['char_stats']['overdrive'] | |
]).')'); | |
} | |
if (isset($res['value']['reward_char_stats'])) { | |
$charas = []; | |
foreach ($res['value']['reward_char_stats'] as $char) { | |
$charas[] = '('.implode(',', [ | |
$char['character_id'], | |
$char['level'], | |
$char['level_exp'], | |
$char['frag'], | |
$char['prog'], | |
$char['overdrive'] | |
]).')'; | |
} | |
if (!empty($charas)) { | |
$mysqli->query('REPLACE INTO chara_stats (id,lv,lv_exp,frag,prog,overdrive) VALUES '.implode(',', $charas)); | |
} | |
} | |
if (!empty($req_param['song_token'])) { | |
$token_db->prepare('DELETE FROM saved_token WHERE token=?')->execute([$req_param['song_token']]); | |
} | |
} else if (substr($uri, 0, 21) === '/'.API_VER.'/score/token/world?') { | |
if (!empty($res['value']['token'])) | |
$token_db->prepare('INSERT INTO saved_token (auth_token,song,difficulty,token) VALUES (?,?,?,?)')->execute([ | |
substr($auth, 7), | |
$song, | |
$diffi, | |
$res['value']['token'] | |
]); | |
} | |
/* | |
v1.8.1 排名rating删除 | |
else if (substr($uri, 0, 13) === '/4/score/song') { | |
if (isset($res['value'][0]['rating'])) { | |
$val = $res['value'][0]; | |
require_once '/data/home/web/task/mysql.php'; | |
$mysqli->select_db('arc'); | |
$mysqli->query('INSERT IGNORE INTO chart_constants (id,difficulty,constant,source_score,source_rating) VALUES ('.implode(',', [ | |
'"'.$val['song_id'].'"', | |
$val['difficulty'], | |
get_constant_from_score_and_rating($val['score'], $val['rating']), | |
$val['score'], | |
$val['rating'] | |
]).')'); | |
} | |
}*/ | |
else if (substr($uri, 0, 16) === '/'.API_VER.'/world/map/me/') { | |
$mapId = substr($uri, 16); | |
require_once '/data/home/web/task/mysql.php'; | |
$mysqli->select_db('arc'); | |
$mysqli->query('REPLACE INTO map_json (`hash`,`id`,`data`) VALUES ('.crc32($mapId).',"'.$mapId.'","'.$mysqli->real_escape_string(gzencode(prettifyJSON(json_encode($res, JSON_UNESCAPED_UNICODE+JSON_UNESCAPED_SLASHES)), 9)).'")'); | |
} else if (substr($uri, 0, 22) == '/'.API_VER.'//user/me/character/') { | |
$charas = []; | |
foreach ($res['value']['character'] as $char) { | |
$charas[] = '('.implode(',', [ | |
$char['character_id'], | |
$char['level'], | |
$char['level_exp'], | |
$char['frag'], | |
$char['prog'], | |
$char['overdrive'] | |
]).')'; | |
} | |
if (!empty($charas)) { | |
require_once '/data/home/web/task/mysql.php'; | |
$mysqli->select_db('arc'); | |
$mysqli->query('REPLACE INTO chara_stats (id,lv,lv_exp,frag,prog,overdrive) VALUES '.implode(',', $charas)); | |
} | |
} else if ($uri == '/'.API_VER.'/world/map/me') { | |
if ($res['success']) { | |
chdir(__DIR__); | |
$token_db = new PDO('sqlite:arc-token.db'); | |
$token_db->prepare('REPLACE INTO player_map_info (id,data) VALUES (?,CAST(? AS BLOB))')->execute([ | |
$res['value']['user_id'], | |
brotli_compress(json_encode($res['value']['maps']), 9) | |
]); | |
} | |
} | |
function execQuery($db, $query) { | |
$returnVal = []; | |
if (!$db) { | |
throw new Exception('Invalid db handle'); | |
} | |
$result = $db->query($query); | |
if ($result === false) { | |
throw new Exception('Failed executing query: '. $query); | |
} | |
$returnVal = $result->fetchAll(PDO::FETCH_ASSOC); | |
return $returnVal; | |
} | |
function get_constant_from_score_and_rating(int $score, float $rating) { | |
$return = 0; | |
if ($score > 10e6) { | |
$return = $rating - 2; | |
} else if ($score > 9.95e6) { | |
$return = $rating - 1.5 - ($score - 9.95e6) / 0.3e6 * 3.0; | |
} else if ($score > 9.8e6) { | |
$return = $rating - 1.0 - ($score - 9.8e6) / 0.3e6 * 0.75; | |
} else if ($rating > 0) { | |
$return = $rating - ($score - 9.5e6) / 0.3e6; | |
} | |
return round($return * 100) / 100; | |
} | |
function prettifyJSON($in) { | |
$in = new MemoryStream($in); | |
$out = new MemoryStream(''); | |
$offset = 0; | |
$length = $in->size; | |
$level = 0; | |
while($offset < $length) { | |
$char = $in->readData(1); | |
switch($char) { | |
case '"': | |
// write until unqoute | |
$out->write($char); | |
$skipNext = false; | |
while (1) { | |
$char = $in->readData(1); | |
$out->write($char); | |
$offset++; | |
if ($skipNext) $skipNext = false; | |
else { | |
if ($char == '\\') $skipNext = true; | |
else if ($char == '"') break; | |
} | |
} | |
break; | |
case '{': | |
case '[': | |
// increase level | |
$level++; | |
$out->write($char); | |
$out->write("\n".str_repeat(' ', $level)); | |
break; | |
case '}': | |
case ']': | |
$level--; | |
$nextChar = $in->readData(1); | |
if ($nextChar == ',') { | |
$out->write("\n".str_repeat(' ', $level)); | |
$out->write($char); | |
$out->write($nextChar); | |
$out->write("\n".str_repeat(' ', $level)); | |
$offset++; | |
} else { | |
$out->write("\n".str_repeat(' ', $level)); | |
$out->write($char); | |
if ($nextChar == '') $out->write("\n"); | |
$in->seek($offset+1); | |
} | |
break; | |
case ',': | |
// add space after comma | |
$out->write($char); | |
$out->write(' '); | |
break; | |
default: | |
$out->write($char); | |
} | |
$offset++; | |
} | |
$out->seek(0); | |
$output = $out->readData($out->size); | |
unset($out); | |
return $output; | |
} | |
abstract class Stream { | |
abstract protected function read($length); | |
abstract public function seek($position); | |
abstract public function position(); | |
abstract protected function getPos(); | |
abstract protected function setPos($pos); | |
public function __get($name) { | |
switch($name) { | |
case 'position': return $this->getPos(); | |
case 'bool': return $this->readBoolean(); | |
case 'byte': return $this->readData(1); | |
case 'short': return $this->readInt16(); | |
case 'ushort': return $this->readUint16(); | |
case 'long': return $this->readInt32(); | |
case 'ulong': return $this->readUint32(); | |
case 'longlong': return $this->readInt64(); | |
case 'ulonglong': return $this->readUint64(); | |
case 'float': return $this->readFloat(); | |
case 'double': return $this->readDouble(); | |
case 'string': return $this->readStringToNull(); | |
case 'line': return $this->readStringToReturn(); | |
default: throw new Exception("Access undefined field ${name} of class ".get_class($this)); | |
} | |
} | |
public function __set($name, $val) { | |
switch($name) { | |
case 'position': return $this->setPos($val); | |
default: throw new Exception("Assign value to undefined field ${name} of class ".get_class($this)); | |
} | |
} | |
public $littleEndian = false; | |
public $size; | |
public function readStringToNull() { | |
$s = ''; | |
while (ord($char = $this->read(1)) != 0) { | |
$s .= $char; | |
} | |
return $s; | |
} | |
public function readStringAt($pos) { | |
$current = $this->position; | |
$this->position = $pos; | |
$data = $this->string; | |
$this->position = $current; | |
return $data; | |
} | |
public function readStringToReturn() { | |
$s = ''; | |
while ($this->position < $this->size && ($char = $this->read(1)) != "\n") { | |
$s .= $char; | |
} | |
return trim($s,"\r"); | |
} | |
public function readBoolean() { | |
return ord($this->byte)>0; | |
} | |
public function readInt16() { | |
$uint = $this->readUint16(); | |
$sint = unpack('s', pack('S', $uint))[1]; | |
return $sint; | |
} | |
public function readUint16() { | |
$int = $this->read(2); | |
if (strlen($int) != 2) return 0; | |
return unpack($this->littleEndian?'v':'n', $int)[1]; | |
} | |
public function readInt32() { | |
$uint = $this->readUint32(); | |
$sint = unpack('l', pack('L', $uint))[1]; | |
return $sint; | |
} | |
public function readUint32() { | |
$int = $this->read(4); | |
if (strlen($int) != 4) return 0; | |
return unpack($this->littleEndian?'V':'N', $int)[1]; | |
} | |
public function readInt64() { | |
$uint = $this->readUint64(); | |
$sint = unpack('q', pack('Q', $uint))[1]; | |
return $sint; | |
} | |
public function readUint64() { | |
$int = $this->read(8); | |
if (strlen($int) != 8) return 0; | |
return unpack($this->littleEndian?'P':'J', $int)[1]; | |
} | |
public function readFloat() { | |
$int = $this->read(4); | |
if (strlen($int) != 4) return 0; | |
if (!$this->littleEndian) $int = $int[3].$int[2].$int[1].$int[0]; | |
return unpack(/*$this->littleEndian?'g':'G'*/ 'f', $int)[1]; | |
} | |
public function readDouble() { | |
$int = $this->read(8); | |
if (strlen($int) != 8) return 0; | |
if (!$this->littleEndian) $int = $int[7].$int[6].$int[5].$int[4].$int[3].$int[2].$int[1].$int[0]; | |
return unpack(/*$this->littleEndian?'e':'E'*/ 'd', $int)[1]; | |
} | |
public function readData($size) { | |
return $this->read($size); | |
} | |
public function readDataAt($pos, $size) { | |
$current = $this->position; | |
$this->position = $pos; | |
$data = $this->readData($size); | |
$this->position = $current; | |
return $data; | |
} | |
public function alignStream($alignment) { | |
$mod = $this->position % $alignment; | |
if ($mod != 0) { | |
$this->position += $alignment - $mod; | |
} | |
} | |
public function readAlignedString($len) { | |
$string = $this->readData($len); | |
$this->alignStream(4); | |
return $string; | |
} | |
} | |
class MemoryStream extends Stream { | |
private $data; | |
private $offset; | |
function __construct($data) { | |
$this->data = $data; | |
$this->size = strlen($data); | |
} | |
function __destruct() { | |
$this->data = NULL; | |
} | |
protected function read($length) { | |
$data = substr($this->data, $this->offset, $length); | |
$this->offset += $length; | |
return $data; | |
} | |
public function seek($position) { | |
$this->offset = $position; | |
} | |
public function write($newData) { | |
$this->data .= $newData; | |
$this->size += strlen($newData); | |
} | |
public function position() { | |
return $this->offset; | |
} | |
protected function getPos() { | |
return $this->offset; | |
} | |
protected function setPos($pos) { | |
$this->offset = $pos; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment