Skip to content

Instantly share code, notes, and snippets.

@esterTion
Last active July 21, 2019 09:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save esterTion/fe184d4e51c2421310bdeba26046f139 to your computer and use it in GitHub Desktop.
Save esterTion/fe184d4e51c2421310bdeba26046f139 to your computer and use it in GitHub Desktop.
Arcaea Api Proxy w/ data recording
<?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