diff --git a/README.md b/README.md index 06bf8ae..9470505 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ PHP 版本 >= 8.0.0 - [Swoole](https://www.swoole.com/) 版本 >= 5.1. + [Swoole](https://www.swoole.com/) 版本 >= 5.1.0 以及对应版本的 [Zstd](https://github.com/kjdev/php-ext-zstd) @@ -66,13 +66,20 @@ $config=[ "public_port"=> 4000,//服务端口 "CLUSTER_ID"=> "", "CLUSTER_SECRET"=> "", - "byoc"=>false, + "byoc"=> false, + "certificates"=>[ //如果 byoc 关闭,以下设置默认禁用 + "use-cert"=> false, //是否使用自己的证书 + "cert"=> "/path/to/cert.crt", + "key"=> "/path/to/key.key", + ], ], "file"=> [ "cache_dir"=> "./cache",//缓存文件夹 "check"=> "size",//检查文件策略(hash:检查文件hash size:检查文件大小 exists:检查文件是否存在) + "database_dir"=> "./database",//访问数据数据库目录 ], "advanced"=> [ + "Centerurl"=> "https://openbmclapi.bangbang93.com",//主控链接(不建议调整) "keepalive"=> 60,//keepalive时间,秒为单位(不建议调整) "MaxConcurrent"=> 30,//下载使用的线程 "Debug"=> false,//Debug开关 @@ -81,12 +88,13 @@ $config=[ ``` ## 📍 Todo -- [ ] Web仪表盘(主要) +- [ ] 支持上报错误 url(主要) - [ ] 支持WebDAV -- [ ] 打包二进制文件 -- [ ] 完善Log系统 - [ ] 添加注释 - [x] 可以正常上线使用 +- [x] 插件系统 +- [x] 完善Log系统 +- [ ] 打包二进制文件 (延期:[原因](https://github.com/crazywhalecc/static-php-cli/issues/479)) ## ❓ FAQ diff --git a/config.php b/config.php index adf6ec8..2d333f0 100644 --- a/config.php +++ b/config.php @@ -7,13 +7,20 @@ "public_port"=> 4000,//服务端口 "CLUSTER_ID"=> "", "CLUSTER_SECRET"=> "", - "byoc"=>false, + "byoc"=> false, + "certificates"=>[ //如果 byoc 关闭,以下设置默认禁用 + "use-cert"=> false, //是否使用自己的证书 + "cert"=> "/path/to/cert.crt", + "key"=> "/path/to/key.key", + ], ], "file"=> [ "cache_dir"=> "./cache",//缓存文件夹 "check"=> "size",//检查文件策略(hash:检查文件hash size:检查文件大小 exists:检查文件是否存在) + "database_dir"=> "./database",//访问数据数据库目录 ], "advanced"=> [ + "Centerurl"=> "https://openbmclapi.staging.bangbang93.com",//主控链接(不建议调整) "keepalive"=> 60,//keepalive时间,秒为单位(不建议调整) "MaxConcurrent"=> 30,//下载使用的线程 "Debug"=> false,//Debug开关 diff --git a/inc/PluginInfoInterface.php b/inc/PluginInfoInterface.php new file mode 100644 index 0000000..f2606cd --- /dev/null +++ b/inc/PluginInfoInterface.php @@ -0,0 +1,7 @@ +false,'isSynchronized'=>false,'uptime'=>0,'token'=>null]; + + public static function getconfig($newConfig = null) { + if (!is_null($newConfig)) { + self::$config = $newConfig; + } + return self::$config; + } + + public static function getinfo($newinfo = null) { + if (!is_null($newinfo)) { + self::$info = $newinfo; + } + return self::$info; + } +} \ No newline at end of file diff --git a/inc/cluster.php b/inc/cluster.php index d4fd156..8c17b7c 100644 --- a/inc/cluster.php +++ b/inc/cluster.php @@ -83,27 +83,25 @@ public function __construct($token,$version){ $this->version = $version; } public function getFileList() { - global $DOWNLOAD_DIR; - if (!file_exists($DOWNLOAD_DIR."/filecache")) { - mkdir($DOWNLOAD_DIR."/filecache",0777,true); + $download_dir = api::getconfig()['file']['cache_dir']; + if (!file_exists($download_dir."/filecache")) { + mkdir($download_dir."/filecache",0777,true); } - $client = new Client(OPENBMCLAPIURL,443,true); + $client = new Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); $client->set(['timeout' => -1]); $client->setHeaders([ - 'Host' => OPENBMCLAPIURL, 'User-Agent' => 'openbmclapi-cluster/'.$this->version, 'Accept' => '*', 'Authorization' => "Bearer {$this->token}" ]); - mlog("Start FileList Download"); - if (!$client->download('/openbmclapi/files',$DOWNLOAD_DIR.'/filecache/filelist.zstd')) { - mlog("FileList Download Failed",2); + mlog("Starting to download fileList"); + if (!$client->get('/openbmclapi/files')) { + mlog("Failed to download fileList",2); $client->close(); } else{ - mlog("FileList Download Success"); + $this->compressedData = zstd_uncompress($client->body); $client->close(); - $this->compressedData = file_get_contents("compress.zstd://".$DOWNLOAD_DIR."/filecache/filelist.zstd"); } $parser = new ParseFileList($this->compressedData); $files = $parser->parse(); @@ -123,14 +121,14 @@ public function __construct($filesList = [], $maxConcurrent = 1) { } private function downloader(Swoole\Coroutine\Http\Client $client, $file,$bar) { - global $DOWNLOAD_DIR; - $filePath = $DOWNLOAD_DIR . '/' . substr($file->hash, 0, 2) . '/'; + $download_dir = api::getconfig()['file']['cache_dir']; + $filePath = $download_dir . '/' . substr($file->hash, 0, 2) . '/'; if (!file_exists($filePath)) { mkdir($filePath, 0777, true); } $savePath = $filePath . $file->hash; $file->path = str_replace(' ', '%20', $file->path); - $downloader = $client->download($file->path,$DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash); + $downloader = $client->download($file->path,$download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash); if (!$downloader) { mlog("Error connecting to the main control:{$client->errMsg}",2); return false; @@ -152,7 +150,7 @@ private function downloader(Swoole\Coroutine\Http\Client $client, $file,$bar) { 'User-Agent' => USERAGENT, 'Accept' => '*/*', ]); - $downloader = $client->download($location_url['path'].'?'.($location_url['query']??''),$DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash); + $downloader = $client->download($location_url['path'].'?'.($location_url['query']??''),$download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash); if (in_array($client->statusCode, [301, 302])) { while(in_array($client->statusCode, [301, 302])){ $location_url = parse_url($client->getHeaders()['location']); @@ -169,7 +167,7 @@ private function downloader(Swoole\Coroutine\Http\Client $client, $file,$bar) { 'User-Agent' => USERAGENT, 'Accept' => '*/*', ]); - $downloader = $client->download($location_url['path'].'?'.($location_url['query']??''),$DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash); + $downloader = $client->download($location_url['path'].'?'.($location_url['query']??''),$download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash); } if (!$downloader) { echo PHP_EOL; @@ -192,7 +190,7 @@ private function downloader(Swoole\Coroutine\Http\Client $client, $file,$bar) { } elseif($client->statusCode >= 400){ echo PHP_EOL; - mlog("Download Failed:{$client->statusCode} | {$file->path} | {$location_url['host']}:{$location_url['port']}",2); + mlog("{$file->path} Download Failed: {$client->statusCode} | {$location_url['host']}:{$location_url['port']}",2); $bar->progress(); return false; } @@ -212,7 +210,7 @@ private function downloader(Swoole\Coroutine\Http\Client $client, $file,$bar) { public function downloadFiles() { $bar = new CliProgressBar(count($this->filesList)); - $bar->setDetails("[Downloader]"); + $bar->setDetails("[Downloader][线程数:{$this->maxConcurrent}]"); $bar->display(); foreach ($this->filesList as $file) { global $shouldExit; @@ -221,12 +219,11 @@ public function downloadFiles() { } $this->semaphore->push(true); go(function () use ($file,$bar) { - $client = new Swoole\Coroutine\Http\Client('openbmclapi.bangbang93.com', 443, true); + $client = new Swoole\Coroutine\Http\Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); $client->set([ 'timeout' => -1 ]); $client->setHeaders([ - 'Host' => 'openbmclapi.bangbang93.com', 'User-Agent' => USERAGENT, 'Accept' => '*/*', ]); @@ -247,24 +244,23 @@ public function downloadFiles() { } public function downloadnopoen($hash) { - global $DOWNLOAD_DIR; - global $tokendata; - $filePath = $DOWNLOAD_DIR . '/' . substr($hash, 0, 2) . '/'; + $download_dir = api::getconfig()['file']['cache_dir']; + $tokenapi = api::getinfo(); + $filePath = $download_dir . '/' . substr($hash, 0, 2) . '/'; if (!file_exists($filePath)) { mkdir($filePath, 0777, true); } $filepath = "/openbmclapi/download/{$hash}?noopen=1"; - $client = new Swoole\Coroutine\Http\Client('openbmclapi.bangbang93.com', 443, true); + $client = new Swoole\Coroutine\Http\Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); $client->set([ 'timeout' => -1 ]); $client->setHeaders([ - 'Host' => 'openbmclapi.bangbang93.com', 'User-Agent' => USERAGENT, 'Accept' => '*/*', - 'Authorization' => "Bearer {$tokendata['token']}" + 'Authorization' => "Bearer {$tokenapi['token']}" ]); - $downloader = $client->download($filepath,$DOWNLOAD_DIR.'/'.substr($hash, 0, 2).'/'.$hash); + $downloader = $client->download($filepath,$download_dir.'/'.substr($hash, 0, 2).'/'.$hash); if (!$downloader) { mlog("Error download to the main control:{$client->errMsg}",2); return false; @@ -291,14 +287,14 @@ public function FilesCheckerhash() { $bar = new CliProgressBar(count($this->filesList)); $bar->setDetails("[FileCheck]"); $bar->display(); + $download_dir = api::getconfig()['file']['cache_dir']; foreach ($this->filesList as $file) { global $shouldExit; - global $DOWNLOAD_DIR; if ($shouldExit) { return; break; } - if (!file_exists($DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ + if (!file_exists($download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ $this->Missfile[] = new BMCLAPIFile( $file->path, $file->hash, @@ -307,7 +303,7 @@ public function FilesCheckerhash() { ); } else{ - if (hash_file('sha1',$DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash) != $file->hash) { + if (hash_file('sha1',$download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash) != $file->hash) { $this->Missfile[] = new BMCLAPIFile( $file->path, $file->hash, @@ -328,14 +324,14 @@ public function FilesCheckersize() { $bar = new CliProgressBar(count($this->filesList)); $bar->setDetails("[FileCheck]"); $bar->display(); + $download_dir = api::getconfig()['file']['cache_dir']; foreach ($this->filesList as $file) { global $shouldExit; - global $DOWNLOAD_DIR; if ($shouldExit) { return; break; } - if (!file_exists($DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ + if (!file_exists($download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ $this->Missfile[] = new BMCLAPIFile( $file->path, $file->hash, @@ -344,7 +340,7 @@ public function FilesCheckersize() { ); } else{ - if (filesize($DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash) != $file->size) { + if (filesize($download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash) != $file->size) { $this->Missfile[] = new BMCLAPIFile( $file->path, $file->hash, @@ -365,14 +361,14 @@ public function FilesCheckerexists() { $bar = new CliProgressBar(count($this->filesList)); $bar->setDetails("[FileCheck]"); $bar->display(); + $download_dir = api::getconfig()['file']['cache_dir']; foreach ($this->filesList as $file) { global $shouldExit; - global $DOWNLOAD_DIR; if ($shouldExit) { return; break; } - if (!file_exists($DOWNLOAD_DIR.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ + if (!file_exists($download_dir.'/'.substr($file->hash, 0, 2).'/'.$file->hash)){ $this->Missfile[] = new BMCLAPIFile( $file->path, $file->hash, diff --git a/inc/database.php b/inc/database.php new file mode 100644 index 0000000..0114a9c --- /dev/null +++ b/inc/database.php @@ -0,0 +1,136 @@ +dirPath) { + $base_dir = api::getconfig(); + $this->dirPath = $base_dir['file']['database_dir']; + } + } + + public function initializeDatabase() { + $filename = $this->dirPath . '/' . date('Ymd'); + if (!is_dir($this->dirPath)) { + mkdir($this->dirPath, 0777, true); + } + if (!file_exists($filename)) { + $initialData = []; + for ($hour = 0; $hour < 24; ++$hour) { + $timeKey = date('Ymd') . str_pad($hour, 2, '0', STR_PAD_LEFT); + $initialData[$timeKey] = ['hits' => 0, 'bytes' => 0]; + } + file_put_contents($filename, json_encode($initialData, JSON_PRETTY_PRINT)); + } + } + + public function writeDatabase($hits, $bytes) { + $filename = $this->dirPath . '/' . date('Ymd'); + if (!file_exists($filename)) { + $this->initializeDatabase(); + } + $data = json_decode(Swoole\Coroutine\System::readFile($filename), true); + $timeKey = date('Ymd') . str_pad(date('G'), 2, '0', STR_PAD_LEFT); + if (!isset($data[$timeKey])) { + $data[$timeKey] = ['hits' => 0, 'bytes' => 0]; + } + $data[$timeKey]['hits'] += $hits; + $data[$timeKey]['bytes'] += $bytes; + $w = Swoole\Coroutine\System::writeFile($filename, json_encode($data, JSON_PRETTY_PRINT)); + } + + public function getDaysData(): array { + $dailyTraffic = []; + $endDate = date('Ymd'); + $startDate = date('Ymd', strtotime('-14 days')); + + for ($i = 0; $i <= 14; ++$i) { + $date = date('Ymd', strtotime("-$i days", strtotime($endDate))); + $filename = $this->dirPath . '/' . $date; + $dailySummary = ['hits' => 0, 'bytes' => 0]; + if (file_exists($filename)) { + $data = json_decode(Swoole\Coroutine\System::readFile($filename), true); + foreach ($data as $hourlyRecord) { + if (isset($hourlyRecord['hits']) && isset($hourlyRecord['bytes'])) { + $dailySummary['hits'] += $hourlyRecord['hits']; + $dailySummary['bytes'] += $hourlyRecord['bytes']; + } + } + } + $dailyTraffic[$date] = $dailySummary; + } + ksort($dailyTraffic); + return $dailyTraffic; + } + + public function getMonthsData(): array { + $monthlyTraffic = []; + $endDate = new DateTime(); // 获取当前日期 + $startDate = clone $endDate; + $startDate->modify('-11 months'); // 回溯11个月,以包含完整的12个月数据 + + while ($startDate <= $endDate) { + $monthStr = $startDate->format('Ym'); // 格式化为YYYYMM格式 + $monthlySummary = ['hits' => 0, 'bytes' => 0]; + + for ($day = 1; $day <= $startDate->format('t'); ++$day) { // 遍历当月的所有天 + $dateStr = $startDate->format('Ymd'); + $filename = $this->dirPath . '/' . $dateStr; + + if (file_exists($filename)) { + $data = json_decode(file_get_contents($filename), true); // 使用file_get_contents以兼容更多环境 + + foreach ($data as $hourlyRecord) { + if (isset($hourlyRecord['hits']) && isset($hourlyRecord['bytes'])) { + $monthlySummary['hits'] += $hourlyRecord['hits']; + $monthlySummary['bytes'] += $hourlyRecord['bytes']; + } + } + } + $startDate->modify('+1 day'); // 移动到下一天 + } + + // 累加完一个月的数据后存入结果数组 + $monthlyTraffic[$monthStr] = $monthlySummary; + } + + return $monthlyTraffic; + } + + public function getYearsData(): array { + $annualTraffic = []; + $currentYear = (int)date('Y'); + $startYear = $currentYear - 5; + + for ($year = $startYear; $year <= $currentYear; ++$year) { + $yearHits = 0; + $yearBytes = 0; + + for ($month = 1; $month <= 12; ++$month) { + for ($day = 1; $day <= 31; ++$day) { // 假定每月最多31天,实际应用需按月份调整 + $date = sprintf('%04d%02d%02d', $year, $month, $day); + $filename = $this->dirPath . '/' . $date; + + if (file_exists($filename)) { + $data = json_decode(Swoole\Coroutine\System::readFile($filename), true); + foreach ($data as $hourlyRecord) { + if (isset($hourlyRecord['hits']) && isset($hourlyRecord['bytes'])) { + $yearHits += $hourlyRecord['hits']; + $yearBytes += $hourlyRecord['bytes']; + } + } + } + } + } + + // 累计完一年的数据后存入结果数组 + $annualTraffic[$year] = [ + 'hits' => $yearHits, + 'bytes' => $yearBytes + ]; + } + + return $annualTraffic; + } +} \ No newline at end of file diff --git a/inc/mlog.class.php b/inc/mlog.class.php index 3ebff5f..9b8f203 100644 --- a/inc/mlog.class.php +++ b/inc/mlog.class.php @@ -1,24 +1,50 @@ 'INFO', + 1 => 'DEBUG', + 2 => 'ERROR' + ]; + + if (!isset($logTypes[$type])) { + trigger_error("Type {$type} not found", E_USER_ERROR); + return; + } + + $logDir = 'logs/'; + if (!file_exists($logDir)) { + mkdir($logDir, 0755, true); + } + + $timePrefix = !$minimalFormat ? '[' . date('Y.n.j-H:i:s') . ']' : ''; + $levelPrefix = !$minimalFormat ? '[' . strtoupper($logTypes[$type]) . ']' : ''; + $logEntry = $timePrefix . $levelPrefix . $content . PHP_EOL; + + // 写入日志文件 + if ($type !== 1 || (isset($GLOBALS['config']['advanced']['Debug']) && $GLOBALS['config']['advanced']['Debug'])) { + $logFile = $logDir . date('Y-n-j') . '.log'; + file_put_contents($logFile, $logEntry, FILE_APPEND); + } + + // 输出到控制台 + switch ($type) { + case 0: // INFO + echo $logEntry; + break; + case 1: // DEBUG + if (isDebugMode()) { + echo $logEntry; + } + break; + case 2: // ERROR + echo $logEntry; + break; + } +} + +function isDebugMode() +{ + return isset(api::getconfig()['advanced']['Debug']) && api::getconfig()['advanced']['Debug']; +} \ No newline at end of file diff --git a/inc/pluginsmanager.php b/inc/pluginsmanager.php new file mode 100644 index 0000000..0f26986 --- /dev/null +++ b/inc/pluginsmanager.php @@ -0,0 +1,42 @@ +pluginsPath)) { + mkdir($this->pluginsPath, 0777, true); + } + } + + public function loadPlugins(&$server) { + $pluginsInfo = []; + $files = scandir($this->pluginsPath); + + foreach ($files as $file) { + if ($file == '.' || $file == '..' || pathinfo($file, PATHINFO_EXTENSION) != 'php') { + continue; + } + + $className = "Plugin\\" . basename($file, '.plugin.php'); // 确保类名包含命名空间 + require_once $this->pluginsPath . '/' . $file; + if (class_exists($className) && in_array('Plugin\PluginInfoInterface', class_implements($className))) { + try { + $pluginInstance = new $className(); + $pluginsInfo[$className] = $pluginInstance->getInfo(); + mlog("已加载插件: " . $pluginsInfo[$className]['Name'] . ", 作者: " . $pluginsInfo[$className]['Author'] . ", 版本: " . $pluginsInfo[$className]['Version']); + if($pluginsInfo[$className]['ServerSupport']){ + $pluginInstance->main($server); + } + else{ + $pluginInstance->main(); + } + } catch (Exception $e) { + error_log("Error instantiating plugin class {$className}: " . $e->getMessage()); + } + } + } + + return $pluginsInfo; + } + +} \ No newline at end of file diff --git a/inc/server.php b/inc/server.php index eb96a03..ffc2e85 100644 --- a/inc/server.php +++ b/inc/server.php @@ -9,24 +9,34 @@ class fileserver { private $server; private $dir; private $secret; - public function __construct($host,$port,$cert,$key,$secret) { - global $DOWNLOAD_DIR; + private $ssl; + private $lock; + public function __construct($host,$port,$cert,$key,$secret,$ssl) { $this->host = $host; $this->port = $port; $this->cert = $cert; $this->key = $key; - $this->dir = $DOWNLOAD_DIR; + $this->ssl = $ssl; + $this->dir = api::getconfig()['file']['cache_dir']; $this->secret = $secret; + $this->lock = new Swoole\Lock(SWOOLE_RWLOCK); } - public function startserver() { - $this->server = $server = new Server($this->host, $this->port, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL); - $server->set([ - 'ssl_cert_file' => './cert/'.$this->cert, - 'ssl_key_file' => './cert/'.$this->key, - 'open_http2_protocol' => true, - 'max_connection' => 10000, - ]); + public function setupserver() { + if($this->ssl){ + $this->server = $server = new Server($this->host, $this->port, true); + $server->set([ + 'ssl_cert_file' => $this->cert, + 'ssl_key_file' => $this->key, + 'heartbeat_check_interval' => 60, // 表示每60秒遍历一次 + ]); + } + else{ + $this->server = $server = new Server($this->host, $this->port); + $server->set([ + 'heartbeat_check_interval' => 60, // 表示每60秒遍历一次 + ]); + } $server->handle('/', function ($request, $response) { $code = 404; $response->status($code); @@ -43,9 +53,8 @@ public function startserver() { $server->handle('/download', function ($request, $response) { $downloadhash = str_replace('/download/', '', $request->server['request_uri']); if(isset($request->server['query_string'])){ - parse_str($request->server['query_string'], $allurl); $filepath = $this->dir.'/'.substr($downloadhash, 0, 2).'/'.$downloadhash; - if ($this->check_sign($downloadhash, $this->secret, $allurl['s'], $allurl['e'])){ + if ($this->check_sign($downloadhash, $this->secret, $request->get['s'], $request->get['e'])){ if (!file_exists($filepath)) { $download = new download(); $download->downloadnopoen($downloadhash); @@ -58,36 +67,33 @@ public function startserver() { $end_byte = filesize($filepath) - 1; } $length = $end_byte - $start_byte + 1; - $fileSize = filesize($filepath); - global $enable; - if ($enable){ - global $kacounters; - $kacounters->incr('1','hits'); - $kacounters->incr('1','bytes',$length); - } $code = 206; $response->header('Content-Type', 'application/octet-stream'); if(isset($request->header['name'])){ - $response->header('Content-Disposition', 'attachment; filename='.$allurl['name']); + $response->header('Content-Disposition', 'attachment; filename='.$request->get['name']); } $response->header('x-bmclapi-hash', $downloadhash); $response->sendfile($filepath,$start_byte,$length); } else{ - global $enable; - if ($enable){ - global $kacounters; - $kacounters->incr('1','hits'); - $kacounters->incr('1','bytes',filesize($filepath)); - } + $length = filesize($filepath); $code = 200; $response->header('Content-Type', 'application/octet-stream'); if(isset($request->header['name'])){ - $response->header('Content-Disposition', 'attachment; filename='.$allurl['name']); + $response->header('Content-Disposition', 'attachment; filename='.$request->get['name']); } $response->header('x-bmclapi-hash', $downloadhash); $response->sendfile($filepath); } + if (api::getinfo()['enable']){ + global $kacounters; + $kacounters->incr('1','hits'); + $kacounters->incr('1','bytes',$length); + + global $dbcounters; + $dbcounters->incr('1','hits'); + $dbcounters->incr('1','bytes',$length); + } } else{ $code = 403; @@ -119,8 +125,7 @@ public function startserver() { } if(isset($request->server['query_string'])){ if(is_numeric($measuresize)){ - parse_str($request->server['query_string'], $allurl); - if ($this->check_sign($request->server['request_uri'], $this->secret, $allurl['s'], $allurl['e'])){ + if ($this->check_sign($request->server['request_uri'], $this->secret, $request->get['s'], $request->get['e'])){ if (!file_exists($this->dir.'/measure/'.$measuresize)) { $file = fopen($this->dir.'/measure/'.$measuresize, 'w+'); $bytesToWrite = $measuresize * 1048576; @@ -158,15 +163,56 @@ public function startserver() { } mlog(" Serve {$code} | {$request->server['remote_addr']} | {$request->server['server_protocol']} | {$url} | {$request->header['user-agent']};") ; }); + + $server->handle('/api/cluster', function ($request, $response) { + $type = $request->server['request_uri'] ? substr($request->server['request_uri'], strlen('/api/cluster') + 1) : ''; + if($type === "type"){ + $code = 200; + $response->header('Content-Type', 'application/json; charset=utf-8'); + $type = new webapi(); + $response->end($type->gettype()); + } + elseif($type === "status"){ + $code = 200; + $response->header('Content-Type', 'application/json; charset=utf-8'); + $type = new webapi(); + $response->end($type->getstatus()); + } + elseif($type === "info"){ + $code = 200; + $response->header('Content-Type', 'application/json; charset=utf-8'); + $type = new webapi(); + $response->end($type->getinfo()); + } + else{ + $code = 403; + $response->status($code); + $response->header('Content-Type', 'text/html; charset=utf-8'); + $response->end("Error
Forbidden
"); + } + + if(!isset($request->server['query_string'])){ + $url = $request->server['request_uri']; + } + else{ + $url = $request->server['request_uri']."?".$request->server['query_string']; + } + + mlog(" Serve {$code} | {$request->server['remote_addr']} | {$request->server['server_protocol']} | {$url} | {$request->header['user-agent']};") ; + }); + return $server; + } + + public function startserver() { mlog("Start Http Server on {$this->host}:{$this->port}"); - $server->start(); + $this->server->start(); } public function stopserver() { - mlog("Stop Http Server"); + mlog("Stop Http Server",1); $this->server->shutdown(); } //你问我这段函数为什么要放在server里面? 因为只有server需要check_sign( - public function check_sign(string $hash, string $secret, string $s, string $e): bool { + public function check_sign(string $hash, string $secret, string $s=null, string $e=null): bool { try { $t = intval($e, 36); } catch (\Exception $ex) { diff --git a/inc/socketio.php b/inc/socketio.php index e6358a9..94f3e10 100644 --- a/inc/socketio.php +++ b/inc/socketio.php @@ -3,20 +3,25 @@ use Swoole\Coroutine\Http\Client; class socketio { private $url; + private $port; + private $ssl; private $token; private $client; private $data; private $certdata; private $kattl; + private $rekeepalive = 1; private $Connected = false; - public function __construct($url,$token,$kattl) { - $this->url = $url; + public function __construct($url=null,$token=null,$kattl=null) { + $this->url = $url['host']; + $this->port = $url['port']; + $this->ssl = $url['ssl']; $this->token = $token; $this->kattl = $kattl; $katimeid = 0; } public function connect() { - $this->client = $client = new Client($this->url, 443, true); + $this->client = $client = new Client($this->url, $this->port, $this->ssl); $ret = $client->upgrade('/socket.io/?EIO=4&transport=websocket'); $auth = [ 'token' => $this->token @@ -24,9 +29,13 @@ public function connect() { if ($ret) { $client->push('40'.json_encode($auth)); } - while(true) { - $alldata = $client->recv(1); + $alldata = $client->recv(); + if (empty($alldata)) { + $client->close(); + mlog("与主控的连接断开"); + break; + } if (!is_bool($alldata)){ $this->data = $data = $alldata->data; preg_match('/^\d+/', $data, $code); @@ -42,7 +51,13 @@ public function connect() { } if ($code[0] == '42'){ $data = substr($data, strlen($code[0])); - mlog("[socket.io]Got data {$data}"); + $jsondata = json_decode($data); + if(isset($jsondata[0])){ + mlog("{$jsondata[1]}"); + } + else{ + mlog("[socket.io]{$data}"); + } } if ($code[0] == '2'){ $client->push('3'); @@ -50,9 +65,9 @@ public function connect() { } if ($code[0] == '41'){ mlog("[socket.io]Close Connection"); - exits(); $client->close(); - return; + exits(); + break; } if ($code[0] == '430'){ $jsondata = json_decode(substr($data, strlen($code[0])),true); @@ -60,37 +75,64 @@ public function connect() { $this->certdata = $jsondata; } elseif (isset($jsondata[0][1]) && $jsondata[0][1] == "1"){ - global $enable; - $enable = true; - mlog("节点已启用 Let's Goooooo!"); - global $kacounters; - $kacounters = new Swoole\Table(1024); - $kacounters->column('hits', Swoole\Table::TYPE_FLOAT); - $kacounters->column('bytes', Swoole\Table::TYPE_FLOAT); - $kacounters->create(); - $kacounters->set('1', ['hits' => 0, 'bytes' => 0]); - global $katimeid; - $katimeid = Swoole\Timer::tick($this->kattl*1000, function () use ($kacounters) { - $this->keepalive($kacounters); - }); + $enable = api::getinfo(); + if(!$enable['enable']){ + $enable = api::getinfo(); + $enable['enable'] = true; + api::getinfo($enable); + mlog("节点已启用 Let's Goooooo!"); + global $kacounters; + $kacounters = new Swoole\Table(1024); + $kacounters->column('hits', Swoole\Table::TYPE_FLOAT); + $kacounters->column('bytes', Swoole\Table::TYPE_FLOAT); + $kacounters->create(); + $kacounters->set('1', ['hits' => 0, 'bytes' => 0]); + global $katimeid; + $katimeid = Swoole\Timer::tick($this->kattl*1000, function () use ($kacounters) { + $this->keepalive($kacounters); + }); + + global $dbcounters; + $dbcounters = new Swoole\Table(1024); + $dbcounters->column('hits', Swoole\Table::TYPE_FLOAT); + $dbcounters->column('bytes', Swoole\Table::TYPE_FLOAT); + $dbcounters->create(); + $dbcounters->set('1', ['hits' => 0, 'bytes' => 0]); + $dbtimeid = Swoole\Timer::tick(3000, function () use ($dbcounters) { + $this->updatedatabase($dbcounters); + }); + } + else{ + $client->close(); + break; + mlog("[socket.io]Close Connection"); + } } elseif (isset($jsondata[0][1]) && $jsondata[0][1] == "0"){ - mlog("[socket.io]节点已掉线"); - exits(); + if($this->rekeepalive <= 3){ + mlog("Keep-Alive失败,正在重试({$this->rekeepalive}/3)"); + global $kadata; + $this->ack("keep-alive",$kadata); + $this->rekeepalive++; + } + else{ + exits(); + } } elseif (isset($jsondata[0][1]) && $this->IsTime($jsondata[0][1])){ + $this->rekeepalive = 1; global $kadata; mlog(" Keep-alive success: hits={$kadata['hits']} bytes={$kadata['bytes']} Time={$jsondata[0][1]}"); } elseif (isset($jsondata[0][0]["message"])){ - mlog("[socket.io]Got data {$jsondata[0][0]["message"]}"); + mlog("[socket.io]{$jsondata[0][0]["message"]}"); if (strpos($jsondata[0][0]["message"], "Error") !== false) { - mlog("[socket.io]节点启用失败"); + mlog("节点启用失败",2); exits(); } } else { - mlog("[socket.io]Got data {$data}"); + mlog("[socket.io]Got data {$data}"); }; //mlog("[socket.io]Got MESSAGE {$data}",1); } @@ -105,17 +147,6 @@ public function connect() { } //var_dump($data); } - global $shouldExit; - global $httpserver; - if ($shouldExit) { - global $enable; - if($enable){ - Swoole\Timer::clear($katimeid); - } - $this->disable(); - $httpserver->stopserver(); - return; - } } } public function Getcert() { @@ -180,18 +211,22 @@ public function keepalive($kacounters) { $kacounters->set('1', ['hits' => 0, 'bytes' => 0]); } + public function updatedatabase($dbcounters) { + $database = new database(); + $database->writeDatabase($dbcounters->get('1','hits'),$dbcounters->get('1','bytes')); + $dbcounters->set('1', ['hits' => 0, 'bytes' => 0]); + } public function IsTime($inputString) { $pattern = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/'; return preg_match($pattern, $inputString) === 1; } public function disable() { - global $enable; + $enable = api::getinfo()['enable']; if ($enable){ $this->ack("disable"); - Coroutine::sleep(2); } - mlog("[socket.io]Close Connection"); $this->client->close(); + $this->Connected = false; } } \ No newline at end of file diff --git a/inc/token.php b/inc/token.php index fccc81b..e4a8b2d 100644 --- a/inc/token.php +++ b/inc/token.php @@ -11,18 +11,18 @@ public function __construct($clusterId,$clusterSecret,$version){ } public function gettoken() { //获取challenge - $client = new Client(OPENBMCLAPIURL,443,true); + $client = new Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); $client->setHeaders([ 'User-Agent' => 'openbmclapi-cluster/'.$this->version, 'Content-Type' => 'application/json; charset=utf-8', ]); - $client->set(['timeout' => 1]); + $client->set(['timeout' => 20]); $client->get('/openbmclapi-agent/challenge?clusterId='.$this->clusterId); $client->close(); $challenge = json_decode($client->body, true); $signature = hash_hmac('sha256', $challenge['challenge'], $this->clusterSecret); //获取token和ttl - $client = new Client(OPENBMCLAPIURL,443,true); + $client = new Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); $client->post( '/openbmclapi-agent/token', array( @@ -38,4 +38,22 @@ public function gettoken() { 'upttl' => $responseData['ttl'] - 600000,//前十分钟更新 ); } + + public function refreshToken($token) { + //刷新token + $client = new Client(OPENBMCLAPIURL['host'],OPENBMCLAPIURL['port'],OPENBMCLAPIURL['ssl']); + $client->post( + '/openbmclapi-agent/token', + array( + 'clusterId' => $this->clusterId, + 'token' => $token, + ) + ); + $client->close(); + $responseData = json_decode($client->body, true); + return array( + 'token' => $responseData["token"], + 'upttl' => $responseData['ttl'] - 600000,//前十分钟更新 + ); + } } \ No newline at end of file diff --git a/inc/webapi.php b/inc/webapi.php new file mode 100644 index 0000000..7839992 --- /dev/null +++ b/inc/webapi.php @@ -0,0 +1,119 @@ + 'php-openbmclapi', + 'openbmclapiVersion' => VERSION, + 'version'=> 'v' . PHPOBAVERSION + ]; + $type = json_encode($array); + return $type; + } + public function getstatus() { + $array = [ + 'clusterStatus' => [ + "isEnabled" => api::getinfo()['enable'], + "isSynchronized" => api::getinfo()['isSynchronized'], + "isTrusted" => true, + "uptime" => api::getinfo()['uptime'], + "systemOccupancy" =>[ + "memoryUsage" => memory_get_usage(), + "loadAverage" => sys_getloadavg()[0] + ] + ] + ]; + $type = json_encode($array); + return $type; + } + public function getinfo() { + + $array = [ + 'data' => [ + "hours" => $this->DataTohours(), + "days" => $this->DataTodays(), + "months" => $this->DataTomonths() + ] + ]; + $type = json_encode($array); + return $type; + } + + public function dataToHours() { + $base_dir = api::getconfig(); + $dirPath = $base_dir['file']['database_dir']; + $currentHourTimestamp = strtotime(date('Y-m-d H:00:00')); + $twelveHoursAgoTimestamp = $currentHourTimestamp - (12 * 3600); + $formattedData = []; + + // 处理今天的数据 + $todayData = @file_get_contents($dirPath . '/' . date('Ymd')); + $dataArrayToday = json_decode($todayData, true); + if (json_last_error() !== JSON_ERROR_NONE) { + echo "Error decoding today's JSON: " . json_last_error_msg(); + return []; + } + + // 如果需要包含昨天的数据 + $yesterdayKey = date('Ymd', strtotime('-1 day')); + if (strtotime(date('Y-m-d')) !== strtotime($yesterdayKey)) { // 确保昨天的日期正确 + echo $yesterdayKey; + $yesterdayData = @file_get_contents($dirPath . '/' . $yesterdayKey); + $dataArrayYesterday = json_decode($yesterdayData, true); + if (json_last_error() !== JSON_ERROR_NONE) { + echo "Error decoding yesterday's JSON: " . json_last_error_msg(); + return []; + } + $dataArray = array_replace($dataArrayYesterday, $dataArrayToday); + } else { + $dataArray = $dataArrayToday; + } + + // 统一处理数据 + foreach ($dataArray as $key => $value) { + $dateStr = substr($key, 0, 8); + $hour = intval(substr($key, 8)); + $timestamp = mktime(0, 0, 0, substr($dateStr, 4, 2), substr($dateStr, 6, 2), substr($dateStr, 0, 4)) + ($hour * 3600); + if ($timestamp >= $twelveHoursAgoTimestamp && $timestamp < $currentHourTimestamp) { + $formattedData[] = [ + 'timestamp' => $timestamp, + 'hits' => $value['hits'], + 'bytes' => $value['bytes'] + ]; + } + } + + return $formattedData; + } + + public function DataTodays() { + $database = new Database(); + $data = $database->getDaysData(); + $formattedData = []; + foreach ($data as $date => $stats) { + $timestamp = strtotime($date); + $formattedData[] = [ + 'timestamp' => $timestamp, + 'hits' => $stats['hits'], + 'bytes' => $stats['bytes'] + ]; + } + + return $formattedData; + } + + public function DataTomonths() { + $database = new Database(); + $data = $database->getMonthsData(); + $formattedData = []; + foreach ($data as $date => $stats) { + $timestamp = strtotime($date . 00); + $formattedData[] = [ + 'timestamp' => $timestamp, + 'hits' => $stats['hits'], + 'bytes' => $stats['bytes'] + ]; + } + return $formattedData; + } +} \ No newline at end of file diff --git a/main.php b/main.php index e348ff6..dd26a4e 100644 --- a/main.php +++ b/main.php @@ -2,95 +2,144 @@ use Swoole\Coroutine; use function Swoole\Coroutine\run; use function Swoole\Timer; -declare(ticks=1) date_default_timezone_set('Asia/Shanghai'); require './config.php'; -const PHPOBAVERSION = '1.6.0'; -const VERSION = '1.10.6'; -global $DOWNLOAD_DIR; -$DOWNLOAD_DIR = $config['file']['cache_dir']; -const USERAGENT = 'openbmclapi-cluster/' . VERSION . ' ' . 'PHP-OpenBmclApi/'.PHPOBAVERSION; -const OPENBMCLAPIURL = 'openbmclapi.bangbang93.com'; $list = glob('inc/*.php'); foreach ($list as $file) { require $file; } -global $pid; -$pid = getmypid(); -global $enable; -$enable = false; -echo"OpenBmclApionPHP v". PHPOBAVERSION . "-" . VERSION . PHP_EOL; -run(function()use ($config){ +api::getconfig($config); +const PHPOBAVERSION = '1.6.0'; +const VERSION = '1.10.9'; +$download_dir = api::getconfig()['file']['cache_dir']; +const USERAGENT = 'openbmclapi-cluster/' . VERSION . ' ' . 'php-openbmclapi/'.PHPOBAVERSION; +mlog("OpenBmclApi on PHP v". PHPOBAVERSION . "-" . VERSION,0,true); + +//预处理主控链接 +$parsed = parse_url(api::getconfig()['advanced']['Centerurl']); +$scheme = isset($parsed['scheme']) ? $parsed['scheme'] : ''; +$host = isset($parsed['host']) ? $parsed['host'] : ''; +$port = isset($parsed['port']) ? $parsed['port'] : ($scheme === 'https' ? 443 : 80); +$ssl = $scheme === 'https' ? true : false; //https支持 +define('OPENBMCLAPIURL', ['host' => $host, 'port' => $port, 'ssl' => $ssl]); + +run(function(){ + $config = api::getconfig(); //注册信号处理器、 function exits() { global $shouldExit; - global $tokentimerid; $shouldExit = true; // 设置退出标志 - Swoole\Timer::clear($tokentimerid); + Swoole\Timer::clearAll(); + global $socketio; + if (is_object($socketio)) { + $socketio->disable(); + } + global $httpserver; + if (is_object($httpserver)) { + $httpserver->stopserver(); + } echo PHP_EOL; mlog("正在退出..."); } function registerSigintHandler() { + global $shouldExit; $shouldExit = false; // 初始化为false Swoole\Process::signal(SIGINT, function ($signo){ exits(); }); } + + //创建数据库 + $database = new database(); + $database->initializedatabase(); + //获取初次Token $token = new token($config['cluster']['CLUSTER_ID'],$config['cluster']['CLUSTER_SECRET'],VERSION); $tokendata = $token->gettoken(); mlog("GetToken:".$tokendata['token'],1); mlog("TokenTTL:".$tokendata['upttl'],1); + $tokenapi = api::getinfo(); + $tokenapi['token'] = $tokendata['token']; + api::getinfo($tokenapi); + //启动更新TokenTimer - global $tokentimerid; $tokentimerid = Swoole\Timer::tick($tokendata['upttl'], function () use ($token) { - $tokendata = $token->gettoken(); + $tokenapi = api::getinfo(); + $tokendata = $token->refreshToken($tokenapi['token']); + $tokenapi['token'] = $tokendata['token']; + api::getinfo($tokenapi); mlog("GetNewToken:".$tokendata['token'],1); }); registerSigintHandler(); mlog("Timer start on ID{$tokentimerid}",1); + //建立socketio连接主控 + global $socketio; $socketio = new socketio(OPENBMCLAPIURL,$tokendata['token'],$config['advanced']['keepalive']); mlog("正在连接主控"); - Coroutine::create(function () use ($socketio){ + Coroutine::create(function () use (&$socketio){ $socketio->connect(); }); Coroutine::sleep(1); - //获取证书 - $socketio->ack("request-cert"); - Coroutine::sleep(1); - $allcert = $socketio->Getcert(); - //写入证书并且是否损坏 - if (!file_exists('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt') && !file_exists('./cert/'.$config['cluster']['CLUSTER_ID'].'.key')) { - mlog("正在获取证书"); - if (!file_exists("./cert")) { - mkdir("./cert",0777,true); + if (!$config['cluster']['byoc']){ + $socketio->ack("request-cert"); + Coroutine::sleep(1); + $allcert = $socketio->Getcert(); + //写入证书并且是否损坏 + if (!file_exists('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt') && !file_exists('./cert/'.$config['cluster']['CLUSTER_ID'].'.key')) { + mlog("正在获取证书"); + if (!file_exists("./cert")) { + mkdir("./cert",0777,true); + } + mlog("已获取证书,到期时间{$allcert['0']['1']['expires']}"); + $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt', 'w'); + $Writtencert = fwrite($cert, $allcert['0']['1']['cert']); + fclose($cert); + $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.key', 'w'); + $Writtencert = fwrite($cert, $allcert['0']['1']['key']); + fclose($cert); } - mlog("已获取证书,到期时间{$allcert['0']['1']['expires']}"); - $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt', 'w'); - $Writtencert = fwrite($cert, $allcert['0']['1']['cert']); - fclose($cert); - $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.key', 'w'); - $Writtencert = fwrite($cert, $allcert['0']['1']['key']); - fclose($cert); + $crt = file_get_contents('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt'); + if ($crt!== $allcert['0']['1']['cert']) { + mlog("证书损坏/过期"); + mlog("已获取新的证书,到期时间{$allcert['0']['1']['expires']}"); + $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt', 'w'); + $Writtencert = fwrite($cert, $allcert['0']['1']['cert']); + fclose($cert); + $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.key', 'w'); + $Writtencert = fwrite($cert, $allcert['0']['1']['key']); + fclose($cert); + } + global $httpserver; + $httpserver = new fileserver($config['cluster']['host'],$config['cluster']['port'],'./cert/'.$config['cluster']['CLUSTER_ID'].'.crt','./cert/'.$config['cluster']['CLUSTER_ID'].'.key',$config['cluster']['CLUSTER_SECRET'],true); } - $crt = file_get_contents('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt'); - if ($crt!== $allcert['0']['1']['cert']) { - mlog("证书损坏/过期"); - mlog("已获取新的证书,到期时间{$allcert['0']['1']['expires']}"); - $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.crt', 'w'); - $Writtencert = fwrite($cert, $allcert['0']['1']['cert']); - fclose($cert); - $cert = fopen('./cert/'.$config['cluster']['CLUSTER_ID'].'.key', 'w'); - $Writtencert = fwrite($cert, $allcert['0']['1']['key']); - fclose($cert); + else{ + if(!$config['cluster']['certificates']['use-cert']){ + global $httpserver; + $httpserver = new fileserver($config['cluster']['host'],$config['cluster']['port'],null,null,$config['cluster']['CLUSTER_SECRET'],false); + mlog("byoc 已开启并且 use-cert 已关闭,请自备反代!"); + } + else{ + global $httpserver; + $httpserver = new fileserver($config['cluster']['host'],$config['cluster']['port'],$config['cluster']['certificates']['cert'],$config['cluster']['certificates']['key'],$config['cluster']['CLUSTER_SECRET'],true); + } } + + //获取证书 + //设置http服务器 + $server = $httpserver->setupserver(); + + //开始加载插件 + $pluginsManager = new PluginsManager(); + $pluginsManager->loadPlugins($server); + //启动http服务器 - global $httpserver; - $httpserver = new fileserver($config['cluster']['host'],$config['cluster']['port'],$config['cluster']['CLUSTER_ID'].'.crt',$config['cluster']['CLUSTER_ID'].'.key',$config['cluster']['CLUSTER_SECRET']); Coroutine::create(function () use ($config,$httpserver){ $httpserver->startserver(); }); + $uptime = api::getinfo(); + $uptime['uptime'] = time(); + api::getinfo($uptime); //下载文件列表 $cluster = new cluster($tokendata['token'],VERSION); @@ -105,6 +154,9 @@ function registerSigintHandler() { elseif($config['file']['check'] == "exists"){ $Missfile = $FilesCheck->FilesCheckerexists(); } + $isSynchronized = api::getinfo(); + $isSynchronized['isSynchronized'] = true; + api::getinfo($isSynchronized); //循环到没有Missfile这个变量 if (is_array($Missfile)){ mlog("缺失/损坏".count($Missfile)."个文件"); @@ -125,6 +177,9 @@ function registerSigintHandler() { //mlog("缺失/损坏".count($Missfile)."个文件"); } else{ + $isSynchronized = api::getinfo(); + $isSynchronized['isSynchronized'] = false; + api::getinfo($isSynchronized); mlog("检查文件完毕,没有缺失/损坏"); } } @@ -132,6 +187,9 @@ function registerSigintHandler() { else{ global $shouldExit; if (!$shouldExit){ + $isSynchronized = api::getinfo(); + $isSynchronized['isSynchronized'] = false; + api::getinfo($isSynchronized); mlog("检查文件完毕,没有缺失/损坏"); } }