'run: script is exception', -101 => 'checkSign: param is missing', -102 => 'checkSign: appid is invalid', -103 => 'checkSign: sign is expire', -104 => 'checkSign: sign is invalid', -105 => 'checkToken: token is missing', -106 => 'checkToken: token is expire', -107 => 'checkToken: appid is error', -108 => 'getTokenData: token is error', -109 => 'getApiParam: api is invalid', -110 => 'getAppidPerm: appid is invalid', -111 => 'getSecret: appid is invalid', -112 => 'parseQuery: api url is empty', -113 => 'parseQuery: api url is error', -114 => 'initParam: api is invalid', -115 => 'apiPermCheck: api is invalid', -116 => 'apiFreqCheck: out of frequency', -117 => 'scriptCheck: script is empty', -118 => 'scriptCheck: script format is error', -119 => 'callback: authtoken is invalid', ]; const Developer = false; public function __construct($postParam = []) { require_once ROOT_PATH.'./source/function/function_core.php'; $this->postParam = $postParam; } public function parseQuery() { $s = rawurldecode($_SERVER['QUERY_STRING']); if(!$s) { $this->error(-112); } if(str_ends_with($s, '=')) { $s = substr($s, 0, -1); } if(!str_starts_with($s, '/')) { $s = '/'.$s; } $e = explode('/', $s); $c = count($e); if(preg_match('/^v\d+$/', $e[$c - 1])) { $api = array_slice($e, 1, -1); $ver = $e[$c - 1]; } else { $api = array_slice($e, 1); $ver = 'v1'; } if(!$api || !$ver) { $this->error(-113); } return [$api, $ver]; } public function initParam($api, $ver) { $apiParams = $this->_getApiParam($api, $ver); $key = '/'.implode('/', $api); if(empty($apiParams[$key])) { $this->error(-114); } $params = $apiParams[$key]; $params['perms'] = [$key.'/'.$ver]; $this->apiParam = $params; return $params['script']; } public function paramDecode($key) { if(empty($this->apiParam[$key])) { return []; } if(is_array($this->apiParam[$key])) { $v = $this->apiParam[$key]; } else { $_tmp = unserialize($this->apiParam[$key]); $v = $_tmp === false ? $this->apiParam[$key] : $_tmp; } return $this->_replacePostParams($v); } public function getRequestParam() { return !empty($this->postParam['_REQUEST']) ? json_decode($this->postParam['_REQUEST'], true) : []; } public function getShutdownFunc() { $output = !empty($this->apiParam['output']) ? $this->apiParam['output'] : []; $rawOutput = !empty($this->apiParam['raw']); $shutdownFunc = 'showOutput'; if($rawOutput) { $shutdownFunc = 'rawOutput'; } elseif($output) { $shutdownFunc = 'convertOutput'; } return [$shutdownFunc, $output]; } private function _replacePostParams($v) { foreach($v as $_k => $_v) { if(is_array($_v)) { $v[$_k] = $this->_replacePostParams($_v); } elseif(is_string($_v)) { if(str_starts_with($_v, '{') && preg_match('/^\{:(\w+):\}$/', $_v, $r)) { $v[$_k] = !empty($this->postParam[$r[1]]) ? $this->postParam[$r[1]] : null; } } } return $v; } public function apiPermCheck() { if(!empty($this->tokenData['_conf']['apis']) && !array_intersect($this->apiParam['perms'], $this->tokenData['_conf']['apis'])) { $this->error(-115); } } public function apiFreqCheck() { $api = $this->apiParam['perms'][0]; if(!empty($this->tokenData['_conf']['freq'][$api])) { $key = self::ApiFreqPre.$this->_appId.'_'.$api; $v = $this->_memory('get', [$key]); if(!$v) { $this->_memory('inc', [$key]); $this->_memory('expire', [$key, 10]);//self::ApiFreqTTL } elseif($v >= $this->tokenData['_conf']['freq'][$api]) { $this->error(-116); } else { $this->_memory('inc', [$key]); } } } public function scriptCheck() { if(empty($this->apiParam['script'])) { $this->error(-117); } if(!preg_match('/^\w+$/', $this->apiParam['script'])) { $this->error(-118); } return $this->apiParam['script']; } public function error($code) { $this->output([ 'ret' => $code, 'msg' => !empty(self::Error[$code]) ? self::Error[$code] : '', ]); } public function output($value) { $this->_setSysVar($return['data'], $value); echo json_encode($value); exit; } public function showOutput() { $return = ['ret' => 0]; $s = ob_get_contents(); ob_end_clean(); $return['data']['content'] = $s; $this->plugin('after', $return['data']); $this->output($return); } public function rawOutput() { $newTokenData = $this->updateTokenData(); if($newTokenData) { $ttl = $this->tokenData['refreshExptime'] - time(); $this->setToken($this->token, $newTokenData, $ttl); } exit; } public function convertOutput($output) { ob_end_clean(); $return = ['ret' => 0]; $tmp = $GLOBALS; foreach($output as $k => $v) { if(str_contains($k, '/')) { $return['data'][$v] = $this->_arrayVar($tmp, $k); } else { $return['data'][$v] = $this->_singleVar($tmp, $k); } } $this->plugin('after', $return['data']); $this->output($return); } public function plugin($type, &$data) { if(empty($this->apiParam['plugin'][$type])) { return; } foreach($this->apiParam['plugin'][$type] as $method => $value) { $vars = explode(':', $method); if(count($vars) == 2) { if(!preg_match('/^\w+$/', $vars[0])) { continue; } require_once ROOT_PATH.'./source/function/function_path.php'; $f = ROOT_PATH.PLUGIN_ROOT.$vars[0].'/restful.class.php'; $c = 'restful_'.$vars[0]; $m = $vars[1]; } else { $f = ROOT_PATH.'./source/class/class_restfulplugin.php'; $c = 'restfulplugin'; $m = $method; } if(!file_exists($f)) { continue; } @require_once $f; if(!class_exists($c) || !method_exists($c, $m)) { continue; } call_user_func_array([$c, $m], [&$data, explode(',', $value['param'] ?? '')]); } } public function sessionDecode() { $session = unserialize(base64_decode($this->tokenData['_session'])); if(!empty($this->postParam['_authSign'])) { $this->decodeAuthSign($session); } return $session; } public function checkSign() { if(empty($_SERVER['HTTP_APPID']) || empty($_SERVER['HTTP_SIGN']) || empty($_SERVER['HTTP_NONCE']) || empty($_SERVER['HTTP_T'])) { $this->error(-101); } $this->_getAppidPerm($_SERVER['HTTP_APPID']); $secret = $this->_getSecret(); if(!$secret) { $this->error(-102); } if(time() - $_SERVER['HTTP_T'] > self::SignTTL) { $this->error(-103); } if($_SERVER['HTTP_SIGN'] != base64_encode(hash('sha256', $_SERVER['HTTP_NONCE'].$_SERVER['HTTP_T'].$secret))) { $this->error(-104); } } public function checkToken() { if(empty($this->_getToken())) { $this->error(-105); } $v = $this->_getTokenData(); if(time() >= $v['exptime']) { $this->error(-106); } $this->tokenData = $v; if(!$v['_appid'] || $v['_appid'] != $_SERVER['HTTP_APPID']) { $this->error(-107); } $this->_getAppidPerm($v['_appid']); $this->postParam['formhash'] = !empty($this->tokenData['_formhash']) ? $this->tokenData['_formhash'] : ''; $this->token = $this->_getToken(); } public function setToken($key, $value, $ttl = self::TokenSaveTTL) { $this->_memory('set', [self::TokenPre.$key, serialize($value), $ttl]); } public function setAuthToken($token, $value) { if($this->getAuthToken($token)) { return false; } global $_G; $value = authcode(serialize($value), 'ENCODE', md5($_G['config']['security']['authkey'])); $this->_memory('set', [self::AuthTokenPre.$token, $value, self::AuthTokenTTL]); return true; } public function getAuthToken($token) { global $_G; $value = $this->_memory('get', [self::AuthTokenPre.$token]); return $value ? dunserialize(authcode($value, 'DECODE', md5($_G['config']['security']['authkey']))) : []; } public function isRefreshToken() { return !empty($this->_getToken()); } private function _getToken() { return !empty($_SERVER['HTTP_TOKEN']) ? $_SERVER['HTTP_TOKEN'] : (!empty($this->postParam['token']) ? $this->postParam['token'] : ''); } private function _getTokenData() { $v = dunserialize($this->_memory('get', [self::TokenPre.$this->_getToken()])); if(empty($v)) { $this->error(-108); } return $v; } public function refreshTokenData() { $v = $this->_getTokenData(); $data['_session'] = $v['_session']; $data['_formhash'] = $this->_singleVar($_G, 'formhash'); $data['_appid'] = $_SERVER['HTTP_APPID']; $data['_conf'] = $this->tokenData['_conf']; $data['exptime'] = time() + self::TokenTTL; $data['refreshExptime'] = time() + self::TokenSaveTTL; return serialize($data); } public function delTokenData() { $this->_memory('rm', [self::TokenPre.$this->_getToken()]); } public function newTokenData() { global $_G; $data = []; $data['_session'] = $this->_sessionEncode($_COOKIE); $data['_formhash'] = $this->_singleVar($_G, 'formhash'); $data['_appid'] = $_SERVER['HTTP_APPID']; $data['_conf'] = $this->tokenData['_conf']; $data['exptime'] = time() + self::TokenTTL; $data['refreshExptime'] = time() + self::TokenSaveTTL; return serialize($data); } private function _getApiParam($api, $ver) { if(self::Developer) { return $this->_getApiParam_Developer($api, $ver); } $v = $this->_memory('get', [self::ApiConfPre.'/'.$api[0].'_'.$ver]); if(!$v) { $this->error(-109); } return $this->apiParam = dunserialize($v); } private function _getAppidPerm($appid) { $v = $this->_memory('get', [self::AppIdConfPre.$appid]); if(!$v) { if(!empty($GLOBALS['discuz'])) { require_once libfile('function/cache'); updatecache('restful'); $v = $this->_memory('get', [self::AppIdConfPre.$appid]); if(!$v) { $this->error(-110); } } else { $this->error(-110); } } $this->_appId = $appid; $v = dunserialize($v); $this->tokenData['_conf'] = $v; } public function updateTokenData() { global $_G; $data = $this->tokenData; $_session = $this->_sessionEncode($_COOKIE); if(!empty($this->tokenData['_session']) && $_session == $this->tokenData['_session']) { return null; } $data['_session'] = $_session; $data['_formhash'] = $this->_singleVar($_G, 'formhash'); return serialize($data); } private function _getSecret() { if(empty($this->tokenData['_conf']['secret'])) { $this->error(-111); } return $this->tokenData['_conf']['secret']; } private function _sessionEncode($v) { return base64_encode(serialize($v)); } private function _setSysVar(&$data, &$output = []) { global $_G; $newTokenData = $this->updateTokenData(); if(!empty($this->tokenData['refreshExptime']) && $newTokenData) { $ttl = $this->tokenData['refreshExptime'] - time(); $this->setToken($this->token, $newTokenData, $ttl); } if(!empty($_G['_multi'])) { $output['multi'] = $_G['_multi']; } unset($_G['config'], $_G['setting']['siteuniqueid'], $_G['setting']['ec_tenpay_opentrans_chnid'], $_G['setting']['ec_tenpay_opentrans_key'], $_G['setting']['ec_tenpay_bargainor'], $_G['setting']['ec_tenpay_key'], $_G['setting']['ec_account'], $_G['setting']['ec_contract']); } private function _singleVar(&$var, $k) { return $var[$k] ?? null; } private function _arrayVar(&$var, $k) { unset($GLOBALS['_L']['_G']); $value = null; $sVar = &$var; $e = explode('/', $k); $count = count($e); foreach($e as $i => $_k) { if($_k == '*') { foreach($sVar as $_k3 => $_v3) { $nKey = implode('/', array_slice($e, $i + 1)); $value[$_k3] = $this->_arrayVar($_v3, $nKey); } break; } $isMulti = str_contains($_k, ','); if(!isset($sVar[$_k]) && !$isMulti) { break; } if($isMulti) { $value = null; foreach(explode(',', $_k) as $_k2) { $value[$_k2] = $this->_singleVar($sVar, $_k2); } break; } else { if($count - 1 == $i) { $value = $this->_singleVar($sVar, $_k); } $sVar = &$sVar[$_k]; } } return $value; } private function _memory($method, $params = []) { if($this->_m == null) { require_once ROOT_PATH.'./source/class/memory/memory_driver_redis.php'; $_config = []; require ROOT_PATH.'./config/config_global.php'; $this->_m = new memory_driver_redis(); if(empty($_config['memory']['redis'])) { return null; } $this->_m->init($_config['memory']['redis']); if(!$this->_m->enable) { return null; } $this->_config = $_config; } if($this->_m == null) { return null; } if(!method_exists($this->_m, $method)) { return null; } return call_user_func_array([$this->_m, $method], $params); } // for Developer private function _getApiParam_Developer($api, $ver) { $xml = ROOT_PATH.'./data/discuz_restful.xml'; require_once ROOT_PATH.'./source/class/class_xml.php'; $data = file_get_contents($xml); $xmldata = xml2array($data); foreach($xmldata['Data']['api'] as $ver => $apis) { if(!preg_match('/^v(\d+)$/', $ver, $r)) { continue; } $this->_parseApis($apis, $ver, '/'); } if(empty($_ENV['api'][$ver]['/'.$api[0]])) { $this->error(-109); } return $this->apiParam = $_ENV['api'][$ver]['/'.$api[0]]; } private function _parseApis($apis, $ver, $uriPre = '/') { if(!is_array($apis)) { return; } foreach($apis as $uri => $api) { $k = $uriPre.$uri; list(, $baseuri) = explode('/', $k); $baseuri = '/'.$baseuri; if(!empty($api['script']) && is_string($api['script'])) { $_ENV['api'][$ver][$baseuri][$k] = []; $_ENV['api'][$ver][$baseuri][$k] = $api; } else { $this->_parseApis($api, $ver, $k.'/'); } } } public function getAuthSign() { static $authSign = null; if($authSign !== null) { return $authSign; } global $_G; $this->_memory('set', [self::AuthPre.$_G['cookie']['sid'], serialize([ 'auth' => $_G['cookie']['auth'], 'saltkey' => $_G['cookie']['saltkey'], 'sid' => $_G['cookie']['sid'] ]), self::AuthTTL]); return $authSign = authcode($_G['cookie']['sid'], 'ENCODE', md5($_G['config']['security']['authkey']), self::AuthTTL); } public function decodeAuthSign(&$session) { $sid = authcode($this->postParam['_authSign'], 'DECODE', md5($this->_config['security']['authkey'])); if(!$sid) { return; } $data = $this->_memory('get', [self::AuthPre.$sid]); if(!$data) { return; } $cookiepre = $this->_config['cookie']['cookiepre'].substr(md5($this->_config['cookie']['cookiepath'].'|'.$this->_config['cookie']['cookiedomain']), 0, 4).'_'; $session[$cookiepre.'auth'] = $data['auth']; $session[$cookiepre.'saltkey'] = $data['saltkey']; $session[$cookiepre.'sid'] = $data['sid']; } }