key = $key; $this->api = "https://api.telegram.org/bot$key/"; $this->file = "https://api.telegram.org/file/bot$key/"; $this->clients = '/config/clients.json'; $this->clients1 = '/config/clients1.json'; $this->pac = '/config/pac.json'; $this->ip = getenv('IP'); $this->i18n = $i18n; $this->language = $this->getPacConf()['language'] ?: 'en'; $this->dns = '1.1.1.1, 8.8.8.8'; $this->mtu = 1350; $this->limit = $this->getPacConf()['limitpage'] ?: 5; $this->adguard = '/config/AdGuardHome.yaml'; $this->update = '/update/json'; $this->hwid = '/config/hwid.json'; $this->logs = [ 'nginx_default_access', 'nginx_domain_access', 'upstream_access', 'xray', ]; $this->reg = '~' . implode('|', [ 'GET / HTTP', 'GET /favicon.ico HTTP', preg_quote($this->getHashBot(1)) ]) . '~'; } public function input() { $this->input_raw = $input = json_decode(file_get_contents('php://input'), true); $this->input = [ 'message' => $input['callback_query']['message']['text'] ?? $input['message']['text'] ?? $input['channel_post']['text'] ?? '', 'message_id' => $input['callback_query']['message']['message_id'] ?? $input['message']['message_id'] ?? $input['channel_post']['message_id'], 'chat' => $input['message']['chat']['id'] ?? $input['callback_query']['message']['chat']['id'] ?? $input['channel_post']['chat']['id'] ?? $input['my_chat_member']['chat']['id'], 'from' => $input['message']['from']['id'] ?? $input['inline_query']['from']['id'] ?? $input['callback_query']['from']['id'] ?? $input['channel_post']['chat']['id'] ?? $input['my_chat_member']['from']['id'], 'username' => $input['message']['from']['username'] ?? $input['inline_query']['from']['username'] ?? $input['callback_query']['from']['username'], 'query' => $input['inline_query']['query'] ?? '', 'inlid' => $input['inline_query']['id'] ?? '', 'group' => 'group' == $input['message']['chat']['type'], 'sticker_id' => $input['message']['sticker']['file_id'] ?? false, 'channel' => !empty($input['channel_post']['message_id']), 'callback' => $input['callback_query']['data'] ?? false, 'callback_id' => $input['callback_query']['id'] ?? false, 'photo' => $input['message']['photo'] ?? false, 'file_name' => $input['message']['document']['file_name'] ?? false, 'file_id' => $input['message']['document']['file_id'] ?? false, 'caption' => $input['message']['caption'] ?? false, 'reply' => $input['message']['reply_to_message']['message_id'] ?? false, 'reply_from' => $input['message']['reply_to_message']['from']['id'] ?? $input['callback_query']['message']['reply_to_message']['from']['id'] ?? false, 'reply_text' => $input['message']['reply_to_message']['text'] ?? false, 'new_member_id' => $input['my_chat_member']['new_chat_member']['user']['id'] ?? false, 'new_member_status' => $input['my_chat_member']['new_chat_member']['status'] ?? false, ]; $this->auth(); $this->session(); $this->action(); $this->callbackCheck(); } public function auth() { if (preg_match('~^/id$~', $this->input['message'])) { return; } $file = __DIR__ . '/config.php'; require $file; if (empty($c['admin'])) { $c['admin'] = [$this->input['from']]; file_put_contents($file, "input['from'], $c['admin'])) { // $this->send($this->input['chat'], 'you are not authorized', $this->input['message_id']); exit; } } public function callbackCheck() { if (empty($this->callback) && !empty($this->input['callback_id'])) { $this->answer($this->input['callback_id'], $GLOBALS['debug'] ? $this->input['callback'] : false); } } public function session() { session_id($this->input['from']); session_start(); if (!empty($_SESSION['reply'])) { if (empty($this->input['reply'])) { foreach ($_SESSION['reply'] as $k => $v) { $this->delete($this->input['chat'], $k); } unset($_SESSION['reply']); } } } public function sd($var, $log = false, $json = false, $raw = false) { if ($log) { if ($json) { file_put_contents('/logs/debug', json_encode($var, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } elseif ($raw) { file_put_contents('/logs/debug', $var); } else { file_put_contents('/logs/debug', var_export($var, true)); } } else { $this->send($this->input['chat'], var_export($var, true), $this->input['message_id']); } } public function action() { switch (true) { // смена айпи сервера case preg_match('~^/menu$~', $this->input['message'], $m): case preg_match('~^/start$~', $this->input['message'], $m): case preg_match('~^/menu$~', $this->input['callback'], $m): case preg_match('~^/menu (?Paddpeer) (?P(?:-)?\d+)$~', $this->input['callback'], $m): case preg_match('~^/menu (?Pwg) (?P(?:-)?\d+)$~', $this->input['callback'], $m): case preg_match('~^/menu (?Pclient) (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): case preg_match('~^/menu (?Ppac|adguard|config|ss|lang|oc|naive|mirror|update)$~', $this->input['callback'], $m): $this->menu(type: $m['type'] ?? false, arg: $m['arg'] ?? false); break; case preg_match('~^/changeWG (\d+)$~', $this->input['callback'], $m): $this->changeWG($m[1]); break; case preg_match('~^/changeTransport(?: (\d+))?$~', $this->input['callback'], $m): $this->changeTransport($m[1] ?: false); break; case preg_match('~^/mirror$~', $this->input['message'], $m): $this->menu('mirror'); break; case preg_match('~^/mainOutbound$~', $this->input['callback'], $m): $this->mainOutbound(); break; case preg_match('~^/importIps (.+)$~', $this->input['callback'], $m): $this->importIps($m[1]); break; case preg_match('~^/switchBanIp$~', $this->input['callback'], $m): $this->switchBanIp(); break; case preg_match('~^/switchMonthlyStats$~', $this->input['callback'], $m): $this->switchMonthlyStats(); break; case preg_match('~^/setIpLimit$~', $this->input['callback'], $m): $this->setIpLimit(); break; case preg_match('~^/hwidLimit$~', $this->input['callback'], $m): $this->hwidLimit(); break; case preg_match('~^/toggleHwidLimit(?: (\w+))?$~', $this->input['callback'], $m): $this->toggleHwidLimit($m[1] ?? null); break; case preg_match('~^/setHwidDevices(?: (\w+))?$~', $this->input['callback'], $m): $this->setHwidDevices($m[1] ?? null); break; case preg_match('~^/hwidUser (\d+)(?:_(\d+))?$~', $this->input['callback'], $m): $this->hwidUser($m[1], $m[2] ?? 0); break; case preg_match('~^/hwidUserToggle (\d+)$~', $this->input['callback'], $m): $this->hwidUserToggle($m[1]); break; case preg_match('~^/hwidUserDefault (\d+)$~', $this->input['callback'], $m): $this->hwidUserDefault($m[1]); break; case preg_match('~^/setHwidUserLimit (\d+)$~', $this->input['callback'], $m): $this->setHwidUserLimit($m[1]); break; case preg_match('~^/hwidUserDel (\d+)_(\d+) (.+)$~', $this->input['callback'], $m): $this->hwidUserDel($m[1], $m[2], $m[3]); break; case preg_match('~^/searchLogs (.+)$~', $this->input['message'], $m): $this->searchLogs($m[1]); break; case preg_match('~^/searchLogs (.+?)(?:\s(.+?))?(?:\s(.+?))?(?:\s(.+?))?$~', $this->input['callback'], $m): $this->searchLogs($m[1], $m[2], $m[3], $m[4]); break; case preg_match('~^/switchSilence$~', $this->input['callback'], $m): $this->switchSilence(); break; case preg_match('~^/switchScanIp$~', $this->input['callback'], $m): $this->switchScanIp(); break; case preg_match('~^/autoScanTimeout$~', $this->input['callback'], $m): $this->autoScanTimeout(); break; case preg_match('~^/autoupdate$~', $this->input['callback'], $m): $this->autoupdate(); break; case preg_match('~^/ports$~', $this->input['callback'], $m): $this->ports(); break; case preg_match('~^/analysisIp(?:\s(\d+))?$~', $this->input['callback'], $m): $this->analysisIp($m[1] ?: 0); break; case preg_match('~^/ipMenu$~', $this->input['callback'], $m): $this->ipMenu(); break; case preg_match('~^/cleanDeny(?:\s(\d))?$~', $this->input['callback'], $m): $this->cleanDeny($m[1]); break; case preg_match('~^/denyList (.+?)(?:\s(\d))?$~', $this->input['callback'], $m): $this->denyList($m[1], $m[2] ?: 0); break; case preg_match('~^/cleanLogs (.+?)(?:\s(1))?$~', $this->input['callback'], $m): $this->cleanLogs($m[1], $m[2]); break; case preg_match('~^/allowIp (.+?) (\d+)(?:\s(\d+))?$~', $this->input['callback'], $m): $this->allowIp($m[1], $m[2], $m[3]); break; case preg_match('~^/searchIp (.+)$~', $this->input['callback'], $m): $this->searchIp($m[1]); break; case preg_match('~^/searchSuspiciousIp (.+)$~', $this->input['callback'], $m): $this->searchSuspiciousIp($m[1]); break; case preg_match('~^/denyIp (.+?)(?:\s(.+?)\s(\d+?)\s(\d))?$~', $this->input['callback'], $m): $this->denyIp($m[1], $m[2], $m[3], $m[4]); break; case preg_match('~^/whiteIp (.+?)(?:\s(.+?)\s(\d+?)\s(\d))?$~', $this->input['callback'], $m): $this->whiteIp($m[1], $m[2], $m[3], $m[4]); break; case preg_match('~^/adgFillAllowedClients(?: (\d+))?$~', $this->input['callback'], $m): $this->adgFillAllowedClients($m[1] ?: false); break; case preg_match('~^/appOutbound$~', $this->input['callback'], $m): $this->appOutbound(); break; case preg_match('~^/domainsOutbound$~', $this->input['callback'], $m): $this->domainsOutbound(); break; case preg_match('~^/finalOutbound$~', $this->input['callback'], $m): $this->finalOutbound(); break; case preg_match('~^/processOutbound$~', $this->input['callback'], $m): $this->processOutbound(); break; case preg_match('~^/offWarp$~', $this->input['callback'], $m): $this->offWarp(); break; case preg_match('~^/addSubdomain$~', $this->input['callback'], $m): $this->addSubdomain(); break; case preg_match('~^/addLinkDomain$~', $this->input['callback'], $m): $this->addLinkDomain(); break; case preg_match('~^/id$~', $this->input['message'], $m): $this->send($this->input['chat'], "your id: {$this->input['from']}\nchat id: {$this->input['chat']}", $this->input['message_id']); break; case preg_match('~^/adguardChBr$~', $this->input['callback'], $m): $this->adguardChBr(); break; case preg_match('~^/mtproto$~', $this->input['callback'], $m): $this->mtproto(); break; case preg_match('~^/deleteAll (\w+)$~', $this->input['callback'], $m): $this->deleteAll($m[1]); break; case preg_match('~^/exportList (\w+)$~', $this->input['callback'], $m): $this->exportList($m[1]); break; case preg_match('~^/hidePort (\w+)$~', $this->input['callback'], $m): $this->hidePort($m[1]); break; case preg_match('~^/deleteYes (\w+)$~', $this->input['callback'], $m): $this->deleteYes($m[1]); break; case preg_match('~^/addCommunityFilter$~', $this->input['callback'], $m): $this->addCommunityFilter(); break; case preg_match('~^/addLegizFilter$~', $this->input['callback'], $m): $this->addLegizFilter(); break; case preg_match('~^/pacMenu (\d+)$~', $this->input['callback'], $m): $this->pacMenu($m[1]); break; case preg_match('~^/applyupdatebot$~', $this->input['callback'], $m): $this->applyupdatebot(); break; case preg_match('~^/restart$~', $this->input['callback'], $m): $this->restart(); break; case preg_match('~^/branches$~', $this->input['callback'], $m): $this->branches(); break; case preg_match('~^/changeBranch (\d+)$~', $this->input['callback'], $m): $this->changeBranch($m[1]); break; case preg_match('~^/getMirror$~', $this->input['callback'], $m): $this->getMirror(); break; case preg_match('~^/logs$~', $this->input['callback'], $m): $this->logs(); break; case preg_match('~^/iodine$~', $this->input['callback'], $m): $this->iodine(); break; case preg_match('~^/iodineDomain$~', $this->input['callback'], $m): $this->iodineDomain(); break; case preg_match('~^/iodinePassword$~', $this->input['callback'], $m): $this->iodinePassword(); break; case preg_match('~^/setIodineDomain (\w+)$~', $this->input['callback'], $m): $this->setIodineDomain($m[1]); break; case preg_match('~^/setIodinePassword (\w+)$~', $this->input['callback'], $m): $this->setIodinePassword($m[1]); break; case preg_match('~^/getLog (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->getLog(...explode('_', $m['arg'])); break; case preg_match('~^/clearLog (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->clearLog(...explode('_', $m['arg'])); break; case preg_match('~^/cleanLog$~', $this->input['callback'], $m): $this->cleanLog(); break; case preg_match('~^/delLog (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->delLog(...explode('_', $m['arg'])); break; case preg_match('~^/debug$~', $this->input['message'], $m): $this->debug(); break; case preg_match('~^/backup$~', $this->input['callback'], $m): $this->backup(); break; case preg_match('~^/generateSecret$~', $this->input['callback'], $m): $this->generateSecret(); break; case preg_match('~^/setSecret$~', $this->input['callback'], $m): $this->setSecret(); break; case preg_match('~^/defaultDNS (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->defaultDNS(...explode('_', $m['arg'])); break; case preg_match('~^/defaultMTU (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->defaultMTU(...explode('_', $m['arg'])); break; case preg_match('~^/subnet (?P-?\d+(?:_-?\d+)?(?:_\d)?)$~', $this->input['callback'], $m): $this->subnet(...explode('_', $m['arg'])); break; case preg_match('~^/subnetAdd (?P-?\d+(?:_-?\d+)?(?:_-?\d+)?)$~', $this->input['callback'], $m): $this->subnetAdd(...explode('_', $m['arg'])); break; case preg_match('~^/subnetDelete (?P-?\d+(?:_-?\d+)?(?:_-?\d+)?(?:_-?\d+)?)$~', $this->input['callback'], $m): $this->subnetDelete(...explode('_', $m['arg'])); break; case preg_match('~^/addSubnets (?P-?\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->addSubnets(...explode('_', $m['arg'])); break; case preg_match('~^/changeAllowedIps (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->changeAllowedIps(...explode('_', $m['arg'])); break; case preg_match('~^/changeMTU (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->changeMTU(...explode('_', $m['arg'])); break; case preg_match('~^/calc$~', $this->input['callback'], $m): $this->calc(); break; case preg_match('~^/changeIps (?P\w+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->changeIps(...explode('_', $m['arg'])); break; case preg_match('~^/selfssl$~', $this->input['callback'], $m): $this->selfssl(); break; case preg_match('~^/sspswd$~', $this->input['callback'], $m): $this->sspswd(); break; case preg_match('~^/changeCamouflage$~', $this->input['callback'], $m): $this->changeCamouflage(); break; case preg_match('~^/changeOcDomain$~', $this->input['callback'], $m): $this->changeOcDomain(); break; case preg_match('~^/changeOcPass$~', $this->input['callback'], $m): $this->changeOcPass(); break; case preg_match('~^/changeNaiveUser$~', $this->input['callback'], $m): $this->changeNaiveUser(); break; case preg_match('~^/changeNaiveSubdomain$~', $this->input['callback'], $m): $this->changeNaiveSubdomain(); break; case preg_match('~^/changeNaivePass$~', $this->input['callback'], $m): $this->changeNaivePass(); break; case preg_match('~^/changeOcDns$~', $this->input['callback'], $m): $this->changeOcDns(); break; case preg_match('~^/addOcUser$~', $this->input['callback'], $m): $this->addOcUser(); break; case preg_match('~^/changeOcExpose$~', $this->input['callback'], $m): $this->changeOcExpose(); break; case preg_match('~^/addXrUser$~', $this->input['callback'], $m): $this->addXrUser(); break; case preg_match('~^/renameXrUser (\d+)$~', $this->input['callback'], $m): $this->renameXrUser($m[1]); break; case preg_match('~^/resetXrUser (\d+)$~', $this->input['callback'], $m): $this->resetXrUser($m[1]); break; case preg_match('~^/resetXrStats$~', $this->input['callback'], $m): $this->resetXrStats(); break; case preg_match('~^/v2ray$~', $this->input['callback'], $m): $this->v2ray(); break; case preg_match('~^/checkdns$~', $this->input['callback'], $m): $this->checkdns(); break; case preg_match('~^/adguardpsswd$~', $this->input['callback'], $m): $this->adguardpsswd(); break; case preg_match('~^/setAdguardKey$~', $this->input['callback'], $m): $this->setAdguardKey(); break; case preg_match('~^/addadmin$~', $this->input['callback'], $m): $this->enterAdmin(); break; case preg_match('~^/enterPage$~', $this->input['callback'], $m): $this->enterPage(); break; case preg_match('~^/geodb$~', $this->input['callback'], $m): $this->geodb(); break; case preg_match('~^/adguardreset$~', $this->input['callback'], $m): $this->adguardreset(); break; case preg_match('~^/addupstream$~', $this->input['callback'], $m): $this->addupstream(); break; case preg_match('~^/checkurl$~', $this->input['callback'], $m): $this->checkurl(); break; case preg_match('~^/setSSL (\w+)$~', $this->input['callback'], $m): $this->setSSL($m[1]); break; case preg_match('~^/lang (\w+)$~', $this->input['callback'], $m): $this->setLang($m[1]); break; case preg_match('~^/deletessl$~', $this->input['callback'], $m): $this->deleteSSL(); break; case preg_match('~^/download (\d+)$~', $this->input['callback'], $m): $this->downloadPeer($m[1]); break; case preg_match('~^/dw (\w+) (\w+)$~', $this->input['callback'], $m): $this->dw($m[1], $m[2]); break; case preg_match('~^/deloc (\d+)$~', $this->input['callback'], $m): $this->deloc($m[1]); break; case preg_match('~^/userXr (\d+)$~', $this->input['callback'], $m): $this->userXr($m[1]); break; case preg_match('~^/choiceTemplate (.+)$~', $this->input['callback'], $m): $this->choiceTemplate($m[1]); break; case preg_match('~^/templateUser (\w+) (\d+)$~', $this->input['callback'], $m): $this->templateUser($m[1], $m[2]); break; case preg_match('~^/timerXr (\d+)$~', $this->input['callback'], $m): $this->timerXr($m[1]); break; case preg_match('~^/switchXr (\d+)$~', $this->input['callback'], $m): $this->switchXr($m[1]); break; case preg_match('~^/delxr (\d+)$~', $this->input['callback'], $m): $this->delxr($m[1]); break; case preg_match('~^/listXr (\d+)$~', $this->input['callback'], $m): $this->listXr($m[1]); break; case preg_match('~^/switchTorrent (\d+)$~', $this->input['callback'], $m): $this->switchTorrent($m[1]); break; case preg_match('~^/switchEndpoint (\d+)$~', $this->input['callback'], $m): $this->switchEndpoint($m[1]); break; case preg_match('~^/switchAmnezia (-?\d+)$~', $this->input['callback'], $m): $this->switchAmnezia($m[1]); break; case preg_match('~^/switchExchange (\d+)$~', $this->input['callback'], $m): $this->switchExchange($m[1]); break; case preg_match('~^/blinkmenuswitch$~', $this->input['callback'], $m): $this->blinkmenuswitch(); break; case preg_match('~^/switchClient (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->switchClient(...explode('_', $m['arg'])); $this->menu('client', $m['arg']); break; case preg_match('~^/deladmin (\d+)$~', $this->input['callback'], $m): $this->delAdmin($m[1]); break; case preg_match('~^/qr (\d+)$~', $this->input['callback'], $m): $this->qrPeer($m[1]); break; case preg_match('~^/qrSS$~', $this->input['callback'], $m): $this->qrSS(); break; case preg_match('~^/qrXray (\d+)(?:_(\d+))?$~', $this->input['callback'], $m): $this->qrXray($m[1], $m[2] ?: false); break; case preg_match('~^/qrMtproto$~', $this->input['callback'], $m): $this->qrMtproto(); break; case preg_match('~^/delupstream (\d+)$~', $this->input['callback'], $m): $this->delupstream($m[1]); break; case preg_match('~^/delete (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->deletePeer(...explode('_', $m['arg'])); break; case preg_match('~^/dns (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->dnsPeer(...explode('_', $m['arg'])); break; case preg_match('~^/deletedns (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->deletednsPeer(...explode('_', $m['arg'])); break; case preg_match('~^/deldomain$~', $this->input['callback'], $m): $this->delDomain(); break; case preg_match('~^/addNipdomain$~', $this->input['callback'], $m): $this->addNipdomain(); break; case preg_match('~^/(?Pchange|delete)(?P\w+) (?P\d+)$~', $this->input['callback'], $m): $this->listPacChange($m['typelist'], $m['action'], $m['arg']); break; case preg_match('~^/paczapret$~', $this->input['callback'], $m): $this->pacZapret(); break; case preg_match('~^/pacupdate$~', $this->input['callback'], $m): $this->pacUpdate(); break; case preg_match('~^/add$~', $this->input['callback'], $m): $this->addPeer(); // добавление клиента "весь траффик" break; case preg_match('~^/add_ips$~', $this->input['callback'], $m): $this->addips(); // ответ с предложением ввести список подсетей break; case preg_match('~^/domain$~', $this->input['callback'], $m): $this->domain(); break; case preg_match('~^/warp$~', $this->input['callback'], $m): $this->warp(); break; case preg_match('~^/warpPlus$~', $this->input['callback'], $m): $this->warpPlus(); break; case preg_match('~^/xray(?: (\d+))?$~', $this->input['callback'], $m): $this->xray($m[1] ?: 0); break; case preg_match('~^/xtlsblock(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlsblock($m[1] ?: 0); break; case preg_match('~^/routes(?: (\d+))?$~', $this->input['callback'], $m): $this->routes($m[1] ?: 0); break; case preg_match('~^/xtlswarp(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlswarp($m[1] ?: 0); break; case preg_match('~^/xtlsproxy(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlsproxy($m[1] ?: 0); break; case preg_match('~^/xtlsapp(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlsapp($m[1] ?: 0); break; case preg_match('~^/xtlsprocess(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlsprocess($m[1] ?: 0); break; case preg_match('~^/xtlssubnet(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlssubnet($m[1] ?: 0); break; case preg_match('~^/xtlsrulesset(?: (\d+))?$~', $this->input['callback'], $m): $this->xtlsrulesset($m[1] ?: 0); break; case preg_match('~^/templateCopy (\w+)(?: (.+))?$~', $this->input['callback'], $m): $this->templateCopy($m[1], $m[2]); break; case preg_match('~^/delTemplate (\w+)(?: (.+))?$~', $this->input['callback'], $m): $this->delTemplate($m[1], $m[2]); break; case preg_match('~^/downloadOrigin (\w+)$~', $this->input['callback'], $m): $this->downloadOrigin($m[1]); break; case preg_match('~^/downloadTemplate (\w+)(?: (.+))?$~', $this->input['callback'], $m): $this->downloadTemplate($m[1], $m[2]); break; case preg_match('~^/defaultTemplate (\w+)(?: (.+))?$~', $this->input['callback'], $m): $this->defaultTemplate($m[1], $m[2]); break; case preg_match('~^/templates (\w+)$~', $this->input['callback'], $m): $this->templates($m[1]); break; case preg_match('~^/templateAdd (\w+)$~', $this->input['callback'], $m): $this->templateAdd($m[1]); break; case preg_match('~^/changeFakeDomain$~', $this->input['callback'], $m): $this->changeFakeDomain(); break; case preg_match('~^/autoCleanLogs$~', $this->input['callback'], $m): $this->autoCleanLogs(); break; case preg_match('~^/selfFakeDomain$~', $this->input['callback'], $m): $this->selfFakeDomain(); break; case preg_match('~^/changeTGDomain$~', $this->input['callback'], $m): $this->changeTGDomain(); break; case preg_match('~^/include (\w+)$~', $this->input['callback'], $m): $this->include($m[1]); break; case preg_match('~^/exclude (\d+)$~', $this->input['callback'], $m): $this->exclude($m[1]); break; case preg_match('~^/reverse (\d+)$~', $this->input['callback'], $m): $this->reverse($m[1]); break; case preg_match('~^/subzones (\d+)$~', $this->input['callback'], $m): $this->subzones($m[1]); break; case preg_match('~^/showreset$~', $this->input['callback'], $m): $this->showreset(); break; case preg_match('~^/reset$~', $this->input['callback'], $m): $this->reset(); break; case preg_match('~^/proxy$~', $this->input['callback'], $m): $this->proxy(); break; case preg_match('~^/addOverrideHtml$~', $this->input['callback'], $m): $this->addOverrideHtml(); break; case preg_match('~^/export$~', $this->input['callback'], $m): $this->pinBackup(); break; case preg_match('~^/import$~', $this->input['callback'], $m): $this->import(); break; case preg_match('~^/importList (\w+)$~', $this->input['callback'], $m): $this->importList($m[1]); break; case preg_match('~^/rename (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->rename(...explode('_', $m['arg'])); break; case preg_match('~^/timer (?P\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->timer(...explode('_', $m['arg'])); break; case !empty($this->input['reply']): $this->reply(); break; } } public function generateSecret() { $this->secretSet(exec('head -c 16 /dev/urandom | xxd -ps')); } public function setSecret() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter key or 0 for stop mtproto", $this->input['message_id'], reply: 'enter key or 0 for stop mtproto', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'secretSet', 'args' => [], ]; } public function secretSet($secret) { file_put_contents('/config/mtprotosecret', $secret); $this->restartTG(); $this->mtproto(); } public function setTelegramDomain($domain) { file_put_contents('/config/mtprotodomain', $domain); $this->restartTG(); $this->mtproto(); } public function restartTG() { $secret = file_get_contents('/config/mtprotosecret'); $fakedomain = file_get_contents('/config/mtprotodomain') ?: 'vk.com'; $this->ssh('pkill mtproto-proxy', 'tg'); if (preg_match('~^\w{32}$~', $secret)) { $p = getenv('TGPORT'); $this->ssh("mtproto-proxy --domain $fakedomain -u nobody -H $p --nat-info 10.10.0.8:{$this->ip} -S $secret --aes-pwd /proxy-secret /proxy-multi.conf -M 1 >/dev/null 2>&1 &", 'tg'); } } public function restartXray($c, $norestart = false) { $c['inbounds'][0]['settings']['clients'] = array_values($c['inbounds'][0]['settings']['clients']); $c['log']['access'] = '/logs/xray'; foreach ($c['inbounds'] as $v) { if ($v['tag'] == 'api') { $inbound = true; break; } } if (empty($inbound)) { $c['inbounds'][] = [ "listen" => "127.0.0.1", "port" => 8080, "protocol" => "dokodemo-door", "settings" => [ "address" => "127.0.0.1" ], "tag" => "api" ]; } foreach ($c['routing']['rules'] as $v) { if ($v['outboundTag'] == 'api') { $rule = true; break; } } if (empty($rule)) { $c['routing']['rules'][] = [ "inboundTag" => ["api"], "outboundTag" => "api", "type" => "field" ]; } $c['stats'] = new stdClass(); $c['api'] = [ 'services' => ['StatsService'], 'tag' => 'api' ]; $l = new stdClass(); $l->{'0'} = [ "statsUserUplink" => true, "statsUserDownlink" => true ]; $c['policy']['levels'] = $l; $c['policy']['system'] = [ "statsInboundUplink" => true, "statsInboundDownlink" => true, "statsOutboundUplink" => true, "statsOutboundDownlink" => true ]; if (empty($norestart)) { $this->collectSession(); file_put_contents('/config/xray.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('pkill xray', 'xr'); $this->ssh('xray run -config /xray.json > /dev/null 2>&1 &', 'xr'); } else { file_put_contents('/config/xray.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } } public function collectSession() { $p = $this->getXrayStats(); $p['global'] = [ 'download' => $p['global']['download'] + $p['session']['download'], 'upload' => $p['global']['upload'] + $p['session']['upload'], ]; $p['session'] = [ 'download' => 0, 'upload' => 0, ]; foreach ($p['users'] as $k => $v) { $p['users'][$k]['global']['download'] += $v['session']['download']; $p['users'][$k]['session']['download'] = 0; $p['users'][$k]['global']['upload'] += $v['session']['upload']; $p['users'][$k]['session']['upload'] = 0; } $this->setXrayStats($p); } public function linkMtproto() { $s = file_get_contents('/config/mtprotosecret'); $p = getenv('TGPORT'); $d = trim(file_get_contents('/config/mtprotodomain') ?: 'vk.com'); $d = exec("echo $d | tr -d '\\n' | xxd -ps -c 200"); $ip = $this->getPacConf()['domain'] ?: $this->ip; return "https://t.me/proxy?server=$ip&port=$p&secret=ee$s$d"; } public function mtproto() { $d = file_get_contents('/config/mtprotodomain') ?: 'vk.com'; $st = $this->ssh('pgrep mtproto-proxy', 'tg') ? 'on' : 'off'; $text[] = "Menu -> MTProto\n"; $text[] = "status: $st\n"; $text[] = "fake domain: $d\n"; if ($st == 'on') { $text[] = $this->linkMtproto(); } $data[] = [ [ 'text' => $this->i18n('generateSecret'), 'callback_data' => "/generateSecret", ], ]; $data[] = [ [ 'text' => $this->i18n('setSecret'), 'callback_data' => "/setSecret", ], ]; $data[] = [ [ 'text' => $this->i18n('changeFakeDomain'), 'callback_data' => "/changeTGDomain", ], ]; $data[] = [ [ 'text' => $this->i18n('show QR'), 'callback_data' => "/qrMtproto", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function setLang($lang) { $conf = $this->getPacConf(); $this->language = $conf['language'] = $lang; $this->setPacConf($conf); $this->menu('config'); } public function checkurl() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter url", $this->input['message_id'], reply: 'enter url', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'urlcheck', 'args' => [], ]; } public function urlcheck($url) { if (file_exists(__DIR__ . '/zapretlists/mpac')) { $domains = explode("\n", file_get_contents(__DIR__ . '/zapretlists/mpac')); foreach ($domains as $k => $v) { if (preg_match("~$v~", $url)) { $flag = 1; break; } } if ($flag) { $text = "$url\nmatch"; } else { $text = "$url\nnot match"; } } else { $text = 'no file, update pac'; } $this->update($this->input['chat'], $this->input['message_id'], $text); sleep(3); $this->menu('pac'); } public function sspswd() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter password", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'sspwdch', 'args' => [], ]; } public function ssPswdCheck() { $c = $this->getSSConfig(); if (empty($c['password']) || ($c['password'] == 'test')) { $this->sspwdch(password_hash(time(), PASSWORD_DEFAULT), 1); } } public function changeCamouflage() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter camouflage key", $this->input['message_id'], reply: 'enter camouflage key', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chockey', 'args' => [], ]; } public function changeOcDomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter subdomain", $this->input['message_id'], reply: 'enter subdomain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chOcSubdomain', 'args' => [], ]; } public function changeOcDns() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter dns", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chocdns', 'args' => [], ]; } public function changeOcPass() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter pass", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chocpass', 'args' => [], ]; } public function changeNaiveUser() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter login", $this->input['message_id'], reply: 'enter login', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chnplogin', 'args' => [], ]; } public function changeNaiveSubdomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter subdomain", $this->input['message_id'], reply: 'enter subdomain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chNpSubdomain', 'args' => [], ]; } public function changeNaivePass() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter password", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chnppass', 'args' => [], ]; } public function addOcUser() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter name", $this->input['message_id'], reply: 'enter name', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'addocus', 'args' => [], ]; } public function addXrUser() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter name", $this->input['message_id'], reply: 'enter name:uuid [,name:uuid]', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'addxrus', 'args' => [], ]; } public function renameXrUser($i) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter name", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'renXrUs', 'args' => [$i], ]; } public function addOverrideHtml() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} attach html", $this->input['message_id'], reply: 'attach html', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setOverrideHtml', 'args' => [], ]; } public function setOverrideHtml() { $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); if (!empty($f = file_get_contents($this->file . $r['result']['file_path']))) { file_put_contents('/app/webapp/override.html', $f); } } public function restartOcserv($conf) { file_put_contents('/config/ocserv.conf', $conf); $this->ssh('pkill ocserv', 'oc'); $pac = $this->getPacConf(); if (!empty($pac['ocserv'])) { $this->ssh('ocserv -c /etc/ocserv/ocserv.conf', 'oc'); } } public function restartNaive() { $pac = $this->getPacConf(); $this->ssh('pkill caddy', 'np'); $c = file_get_contents('/config/Caddyfile'); $t = preg_replace('~^(\t+)?basic_auth[^\n]+~sm', '$1basic_auth ' . ($pac['naive']['user'] ?? '_') . ' ' . ($pac['naive']['pass'] ?? '__'), $c); file_put_contents('/config/Caddyfile', $t); if (!empty($pac['naive']['pass'])) { $this->ssh('caddy run -c /config/Caddyfile > /dev/null 2>&1 &', 'np', false); } } public function chocdns($dns) { $c = file_get_contents('/config/ocserv.conf'); $t = preg_replace('~^dns[^\n]+~sm', "dns = $dns", $c); $this->restartOcserv($t); $this->menu('oc'); } public function chOcSubdomain($domain) { $pac = $this->getPacConf(); if (empty($domain)) { unset($pac["oc_domain"]); } else { $pac["oc_domain"] = $domain; } $this->setPacConf($pac); $this->chocdomain($pac['domain']); $this->setUpstreamDomainOcserv($pac['domain']); $this->menu('oc'); } public function chNpSubdomain($domain) { $pac = $this->getPacConf(); if (!empty($data)) { unset($pac['np_domain']); } else { $pac['np_domain'] = $domain; } $this->setPacConf($pac); $this->restartNaive(); $this->setUpstreamDomainNaive($pac['domain']); $this->menu('naive'); } public function chnplogin($user) { $pac = $this->getPacConf(); $pac['naive']['user'] = $user; $this->setPacConf($pac); $this->restartNaive(); $this->menu('naive'); } public function chnppass($pass) { $pac = $this->getPacConf(); $pac['naive']['pass'] = $pass; $this->setPacConf($pac); $this->restartNaive(); $this->menu('naive'); } public function chockey($pass) { $c = file_get_contents('/config/ocserv.conf'); $t = preg_replace('~^camouflage_secret[^\n]+~sm', "camouflage_secret = \"$pass\"", $c); $this->restartOcserv($t); $this->menu('oc'); } public function chocdomain($domain) { $oc = $this->getHashSubdomain('oc'); $c = file_get_contents('/config/ocserv.conf'); $t = preg_replace('~^default-domain[^\n]+~sm', "default-domain = $oc.$domain", $c); $this->restartOcserv($t); } public function chocpass($pass) { $pac = $this->getPacConf(); $pac['ocserv'] = $pass; $this->setPacConf($pac); $clients = $this->getClientsOc(); foreach ($clients as $k => $v) { $this->ssh("echo '$pass' | ocpasswd -c /etc/ocserv/ocserv.passwd $v", 'oc'); } $this->restartOcserv(file_get_contents('/config/ocserv.conf')); $this->menu('oc'); } public function sspwdch($pass, $nomenu = false) { $this->ssh('pkill sslocal', 'proxy'); $this->ssh('pkill ssserver', 'ss'); $c = $this->getSSConfig(); $l = $this->getSSLocalConfig(); $c['password'] = $l['password'] = $pass; file_put_contents('/config/ssserver.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); file_put_contents('/config/sslocal.json', json_encode($l, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('ssserver -v -d -c /config.json', 'ss'); $this->ssh('sslocal -v -d -c /config.json', 'proxy'); if (empty($nomenu)) { $this->menu('ss'); } } public function v2ray() { $this->ssh('pkill sslocal', 'proxy'); $this->ssh('pkill ssserver', 'ss'); $ssl = $this->nginxGetTypeCert(); $c = $this->getSSConfig(); $l = $this->getSSLocalConfig(); $domain = $this->getPacConf()['domain'] ?: $this->ip; if ($c['plugin']) { unset($c['plugin']); unset($c['plugin_opts']); unset($l['plugin']); unset($l['plugin_opts']); $l['server'] = 'ss'; $l['server_port'] = (int) getenv('SSPORT'); $c['server_port'] = (int) getenv('SSPORT'); } else { $c['plugin'] = 'v2ray-plugin'; $c['plugin_opts'] = 'server;loglevel=none'; $l['server'] = 'up'; $l['server_port'] = $ssl ? 443 : 80; $l['plugin'] = 'v2ray-plugin'; $l['plugin_opts'] = ($ssl ? 'tls;' : '') . "fast-open;path=/v2ray;host=$domain"; } file_put_contents('/config/ssserver.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); file_put_contents('/config/sslocal.json', json_encode($l, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('ssserver -v -d -c /config.json', 'ss'); $this->ssh('sslocal -v -d -c /config.json', 'proxy'); $this->menu('ss'); } public function rename(int $client, $page) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter the title:", $this->input['message_id'], reply: 'enter the title:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'renameClient', 'args' => [$client, $page], ]; } public function timer(int $client, $page) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter time like https://www.php.net/manual/ru/function.strtotime.php:", $this->input['message_id'], reply: 'enter time like https://www.php.net/manual/ru/function.strtotime.php:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'timerClient', 'args' => [$client, $page], ]; } public function importList($type) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send the export file:", $this->input['message_id'], reply: 'send the export file:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'importListFile', 'args' => [$type], ]; } public function importListFile($text = '', $type) { $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); $f = file_get_contents($this->file . $r['result']['file_path']); if (!empty($f)) { foreach (explode("\n", $f) as $v) { if (!empty($s = trim($v))) { $t = explode(';', $s); if ($type == 'rulessetlist') { if (preg_match('~^.+:.+:https?://.+~', $t[0])) { $list[$t[0]] = (bool) $t[1]; } } else { $list[$t[0]] = (bool) $t[1]; } } } $p = $this->getPacConf(); $p[$type] = $list; $this->setPacConf($p); } $this->backXtlsList($type); } public function timerClient(string $time, int $client) { $clients = $this->readClients(); if ($clients[$client]['# off']) { $this->switchClient($client); $clients = $this->readClients(); } $server = $this->readConfig(); switch (true) { case preg_match('~^0$~', $time): unset($clients[$client]['interface']['## time']); foreach ($server['peers'] as $k => $v) { if ($v['AllowedIPs'] == $clients[$client]['interface']['Address']) { unset($server['peers'][$k]['## time']); } } break; default: $date = date('Y-m-d H:i:s', strtotime($time)); $clients[$client]['interface']['## time'] = $date; foreach ($server['peers'] as $k => $v) { if ($v['AllowedIPs'] == $clients[$client]['interface']['Address']) { $server['peers'][$k]['## time'] = $date; } } break; } $this->saveClients($clients); $this->restartWG($this->createConfig($server)); $this->menu('client', implode('_', $_SESSION['reply'][$this->input['reply']]['args'])); } public function cron() { $period = 10; while (true) { $this->shutdownClient(); $this->shutdownClientXr(); $this->checkVersion(); $this->checkBackup(); $this->checkLogs(); $this->checkResetXrayStats(); $this->checkCert(); $this->autoAnalyzeLogs(); $this->xrayStatsUser(); sleep($period); } } public function xrayStatsUser() { if (empty($this->time_xray_stats) || time() - $this->time_xray_stats > 60) { $this->time_xray_stats = time(); try { $x = $this->getXray(); $td = json_decode($this->ssh('xray api stats --server=127.0.0.1:8080 -name "inbound>>>vless_tls>>>traffic>>>downlink" 2>&1', 'xr'), true)['stat']['value'] ?: 0; $tu = json_decode($this->ssh('xray api stats --server=127.0.0.1:8080 -name "inbound>>>vless_tls>>>traffic>>>uplink" 2>&1', 'xr'), true)['stat']['value'] ?: 0; $p = $this->getXrayStats(); $p['session'] = [ 'download' => $td, 'upload' => $tu, ]; if (!empty($users = $x['inbounds'][0]['settings']['clients'])) { $tmp = []; foreach ($users as $k => $v) { $d = json_decode($this->ssh('xray api stats --server=127.0.0.1:8080 -name "user>>>' . $v['email'] . '>>>traffic>>>downlink" 2>&1', 'xr'), true)['stat']['value'] ?: 0; $u = json_decode($this->ssh('xray api stats --server=127.0.0.1:8080 -name "user>>>' . $v['email'] . '>>>traffic>>>uplink" 2>&1', 'xr'), true)['stat']['value'] ?: 0; $tmp[$k] = [ 'session' => [ 'download' => $d, 'upload' => $u, ], 'global' => [ 'download' => $p['users'][$k]['global']['download'], 'upload' => $p['users'][$k]['global']['upload'], ] ]; } $p['users'] = $tmp; } $this->setXrayStats($p); } catch (\Throwable $th) { } } } public function autoAnalyzeLogs() { try { $pac = $this->getPacConf(); if (!empty($pac['autoscan'])) { require __DIR__ . '/config.php'; if (!empty($c['admin']) && (empty($this->time3) || ((time() - $this->time3) > $pac['autoscan_timeout']))) { $this->time3 = time(); $r = $this->analysisIp(return: 1); if (!empty($r)) { foreach ($r as $k => $v) { foreach ($v as $i) { $t[$i['title']][$k] = 1; } } foreach ($t as $k => $v) { $text .= "\n" . count($v) . " $k"; } if (!empty($pac['autodeny'])) { $this->denyIp(array_keys($r)); $ban = count(array_keys($r)); foreach (array_keys($r) as $v) { $ips[] = [[ 'text' => $v, 'callback_data' => "/searchLogs $v", ]]; } } if ($pac['silence'] == 0 || $pac['silence'] == 1) { foreach ($c['admin'] as $k => $v) { $this->send($v, "suspicious ips found: $text" . ($ban ? "\nbanned:$ban" : ''), button: $ips ?: [[ [ 'text' => $this->i18n('analyze'), 'callback_data' => '/analysisIp', ], ]], disable_notification: $pac['silence'] ? true : false); } } } } } } catch (Exception $e) { file_put_contents('/logs/php_error', $e->getMessage()); } } public function checkBackup() { $c = $this->getPacConf(); if (!empty($c['backup'])) { $now = time(); [$start, $period] = explode('/', $c['backup']); $start = strtotime(trim($start)); $period = strtotime(trim($period), 0); if ( !empty($start) && !empty($period) && $now >= $start ) { // Вычисляем, сколько полных периодов прошло с момента start $elapsed = $now - $start; $periodsElapsed = floor($elapsed / $period); // Время последнего планового бэкапа $lastScheduledBackup = $start + ($periodsElapsed * $period); // Проверяем, делали ли уже бэкап в этом периоде $lastBackupTime = $c['last_backup_time'] ?? 0; // Если последний бэкап был сделан до начала текущего периода - делаем бэкап if ($lastBackupTime < $lastScheduledBackup) { $c['last_backup_time'] = $now; $this->setPacConf($c); $this->pinBackup(); } } } } public function checkResetXrayStats() { $pac = $this->getPacConf(); if (!empty($pac['reset_monthly'])) { $now = time(); $start = strtotime('first day of previous month midnight'); $period = strtotime('1 month', 0); if ( !empty($start) && !empty($period) && $now >= $start ) { // Вычисляем, сколько полных периодов прошло с момента start $elapsed = $now - $start; $periodsElapsed = floor($elapsed / $period); // Время последнего планового сброса статистики $lastScheduledReset = $start + ($periodsElapsed * $period); // Проверяем, делали ли уже сброс в этом периоде $lastResetTime = $pac['last_reset_xray_time'] ?? 0; // Если последний сброс был сделан до начала текущего периода - делаем сброс if ($lastResetTime < $lastScheduledReset) { $pac['last_reset_xray_time'] = $now; $this->setPacConf($pac); $this->resetXrStats(1); require __DIR__ . '/config.php'; foreach ($c['admin'] as $admin) { $this->send($admin, "vless: reset stats"); } } } } } public function checkLogs() { $c = $this->getPacConf(); if (!empty($c['autocleanlogs'])) { $now = time(); [$start, $period] = explode('/', $c['autocleanlogs']); $start = strtotime(trim($start)); $period = strtotime(trim($period), 0); if ( !empty($start) && !empty($period) && $now >= $start ) { // Вычисляем, сколько полных периодов прошло с момента start $elapsed = $now - $start; $periodsElapsed = floor($elapsed / $period); // Время последней плановой очистки логов $lastScheduledClean = $start + ($periodsElapsed * $period); // Проверяем, делали ли уже очистку в этом периоде $lastCleanTime = $c['last_clean_logs_time'] ?? 0; // Если последняя очистка была сделана до начала текущего периода - делаем очистку if ($lastCleanTime < $lastScheduledClean) { $c['last_clean_logs_time'] = $now; $this->setPacConf($c); $this->cleanLog(); } } } } public function cleanQueue(): void { $r = $this->request('deleteWebhook', []); $r = $this->request('getUpdates', ['offset' => -1]); } public function pinAdmin($pin, $unpin = false) { require __DIR__ . '/config.php'; if ($unpin) { return $this->unpin($c['admin'][0], $pin); } else { return $this->pin($c['admin'][0], $pin); } } public function pinBackup($file = false) { require __DIR__ . '/config.php'; $conf = $this->getPacConf(); $bot = preg_replace('~[\W]~iu', '_', $this->request('getMyName', [])['result']['name']); $json = $this->export(); if (!empty($file)) { file_put_contents($file, $json); } if (!empty($conf['pinbackup'])) { $this->pinAdmin($conf['pinbackup'], 1); } $conf['pinbackup'] = $this->upload("{$bot}_export_" . date('d_m_Y_H_i') . '.json', $json, $c['admin'][0])['result']['message_id']; $this->setPacConf($conf); $this->pinAdmin($conf['pinbackup']); } public function checkVersion() { try { require __DIR__ . '/config.php'; if (!empty($c['admin']) && (empty($this->time) || ((time() - $this->time) > 3600))) { $this->time = time(); $current = file_get_contents('/version'); $b = exec('git -C / rev-parse --abbrev-ref HEAD'); $last = file_get_contents("https://raw.githubusercontent.com/mercurykd/vpnbot/$b/version"); if (!empty($last) && $last != $this->last && $last != $current) { $this->last = $last; $diff = array_slice(explode("\n", $last), 0, count(explode("\n", $last)) - count(explode("\n", $current))); $diff = array_slice($diff, 0, 10); if (!empty($diff)) { exec('git -C / fetch'); foreach ($c['admin'] as $k => $v) { $this->send($v, implode("\n", $diff), 0, [ [ [ 'text' => 'changelog', 'web_app' => ['url' => "https://raw.githubusercontent.com/mercurykd/vpnbot/$b/version"], ], [ 'text' => $this->i18n('update bot'), 'callback_data' => "/applyupdatebot", ], ] ]); } if ($this->getPacConf()['autoupdate']) { $this->input['chat'] = $this->input['from'] = $c['admin'][0]; $this->applyupdatebot(); } } } } } catch (Exception $e) { } } public function checkCert() { try { require __DIR__ . '/config.php'; if (!empty($c['admin']) && date('H') == 12 && (empty($this->time2) || ((time() - $this->time2) > 4600))) { $this->time2 = time(); $cert = $this->expireCert(); if (!empty($cert) && $cert - 60 * 60 * 24 * 14 < time()) { foreach ($c['admin'] as $k => $v) { $this->send($v, "certificate expire: " . date('Y-m-d H:i:s', $cert)); } } } } catch (Exception $e) { } } public function getTime(int $seconds) { $seconds = ($seconds - time()) > 0 ? $seconds - time() : 0; $items = [ 'Y' => [ 'diff' => 1970, 'sign' => 'y', ], 'm' => [ 'diff' => 1, 'sign' => 'mon', ], 'd' => [ 'diff' => 1, 'sign' => 'd', ], 'H' => [ 'diff' => 0, 'sign' => 'h', ], 'i' => [ 'diff' => 0, 'sign' => 'min', ], 's' => [ 'diff' => 0, 'sign' => 's', ], ]; foreach ($items as $k => $v) { if (($t = gmdate($k, $seconds) - $v['diff']) > 0) { $text .= " $t{$v['sign']}"; if (!empty($i)) { break; } $i++; } } return trim($text) ?: '♾'; } public function shutdownClient() { try { for ($i=0; $i < 2; $i++) { $this->wg = $i; $clients = $this->readClients(); if ($clients) { foreach ($clients as $k => $v) { if (!empty($v['interface']['## time'])) { if (strtotime($v['interface']['## time']) < time()) { $this->switchClient($k); } } } } } } catch (Exception $e) { } } public function shutdownClientXr() { try { $c = $this->getXray(); foreach ($c['inbounds'][0]['settings']['clients'] as $k => $v) { if (!empty($v['time']) && ($v['time'] < time())) { $this->switchXr($k, 1); } } } catch (Exception $e) { } } public function renameClient(string $name, int $client) { $clients = $this->readClients(); $clients[$client]['interface']['## name'] = $name; $this->saveClients($clients); $server = $this->readConfig(); foreach ($server['peers'] as $k => $v) { if ($v['AllowedIPs'] == $clients[$client]['interface']['Address']) { $server['peers'][$k]['## name'] = $name; } } $this->restartWG($this->createConfig($server)); $this->menu('client', implode('_', $_SESSION['reply'][$this->input['reply']]['args'])); } public function readClients(): array { return json_decode(file_get_contents($this->getInstanceWG(1) ? $this->clients1 : $this->clients), true) ?: []; } public function export() { $this->wg = 0; $wg = [ 'server' => $this->readConfig(), 'clients' => json_decode(file_get_contents($this->clients), true) ?: [], ]; $this->wg = 1; $wg1 = [ 'server' => $this->readConfig(), 'clients' => json_decode(file_get_contents($this->clients1), true) ?: [], ]; $conf = [ 'wg' => $wg, 'wg1' => $wg1, 'ad' => yaml_parse_file($this->adguard), 'pac' => $this->getPacConf(), 'hwid' => file_exists($this->hwid) ? (json_decode(file_get_contents($this->hwid), true) ?: []) : [], 'ssl' => file_exists('/certs/cert_private') && preg_match('~BEGIN PRIVATE KEY~', file_get_contents('/certs/cert_private')) ? [ 'private' => file_get_contents('/certs/cert_private'), 'public' => file_get_contents('/certs/cert_public'), ] : false, 'mtproto' => file_get_contents('/config/mtprotosecret'), 'mtprotodomain' => file_get_contents('/config/mtprotodomain'), 'xray' => $this->getXray(), 'oc' => file_get_contents('/config/ocserv.conf'), 'ocu' => file_get_contents('/config/ocserv.passwd'), 'ss' => $this->getSSConfig(), 'sl' => $this->getSSLocalConfig(), 'xraystats' => $this->getXrayStats(), ]; return json_encode($conf, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } public function import() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send the export file:", $this->input['message_id'], reply: 'send the export file:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'importFile', 'args' => [], ]; } public function importFile($file = false) { if (!empty($file)) { $json = json_decode(file_get_contents($file), true); } else { $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); $json = json_decode(file_get_contents($this->file . $r['result']['file_path']), true); } if (empty($json) || !is_array($json)) { $this->answer($this->input['callback_id'], 'error', true); } else { // certs if (!empty($json['ssl'])) { $out[] = 'update certificates'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); file_put_contents('/certs/cert_private', $json['ssl']['private']); file_put_contents('/certs/cert_public', $json['ssl']['public']); } // pac if (!empty($json['pac'])) { $out[] = 'update pac'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); if ($this->getPacConf()['amnezia'] != $json['pac']['amnezia']) { $switch_amnezia = 1; } if ($this->getPacConf()['wg1_amnezia'] != $json['pac']['wg1_amnezia']) { $switch_wg1amnezia = 1; } $this->setPacConf($json['pac']); $out[] = 'update naiveproxy'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->restartNaive(); $this->pacUpdate('1'); } // wg if (!empty($json['wg'])) { $out[] = 'update wireguard'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->wg = 0; $this->saveClients($json['wg']['clients']); $this->restartWG($this->createConfig($json['wg']['server']), $switch_amnezia); $this->iptablesWG(); } // wg1 if (!empty($json['wg1'])) { $out[] = 'update wireguard 1'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->wg = 1; $this->saveClients($json['wg1']['clients']); $this->restartWG($this->createConfig($json['wg1']['server']), $switch_wg1amnezia); $this->iptablesWG(); } // ad if (!empty($json['ad'])) { $out[] = 'update adguard'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->stopAd(); yaml_emit_file($this->adguard, $json['ad']); $this->startAd(); } // ss if (!empty($json['ss'])) { $out[] = 'update shadowsocks server'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->ssh('pkill ssserver', 'ss'); file_put_contents('/config/ssserver.json', json_encode($json['ss'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('ssserver -v -d -c /config.json', 'ss'); } // sl if (!empty($json['sl'])) { $out[] = 'update shadowsocks proxy'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->ssh('pkill sslocal', 'proxy'); file_put_contents('/config/sslocal.json', json_encode($json['sl'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('sslocal -v -d -c /config.json', 'proxy'); } // mtproto if (!empty($json['mtproto'])) { $out[] = 'update mtproto'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); file_put_contents('/config/mtprotosecret', $json['mtproto']); file_put_contents('/config/mtprotodomain', $json['mtprotodomain'] ?: ''); $this->restartTG(); } // hwid if (array_key_exists('hwid', $json)) { $out[] = 'update hwid devices'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $data = is_array($json['hwid']) ? $json['hwid'] : []; file_put_contents($this->hwid, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } // xray if (!empty($json['xray'])) { $out[] = 'update xray'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->restartXray($json['xray']); $this->adguardXrayClients(); $this->setUpstreamDomain($json['pac']['transport'] != 'Reality' ? 't' : ($json['pac']['reality']['domain'] ?: $json['xray']['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0])); } // xraystats if (!empty($json['xraystats'])) { $out[] = 'update xray stats'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->setXrayStats($json['xraystats']); } // ocserv if (!empty($json['oc'])) { $out[] = 'update ocserv'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); file_put_contents('/config/ocserv.passwd', $json['ocu']); $this->restartOcserv($json['oc']); } if (!empty($json['pac']['domain'])) { $this->setUpstreamDomainOcserv($json['pac']['domain']); $this->setUpstreamDomainNaive($json['pac']['domain']); } // nginx $out[] = 'reset nginx'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->cloakNginx(); $out[] = "end import"; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->language = $this->getPacConf()['language'] ?: 'en'; $this->limit = $this->getPacConf()['limitpage'] ?: 5; if (empty($file)) { sleep(3); $this->menu(); } } } public function dw($u, $t) { $pac = $this->getPacConf(); $c = $this->getXray()['inbounds'][0]['settings']['clients'][$u]; $_GET['s'] = $c['id']; $_GET['t'] = $t; $_SERVER['SERVER_NAME'] = $this->getDomain($pac['transport'] != 'Reality'); $conf = $this->subscription(1); $this->sendFile($this->input['from'], new CURLStringFile($conf, $c['email'] . ($t == 'cl' ? '_mihomo.yaml' :($t == 'si' ? '_singbox.json' : '_v2ray.json')))); } public function downloadPeer($client) { $cl = $client; $client = $this->readClients()[$client]; $name = $this->getName($client['interface']); $code = $this->createConfig($client); $this->upload(preg_replace(['~\s+~', '~\(|\)~'], ['_', ''], $name) . ".conf", $code); if ($this->getPacConf()['blinkmenu']) { $this->delete($this->input['chat'], $this->input['message_id']); $this->input['message_id'] = $this->send($this->input['chat'], '.')['result']['message_id']; $this->menu('client', "{$cl}_0"); } } public function switchClient($client) { $clients = $this->readClients(); if ($clients[$client]['# off']) { unset($clients[$client]['# off']); } else { $clients[$client]['# off'] = 1; } unset($clients[$client]['interface']['## time']); $this->saveClients($clients); $server = $this->readConfig(); if (array_key_exists('# PublicKey', $server['peers'][$client])) { foreach ($server['peers'][$client] as $k => $v) { $new[trim(preg_replace('~#~', '', $k, 1))] = $v; } } else { foreach ($server['peers'][$client] as $k => $v) { $new["# $k"] = $v; } } unset($new['## time']); unset($new['# ## time']); $server['peers'][$client] = $new; $this->restartWG($this->createConfig($server)); } public function switchAmnezia($page = 0) { $c = $this->getPacConf(); $amnezia = $c[$this->getInstanceWG(1) . 'amnezia'] = $c[$this->getInstanceWG(1) . 'amnezia'] ? 0 : 1; $this->setPacConf($c); $pk = $this->presharedKey(); $ak = $this->amneziaKeys(); $clients = $this->readClients(); foreach ($clients as $k => $v) { if (!empty($amnezia)) { $clients[$k]['peers'][0]['PresharedKey'] = $pk; $clients[$k]['interface']['Jc'] = $ak['Jc']; $clients[$k]['interface']['Jmin'] = $ak['Jmin']; $clients[$k]['interface']['Jmax'] = $ak['Jmax']; $clients[$k]['interface']['S1'] = $ak['S1']; $clients[$k]['interface']['S2'] = $ak['S2']; $clients[$k]['interface']['H1'] = $ak['H1']; $clients[$k]['interface']['H2'] = $ak['H2']; $clients[$k]['interface']['H3'] = $ak['H3']; $clients[$k]['interface']['H4'] = $ak['H4']; } else { unset($clients[$k]['peers'][0]['PresharedKey']); unset($clients[$k]['interface']['Jc']); unset($clients[$k]['interface']['Jmin']); unset($clients[$k]['interface']['Jmax']); unset($clients[$k]['interface']['S1']); unset($clients[$k]['interface']['S2']); unset($clients[$k]['interface']['H1']); unset($clients[$k]['interface']['H2']); unset($clients[$k]['interface']['H3']); unset($clients[$k]['interface']['H4']); } } $this->saveClients($clients); $wg = $this->readConfig(); if (!empty($amnezia)) { $wg['interface']['Jc'] = $ak['Jc']; $wg['interface']['Jmin'] = $ak['Jmin']; $wg['interface']['Jmax'] = $ak['Jmax']; $wg['interface']['S1'] = $ak['S1']; $wg['interface']['S2'] = $ak['S2']; $wg['interface']['H1'] = $ak['H1']; $wg['interface']['H2'] = $ak['H2']; $wg['interface']['H3'] = $ak['H3']; $wg['interface']['H4'] = $ak['H4']; } else { unset($wg['interface']['Jc']); unset($wg['interface']['Jmin']); unset($wg['interface']['Jmax']); unset($wg['interface']['S1']); unset($wg['interface']['S2']); unset($wg['interface']['H1']); unset($wg['interface']['H2']); unset($wg['interface']['H3']); unset($wg['interface']['H4']); } foreach ($wg['peers'] as $k => $v) { if (!empty($amnezia)) { $wg['peers'][$k]['PresharedKey'] = $pk; } else { unset($wg['peers'][$k]['PresharedKey']); } } $this->restartWG($this->createConfig($wg), 1); $this->menu('wg', $page); } public function switchTorrent($page = 0, $restart = false) { $c = $this->getPacConf(); $c[$this->getInstanceWG(1) . 'blocktorrent'] = $c[$this->getInstanceWG(1) . 'blocktorrent'] ? 0 : 1; $this->setPacConf($c); $this->iptablesWG(); $this->answer($this->input['callback_id'], 'доступ к торрентам ' . ($c[$this->getInstanceWG(1) . 'blocktorrent'] ? 'заблокирован' : 'разблокирован'), true); $this->menu('wg', $page); } public function switchEndpoint($page = 0) { $c = $this->getPacConf(); $c[$this->getInstanceWG(1) . 'endpoint'] = $c[$this->getInstanceWG(1) . 'endpoint'] ? 0 : 1; $this->setPacConf($c); $this->menu('wg', $page); } public function iptablesWG() { $c = $this->getPacConf(); $this->ssh('iptables -F', $this->getInstanceWG()); if ($c['exchange']) { $this->ssh('bash /block_exchange.sh', $this->getInstanceWG()); } if ($c['blocktorrent']) { $this->ssh('bash /block_torrent.sh', $this->getInstanceWG()); } } public function switchExchange($page) { $c = $this->getPacConf(); $c[$this->getInstanceWG(1) . 'exchange'] = $c[$this->getInstanceWG(1) . 'exchange'] ? 0 : 1; $this->setPacConf($c); $this->iptablesWG(); $this->answer($this->input['callback_id'], 'обмен между пользователями ' . ($c[$this->getInstanceWG(1) . 'exchange'] ? 'заблокирован' : 'разблокирован'), true); $this->menu('wg', $page); } public function blinkmenuswitch() { $c = $this->getPacConf(); $c['blinkmenu'] = $c['blinkmenu'] ? 0 : 1; $this->setPacConf($c); $this->menu('config'); } public function sendQr($name, $code, $title = false) { $qr = preg_replace(['~\s+~', '~\(~', '~\)~'], ['_'], $name); $qr_file = __DIR__ . "/qr/$qr.png"; exec("qrencode -t png -o $qr_file '$code'"); $r = $this->sendPhoto( $this->input['chat'], curl_file_create($qr_file), $title ?: $name ); unlink($qr_file); } public function qrPeer($client) { $cl = $client; $client = $this->readClients()[$client]; $name = $this->getName($client['interface']); if ($this->getWGType() == 'awg') { $this->sendQr($name, preg_replace('/^vpn:\/\//', '', $this->getAmneziaShortLink($client)), "$name for AmneziaVPN"); $this->sendQr($name, $this->createConfig($client), "$name for AmneziaWG"); } else { $this->sendQr($name, $this->createConfig($client), "$name for Wireguard"); } if ($this->getPacConf()['blinkmenu']) { $this->delete($this->input['chat'], $this->input['message_id']); $this->input['message_id'] = $this->send($this->input['chat'], '.')['result']['message_id']; $this->menu('client', "{$cl}_0"); } } public function qrSS() { $conf = $this->getPacConf(); $ip = $this->ip; $domain = $this->getDomain(); $scheme = empty($ssl = $this->nginxGetTypeCert()) ? 'http' : 'https'; $ss = $this->getSSConfig(); $port = !empty($ss['plugin']) ? (!empty($ssl) ? 443 : 80) : getenv('SSPORT'); $ss_link = preg_replace('~==~', '', 'ss://' . base64_encode("{$ss['method']}:{$ss['password']}")) . "@$domain:$port" . (!empty($ss['plugin']) ? '?plugin=' . urlencode("v2ray-plugin;path=/v2ray;host=$domain" . (!empty($ssl) ? ';tls' : '')) : ''); $qr_file = __DIR__ . "/qr/shadowsocks.png"; exec("qrencode -t png -o $qr_file '$ss_link'"); $r = $this->sendPhoto( $this->input['chat'], curl_file_create($qr_file), "$ss_link" ); unlink($qr_file); if ($this->getPacConf()['blinkmenu']) { $this->delete($this->input['chat'], $this->input['message_id']); $this->input['message_id'] = $this->send($this->input['chat'], '.')['result']['message_id']; $this->menu('ss'); } } public function qrXray($i, $s = false) { $link = $this->linkXray($i, $s); $qr_file = __DIR__ . "/qr/xray.png"; exec("qrencode -t png -o $qr_file '$link'"); $r = $this->sendPhoto( $this->input['chat'], curl_file_create($qr_file), "$link" ); unlink($qr_file); if ($this->getPacConf()['blinkmenu']) { $this->delete($this->input['chat'], $this->input['message_id']); $this->input['message_id'] = $this->send($this->input['chat'], '.')['result']['message_id']; $this->xray(); } } public function qrMtproto() { $link = $this->linkMtproto(); $qr_file = __DIR__ . "/qr/mtproto.png"; exec("qrencode -t png -o $qr_file '$link'"); $r = $this->sendPhoto( $this->input['chat'], curl_file_create($qr_file), "$link" ); unlink($qr_file); if ($this->getPacConf()['blinkmenu']) { $this->delete($this->input['chat'], $this->input['message_id']); $this->input['message_id'] = $this->send($this->input['chat'], '.')['result']['message_id']; $this->mtproto(); } } public function upload($name, $code, $chat = false) { $path = "/logs/$name"; file_put_contents($path, $code); $r = $this->sendFile( $chat ?: $this->input['chat'], curl_file_create($path), ); unlink($path); return $r; } public function proxy() { $proxy = trim($this->ssh("getent hosts proxy | awk '{ print $1 }'")); $this->createPeer("$proxy/32", 'proxy'); } public function addSubnets($page = 0) { $this->createPeer(implode(',', $this->getPacConf()['subnets']), 'list'); } public function change_server_ip($ip) { $conf = $this->readConfig(); $conf['interface']['Address'] = $ip; $this->restartWG($this->createConfig($conf)); } public function reply() { if (!empty($_SESSION['reply'][$this->input['reply']])) { $this->delete($this->input['chat'], $this->input['reply']); $this->delete($this->input['chat'], $this->input['message_id']); $callback = $_SESSION['reply'][$this->input['reply']]['callback']; $this->input['message_id'] = $this->input['callback_id'] = $_SESSION['reply'][$this->input['reply']]['start_message']; $this->{$callback}($this->input['message'], ...$_SESSION['reply'][$this->input['reply']]['args']); $this->answer($_SESSION['reply'][$this->input['reply']]['start_message']); unset($_SESSION['reply'][$this->input['reply']]); } } public function addNipdomain() { $this->addDomain(str_replace('.', '-', $this->ip) . '.nip.io'); } public function addDomain($domain, $nomenu = false) { $domain = trim($domain); if (!empty($domain)) { $conf = $this->getPacConf(); $conf['domain'] = idn_to_ascii($domain); $this->setPacConf($conf); $this->chocdomain($domain); $this->setUpstreamDomainOcserv($domain); $this->setUpstreamDomainNaive($domain); $this->cloakNginx(); } if (empty($nomenu)) { sleep(3); $this->menu('config'); } } public function sslip() { require __DIR__ . '/config.php'; $p = $this->getPacConf(); $ip = getenv('IP'); $r = $this->send($c['admin'][0], "start $ip"); $this->input['chat'] = $c['admin'][0]; $this->input['message_id'] = $r['result']['message_id']; $this->input['callback_id'] = false; if (empty($p)) { $this->addDomain(str_replace('.', '-', $this->ip) . '.nip.io', 1); $this->setSSL('letsencrypt'); } $this->menu(); } public function comment($text, $tag) { $text = explode("\n", $text); foreach ($text as $k => $v) { if (preg_match("~##$tag~", $v)) { $text[$k] = "#-$tag"; continue; } $text[$k] = "#$v"; } return implode("\n", $text); } public function uncomment($text, $tag) { $text = explode("\n", $text); foreach ($text as $k => $v) { if (preg_match("~#-$tag~", $v)) { $text[$k] = "##$tag"; continue; } $text[$k] = preg_replace('~#~', '', $v, 1); } return implode("\n", $text); } public function deleteSSL($notmenu = false) { unlink('/certs/cert_private'); unlink('/certs/cert_public'); $conf = $this->getPacConf(); unset($conf['letsencrypt']); $this->setPacConf($conf); $this->adguardSync(); $this->cloakNginx(); if (!$notmenu) { $this->menu('config'); } } public function updateUnitInitConfig() { $unit = $this->controlUnit('config'); file_put_contents('/config/unit.json', $unit); } public function setSSL($name) { $conf = $this->getPacConf(); switch ($name) { case 'letsencrypt': $out[] = 'Install certificate:'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $adguardClient = $conf['adguardkey'] ? "-d {$conf['adguardkey']}.{$conf['domain']}" : ''; $oc = $this->getHashSubdomain('oc'); $np = $this->getHashSubdomain('np'); exec("certbot certonly --force-renew --preferred-chain 'ISRG Root X1' -n --agree-tos --email mail@{$conf['domain']} -d {$conf['domain']} -d $oc.{$conf['domain']} -d $np.{$conf['domain']} $adguardClient --webroot -w /certs/ --logs-dir /logs --max-log-backups 0 2>&1", $out, $code); if ($code > 0) { $this->send($this->input['chat'], "ERROR\n" . implode("\n", $out)); break; } $out[] = 'Generate bundle'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $bundle = file_get_contents("/etc/letsencrypt/live/{$conf['domain']}/privkey.pem") . file_get_contents("/etc/letsencrypt/live/{$conf['domain']}/fullchain.pem"); $conf['letsencrypt'] = 'letsencrypt'; break; case 'self': $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); $bundle = file_get_contents($this->file . $r['result']['file_path']); $conf['letsencrypt'] = 'self'; break; } if (preg_match('~[^\s]+BEGIN PRIVATE KEY.+?END PRIVATE KEY[^\s]+~s', $bundle, $m)) { $this->setPacConf($conf); file_put_contents('/certs/cert_private', $m[0]); file_put_contents('/certs/cert_public', preg_replace('~[^\s]+BEGIN PRIVATE KEY.+?END PRIVATE KEY[^\s]+~s', '', $bundle)); $this->adguardSync(); $this->cloakNginx(); } else { $this->update($this->input['chat'], $this->input['message_id'], "wrong format key"); } sleep(3); $this->menu('config'); } public function controlUnit($url, $method = 'GET', $json = false, $bundle = false) { $ch = curl_init(); $opt = [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_URL => "http://localhost/$url", CURLOPT_RETURNTRANSFER => 1, CURLOPT_UNIX_SOCKET_PATH => '/var/run/control.unit.sock', CURLOPT_TIMEOUT => 10, ]; if ($json) { $opt[CURLOPT_POSTFIELDS] = $json; } if ($bundle) { $opt[CURLOPT_POSTFIELDS] = ['file' => new CURLStringFile($bundle, 'bundle.pem', 'text/plain')]; } curl_setopt_array($ch, $opt); $r = curl_exec($ch); curl_close($ch); return $r ?: 'lost connect to unit'; } public function delDomain() { $this->deleteSSL(1); $conf = $this->getPacConf(); unset($conf['domain']); $this->setPacConf($conf); $this->setUpstreamDomainOcserv(''); $this->chocdomain(''); $this->adguardSync(); $this->cloakNginx(); $this->menu('config'); } public function addips() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} list subnets separated by commas", $this->input['message_id'], reply: 'list subnets separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'createPeer', 'args' => ['subnet'], ]; } public function getPacConf() { return json_decode(file_get_contents($this->pac), true); } public function setPacConf(array $conf) { return file_put_contents($this->pac, json_encode($conf, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } public function domain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter domain", $this->input['message_id'], reply: 'enter domain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addDomain', 'args' => [], ]; } public function selfssl() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send file with your certificate chain and private key cat key.pem ca.pem cert.pem", $this->input['message_id'], reply: 'send file with your certificate chain and private key', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'selfsslInstall', 'args' => [], ]; } public function adguardSync() { $pac = $this->getPacConf(); $pac['adpswd'] = $pac['adpswd'] ?: substr(hash('md5', time()), 0, 10); $this->setPacConf($pac); $ssl = $this->nginxGetTypeCert(); $c = yaml_parse_file($this->adguard); $this->stopAd(); $c['users'][0]['password'] = password_hash($pac['adpswd'], PASSWORD_DEFAULT); if (!empty($ssl) && !empty($pac['domain']) && empty($c['tls']['enabled'])) { $c['tls']['enabled'] = true; $c['tls']['server_name'] = $pac['domain']; } yaml_emit_file($this->adguard, $c); $this->startAd(); } public function adguardpsswd() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter password", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'chpsswd', 'args' => [], ]; } public function setAdguardKey() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter key", $this->input['message_id'], reply: 'enter key', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setAdKey', 'args' => [], ]; } public function timerXr($k) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter time like https://www.php.net/manual/ru/function.strtotime.php:", $this->input['message_id'], reply: 'enter time like https://www.php.net/manual/ru/function.strtotime.php:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setTimerXr', 'args' => [$k], ]; } public function setAdKey($key) { $c = $this->getPacConf(); $c['adguardkey'] = $key; $this->setPacConf($c); $this->menu('adguard'); } public function enterAdmin() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter id", $this->input['message_id'], reply: 'enter id', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addAdmin', 'args' => [], ]; } public function addSubdomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter subdomain", $this->input['message_id'], reply: 'enter subdomain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setSubdomain', 'args' => [], ]; } public function setIpLimit() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter seconds and count ip", $this->input['message_id'], reply: 'enter seconds:count_ip', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'switchIpLimit', 'args' => [], ]; } public function addLinkDomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter domain for link", $this->input['message_id'], reply: 'enter domain for link', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setLinkDomain', 'args' => [], ]; } public function setLinkDomain($text) { $c = $this->getPacConf(); if (empty($text)) { unset($c['linkdomain']); } else { $c['linkdomain'] = trim($text); } $this->setPacConf($c); $this->xray(); } public function setSubdomain($text) { $c = $this->getPacConf(); if (empty($text)) { unset($c['subdomain']); } else { $c['subdomain'] = array_filter(explode(',', $text), fn($e) => !empty(trim($e))); } $this->setPacConf($c); $this->menu('config'); } public function enterPage() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter limit on page", $this->input['message_id'], reply: 'enter limit on page', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setPage', 'args' => [], ]; } public function setPage($text) { $c = $this->getPacConf(); $c['limitpage'] = (int) $text; $this->setPacConf($c); $this->menu('config'); } public function addAdmin($id) { $file = __DIR__ . '/config.php'; require $file; $c['admin'][] = $id; file_put_contents($file, "menu('config'); } public function delAdmin($id) { $file = __DIR__ . '/config.php'; require $file; unset($c['admin'][array_search($id, $c['admin'])]); file_put_contents($file, "menu('config'); } public function chpsswd($pass) { $out[] = 'Restart Adguard Home'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $out[] = $this->stopAd(); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $c = yaml_parse_file($this->adguard); $c['users'][0]['password'] = password_hash($pass, PASSWORD_DEFAULT); yaml_emit_file($this->adguard, $c); $p = $this->getPacConf(); $p['adpswd'] = $pass; $this->setPacConf($p); $out[] = $this->startAd(); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); sleep(3); $this->menu('adguard'); } public function adguardreset() { $out[] = 'Restart Adguard Home'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); exec('git -C / checkout config/AdGuardHome.yaml'); $this->adguardSync(); $this->cloakNginx(); sleep(3); $this->menu('adguard'); } public function guidv4($data = null) { // Generate 16 bytes (128 bits) of random data or use the data passed into the function. $data = $data ?? random_bytes(16); assert(strlen($data) == 16); // Set version to 0100 $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // Set bits 6-7 to 10 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // Output the 36 character UUID. return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } public function adguardXrayClients() { $xr = $this->getXray(); $ad = yaml_parse_file($this->adguard); foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) { $tmp[] = [ 'safe_search' => [ 'enabled' => true, 'bing' => true, 'duckduckgo' => true, 'google' => true, 'pixabay' => true, 'yandex' => true, 'youtube' => true, ], 'blocked_services' => [ 'schedule' => ['time_zone' => date_default_timezone_get()], 'ids' => [], ], 'name' => $v['email'], 'ids' => [$v['id']], 'tags' => [], 'upstreams' => [], 'uid' => $v['id'], 'upstreams_cache_size' => 0, 'upstreams_cache_enabled' => false, 'use_global_settings' => true, 'filtering_enabled' => false, 'parental_enabled' => false, 'safebrowsing_enabled' => false, 'use_global_blocked_services' => true, 'ignore_querylog' => false, 'ignore_statistics' => false, ]; } $ad['clients']['persistent'] = $tmp; yaml_emit_file($this->adguard, $ad); $this->stopAd(); $this->startAd(); } public function checkdns() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter dns address Plain DNS: example.org 94.140.14.14 DNS-over-TLS: example.org tls://dns.adguard.com DNS-over-TLS with IP: example.org tls://dns.adguard.com 94.140.14.14 DNS-over-HTTPS with HTTP/2: example.org https://dns.adguard.com/dns-query DNS-over-HTTPS forcing HTTP/3 only: example.org h3://dns.google/dns-query DNS-over-HTTPS with IP: example.org https://dns.adguard.com/dns-query 94.140.14.14", $this->input['message_id'], reply: 'enter command', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'dnscheck', 'args' => [], ]; } public function dnscheck($dns) { exec("JSON=1 dnslookup $dns", $out, $code); if ($code) { $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out), mode: false); } else { $this->send($this->input['chat'], "JSON=1 dnslookup $dns\n" . implode("\n", $out), mode: false); } sleep(3); $this->menu('adguard'); } public function addupstream() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter address upstream", $this->input['message_id'], reply: 'enter address upstream', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'upstream', 'args' => [], ]; } public function upstream($url) { $out[] = 'Restart Adguard Home'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $out[] = $this->stopAd(); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $c = yaml_parse_file($this->adguard); $c['dns']['upstream_dns'][] = $url; yaml_emit_file($this->adguard, $c); $out[] = $this->startAd(); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); sleep(3); $this->menu('adguard'); } public function delupstream($k) { $out[] = 'Restart Adguard Home'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->stopAd(); $c = yaml_parse_file($this->adguard); unset($c['dns']['upstream_dns'][$k]); $c['dns']['upstream_dns'] = array_values($c['dns']['upstream_dns']); yaml_emit_file($this->adguard, $c); $this->startAd(); $this->menu('adguard'); } public function startAd() { return $this->ssh('/opt/adguardhome/AdGuardHome --no-check-update --pidfile /opt/adguardhome/pid -c /config/AdGuardHome.yaml -h 0.0.0.0 -w /opt/adguardhome/work > /dev/null 2>&1 &', 'ad', false); } public function stopAd() { return $this->ssh('kill -15 $(cat /opt/adguardhome/pid)', 'ad'); } public function selfsslInstall() { $this->setSSL('self'); } public function include($type) { switch ($type) { case 'rulessetlist': $r = $this->send( $this->input['chat'], "@{$this->input['username']} outbound[:behavior]:time:URL", $this->input['message_id'], reply: 'outbound[:behavior]:time:URL', ); break; default: $r = $this->send( $this->input['chat'], "@{$this->input['username']} list separated by commas", $this->input['message_id'], reply: 'list separated by commas', ); break; } $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addInclude', 'args' => [$type], ]; } public function addInclude(string $domains, $type) { if ($type == 'rulessetlist' && !preg_match('~^.+:.+:https?://.+~', $domains)) { $this->send($this->input['from'], 'wrong pattern, enter [direct|block|proxy|custom outbound]:time:URL'); return; } $domains = explode(',', $domains); $domains = array_filter($domains, fn($x) => !empty(trim($x))); if (!empty($domains)) { $conf = $this->getPacConf(); foreach ($domains as $k => $v) { if (in_array($type, ['white', 'deny'])) { $conf[$type][] = $v; } else { $conf[$type][in_array($type, ['rulessetlist', 'packagelist', 'processlist']) ? trim($v) : idn_to_ascii(trim($v))] = true; } } ksort($conf[$type]); $this->setPacConf($conf); $page = (int) floor(array_search($v, array_keys($conf[$type])) / $this->limit); } $page = $page ?: -2; $this->backXtlsList($type); } public function backXtlsList($type) { switch ($type) { case 'includelist': $this->pacUpdate($_SESSION['proxylistentry']); if (!empty($_SESSION['proxylistentry'])) { $this->xtlsproxy(); } break; case 'blocklist': $this->xrayUpdateRules(); $this->xtlsblock(); break; case 'warplist': $this->xrayUpdateRules(); $this->xtlswarp(); break; case 'processlist': $this->xtlsprocess(); break; case 'packagelist': $this->xtlsapp(); break; case 'subnetlist': $this->xtlssubnet(); break; case 'rulessetlist': $this->xtlsrulesset(); break; case 'white': case 'deny': $this->syncDeny(); $this->denyList(0, $type == 'white' ? 1 : 0); break; } } public function xrayUpdateRules() { $c = $this->getPacConf(); $xr = $this->getXray(); $xr['outbounds'] = [ [ "protocol" => "freedom", "tag" => "direct", ], [ "protocol" => "blackhole", "tag" => "block", ], [ "protocol" => "socks", "tag" => "warp", "settings" => [ 'servers' => [ [ "address" => "10.10.0.13", "port" => 4000, ], ], ], ], ]; if (!empty($c['blocklist']) && !empty(array_filter($c['blocklist']))) { $domains = array_keys(array_filter($c['blocklist'], function ($v, $k){ if (!empty($v)) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)); if (!empty($domains)) { $rules[] = [ "type" => "field", "outboundTag" => "block", "domain" => array_keys(array_filter($c['blocklist'], function ($v, $k){ if (!empty($v)) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)), ]; } } if (!empty($c['blocklist']) && !empty(array_filter($c['blocklist']))) { $ips = array_keys(array_filter($c['blocklist'], function ($v, $k){ if (!empty($v)) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)); if (!empty($ips)) { $rules[] = [ "type" => "field", "outboundTag" => "block", "ip" => array_keys(array_filter($c['blocklist'], function ($v, $k){ if (!empty($v)) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)), ]; } } if (!empty($c['warplist']) && !empty(array_filter($c['warplist']))) { $domains = array_keys(array_filter($c['warplist'], function ($v, $k){ if (!empty($v)) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)); if (!empty($domains)) { $rules[] = [ "type" => "field", "outboundTag" => "warp", "domain" => array_keys(array_filter($c['warplist'], function ($v, $k){ if (!empty($v)) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)), ]; } } if (!empty($c['warplist']) && !empty(array_filter($c['warplist']))) { $ips = array_keys(array_filter($c['warplist'], function ($v, $k){ if (!empty($v)) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)); if (!empty($ips)) { $rules[] = [ "type" => "field", "outboundTag" => "warp", "ip" => array_keys(array_filter($c['warplist'], function ($v, $k){ if (!empty($v)) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $k)) { return true; } } return false; }, ARRAY_FILTER_USE_BOTH)), ]; } } $xr['routing']['rules'] = $rules ?: []; $this->restartXray($xr); } public function reverse(int $count) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} list domains separated by commas", $this->input['message_id'], reply: 'list domains separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addReverse', 'args' => [$count], ]; } public function addReverse(string $domains, int $count) { $domains = explode(',', $domains); $domains = array_filter($domains, fn($x) => !empty(trim($x))); if (!empty($domains)) { $conf = $this->getPacConf(); foreach ($domains as $k => $v) { $conf['reverselist'][idn_to_ascii(trim($v))] = true; } ksort($conf['reverselist']); $this->setPacConf($conf); $page = (int) floor(array_search($v, array_keys($conf['reverselist'])) / $count); } $page = $page ?: -2; $this->menu('reverselist', $page); } public function subzones(int $count) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} list subdomains separated by commas", $this->input['message_id'], reply: 'list subdomains separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addSubzones', 'args' => [$count], ]; } public function addSubzones(string $domains, int $count) { $domains = explode(',', $domains); $domains = array_filter($domains, fn($x) => !empty(trim($x))); if (!empty($domains)) { $conf = $this->getPacConf(); foreach ($domains as $k => $v) { $conf['subzoneslist'][idn_to_ascii(trim($v))] = true; } ksort($conf['subzoneslist']); $this->setPacConf($conf); $page = (int) floor(array_search($v, array_keys($conf['subzoneslist'])) / $count); } $page = $page ?: -2; $this->menu('subzoneslist', $page); } public function exclude(int $count) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter regular expression", $this->input['message_id'], reply: 'enter regular expression', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'addExclude', 'args' => [$count], ]; } public function addExclude(string $reg, int $count) { $reg = trim($reg); if (!empty($reg)) { $conf = $this->getPacConf(); $conf['excludelist'][$reg] = true; ksort($conf['excludelist']); $this->setPacConf($conf); $page = (int) floor(array_search($reg, array_keys($conf['excludelist'])) / $count); } $page = $page ?: -2; $this->menu('excludelist', $page); } public function showreset() { $data = [ [ [ 'text' => "confirm", 'callback_data' => "/reset", ], [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ], ]; $this->update( $this->input['chat'], $this->input['message_id'], "Reset settings?", $data, ); } public function reset() { $conf = $this->readConfig(); $address = getenv('ADDRESS'); $port = getenv('WGPORT'); $r = $this->ssh("/bin/sh /reset_wg.sh $address $port"); file_put_contents($this->clients, ''); $this->menu(); } public function addPeer() { $this->createPeer(name: 'all'); } public function config() { $conf = $this->createConfig($this->readConfig()); $data = [ [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ], ]; $this->update( $this->input['chat'], $this->input['message_id'], "Server config:\n\n$conf", $data, ); } public function deletePeer($client, $page, $menu = true) { $conf = $this->readConfig(); $this->deleteClient($client); unset($conf['peers'][$client]); $this->restartWG($this->createConfig($conf)); if ($menu) { $this->menu('wg', $page); } } public function dnsPeer($client, $page) { $clients = $this->readClients(); $clients[$client]['interface']['DNS'] = '10.10.0.5'; $this->saveClients($clients); $this->menu('client', "{$client}_$page"); } public function deletednsPeer($client, $page) { $clients = $this->readClients(); unset($clients[$client]['interface']['DNS']); $this->saveClients($clients); $this->menu('client', "{$client}_$page"); } public function pad($text, $length, $symbol = ' ') { for ($i = 0; $i < $length; $i++) { $text .= $symbol; } return $text; } public function getTitleWG() { $c = $this->getPacConf(); return $this->i18n($c[$this->getInstanceWG(1) . 'amnezia'] ? 'amnezia' : 'wg_title') . ' ' . $c['wg_instance']; } public function statusWg(int $page = 0) { $c = $this->getPacConf(); $conf = $this->readConfig(); $status = $this->readStatus(); if (empty($status)) { return [ 'text' => "Menu -> " . $this->getTitleWG() . "\n\nerror status", 'data' => [[ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]], ]; } $clients = $this->getClients($page); $bt = $c[$this->getInstanceWG(1) . 'blocktorrent']; $ex = $c[$this->getInstanceWG(1) . 'exchange']; $dns = $c[$this->getInstanceWG(1) . 'dns']; $mtu = $c[$this->getInstanceWG(1) . 'mtu'] ?: $this->mtu; $am = $c[$this->getInstanceWG(1) . 'amnezia']; $end = $c[$this->getInstanceWG(1) . 'endpoint']; $data = [ [ [ 'text' => $this->i18n($am ? 'on' : 'off') . " amnezia", 'callback_data' => "/switchAmnezia $page", ], ], [ [ 'text' => $this->i18n(!$bt ? 'on' : 'off') . " {$this->i18n('torrent')} ", 'callback_data' => "/switchTorrent $page", ], [ 'text' => $this->i18n(!$ex ? 'on' : 'off') . " {$this->i18n('exchange')} ", 'callback_data' => "/switchExchange $page", ], [ 'text' => $this->i18n('listSubnet'), 'callback_data' => "/subnet $page", ], ], [ [ 'text' => $this->i18n('defaultDNS') . ': ' . ($dns ?: $this->dns), 'callback_data' => "/defaultDNS $page", ], [ 'text' => $this->i18n('defaultMTU') . ': ' . $mtu, 'callback_data' => "/defaultMTU $page", ], ], [ [ 'text' => $this->i18n('endpoint') . ': ' . ($end ? $this->ip : $this->getDomain()), 'callback_data' => "/switchEndpoint $page", ], ], [ [ 'text' => $this->i18n('add peer'), 'callback_data' => "/menu addpeer $page", ], ], ]; if ($clients) { $data = array_merge($data, $clients); } if (!empty($conf['peers'])) { $all = (int) ceil(count($conf['peers']) / $this->limit); $page = min($page, $all - 1); $page = $page == -2 ? $all - 1 : $page; $conf['peers'] = array_slice($conf['peers'], $page * $this->limit, $this->limit, true); foreach ($conf['peers'] as $k => $v) { if (!empty($v['# PublicKey'])) { $conf['peers'][$k]['online'] = 'off'; } else { $conf['peers'][$k]['status'] = $status ? $this->getStatusPeer($v['PublicKey'], $status['peers']) : 'error'; $conf['peers'][$k]['online'] = preg_match('~^(\d+ seconds|[12] minute)~', $conf['peers'][$k]['status']['latest handshake']) ? 'online' : ''; } } foreach ($conf['peers'] as $k => $v) { if (empty($v['# PublicKey'])) { preg_match_all('~([0-9.]+\.?)\s(\w+)~', $v['status']['transfer'], $m); $tr = $m[0] ? ceil($m[1][1]) . '↓' . substr($m[2][1], 0, 1) . '/' . ceil($m[1][0]) . '↑' . substr($m[2][0], 0, 1) : ''; } else { $tr = ''; } $t = [ 'name' => $this->getName($v), 'time' => $this->getTime(strtotime($v['## time'])), 'status' => $v['online'] == 'off' ? '🚷' : $this->i18n($v['online'] ? 'on' : 'off'), 'traffic' => $tr, ]; $pad = [ 'name' => max(mb_strlen($t['name']), $pad['name']), 'time' => max($t['time'] == '♾' ? 4 : mb_strlen($t['time']), $pad['time']), 'status' => max(mb_strlen($t['status']), $pad['status']), 'traffic' => max(mb_strlen($t['traffic']), $pad['traffic']), ]; $peers[] = $t; } foreach ($peers as $k => $v) { $text[] = implode('', [ $this->pad($v['name'], $pad['name'] - mb_strlen($v['name'])), $this->pad(" {$v['time']}", $pad['time'] - mb_strlen($v['time'])), $this->pad($v['status'], $pad['status'] - mb_strlen($v['status'])), $this->pad(" {$v['traffic']}", $pad['traffic'] - mb_strlen($v['traffic'])), ]); } } $text = "Menu -> " . $this->getTitleWG() . "\n\n" . implode(PHP_EOL, $text ?: []) . ''; $data[] = [ [ 'text' => $this->i18n('update status'), 'callback_data' => "/menu wg $page", ], [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ] ]; return [ 'text' => $text, 'data' => $data, ]; } public function defaultDNS($page = 0) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter dns separated by commas", $this->input['message_id'], reply: 'enter dns separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setDNS', 'args' => [$page], ]; } public function defaultMTU($page = 0) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter MTU", $this->input['message_id'], reply: 'enter MTU', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setMTU', 'args' => [$page], ]; } public function changeMTU($client, $page = 0) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter MTU", $this->input['message_id'], reply: 'enter MTU', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'changeClientMTU', 'args' => [$client, $page], ]; } public function setDNS($text, $page = 0) { $c = $this->getPacConf(); if ($text) { $c[$this->getInstanceWG(1) . 'dns'] = $text; } else { unset($c[$this->getInstanceWG(1) . 'dns']); } $this->setPacConf($c); $this->menu('wg', $page); } public function setMTU($text, $page = 0) { $c = $this->getPacConf(); if ($text) { $c[$this->getInstanceWG(1) . 'mtu'] = $text; } else { unset($c[$this->getInstanceWG(1) . 'mtu']); } $this->setPacConf($c); $this->menu('wg', $page); } public function changeClientMTU($text, $client, $page = 0) { $clients = $this->readClients(); if (!empty((int) $text)) { $clients[$client]['interface']['MTU'] = $text; } else { unset($clients[$client]['interface']['MTU']); } $this->saveClients($clients); $this->menu('client', "{$client}_$page"); } public function subnetAdd($wgpage, $page, $openconnect) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter subnet separated by commas", $this->input['message_id'], reply: 'enter subnet separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'subnetSave', 'args' => [$wgpage, $page, $openconnect], ]; } public function subnetSave($text, $wgpage, $page, $openconnect) { $c = $this->getPacConf(); $subnets = explode(',', $text); if ($subnets) { $c['subnets'] = array_merge($c['subnets'] ?: [], array_filter(array_map(fn ($e) => trim($e), $subnets))); $this->setPacConf($c); $page = floor(count($c['subnets']) / $this->limit); } if (!empty($openconnect)) { $this->ocservRoute(); } $this->subnet($wgpage, $page, $openconnect); } public function subnetDelete($wgpage, $k, $page = 0, $openconnect = 0) { $c = $this->getPacConf(); unset($c['subnets'][$k]); $this->setPacConf($c); if (!empty($openconnect)) { $this->ocservRoute(); } $this->subnet($wgpage, $page, $openconnect); } public function ocservRoute() { $p = $this->getPacConf(); $c = file_get_contents('/config/ocserv.conf'); $t = preg_replace('~^route[^\n]+~sm', '', $c); if (!empty($p['subnets'])) { foreach ($p['subnets'] as $v) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}~', $v)) { $t .= "route = $v"; $flag = true; } } if (empty($flag)) { $t .= 'route = default'; } } else { $t .= 'route = default'; } $this->restartOcserv($t); } public function calc() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter like '10.0.0.0/24, -10.0.0.5/32'", $this->input['message_id'], reply: 'enter like \'10.0.0.0/24, -10.0.0.5/32\'', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'calcSubnet', 'args' => [], ]; } public function calcSubnet($text) { $text = explode(',', $text); $text = array_map(fn ($e) => trim($e), $text); if (!empty($text)) { foreach ($text as $k => $v) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}~', $v)) { $include[] = $v; } if (preg_match('~^-(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2})~', $v, $m)) { $exclude[] = $m[1]; } } } if (!empty($include)) { foreach ($include as $k => $v) { $t = explode('/', $v); $include[$k] = [ip2long($t[0]), ip2long($t[0]) + (1 << (32 - $t[1])) - 1]; } if (!empty($exclude)) { foreach ($exclude as $k => $v) { $t = explode('/', $v); $exclude[$k] = [ip2long($t[0]), ip2long($t[0]) + (1 << (32 - $t[1])) - 1]; } } $c = new Calc(); $r = $c->prepare($include, $exclude ?: []); if (!empty($r)) { $t = []; foreach ($r as $k => $v) { $t = array_merge($t, $c->toCIDR($v[0], $v[1])); } $this->send($this->input['chat'], '
' . implode(', ', $t) . '
'); } } } public function subnet($wgpage = 0, $page = 0, $openconnect = 0) { $count = $this->limit; $text = 'Menu -> ' . ($openconnect ? 'Openconnect' : 'Wireguard') . ' -> ' . $this->i18n('listSubnet') . "\n"; $data[] = [ [ 'text' => $this->i18n('calc'), 'callback_data' => "/calc", ], ]; $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/subnetAdd {$wgpage}_{$page}_$openconnect", ], ]; $subnets = $this->getPacConf()['subnets']; if (!empty($subnets)) { $all = (int) ceil(count($subnets) / $count); $page = min($page, $all - 1); $page = $page == -2 ? $all - 1 : $page; $subnets = $page != -1 ? array_slice($subnets, $page * $count, $count, true) : $subnets; foreach ($subnets as $k => $v) { $data[] = [ [ 'text' => $this->i18n('delete') . " $v", 'callback_data' => "/subnetDelete {$wgpage}_{$k}_{$page}_$openconnect", ], ]; } if ($page != -1 && $all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/subnet {$wgpage}_" . ($page - 1 >= 0 ? $page - 1 : $all - 1) . ($openconnect ? '_1' : ''), ], [ 'text' => $page + 1, 'callback_data' => "/subnet {$wgpage}_$page" . ($openconnect ? '_1' : ''), ], [ 'text' => '>>', 'callback_data' => "/subnet {$wgpage}_" . ($page < $all - 1 ? $page + 1 : 0) . ($openconnect ? '_1' : ''), ], ]; } } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => $openconnect ? '/menu oc' : "/menu wg $wgpage", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function changeAllowedIps($k, $page = 0) { $clients = $this->readClients(); $name = $this->getName($clients[$k]['interface']); $text = "Menu -> Wireguard -> $name -> Change AllowedIPs\n\n"; $data[] = [ [ 'text' => $this->i18n('all traffic'), 'callback_data' => "/changeIps all_{$k}_$page", ] ]; $data[] = [ [ 'text' => $this->i18n('subnet'), 'callback_data' => "/changeIps subnet_{$k}_$page", ] ]; if ($this->getPacConf()['subnets']) { $data[] = [ [ 'text' => $this->i18n('listSubnet'), 'callback_data' => "/changeIps list_{$k}_$page", ] ]; } $data[] = [ [ 'text' => $this->i18n('proxy ip'), 'callback_data' => "/changeIps proxy_{$k}_$page", ] ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu client {$k}_$page", ] ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function changeIps($type, $k, $page = 0) { switch ($type) { case 'all': $this->setIps('0.0.0.0/0', $k, $page); break; case 'subnet': $r = $this->send( $this->input['chat'], "@{$this->input['username']} list subnets separated by commas", $this->input['message_id'], reply: 'list subnets separated by commas', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setIps', 'args' => [$k, $page], ]; break; case 'list': $this->setIps(implode(',', $this->getPacConf()['subnets']), $k, $page); break; case 'proxy': $this->setIps(trim($this->ssh("getent hosts proxy | awk '{ print $1 }'")) . '/32', $k, $page); break; } } public function setIps($ips, $k, $page = 0) { $clients = $this->readClients(); $clients[$k]['peers'][0]['AllowedIPs'] = $ips; $this->saveClients($clients); $this->menu('client', "{$k}_$page"); } public function getAmneziaShortLink($client) { $dns = explode(',', $client['interface']['DNS']); $c = json_encode([ "containers" => [ [ "awg" => [ "isThirdPartyConfig" => True, "last_config" => json_encode([ "H1" => "{$client['interface']['H1']}", "H2" => "{$client['interface']['H2']}", "H3" => "{$client['interface']['H3']}", "H4" => "{$client['interface']['H4']}", "Jc" => "{$client['interface']['Jc']}", "Jmax" => "{$client['interface']['Jmax']}", "Jmin" => "{$client['interface']['Jmin']}", "S1" => "{$client['interface']['S1']}", "S2" => "{$client['interface']['S2']}", "client_ip" => explode('/', $client['interface']['Address'])[0], "client_priv_key" => $client['interface']['PrivateKey'], "client_pub_key" => "0", "config" => $this->createConfig($client), "hostName" => $this->ip, "port" => (int) getenv('WG1PORT'), "psk_key" => $client['peers'][0]['PresharedKey'], "server_pub_key" => $client['peers'][0]['PublicKey'] ]), "port" => (int) getenv('WG1PORT'), "transport_proto" => "udp" ], "container" => "amnezia-awg" ] ], "defaultContainer" => "amnezia-awg", "description" => $client['interface']['## name'], "dns1" => $dns[0], "dns2" => $dns[1] ?: '', "hostName" => $this->ip ]); exec("echo '$c' | python amnezia.py", $o); return $o[0]; } public function getClient($client, $page) { $clients = $this->readClients(); if ($clients) { $name = $this->getName($clients[$client]['interface']); $conf = $this->createConfig($clients[$client]); if ($this->getWGType() == 'awg') { $sl = $this->getAmneziaShortLink($clients[$client]); } return [ 'text' => "
$conf
\n\n$sl\n\n$name ({$this->getTitleWG()})", 'data' => [ [ [ 'text' => $this->i18n('rename'), 'callback_data' => "/rename {$client}_$page", ], [ 'text' => $this->i18n('timer'), 'callback_data' => "/timer {$client}_$page", ], ], [ [ 'text' => $this->i18n('show QR'), 'callback_data' => "/qr $client", ], [ 'text' => $this->i18n('download config'), 'callback_data' => "/download $client", ], ], [ [ 'text' => $this->i18n($clients[$client]['# off'] ? 'off' : 'on'), 'callback_data' => "/switchClient {$client}_$page", ], [ 'text' => $this->i18n($clients[$client]['interface']['DNS'] ? 'delete internal dns' : 'set internal dns'), 'callback_data' => "/" . ($clients[$client]['interface']['DNS'] ? 'delete' : '') . "dns {$client}_$page", ], ], [ [ 'text' => $this->i18n('AllowedIPs'), 'callback_data' => "/changeAllowedIps {$client}_$page", ], ], [ [ 'text' => $this->i18n('MTU') . " " . ($clients[$client]['interface']['MTU'] ?: $this->getPacConf()[$this->getInstanceWG(1) . 'mtu'] ?: $this->mtu), 'callback_data' => "/changeMTU {$client}_$page", ], ], [ [ 'text' => $this->i18n('delete'), 'callback_data' => "/delete {$client}_$page", ], [ 'text' => $this->i18n('back'), 'callback_data' => "/menu wg $page", ], ], ], ]; } return [ 'text' => "no clients", 'data' => false ]; } public function getClients(int $page, int $count = 5) { $count = $this->limit; $clients = $this->readClients(); if (!empty($clients)) { $all = (int) ceil(count($clients) / $count); $page = min($page, $all - 1); $page = $page == -2 ? $all - 1 : $page; $clients = $page != -1 ? array_slice($clients, $page * $count, $count, true) : $clients; foreach ($clients as $k => $v) { $data[] = [[ 'text' => $this->getName($v['interface']), 'callback_data' => "/menu client {$k}_$page", ]]; } if ($page != -1 && $all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/menu wg " . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ 'text' => $page + 1, 'callback_data' => "/menu wg $page", ], [ 'text' => '>>', 'callback_data' => "/menu wg " . ($page < $all - 1 ? $page + 1 : 0), ] ]; } } return $data; } public function sizeFormat($bytes) { if (floor($bytes / 1024 ** 2) > 0) { $r = round($bytes / 1024 ** 2, 2) . 'MB'; } elseif (floor($bytes / 1024) > 0) { $r = round($bytes / 1024, 2) . 'KB'; } else { $r = $bytes . 'B'; } return $r; } public function addCommunityFilter() { $pac = $this->getPacConf(); $l = array_filter(array_map(fn($e) => trim($e), explode("\n", file_get_contents('https://community.antifilter.download/list/domains.lst')))); if (!empty($l)) { foreach ($l as $k => $v) { $pac['includelist'][$v] = true; } } $this->setPacConf($pac); $this->pacUpdate(); } public function addLegizFilter() { $pac = $this->getPacConf(); $l = array_filter(array_map(fn($e) => trim($e), explode("\n", file_get_contents('https://github.com/legiz-ru/sb-rule-sets/raw/main/ru-bundle.lst')))); if (!empty($l)) { foreach ($l as $k => $v) { $pac['includelist'][$v] = true; } } $this->setPacConf($pac); $this->pacUpdate(); } public function pacMenu($page = 0) { unset($_SESSION['proxylistentry']); $rmpac = stat(__DIR__ . '/zapretlists/rmpac'); $rpac = stat(__DIR__ . '/zapretlists/rpac'); $mpac = stat(__DIR__ . '/zapretlists/mpac'); $pac = stat(__DIR__ . '/zapretlists/pac'); $conf = $this->getPacConf(); $ip = $this->getDomain(); $hash = $this->getHashBot(); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $text = << pac text; if ($pac) { $pac['time'] = date('d.m.Y H:i:s', $pac['mtime']); $pac['sz'] = $this->sizeFormat($pac['size']); $text .= <<PAC ({$pac['time']} / {$pac['sz']}): $scheme://$ip/pac$hash?a=127.0.0.1&p=1080 text; $urls[0][] = [ 'text' => "PAC", 'web_app' => ['url' => "https://$ip/pac$hash&a=127.0.0.1&p=1080"], ]; } if ($mpac) { $mpac['time'] = date('d.m.Y H:i:s', $mpac['mtime']); $mpac['sz'] = $this->sizeFormat($mpac['size']); $text .= <<Shadowsocks-android PAC ({$mpac['time']} / {$mpac['sz']}): $scheme://$ip/pac$hash&t=mpac text; $urls[0][] = [ 'text' => "PAC ShadowSocks(Android)", 'web_app' => ['url' => "https://$ip/pac$hash&t=mpac"], ]; } if ($rpac) { $rpac['time'] = date('d.m.Y H:i:s', $rpac['mtime']); $rpac['sz'] = $this->sizeFormat($rpac['size']); $text .= <<Reverse PAC ({$rpac['time']} / {$rpac['sz']}): $scheme://$ip/pac$hash&t=rpac&a=127.0.0.1&p=1080 text; $urls[0][] = [ 'text' => "Reverse PAC", 'url' => "$scheme://$ip/pac$hash&t=rpac", ]; $urls[1][] = [ 'text' => "Reverse PAC Wireguard proxy", 'url' => "$scheme://$ip/pac$hash&t=rpac&a=10.10.0.3", ]; } if ($rmpac) { $rmpac['time'] = date('d.m.Y H:i:s', $rmpac['mtime']); $rmpac['sz'] = $this->sizeFormat($rmpac['size']); $text .= <<Reverse shadowsocks-android PAC ({$rmpac['time']} / {$rmpac['sz']}): $scheme://$ip/pac$hash&t=rmpac text; $urls[2][] = [ 'text' => "Reverse PAC SS(Android)", 'url' => "$scheme://$ip/pac$hash&t=rmpac", ]; } if ($urls) { $data = $urls; } $data[] = [ [ 'text' => $this->i18n('add') . ' community antifilter', 'callback_data' => "/addCommunityFilter", ], ]; $data[] = [ [ 'text' => $this->i18n('add') . ' ru-bundle', 'callback_data' => "/addLegizFilter", ], ]; $data = array_merge($data, $this->listPac('includelist', $page, 'pacMenu')[0]); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function deleteYes($type) { $c = $this->getPacConf(); unset($c[$type]); $this->setPacConf($c); switch ($type) { case 'includelist': $this->pacUpdate(); break; case 'blocklist': $this->xtlsblock(); break; case 'warplist': $this->xtlswarp(); break; case 'packagelist': $this->xtlsapp(); break; case 'processlist': $this->xtlsprocess(); break; case 'rulessetlist': $this->xtlsrulesset(); break; } } public function deleteAll($type) { switch ($type) { case 'includelist': $dir = 'PAC'; break; case 'warplist': $dir = 'WARP'; break; case 'blocklist': $dir = 'BLOCK'; break; case 'packagelist': $dir = 'PACKAGE'; break; case 'processlist': $dir = 'PROCESS'; break; case 'subnetlist': $dir = 'SUBNET'; break; case 'rulessetlist': $dir = 'rulesset'; break; } $text = << $dir -> delete all text; $data[] = [ [ 'text' => $this->i18n('yes'), 'callback_data' => "/deleteYes $type", ], ]; switch ($type) { case 'includelist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/pacMenu 0", ], ]; break; case 'warplist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlswarp", ], ]; break; case 'blocklist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlsblock", ], ]; break; case 'packagelist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlsapp", ], ]; break; case 'processlist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlsprocess", ], ]; break; case 'subnetlist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlssubnet", ], ]; break; case 'rulessetlist': $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xtlsrulesset", ], ]; break; } $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function exportList($type) { $domains = $this->getPacConf()[$type]; if (!empty($domains)) { foreach ($domains as $k => $v) { $text .= "$k;$v\n"; } $this->sendFile( $this->input['chat'], new CURLStringFile($text, "$type.csv", 'application/csv'), to: $this->input['message_id'], ); } } public function xtlsblock($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> block list'; [$data] = $this->listPac('blocklist', $page, 'xtlsblock'); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function xtlswarp($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> warp list'; [$data] = $this->listPac('warplist', $page, 'xtlswarp'); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function xtlsproxy($page = 0) { $_SESSION['proxylistentry'] = 1; $p = $this->getPacConf(); $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> proxy list'; [$data] = $this->listPac('includelist', $page, 'xtlsproxy'); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function appOutbound() { $p = $this->getPacConf(); $p['app_outbound'] = !$p['app_outbound']; $p = $this->setPacConf($p); $this->xtlsapp(); } public function domainsOutbound() { $p = $this->getPacConf(); $p['domains_outbound'] = !$p['domains_outbound']; $p = $this->setPacConf($p); $this->xtlsproxy(); } public function finalOutbound() { $p = $this->getPacConf(); $p['final_outbound'] = !$p['final_outbound']; $p = $this->setPacConf($p); $this->routes(); } public function processOutbound() { $p = $this->getPacConf(); $p['process_outbound'] = !$p['process_outbound']; $p = $this->setPacConf($p); $this->xtlsprocess(); } public function xtlsapp($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> package list'; [$data] = $this->listPac('packagelist', $page, 'xtlsapp'); $p = $this->getPacConf(); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function xtlsprocess($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> process list'; [$data] = $this->listPac('processlist', $page, 'xtlsprocess'); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function xtlssubnet($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> subnet'; [$data] = $this->listPac('subnetlist', $page, 'xtlssubnet'); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function xtlsrulesset($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> ' . $this->i18n('routes') . ' -> rulesset list'; [$data, $tmp] = $this->listPac('rulessetlist', $page, 'xtlsrulesset', 1); $text = array_merge($text, $tmp ?: []); $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/routes", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function listPac($type, $page, $menu, $basename = false) { $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/include $type", ], ]; $domains = $this->getPacConf()[$type]; if (!empty($domains)) { $all = (int) ceil(count($domains) / $this->limit); $page = min($page, $all - 1); $page = $page < 0 ? $all - 1 : $page; $domains = array_slice($domains, $page * $this->limit, $this->limit, true); $i = 0; foreach ($domains as $k => $v) { if ($type == 'rulessetlist') { $text[] = "
$k
"; } $data[] = [ [ 'text' => $this->i18n($v ? 'on' : 'off') . ' ' . ($basename ? basename($k) . ' ' : '') . (in_array($type, ['rulessetlist', 'packagelist', 'processlist', 'subnetlist']) ? $k : idn_to_utf8($k)), 'callback_data' => "/change$type " . ($i + $page * $this->limit), ], [ 'text' => 'delete', 'callback_data' => "/delete$type " . ($i + $page * $this->limit), ], ]; $i++; } if ($all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/$menu " . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ 'text' => $page + 1, 'callback_data' => "/$menu $page", ], [ 'text' => '>>', 'callback_data' => "/$menu " . ($page < $all - 1 ? $page + 1 : 0), ], ]; } $data[] = [ [ 'text' => $this->i18n('delete all'), 'callback_data' => "/deleteAll $type", ], [ 'text' => $this->i18n('export'), 'callback_data' => "/exportList $type", ], [ 'text' => $this->i18n('import'), 'callback_data' => "/importList $type", ], ]; } else { $data[] = [ [ 'text' => $this->i18n('import'), 'callback_data' => "/importList $type", ], ]; } return [$data, $text]; } public function listPacChange($type, $action, $key) { $conf = $this->getPacConf(); $i = 0; foreach ($conf[$type] as $k => $v) { if ($key == $i) { switch ($action) { case 'change': $conf[$type][$k] = !$v; break; case 'delete': unset($conf[$type][$k]); break; } break; } $i++; } $this->setPacConf($conf); $this->backXtlsList($type); } public function pacZapret() { $conf = $this->getPacConf(); $conf['zapret'] = !$conf['zapret']; $this->setPacConf($conf); $this->menu('pac'); } public function pacUpdate($import = '') { exec("php updatepac.php start {$this->input['chat']} {$this->input['message_id']} {$this->input['callback_id']} $import > /dev/null &"); } public function getSSConfig() { return json_decode(file_get_contents('/config/ssserver.json'), true); } public function getSSLocalConfig() { return json_decode(file_get_contents('/config/sslocal.json'), true); } public function menuSS() { $hash = $this->getHashBot(); $domain = $this->getDomain(); $ss = $this->getSSConfig(); $v2ray = !empty($ss['plugin']) ? 'ON' : 'OFF'; $port = !empty($ss['plugin']) ? 443 : getenv('SSPORT'); $options = !empty($ss['plugin']) ? "tls;fast-open;path=/v2ray$hash;host=$domain" : "path=/v2ray$hash;host=$domain"; $text = "Menu -> ShadowSocks"; $data[] = [ [ 'text' => $this->i18n('change password'), 'callback_data' => "/sspswd", ], ]; $ss_link = preg_replace('~==~', '', 'ss://' . base64_encode("{$ss['method']}:{$ss['password']}")) . "@$domain:$port" . (!empty($ss['plugin']) ? '?plugin=' . urlencode("v2ray-plugin;path=/v2ray$hash;host=$domain;tls") : ''); $text .= "\n\n$ss_link\n"; $text .= "\n\npassword: {$ss['password']}"; $text .= "\n\nserver: $domain:$port"; $text .= "\n\nmethod: {$ss['method']}"; $text .= "\n\nnameserver: 10.10.0.5"; if ($ss['plugin']) { $text .= "\n\nplugin: v2ray-plugin"; $text .= "\n\nv2ray options: $options"; } $data[] = [ [ 'text' => "v2ray: $v2ray", 'callback_data' => "/v2ray", ], ]; $data[] = [ [ 'text' => $this->i18n('show QR'), 'callback_data' => "/qrSS", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => $text, 'data' => $data, ]; } public function i18n(string $menu): string { return $this->i18n[$menu][$this->language] ?: $menu; } public function changeWG($i) { $c = $this->getPacConf(); $c['wg_instance'] = $i; $this->setPacConf($c); $this->menu('wg', 0); } public function alignColumns(array $columns): string { // Находим максимальную длину для каждого столбца $columnLengths = []; foreach ($columns as $column) { $maxLength = 0; foreach ($column as $cell) { $len = mb_strlen($cell, 'UTF-8'); $maxLength = max($maxLength, $len); } $columnLengths[] = $maxLength; } // Получаем количество строк из первого столбца $rowCount = count($columns[0]); $columnCount = count($columns); // Формируем строки с выравниванием $result = []; for ($row = 0; $row < $rowCount; $row++) { $line = ''; for ($col = 0; $col < $columnCount; $col++) { $cell = $columns[$col][$row]; $padding = str_repeat(' ', $columnLengths[$col] - mb_strlen($cell, 'UTF-8')); $line .= $cell . $padding; // Добавляем разделитель между столбцами, кроме последнего if ($col < $columnCount - 1) { $line .= ' '; // Два пробела между столбцами } } $result[] = $line; } return implode("\n", $result); } public function iodineDomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter domain", $this->input['message_id'], reply: 'enter domain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setIodineDomain', 'args' => [], ]; } public function iodinePassword() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter password", $this->input['message_id'], reply: 'enter password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'setIodinePassword', 'args' => [], ]; } public function setIodinePassword($text) { $c = $this->getPacConf(); if ($text) { $c['iodinePassword'] = $text; } else { unset($c['iodinePassword']); } $this->setPacConf($c); $this->iodineRestart(); $this->iodine(); } public function setIodineDomain($text) { $c = $this->getPacConf(); if ($text) { $c['iodineDomain'] = $text; } else { unset($c['iodineDomain']); } $this->setPacConf($c); $this->iodineRestart(); $this->iodine(); } public function iodineRestart() { $c = $this->getPacConf(); $this->ssh('pkill iodine', 'io'); if (!empty($c['iodineDomain']) && !empty($c['iodinePassword'])) { $this->ssh("iodined -c -P {$c['iodinePassword']} 10.0.0.1 {$c['iodineDomain']}", 'io'); } } public function iodine() { $c = $this->getPacConf(); $text[] = 'Iodine'; $text[] = "domain: {$c['iodineDomain']}"; $text[] = "password: {$c['iodinePassword']}"; $data[] = [ [ 'text' => $this->i18n('set domain'), 'callback_data' => "/iodineDomain", ], ]; $data[] = [ [ 'text' => $this->i18n('set password'), 'callback_data' => "/iodinePassword", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text), $data ?: false, ); } public function menu($type = false, $arg = false, $return = false) { $conf = $this->getPacConf(); $domain = $conf['domain'] ?: $this->ip; $hash = $this->getHashBot(); if ($type == false) { $update = exec('git -C / rev-list --count HEAD..@{u}'); $branch = exec('git -C / rev-parse --abbrev-ref HEAD'); $backup = array_filter(explode('/', $conf['backup'])); if (!empty($backup)) { if (!empty(strtotime($backup[0])) && !empty(strtotime($backup[1]))) { $backup = "{$backup[0]} start / {$backup[1]} period"; } else { $backup = "{$conf['backup']} - wrong format"; } } $cron = $this->dontshowcron ? '' : $this->i18n($this->ssh('pgrep -f cron.php', 'service') ? 'on' : 'off') . ' cron'; $f = '/docker/compose'; $c = yaml_parse_file($f)['services']; $main[] = 'v' . getenv('VER') . " $branch" . ($update ? ' (have updates)' : ''); if (!empty($conf['domain'])) { $main[] = ''; $oc = $this->getHashSubdomain('oc'); $np = $this->getHashSubdomain('np'); if (!empty($conf['domain'])) { $ssl_expiry = $this->expireCert(); $certs = $this->domainsCert() ?: []; $main[] = "
"; $main[] = "Domains:"; $main[] = $conf['domain'] . (in_array($conf['domain'], $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); $main[] = 'naive ' . "$np.{$conf['domain']}" . (in_array("$np.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); $main[] = 'openconnect ' . "$oc.{$conf['domain']}" . (in_array("$oc.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); if (!empty($conf['adguardkey'])) { $main[] = "{$conf['adguardkey']}.{$conf['domain']}" . (in_array("{$conf['adguardkey']}.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : '') . ' adguard DOT';; } $main[] = "
"; } else { $main[] = $this->i18n('domain explain'); } } $main[] = ''; $main[] = ''; $main[] = $this->alignColumns([ [ $this->i18n($this->ssh($this->getPacConf()['amnezia'] ? 'awg' : 'wg', 'wg') ? 'on' : 'off') . ' ' . $this->i18n($this->getPacConf()['amnezia'] ? 'amnezia' : 'wg_title'), $this->i18n($this->ssh($this->getPacConf()['wg1_amnezia'] ? 'awg' : 'wg', 'wg1') ? 'on' : 'off') . ' ' . $this->i18n($this->getPacConf()['wg1_amnezia'] ? 'amnezia' : 'wg_title'), $this->i18n($this->ssh('pgrep xray', 'xr') ? 'on' : 'off') . ' ' . $this->i18n('xray'), $this->i18n($this->ssh('pgrep caddy', 'np') ? 'on' : 'off') . ' ' . $this->i18n('naive'), $this->i18n($this->ssh('pgrep ocserv', 'oc') ? 'on' : 'off') . ' ' . $this->i18n('ocserv'), $this->i18n($this->ssh('pgrep mtproto-proxy', 'tg') ? 'on' : 'off') . ' ' . $this->i18n('mtproto'), $this->i18n(exec("JSON=1 timeout 2 dnslookup google.com ad") ? 'on' : 'off') . ' ' . $this->i18n('ad_title'), $this->i18n($this->ssh('pgrep ssserver', 'ss') ? 'on' : 'off') . ' ' . $this->i18n('sh_title'), $this->i18n($this->ssh('pgrep iodine', 'io') ? 'on' : 'off') . ' ' . $this->i18n('Iodine'), $this->i18n($this->warpStatus()) . ' ' . $this->i18n('warp'), ], [ $this->i18n($c['wg'] ? 'on' : 'off') . ' ' . getenv('WGPORT'), $this->i18n($c['wg1'] ? 'on' : 'off') . ' ' . getenv('WG1PORT'), $this->i18n('on') . ' 443', $this->i18n('on') . ' 443', $this->i18n('on') . ' 443', $this->i18n($c['tg'] ? 'on' : 'off') . ' ' . getenv('TGPORT'), $this->i18n($c['ad'] ? 'on' : 'off') . ' 853', $this->i18n($c['ss'] ? 'on' : 'off') . ' ' . getenv('SSPORT'), $this->i18n($c['io'] ? 'on' : 'off') . ' 53', '', ], ]); $main[] = ''; $main[] = $this->alignColumns([ [ $this->i18n($backup ? 'on' : 'off') . ' autobackup', $this->i18n($conf['autoupdate'] ? 'on' : 'off') . ' autoupdate', $this->i18n($conf['autoscan'] ? 'on' : 'off') . ' autoscan', ], [ $this->i18n($conf['autodeny'] ? 'on' : 'off') . ' autoblock' . ($conf['deny'] ? ': ' . count($conf['deny']) : ''), $this->i18n($conf['reset_monthly'] ? 'on' : 'off') . ' autoreset', $cron, ], ]); $main[] = ''; } $menu = [ 'main' => [ 'text' => implode("\n", $main ?: []), 'data' => [ [ [ 'text' => $this->i18n($this->getPacConf()['amnezia'] ? 'amnezia' : 'wg_title'), 'callback_data' => "/changeWG 0", ], [ 'text' => $this->i18n($this->getPacConf()['wg1_amnezia'] ? 'amnezia' : 'wg_title'), 'callback_data' => "/changeWG 1", ], ], [ [ 'text' => $this->i18n('xray'), 'callback_data' => "/xray", ], [ 'text' => $this->i18n('naive'), 'callback_data' => "/menu naive", ], ], [ [ 'text' => $this->i18n('ocserv'), 'callback_data' => "/menu oc", ], [ 'text' => $this->i18n('mtproto'), 'callback_data' => "/mtproto", ], ], [ [ 'text' => $this->i18n('ad_title'), 'callback_data' => "/menu adguard", ], [ 'text' => $this->i18n('warp'), 'callback_data' => "/warp", ], ], [ [ 'text' => $this->i18n('sh_title'), 'callback_data' => "/menu ss", ], [ 'text' => $this->i18n('pac'), 'callback_data' => "/pacMenu 0", ], ], [ [ 'text' => $this->i18n('Iodine'), 'callback_data' => "/iodine", ], ], [ [ 'text' => $this->i18n('config'), 'callback_data' => "/menu config", ], ], [ [ 'text' => $this->i18n('chat'), 'url' => base64_decode('aHR0cHM6Ly90Lm1lLys0RzMtUTRkX3ZGRXhPRGN5'), ], [ 'text' => $this->i18n('donate'), 'web_app' => [ 'url' => "https://$domain/webapp$hash/donate.html", ] ], ], ], ], 'wg' => $type == 'wg' ? $this->statusWg($arg) : false, 'client' => $type == 'client' ? $this->getClient(...explode('_', $arg)) : false, 'addpeer' => $type == 'addpeer' ? $this->addWg(...explode('_', $arg)) : false, 'pac' => $type == 'pac' ? $this->pacMenu($arg) : false, 'adguard' => $type == 'adguard' ? $this->adguardMenu() : false, 'config' => $type == 'config' ? $this->configMenu() : false, 'ss' => $type == 'ss' ? $this->menuSS() : false, 'lang' => $type == 'lang' ? $this->menuLang() : false, 'oc' => $type == 'oc' ? $this->ocMenu() : false, 'naive' => $type == 'naive' ? $this->naiveMenu() : false, 'mirror' => $type == 'mirror' ? $this->mirrorMenu() : false, 'update' => $type == 'update' ? $this->updatebot() : false, ]; $text = $menu[$type ?: 'main' ]['text']; $data = $menu[$type ?: 'main' ]['data']; if (empty($type) && $update) { $b = exec('git -C / rev-parse --abbrev-ref HEAD'); array_unshift($data, [ [ 'text' => 'changelog', 'web_app' => ['url' => "https://raw.githubusercontent.com/mercurykd/vpnbot/$b/version"], ], [ 'text' => $this->i18n('update bot'), 'callback_data' => "/applyupdatebot", ], ]); } if ($return) { return [$text, $data]; } if (!empty($this->input['callback_id'])) { $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } else { $this->send( $this->input['chat'], $text, $this->input['message_id'], $data ?: false, ); } } public function switchScanIp() { $c = $this->getPacConf(); $c['autoscan'] = $c['autoscan'] ? 0 : 1; $this->setPacConf($c); $this->ipMenu(); } public function switchBanIp() { $c = $this->getPacConf(); $c['autodeny'] = $c['autodeny'] ? 0 : 1; $this->setPacConf($c); $this->ipMenu(); } public function switchMonthlyStats() { $c = $this->getPacConf(); $c['reset_monthly'] = $c['reset_monthly'] ? 0 : 1; $this->setPacConf($c); $this->xray(); } public function switchIpLimit($limit) { $limit = explode(':', $limit); $c = $this->getPacConf(); if ((int) $limit[0] <= 0) { unset($c['ip_limit']); unset($c['ip_count']); } else { $c['ip_limit'] = (int) $limit[0]; $c['ip_count'] = (int) $limit[1] ?: 1; } $this->setPacConf($c); $this->xray(); } public function hwidLimit() { $pac = $this->getPacConf(); $enabled = !empty($pac['hwid_limit_enabled']); $count = max(1, (int) ($pac['hwid_device_count'] ?: 1)); $text[] = 'Settings -> ' . $this->i18n('hwid limit'); $text[] = $this->i18n('hwid notice'); $text[] = $this->i18n('hwid limit') . ': ' . ($enabled ? $count : $this->i18n('off')); $data[] = [ [ 'text' => $this->i18n($enabled ? 'on' : 'off'), 'callback_data' => '/toggleHwidLimit', ], ]; $data[] = [ [ 'text' => $this->i18n('set hwid devices count') . ': ' . $count, 'callback_data' => '/setHwidDevices', ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => '/xray', ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function toggleHwidLimit($context = null) { $pac = $this->getPacConf(); $pac['hwid_limit_enabled'] = $pac['hwid_limit_enabled'] ? 0 : 1; if (!empty($pac['hwid_limit_enabled']) && empty($pac['hwid_device_count'])) { $pac['hwid_device_count'] = 1; } $this->setPacConf($pac); $this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true); if ($context === 'xray') { $this->xray(); } else { $this->hwidLimit(); } } public function setHwidDevices($context = null) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter hwid devices count", $this->input['message_id'], reply: 'enter hwid devices count', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'saveHwidDevices', 'args' => [$context], ]; } public function saveHwidDevices($count, $context = null) { $count = (int) $count; if ($count <= 0) { $count = 1; } $pac = $this->getPacConf(); $pac['hwid_device_count'] = $count; $this->setPacConf($pac); $this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']); if ($context === 'xray') { $this->xray(); } else { $this->hwidLimit(); } } public function getHwidStorage() { if (!file_exists($this->hwid)) { return []; } $data = json_decode(file_get_contents($this->hwid), true); return is_array($data) ? $data : []; } public function setHwidStorage(array $storage) { file_put_contents($this->hwid, json_encode($storage, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } public function getHwidDevicesByUser($uid) { $storage = $this->getHwidStorage(); return $storage[$uid] ?? []; } public function setHwidDevice($uid, $hwid, array $info) { $storage = $this->getHwidStorage(); $storage[$uid][$hwid] = $info; $this->setHwidStorage($storage); } public function deleteHwidDevice($uid, $hwid) { $storage = $this->getHwidStorage(); if (isset($storage[$uid][$hwid])) { unset($storage[$uid][$hwid]); if (empty($storage[$uid])) { unset($storage[$uid]); } $this->setHwidStorage($storage); } } public function deleteHwidUser($uid) { $storage = $this->getHwidStorage(); if (isset($storage[$uid])) { unset($storage[$uid]); $this->setHwidStorage($storage); } } protected function getHwidTokenScope($index) { return ($this->input['chat'] ?? 'global') . ':' . $index; } protected function rememberHwidToken($scope, $hwid) { if (!isset($_SESSION['hwidTokens'])) { $_SESSION['hwidTokens'] = []; } if (!isset($_SESSION['hwidTokens'][$scope])) { $_SESSION['hwidTokens'][$scope] = []; } do { try { $token = bin2hex(random_bytes(5)); } catch (\Throwable $e) { $token = substr(hash('sha256', $hwid . microtime(true)), 0, 10); } } while (isset($_SESSION['hwidTokens'][$scope][$token])); $_SESSION['hwidTokens'][$scope][$token] = $hwid; return $token; } protected function resolveHwidToken($scope, $token) { if (isset($_SESSION['hwidTokens'][$scope][$token])) { $hwid = $_SESSION['hwidTokens'][$scope][$token]; unset($_SESSION['hwidTokens'][$scope][$token]); return $hwid; } $decoded = base64_decode($token, true); return $decoded !== false ? $decoded : ''; } public function processHwidRequest(array $client) { $pac = $this->getPacConf(); if (empty($pac['hwid_limit_enabled']) || !empty($client['hwid_disabled'])) { return true; } $limit = (int) ($client['hwid_limit'] ?: ($pac['hwid_device_count'] ?: 0)); if ($limit <= 0) { return true; } $devices = $this->getHwidDevicesByUser($client['id']); $hwid = trim($_SERVER['HTTP_X_HWID'] ?? ''); $isBrowser = $this->isBrowserRequest(); if ($hwid === '') { if ($isBrowser) { return true; } $message = 'HWID device limit exceeded'; header('announce: base64:' . base64_encode($message)); header('X-HWID-Status: ' . $message); header('HTTP/1.1 429 Too Many Requests', true, 429); return false; } $isNew = !isset($devices[$hwid]); if ($isNew && count($devices) >= $limit) { $message = 'HWID device limit exceeded'; header('announce: base64:' . base64_encode($message)); header('X-HWID-Status: ' . $message); header('HTTP/1.1 429 Too Many Requests', true, 429); return false; } $this->setHwidDevice($client['id'], $hwid, [ 'time' => time(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'device_os' => $_SERVER['HTTP_X_DEVICE_OS'] ?? '', 'os_version' => $_SERVER['HTTP_X_VER_OS'] ?? '', 'device_model' => $_SERVER['HTTP_X_DEVICE_MODEL'] ?? '', ]); return true; } protected function isBrowserRequest() { $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? ''; $accept = $_SERVER['HTTP_ACCEPT'] ?? ''; if ($userAgent === '' && $accept === '') { return false; } $browserPatterns = [ 'Mozilla/', 'Chrome/', 'Safari/', 'Firefox/', 'Edge/', 'Edg/', 'MSIE ', 'Trident/', 'Opera/', 'OPR/', ]; foreach ($browserPatterns as $pattern) { if (stripos($userAgent, $pattern) !== false) { return true; } } if (stripos($accept, 'text/html') !== false) { return true; } return false; } public function switchSilence() { $c = $this->getPacConf(); $c['silence'] = (($c['silence'] ?: 0) + 1) % 3; $this->setPacConf($c); $this->ipMenu(); } public function ipMenu() { $text = 'Settings -> IP & Logs'; $pac = $this->getPacConf(); $d = count($pac['deny'] ?: []); $w = count($pac['white'] ?: []); $data[] = [ [ 'text' => $this->i18n('autoscan') . ': ' . ($pac['autoscan'] ? $this->getTime(strtotime(($pac['autoscan_timeout'] ?: 3600) . ' seconds')) : $this->i18n('off')), 'callback_data' => '/autoScanTimeout', ], ]; if (!empty($pac['autoscan'])) { $data[] = [ [ 'text' => $this->i18n('autoblock') . ': ' . $this->i18n($pac['autodeny'] ? 'on' : 'off'), 'callback_data' => '/switchBanIp', ], [ 'text' => $this->i18n('notify') . ': ' . ((function ($pac) { switch ($pac['silence']) { case 0: return '🔊'; case 1: return '🔈'; case 2: return '🔇'; } })($pac)), 'callback_data' => '/switchSilence', ], ]; } $data[] = [ [ 'text' => $this->i18n('ignorelist') . ": $w", 'callback_data' => '/denyList 0 1', ], [ 'text' => $this->i18n('blocklist') . ": $d", 'callback_data' => '/denyList 0 0', ], ]; $data[] = [ [ 'text' => $this->i18n('analyze'), 'callback_data' => '/analysisIp', ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu config", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function ipInRange($ip, $range) { [$range, $netmask] = explode('/', $range, 2); $rangeDecimal = ip2long($range); $ipDecimal = ip2long($ip); $wildcardDecimal = pow(2, 32 - $netmask) - 1; $netmaskDecimal = ~$wildcardDecimal; return ($ipDecimal & $netmaskDecimal) == ($rangeDecimal & $netmaskDecimal); } public function suspicious($regexp, $file, $ranges, $title, $reverse = false) { if ($r = fopen($file, 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { if ($reverse xor preg_match($regexp, $l)) { if (is_array($ranges)) { $flag = true; foreach ($ranges as $range) { if ($this->ipInRange($m[1], $range)) { $flag = false; break; } } if ($flag) { $ret[$m[1]][] = [ 'title' => $title, 'log' => $l, ]; } } else { if ($this->ipInRange($m[1], $ranges)) { $ret[$m[1]][] = [ 'title' => $title, 'log' => $l, ]; } } } } } fclose($r); } return $ret ?: []; } public function analysisIp(int $page = 0, $return = false) { $pac = $this->getPacConf(); $xr = []; foreach (array_merge($pac['white'] ?: [], $pac['deny'] ?: [], ['10.10.0.0/23']) as $v) { if (preg_match('~^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:(/\d{1,2}))?$~', $v, $m)) { if (!in_array($m[1] . ($m[2] ?: '/32'), $xr)) { $xr[] = $m[1] . ($m[2] ?: '/32'); } } } if ($r = fopen('/logs/nginx_tlgrm_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { if (!in_array("{$m[1]}/32", $xr)) { $xr[] = "{$m[1]}/32"; } } } fclose($r); } if ($r = fopen('/logs/nginx_doh_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { if (!in_array("{$m[1]}/32", $xr)) { $xr[] = "{$m[1]}/32"; } } } fclose($r); } if ($r = fopen('/logs/xray', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)(?=.+accepted)~', $l, $m)) { if (!in_array("{$m[1]}/32", $xr)) { $xr[] = "{$m[1]}/32"; } } } fclose($r); } $t = [ $this->suspicious($this->reg, '/logs/nginx_default_access', $xr, 'possibly a scanner', true), $this->suspicious($this->reg, '/logs/nginx_domain_access', $xr, 'possibly a scanner', true), ]; $ip = []; foreach ($t as $r) { foreach ($r as $k => $v) { $ip[$k] = $v; } } $r = $this->suspicious('~\d+\.\d+\.\d+\.\d+.+200\s\d+\s0$~', '/logs/upstream_access', $xr, 'possibly a Reality Degenerate'); if (!empty($r)) { foreach ($r as $k => $v) { if (count($v) > 30) { $ip[$k] = $v; } } } if (!empty($return)) { return $ip; } if (!empty($ip)) { foreach ($ip as $k => $v) { $data[] = [ [ 'text' => $k, 'callback_data' => "/searchLogs $k analysisIp $page 0", ] ]; } $all = (int) ceil(count($data) / $this->limit); $page = min($page, $all - 1); $page = $page < 0 ? $all - 1 : $page; $data = array_slice($data ?: [], $page * $this->limit, $this->limit); if ($all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/analysisIp " . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ 'text' => $page + 1, 'callback_data' => "/analysisIp $page", ], [ 'text' => '>>', 'callback_data' => "/analysisIp " . ($page < $all - 1 ? $page + 1 : 0), ], ]; } } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/ipMenu", ], ]; $this->update($this->input['from'], $this->input['message_id'], count($ip) ?: 'empty', $data); } public function searchLogs($search, $fun = false, $page = 0, $white = 0) { if (preg_match('~^\d+\.\d+\.\d+\.\d+$~', $search)) { $info = file_get_contents("https://ipinfo.io/$search/json", context: stream_context_create(['http' => ['timeout' => 2]])); $text = "$search\n
$info
"; $data[] = [ [ 'text' => $this->i18n('block'), 'callback_data' => "/denyIp $search" . ($fun ? " $fun $page $white" : ''), ], [ 'text' => $this->i18n('ignore'), 'callback_data' => "/whiteIp $search" . ($fun ? " $fun $page $white" : ''), ], ]; $data[] = [ [ 'text' => $this->i18n('all logs'), 'callback_data' => "/searchIp $search", ], [ 'text' => $this->i18n('suspicious log'), 'callback_data' => "/searchSuspiciousIp $search", ], ]; $data[] = [ [ 'text' => $this->i18n("clean logs $search"), 'callback_data' => "/cleanLogs $search", ], ]; if (!empty($fun)) { $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/$fun $page" . ($white ? " $white" : ''), ], ]; $this->update($this->input['from'], $this->input['message_id'], $text, button: $data); } else { if (empty($this->input['callback_id'])) { $this->delete($this->input['from'], $this->input['message_id']); } $this->send($this->input['from'], $text, button: $data); } } } public function searchIp($ip) { foreach ($this->logs as $v) { if ($r = fopen("/logs/$v", 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~' . preg_quote($ip) . '~', $l)) { $res[$v][] = $l; } } fclose($r); } } if (!empty($res)) { foreach ($res as $k => $v) { $head= "$k:\n"; $t = array_chunk($v, 10); foreach ($t as $j) { $text = "$head
";
                    foreach ($j as $i) {
                        $text .= htmlspecialchars($i, ENT_HTML5, 'UTF-8');
                    }
                    $text .= '
'; $this->send($this->input['from'], $text, $this->input['message_id']); } } } else { $this->answer($this->input['callback_id'], 'empty'); } } public function searchSuspiciousIp($ip) { $t = [ $this->suspicious('~\d+\.\d+\.\d+\.\d+.+200\s\d+\s0$~', '/logs/upstream_access', "$ip/32", 'possibly a Reality Degenerate'), $this->suspicious($this->reg, '/logs/nginx_default_access', "$ip/32", 'possibly a scanner', true), $this->suspicious($this->reg, '/logs/nginx_domain_access', "$ip/32", 'possibly a scanner', true), ]; foreach ($t as $r) { if (!empty($r)) { foreach ($r as $v) { foreach ($v as $k) { $logs[$k['title']][] = $k['log']; } } } } if (!empty($logs)) { foreach ($logs as $k => $v) { $head= "$k:\n"; $t = array_chunk($v, 10); foreach ($t as $j) { $text = "$head
";
                    foreach ($j as $i) {
                        $text .= htmlspecialchars($i, ENT_HTML5, 'UTF-8');
                    }
                    $text .= '
'; $this->send($this->input['from'], $text, $this->input['message_id']); } } } else { $this->answer($this->input['callback_id'], 'empty'); } } public function importIps($type) { switch ($type) { case 'telegram': $r = file_get_contents('https://core.telegram.org/resources/cidr.txt'); if (!empty($r)) { $domains = explode("\n", $r); } break; case 'gcore': $r = json_decode(file_get_contents('https://api.gcore.com/cdn/public-ip-list'), true); if (!empty($r['addresses'])) { $domains = $r['addresses']; } break; case 'cloudflare': $r = json_decode(file_get_contents('https://api.cloudflare.com/client/v4/ips'), true); if (!empty($r['result']['ipv4_cidrs'])) { $domains = $r['result']['ipv4_cidrs']; } break; } if (!empty($domains = array_filter($domains ?: [], fn($e) => preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}~', $e)))) { $this->addInclude(implode(',', $domains), 'white'); } } public function denyList($page = 0, $white = 0) { $text = 'Menu -> IP -> ' . ($white ? 'ignore' : 'block') . 'list'; $domains = $this->getPacConf()[$white ? 'white' : 'deny'] ?: []; $all = (int) ceil(count($domains) / $this->limit); $page = min($page, $all - 1); $page = $page < 0 ? $all - 1 : $page; if (!empty($white)) { $data[] = [ [ 'text' => $this->i18n('telegram IPs'), 'callback_data' => "/importIps telegram", ], ]; $data[] = [ [ 'text' => $this->i18n('gcore IPs'), 'callback_data' => "/importIps gcore", ], ]; $data[] = [ [ 'text' => $this->i18n('cloudflare IPs'), 'callback_data' => "/importIps cloudflare", ], ]; } $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/include " . ($white ? 'white' : 'deny'), ], ]; if (!empty($domains)) { foreach (array_slice($domains, $page * $this->limit, $this->limit) as $v) { $data[] = [ [ 'text' => $v, 'callback_data' => "/searchLogs $v denyList $page $white", ], [ 'text' => $this->i18n('delete'), 'callback_data' => "/allowIp $v $page" . ($white ? " 1" : ''), ], ]; } if ($all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/denyList " . ($page - 1 >= 0 ? $page - 1 : $all - 1) . ($white ? " 1" : ' 0'), ], [ 'text' => $page + 1, 'callback_data' => "/denyList $page" . ($white ? " 1" : ' 0'), ], [ 'text' => '>>', 'callback_data' => "/denyList " . ($page < $all - 1 ? $page + 1 : 0) . ($white ? " 1" : ' 0'), ], ]; } $data[] = [ [ 'text' => $this->i18n('delete all'), 'callback_data' => "/cleanDeny" . ($white ? " 1" : ''), ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/ipMenu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function cleanDeny($white = 0) { $pac = $this->getPacConf(); unset($pac[$white ? 'white' : 'deny']); $this->setPacConf($pac); $this->syncDeny(); $this->ipMenu(); } public function denyIp($ip, $fun = false, $page = 0, $white = 0) { $pac = $this->getPacConf(); if (is_array($ip)) { foreach ($ip as $v) { $pac['deny'][] = $v; if (($t = array_search($v, $pac['white'] ?: [])) !== false) { unset($pac['white'][$t]); } } } else { $pac['deny'][] = $ip; if (($t = array_search($ip, $pac['white'] ?: [])) !== false) { unset($pac['white'][$t]); } } $this->setPacConf($pac); if (empty($fun)) { $this->delete($this->input['from'], $this->input['message_id']); } $this->syncDeny(); if (!empty($fun)) { $this->{$fun}($page, $white); } } public function whiteIp($ip, $fun = false, $page = 0, $white = 0) { $pac = $this->getPacConf(); if (is_array($ip)) { foreach ($ip as $v) { $pac['white'][] = $v; if (($t = array_search($v, $pac['deny'] ?: [])) !== false) { unset($pac['deny'][$t]); } } } else { $pac['white'][] = $ip; if (($t = array_search($ip, $pac['deny'] ?: [])) !== false) { unset($pac['deny'][$t]); } } $this->setPacConf($pac); if (empty($fun)) { $this->delete($this->input['from'], $this->input['message_id']); } $this->syncDeny(); if (!empty($fun)) { $this->{$fun}($page, $white); } } public function allowIp($ip, $page, $white = 0) { $pac = $this->getPacConf(); unset($pac[$white ? 'white' : 'deny'][array_search($ip, $pac[$white ? 'white' : 'deny'])]); $this->setPacConf($pac); $this->syncDeny(); $this->denyList($page, $white); } public function cleanLogs($ip, $nodelete = false) { foreach ($this->logs as $v) { exec("sed -i '/$ip/d' /logs/$v"); } if (empty($nodelete)) { $this->delete($this->input['from'], $this->input['message_id']); } } public function syncDeny() { $pac = $this->getPacConf(); if ($r = fopen('/logs/nginx_tlgrm_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { $xr[$m[1]] = true; } } fclose($r); } if (!empty($xr)) { foreach (array_keys($xr) as $v) { $text .= "allow $v;\n"; } } if (!empty($pac['white'])) { $pac['white'] = array_unique($pac['white']); sort($pac['white']); foreach ($pac['white'] as $v) { $text .= "allow $v;\n"; } } if (!empty($pac['deny'])) { $pac['deny'] = array_unique($pac['deny']); sort($pac['deny']); foreach ($pac['deny'] as $k => $v) { if (!in_array($v, $pac['white'] ?: []) && !in_array($v, array_keys($xr ?: []))) { $text .= "deny $v;\n"; } else { unset($pac['deny'][$k]); } } } $this->setPacConf($pac); file_put_contents('/config/deny', $text ?: ''); $this->ssh('nginx -s reload', 'up'); } public function linkXray($i, $s = false) { $c = $this->getXray(); $pac = $this->getPacConf(); $domain = $this->getDomain($pac['transport'] != 'Reality'); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); $si = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $c['inbounds'][0]['settings']['clients'][$i]['id'], ])); $v2 = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', 's' => $c['inbounds'][0]['settings']['clients'][$i]['id'], ])); switch ($s) { case 1: return "v2rayng://install-config?url=$v2#{$c['inbounds'][0]['settings']['clients'][$i]['id']}"; case 2: return "sing-box://import-remote-profile/?url={$si}#{$c['inbounds'][0]['settings']['clients'][$i]['email']}"; default: if ($pac['transport'] != 'Reality') { return "vless://{$c['inbounds'][0]['settings']['clients'][$i]['id']}@$domain:443?flow=&path=%2Fws$hash&security=tls&sni=$domain&fp=chrome&type=ws#{$c['inbounds'][0]['settings']['clients'][$i]['email']}"; } return "vless://{$c['inbounds'][0]['settings']['clients'][$i]['id']}@$domain:443?security=reality&sni={$c['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0]}&fp=chrome&pbk={$pac['xray']}&sid={$c['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0]}&type=tcp&flow=xtls-rprx-vision#{$c['inbounds'][0]['settings']['clients'][$i]['email']}"; } } public function dockerApi($url, $method = 'GET', $data = []) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_POSTFIELDS => !empty($data) ? json_encode($data) : null, CURLOPT_URL => "http://localhost$url", CURLOPT_RETURNTRANSFER => true, CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock' ]); $r = json_decode(curl_exec($ch), true); curl_close($ch); return $r; } public function cleanDocker() { $r = $this->dockerApi('/images/json'); foreach ($r as $v) { if (!empty($v['RepoTags'])) { foreach ($v['RepoTags'] as $j) { if (preg_match('~^mercurykd/vpnbot~', $j)) { $i[] = $v['Id']; break; } } } } $r = $this->dockerApi('/containers/json?all=1'); foreach ($r as $v) { if (preg_match('~^mercurykd/vpnbot~', $v['Image'])) { $c[] = $v['ImageID']; } } if (!empty($d = array_diff($i, $c))) { foreach ($d as $v) { $this->dockerApi("/images/$v", 'DELETE'); } } $this->dockerApi('/images/prune', 'POST', ['dangling' => true]); $this->dockerApi('/build/prune', 'POST'); } public function naiveMenu() { $pac = $this->getPacConf(); $domain = $this->getDomain(); $text[] = "Menu -> NaiveProxy"; $np = $this->getHashSubdomain('np'); $text[] = "https://{$pac['naive']['user']}:{$pac['naive']['pass']}@$np.$domain"; $data[] = [ [ 'text' => $this->i18n('change subdomain'), 'callback_data' => "/changeNaiveSubdomain", ], ]; $data[] = [ [ 'text' => $this->i18n('change login'), 'callback_data' => "/changeNaiveUser", ], [ 'text' => $this->i18n('change password'), 'callback_data' => "/changeNaivePass", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => implode("\n", $text), 'data' => $data, ]; } public function mirrorMenu() { $ip = $this->getPacConf()['domain'] ?: $this->ip; $text[] = "Menu -> Mirror"; $text[] = <<client -> intermediate VPS -> vpnbot ^ | | install | | mirror | ----------- PNG; $data[] = [ [ 'text' => $this->i18n('download'), 'callback_data' => "/getMirror", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => implode("\n", $text), 'data' => $data, ]; } public function getMirror() { $s = file_get_contents('/mirror/start_socat.sh'); $t = str_replace([ '~ip~', '~tg~', '~ss~', '~wg1~', '~wg2~', ], [ getenv('IP'), getenv('TGPORT'), getenv('SSPORT'), getenv('WGPORT'), getenv('WG1PORT'), ], $s); $this->sendFile($this->input['from'], new CURLStringFile($t, 'socat.sh', 'application/x-sh')); } public function ocMenu() { $pac = $this->getPacConf(); $domain = $this->getDomain(); $ocserv = file_get_contents('/config/ocserv.conf'); preg_match('~^camouflage_secret[^\n]+?"([^"]+)*"~sm', $ocserv, $m); $cs = $m[1]; preg_match('~^dns = ([^\n]+)~sm', $ocserv, $m); $dns = $m[1]; preg_match('~^expose-iroutes = (true)~sm', $ocserv, $m); $expose = $m[1]; $pass = htmlspecialchars($pac['ocserv']); $text[] = "Menu -> OpenConnect"; if (!empty($cs)) { $oc = $this->getHashSubdomain('oc'); $text[] = "https://$oc.$domain/?$cs"; } $text[] = "password: $pass"; $data[] = [ [ 'text' => $this->i18n('change subdomain'), 'callback_data' => "/changeOcDomain", ], ]; $data[] = [ [ 'text' => $this->i18n('change secret'), 'callback_data' => "/changeCamouflage", ], [ 'text' => $this->i18n('change password'), 'callback_data' => "/changeOcPass", ], ]; $data[] = [ [ 'text' => $this->i18n('dns') . ": $dns", 'callback_data' => "/changeOcDns", ], ]; $data[] = [ [ 'text' => $this->i18n('listSubnet'), 'callback_data' => "/subnet 0_0_1", ], ]; $data[] = [ [ 'text' => $this->i18n('expose-iroutes') . ' ' . $this->i18n($expose ? 'on' : 'off'), 'callback_data' => "/changeOcExpose", ], ]; $data[] = [ [ 'text' => $this->i18n('add peer'), 'callback_data' => "/addOcUser", ], ]; $clients = $this->getClientsOc(); foreach ($clients as $k => $v) { $data[] = [ [ 'text' => $this->i18n('delete') . " $v", 'callback_data' => "/deloc $k", ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => implode("\n", $text), 'data' => $data, ]; } public function changeOcExpose() { $c = file_get_contents('/config/ocserv.conf'); preg_match('~^expose-iroutes = ([^\n]+)~sm', $c, $m); $t = preg_replace('~^expose-iroutes[^\n]+~sm', "expose-iroutes = " . ($m[1] == 'true' ? 'false' : 'true'), $c); $this->restartOcserv($t); $this->menu('oc'); } public function deloc($i) { $clients = $this->getClientsOc(); foreach ($clients as $k => $v) { if ($i == $k) { $this->ssh("ocpasswd -c /etc/ocserv/ocserv.passwd -d $v", 'oc'); break; } } $this->menu('oc'); } public function delxr($i) { $r = $this->getXray(); $st = $this->getXrayStats(); foreach ($r['inbounds'][0]['settings']['clients'] as $k => $v) { if ($i == $k) { $this->deleteHwidUser($r['inbounds'][0]['settings']['clients'][$k]['id']); unset($r['inbounds'][0]['settings']['clients'][$k]); unset($st['users'][$k]); $this->setXrayStats($st); $this->restartXray($r); $this->adguardXrayClients(); break; } } $this->xray(); } public function getClientsOc() { $users = array_filter(explode("\n", file_get_contents('/config/ocserv.passwd')), fn ($e) => !empty($e)); return array_map(fn($e) => explode(':', $e)[0], $users); } public function addocus($user) { $pac = $this->getPacConf(); $this->ssh("echo '{$pac['ocserv']}' | ocpasswd -c /etc/ocserv/ocserv.passwd $user", 'oc'); $this->menu('oc'); } public function addxrus($users) { $c = $this->getXray(); $p = $this->getPacConf(); $users = array_map(fn ($e) => trim($e), explode(',', $users)); $users = array_map(fn ($e) => explode(':', $e), $users); foreach ($c['inbounds'][0]['settings']['clients'] as $k => $v) { $uuids[] = $v['id']; $emails[] = $v['email']; } foreach ($users as $user) { $uuid = $user[1] ?: trim($this->ssh('xray uuid', 'xr')); if (in_array($uuid, $uuids ?: []) || in_array($user[0], $emails ?: [])) { $this->send($this->input['chat'], "user {$user[0]} already exists"); return $this->xray(); } $c['inbounds'][0]['settings']['clients'][] = $p['transport'] != 'Reality' ? [ 'id' => $uuid, 'email' => $user[0], ] : [ 'id' => $uuid, 'flow' => 'xtls-rprx-vision', 'email' => $user[0], ]; } $this->restartXray($c); $this->adguardXrayClients(); if (count($users) == 1) { $this->userXr(count($c['inbounds'][0]['settings']['clients']) - 1); } else { $this->xray(); } } public function setTimerXr($time, $i) { $c = $this->getXray(); if (empty($time)) { unset($c['inbounds'][0]['settings']['clients'][$i]['time']); } else { $time = strtotime($time); if ($time === false) { $this->send($this->input['chat'], 'wrong format'); return; } $c['inbounds'][0]['settings']['clients'][$i]['time'] = $time; } $this->restartXray($c, 1); if (!empty($c['inbounds'][0]['settings']['clients'][$i]['off'])) { $this->switchXr($i, 0, 1); } else { $this->userXr($i); } } public function switchXr($i, $nm = 0, $time = false) { $c = $this->getXray(); if (empty($time)) { unset($c['inbounds'][0]['settings']['clients'][$i]['time']); } if (empty($c['inbounds'][0]['settings']['clients'][$i]['off'])) { $c['inbounds'][0]['settings']['clients'][$i]['off'] = $c['inbounds'][0]['settings']['clients'][$i]['id']; $c['inbounds'][0]['settings']['clients'][$i]['id'] = trim($this->ssh('xray uuid', 'xr')); } else { $c['inbounds'][0]['settings']['clients'][$i]['id'] = $c['inbounds'][0]['settings']['clients'][$i]['off']; unset($c['inbounds'][0]['settings']['clients'][$i]['off']); } $this->restartXray($c); if (empty($nm)) { $this->userXr($i); } } public function renXrUs($name, $i) { $c = $this->getXray(); $c['inbounds'][0]['settings']['clients'][$i]['email'] = $name; $this->restartXray($c); $this->adguardXrayClients(); $this->userXr($i); } public function getXrayStats() { return json_decode(file_get_contents('/config/xray.stats'), true) ?: []; } public function setXrayStats($x) { file_put_contents('/config/xray.stats', json_encode($x)); } public function resetXrUser($i) { $c = $this->getXrayStats(); unset($c['users'][$i]); $this->restartXray($this->getXray()); $this->userXr($i); } public function resetXrStats($nomenu = false) { $this->restartXray($this->getXray()); $this->setXrayStats([]); if (empty($nomenu)) { $this->xray(); } } public function listXr($i) { $c = $this->getPacConf(); $c['xtlslist'] = $i; $this->setPacConf($c); $this->xray(); } public function templateAdd($type) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send the template file:", $this->input['message_id'], reply: 'send the template file:', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'addTemplate', 'args' => [$type], ]; } public function autoScanTimeout() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send time like 1 hour or 1 day etc", $this->input['message_id'], reply: 'send time like 1 hour or 1 day etc', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setAutoScanTimeout', 'args' => [], ]; } public function setAutoScanTimeout($time) { $pac = $this->getPacConf(); if (empty($time)) { unset($pac['autoscan_timeout']); unset($pac['autoscan']); } elseif ($t = strtotime($time, 0)) { $pac['autoscan_timeout'] = $t; $pac['autoscan'] = 1; } else { $this->send($this->input['from'], "$time - wrong format", $this->input['message_id']); } $this->setPacConf($pac); $this->ipMenu(); } public function templateCopy($type) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send the template name", $this->input['message_id'], reply: 'send the template name', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'copyTemplate', 'args' => [$type], ]; } public function addTemplate($n, $type) { if (empty($this->input['caption'])) { $this->send($this->input['chat'], 'empty name'); return; } $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); $json = json_decode(file_get_contents($this->file . $r['result']['file_path']), true); if ($json === false) { $this->send($this->input['chat'], 'wrong format'); return; } $pac = $this->getPacConf(); $pac["{$type}templates"][$this->input['caption']] = $json; $this->setPacConf($pac); $this->templates($type); } public function saveTemplate($name, $type, $json) { if (json_decode($json, true) === false) { return [ 'status' => false, 'message' => 'wrong format', ]; } $pac = $this->getPacConf(); switch ($name) { case 'origin': file_put_contents("/config/$type.json", json_encode(json_decode($json, true), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); break; default: $pac["{$type}templates"][$name] = json_decode($json, true); break; } $this->setPacConf($pac); return [ 'status' => true, ]; } public function delTemplate($type, $name) { $pac = $this->getPacConf(); unset($pac["{$type}templates"][base64_decode($name)]); $this->setPacConf($pac); $this->templates($type); } public function copyTemplate($name, $type) { $pac = $this->getPacConf(); $pac["{$type}templates"][$name] = json_decode(file_get_contents("/config/$type.json"), true); $this->setPacConf($pac); $this->templates($type); } public function downloadOrigin($type) { $f = new \CURLFile("/config/$type.json", 'application/json', 'origin.json'); $this->sendFile($this->input['chat'], $f); } public function downloadTemplate($type, $name) { $pac = $this->getPacConf(); $f = new \CURLStringFile(json_encode($pac["{$type}templates"][base64_decode($name)], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), base64_decode($name) . '.json', 'application/json'); $this->sendFile($this->input['chat'], $f); } public function defaultTemplate($type, $name) { $pac = $this->getPacConf(); if (!empty($name)) { $pac["default{$type}template"] = $name; } else { unset($pac["default{$type}template"]); } $this->setPacConf($pac); $this->templates($type); } public function templates($type) { $pac = $this->getPacConf(); $domain = $this->getDomain(); $hash = $this->getHashBot(); $text[] = "Menu -> " . $this->i18n('xray') . " -> " . $this->i18n($type) . " templates"; $text[] = <<~outbound~ ~pac~ ~package~ ~process~ ~subnet~ ~block~ ~warp~ ~dns~ ~dnspath~ ~uid~ ~domain~ ~directdomain~ ~cdndomain~ ~short_id~ ~email~ ~public_key~ ~server_name~ ~ip~ TEXT; $templates = $pac["{$type}templates"]; $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/templateAdd $type", ], ]; $data[] = [ [ 'text' => "origin", 'web_app' => ['url' => "https://$domain/pac$hash?t=te&ty=$type"], ], [ 'text' => $this->i18n('download'), 'callback_data' => "/downloadOrigin $type", ], [ 'text' => $this->i18n('copy'), 'callback_data' => "/templateCopy $type", ], [ 'text' => $this->i18n($pac["default{$type}template"] && !empty($pac["{$type}templates"][base64_decode($pac["default{$type}template"])]) ? 'off' : 'on'), 'callback_data' => "/defaultTemplate $type", ], ]; foreach ($templates as $k => $v) { $data[] = [ [ 'text' => $k, 'web_app' => ['url' => "https://$domain/pac$hash?t=te&ty=$type&te=" . urlencode($k)], ], [ 'text' => $this->i18n('download'), 'callback_data' => "/downloadTemplate $type " . base64_encode($k), ], [ 'text' => $this->i18n('delete'), 'callback_data' => "/delTemplate $type " . base64_encode($k), ], [ 'text' => $this->i18n($pac["default{$type}template"] == base64_encode($k) ? 'on' : 'off'), 'callback_data' => "/defaultTemplate $type " . base64_encode($k), ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xray", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function mainOutbound() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} send name", $this->input['message_id'], reply: 'send name', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setMainOutbound', 'args' => [], ]; } public function setMainOutbound($text) { $pac = $this->getPacConf(); if (!empty($text)) { $pac['outbound'] = $text; } else { unset($pac['outbound']); } $this->setPacConf($pac); $this->xray(); } public function getBytes($bytes) { $t = [ 'B', 'KB', 'MB', 'GB', 'TB', ]; foreach ($t as $k => $v) { if ($k == 0) { continue; } if ($bytes / (1024 ** $k) < 1) { return round($bytes / (1024 ** ($k - 1)), 2) . " {$t[$k - 1]}"; } } } public function xray($page = 0) { $c = $this->getXray(); $p = $this->getPacConf(); $text[] = "Menu -> " . $this->i18n('xray'); if (!empty($fake = $c['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0])) { $text[] = "fake domain: $fake"; } $text[] = 'transport: ' . ($p['transport'] ?: 'Websocket'); $st = $this->getXrayStats(); $td = $this->getBytes($st['global']['download'] + $st['session']['download']); $tu = $this->getBytes($st['global']['upload'] + $st['session']['upload']); $text[] = "↓$td ↑$tu"; $data[] = [ [ 'text' => $this->i18n('reset stats'), 'callback_data' => '/resetXrStats', ], [ 'text' => $this->i18n('reset monthly') . ": " . $this->i18n($this->getPacConf()['reset_monthly'] ? 'on' : 'off'), 'callback_data' => '/switchMonthlyStats', ], ]; $data[] = [ [ 'text' => $this->i18n('main outbound name: ') . ($p['outbound'] ?: 'proxy'), 'callback_data' => '/mainOutbound', ], ]; $data[] = [ [ 'text' => $p['linkdomain'] ?: $this->i18n('cdn'), 'callback_data' => '/addLinkDomain', ], ]; $data[] = [ [ 'text' => $this->i18n('Reality') . ' ' . ($p['transport'] == 'Reality' ? $this->i18n('on') : $this->i18n('off')), 'callback_data' => "/changeTransport", ], [ 'text' => $this->i18n('Websocket') . ' ' . ($p['transport'] != 'Reality' ? $this->i18n('on') : $this->i18n('off')), 'callback_data' => "/changeTransport 1", ], ]; $ip_count = $p['ip_count'] ?: 1; $hwidEnabled = !empty($p['hwid_limit_enabled']); $defaultHwids = max(1, (int) ($p['hwid_device_count'] ?: 1)); $data[] = [ [ 'text' => $this->i18n('ip limit') . ' ' . ($p['ip_limit'] ? ": {$p['ip_limit']} sec & $ip_count" : $this->i18n('off')), 'callback_data' => "/setIpLimit", ], ]; $data[] = [ [ 'text' => $this->i18n('hwid limit') . ': ' . $this->i18n($hwidEnabled ? 'on' : 'off') . " ({$defaultHwids})", 'callback_data' => '/toggleHwidLimit xray', ], [ 'text' => $this->i18n('set hwid devices count'), 'callback_data' => '/setHwidDevices xray', ], ]; if ($p['transport'] == 'Reality') { $data[] = [ [ 'text' => $this->i18n('changeFakeDomain'), 'callback_data' => "/changeFakeDomain", ], [ 'text' => $this->i18n('selfFakeDomain'), 'callback_data' => "/selfFakeDomain", ], ]; } $data[] = [ [ 'text' => $this->i18n('v2ray templates'), 'callback_data' => "/templates v2ray", ], [ 'text' => $this->i18n('sing-box templates'), 'callback_data' => "/templates sing", ], [ 'text' => $this->i18n('mihomo templates'), 'callback_data' => "/templates clash", ], ]; $data[] = [ [ 'text' => $this->i18n('routes'), 'callback_data' => "/routes", ], ]; foreach ($c['inbounds'][0]['settings']['clients'] as $k => $v) { if (!empty($v['off'])) { $off++; } else { $on++; } } $type = $this->getPacConf()['xtlslist']; $clients = array_filter($c['inbounds'][0]['settings']['clients'], fn($e) => !$type ? empty($e['off']) : !empty($e['off'])); uasort($clients, fn($a, $b) => ($a['time'] ?: PHP_INT_MAX) <=> ($b['time'] ?: PHP_INT_MAX)); $all = (int) ceil(count($clients) / $this->limit); $page = min($page, $all - 1); $page = $page == -2 ? $all - 1 : $page; $clients = $page != -1 ? array_slice($clients, $page * $this->limit, $this->limit, true) : $clients; foreach ($clients as $k => $v) { $download = $this->getBytes($st['users'][$k]['global']['download'] + $st['users'][$k]['session']['download']); $upload = $this->getBytes($st['users'][$k]['global']['upload'] + $st['users'][$k]['session']['upload']); $time = $v['time'] ? $this->getTime($v['time']) : ''; $data[] = [ [ 'text' => "{$v['email']}" . ($time ? ": $time" : '') . " (↓$download ↑$upload)", 'callback_data' => "/userXr $k", ], ]; } if ($page != -1 && $all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/xray " . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ 'text' => $page + 1, 'callback_data' => "/xray $page", ], [ 'text' => '>>', 'callback_data' => "/xray " . ($page < $all - 1 ? $page + 1 : 0), ], ]; } $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/addXrUser", ], [ 'text' => $this->i18n('on') . " $on " . (!$type ? "✅" : ''), 'callback_data' => "/listXr 0", ], [ 'text' => $this->i18n('off') . " $off " . ($type ? "✅" : ''), 'callback_data' => "/listXr 1", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function routes($page = 0) { $text[] = "Menu -> " . $this->i18n('xray') . ' -> routes'; $data = [ [[ 'text' => $this->i18n('block'), 'callback_data' => "/xtlsblock", ]], [[ 'text' => $this->i18n('warp'), 'callback_data' => "/xtlswarp", ]], [[ 'text' => 'domains', 'callback_data' => "/xtlsproxy", ]], [[ 'text' => "subnet", 'callback_data' => "/xtlssubnet", ]], [[ 'text' => 'process', 'callback_data' => "/xtlsprocess", ]], [[ 'text' => 'package', 'callback_data' => "/xtlsapp", ]], [[ 'text' => $this->i18n('rulesset'), 'callback_data' => "/xtlsrulesset", ]], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xray", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function warpPlus() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter key", $this->input['message_id'], reply: 'enter key', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'addWarpPlus', 'args' => [], ]; } public function addWarpPlus($key) { $c = $this->getPacConf(); if (!empty($key)) { $c['warp'] = $key; $this->send($this->input['chat'], 'Warp registration license: ' . $this->ssh("warp-cli --accept-tos registration license $key 2>&1", 'wp')); } else { unset($c['warp']); } $this->setPacConf($c); sleep(1); $this->warp(); } public function warpStatus() { if (!empty($this->ssh('pgrep warp-svc', 'wp'))) { $st = $this->ssh('curl -m 1 -x socks5://127.0.0.1:40000 https://cloudflare.com/cdn-cgi/trace', 'wp'); preg_match('~warp=(\w+)~', $st, $m); return trim($m[1]) ?: 'off'; } return 'off'; } public function analyzeXray() { while (true) { if (!empty($this->getPacConf()['ip_limit'])) { $this->pool = []; $log = '/logs/xray'; $r = fopen($log, 'r'); fseek($r, 0, SEEK_END); while (true) { $pac = $this->getPacConf(); if (empty($pac['ip_limit'])) { break; } $currentPosition = ftell($r); clearstatcache(); $fileSize = filesize($log); if ($fileSize > $currentPosition) { fseek($r, $currentPosition); // сброс флага feof while (!feof($r)) { $line = fgets($r); if ($line !== false) { $this->frequencyAnalyze($line, $pac); } } } sleep(1); } fclose($r); } sleep(10); } } public function frequencyAnalyze($line, $pac) { if (!empty($this->pool)) { foreach ($this->pool as $i => $j) { if (!empty($j['ips'])) { foreach ($j['ips'] as $k => $v) { if ($v + $pac['ip_limit'] < time()) { unset($this->pool[$i]['ips'][$k]); } } } } } if (empty(preg_match('~(?.+)\sfrom\s(?\d+\.\d+\.\d+\.\d+)(?=.+email:\s(?.+))~', $line, $m))) { return; } if (!empty($m['ip']) && !empty($m['email'])) { $ip = ip2long($m['ip']); if (!empty($this->pool[$m['email']]['ip']) && $this->pool[$m['email']]['ip'] != $ip) { $this->pool[$m['email']]['ips'][$ip] = time(); } if (!empty($this->pool[$m['email']]['ips']) && count($this->pool[$m['email']]['ips']) > ($pac['ip_count'] ?: 1)) { $xr = $this->getXray(); foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) { if (empty($v['off']) && $v['email'] == $m['email']) { require __DIR__ . '/config.php'; foreach ($c['admin'] as $admin) { $this->send($admin, "vless: {$m['email']} limit ip " . count($this->pool[$m['email']]['ips']) . ' > ' . ($pac['ip_count'] ?: 1), button: [[ [ 'text' => $this->i18n($c['off'] ? 'off' : 'on'), 'callback_data' => "/switchXr $k", ], ]]); } unset($this->pool[$m['email']]); break; } } } $this->pool[$m['email']]['ip'] = $ip; } } public function offWarp() { $p = $this->getPacConf(); if (!empty($this->selfupdate)) { if (!empty($p['warpoff'])) { $this->ssh('warp-cli --accept-tos registration delete 2>&1', 'wp'); $this->ssh('pkill warp-svc', 'wp'); } } elseif (!empty($p['warpoff'])) { $this->ssh('warp-svc > /dev/null 2>&1 &', 'wp'); sleep(3); if (empty($this->ssh('[ -f "/var/lib/cloudflare-warp/conf.json" ] && echo 1', 'wp'))) { $this->send($this->input['chat'], 'Registration: ' . $this->ssh('warp-cli --accept-tos registration new 2>&1', 'wp')); if (!empty($p['warp'])) { $this->send($this->input['chat'], 'License: ' . $this->ssh("warp-cli --accept-tos registration license {$p['warp']} 2>&1", 'wp')); } } $this->send($this->input['chat'], 'Proxy mode: ' . $this->ssh('warp-cli --accept-tos mode proxy 2>&1', 'wp')); $this->send($this->input['chat'], 'Connect: ' . $this->ssh('warp-cli --accept-tos connect 2>&1', 'wp')); unset($p['warpoff']); } else { $this->send($this->input['chat'], 'Registration delete: ' . $this->ssh('warp-cli --accept-tos registration delete 2>&1', 'wp')); $this->ssh('pkill warp-svc', 'wp'); $p['warpoff'] = 1; } $this->setPacConf($p); if (empty($this->selfupdate)) { $this->warp(); } } public function warp() { $p = $this->getPacConf(); $text[] = "Menu -> " . $this->i18n('warp'); $text[] = "status: " . $this->warpStatus(); $text[] = "key: {$this->getPacConf()['warp']}"; $data[] = [ [ 'text' => $this->i18n($p['warpoff'] ? 'off' : 'on'), 'callback_data' => "/offWarp", ], ]; $data[] = [ [ 'text' => $this->i18n('set key'), 'callback_data' => "/warpPlus", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function choiceTemplate($arg) { $arg = explode('_', $arg); $c = $this->getXray(); if (!empty($arg[2])) { $c['inbounds'][0]['settings']['clients'][$arg[1]]["{$arg[0]}template"] = $arg[2]; } else { unset($c['inbounds'][0]['settings']['clients'][$arg[1]]["{$arg[0]}template"]); } file_put_contents('/config/xray.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->userXr($arg[1]); } public function templateUser($type, $i) { $c = $this->getXray(); $pac = $this->getPacConf(); $text[] = "Menu -> " . $this->i18n('xray') . " -> {$c['inbounds'][0]['settings']['clients'][$i]['email']}\n"; $templates = $pac["{$type}templates"]; $data[] = [ [ 'text' => 'default', 'callback_data' => "/choiceTemplate {$type}_$i", ], ]; $data[] = [ [ 'text' => 'origin', 'callback_data' => "/choiceTemplate {$type}_{$i}_" . base64_encode('origin'), ], ]; foreach ($templates as $k => $v) { $data[] = [ [ 'text' => $k, 'callback_data' => "/choiceTemplate {$type}_{$i}_" . base64_encode($k), ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/userXr $i", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function userXr($i) { $xray = $this->getXray(); $c = $xray['inbounds'][0]['settings']['clients'][$i]; $pac = $this->getPacConf(); $domain = $this->getDomain($pac['transport'] != 'Reality'); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); $devices = $this->getHwidDevicesByUser($c['id']); $hwidEnabled = !empty($pac['hwid_limit_enabled']) && empty($c['hwid_disabled']); $defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1)); $hwidLimit = $c['hwid_limit'] ? (int) $c['hwid_limit'] : $defaultHwid; $text[] = "Menu -> " . $this->i18n('xray') . " -> {$c['email']}\n"; if (file_exists(__DIR__ . '/subscription.php')) { $text[] = "subscription"; } $text[] = "
{$this->linkXray($i)}
\n"; $text[] = "import://v2rayng"; $text[] = "import://sing-box"; $text[] = "import://streisand"; $text[] = "import://hiddify"; $text[] = "import://karing"; $text[] = "import://mihomo"; $si = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $c['id'], ])); $xr = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', 's' => $c['id'], ])); $cl = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'cl', 's' => $c['id'], ])); $text[] = "\nxray config:
$xr
"; $text[] = "sing-box config:
$si
"; $text[] = "mihomo config:
$cl
"; $text[] = "sing-box windows: windows service"; $st = $this->getXrayStats(); $download = $this->getBytes($st['users'][$i]['global']['download'] + $st['users'][$i]['session']['download']); $upload = $this->getBytes($st['users'][$i]['global']['upload'] + $st['users'][$i]['session']['upload']); $data[] = [ [ 'text' => $this->i18n('reset stats') . ": ↓$download ↑$upload", 'callback_data' => "/resetXrUser $i", ], ]; $data[] = [ [ 'text' => $this->i18n('v2ray'), 'web_app' => ['url' => "https://{$domain}/pac$hash?t=s&s={$c['id']}"] ], [ 'text' => $this->i18n('singbox'), 'web_app' => ['url' => "https://{$domain}/pac$hash?t=si&s={$c['id']}"] ], [ 'text' => $this->i18n('mihomo'), 'web_app' => ['url' => "https://{$domain}/pac$hash?t=cl&s={$c['id']}"] ], ]; $data[] = [ [ 'text' => $this->i18n('v2ray ⬇️'), 'callback_data' => "/dw {$i} s", ], [ 'text' => $this->i18n('singbox ⬇️'), 'callback_data' => "/dw {$i} si", ], [ 'text' => $this->i18n('mihomo ⬇️'), 'callback_data' => "/dw {$i} cl", ], ]; $data[] = [ [ 'text' => $c['time'] ? "timer: " . $this->getTime($c['time']) : $this->i18n('timer'), 'callback_data' => "/timerXr $i", ], [ 'text' => $this->i18n($c['off'] ? 'off' : 'on'), 'callback_data' => "/switchXr $i", ], ]; $singtemplate = $c['singtemplate'] ? base64_decode($c['singtemplate']) : 'default(' . ($pac['defaultsingtemplate'] && !empty($pac['singtemplates'][base64_decode($pac['defaultsingtemplate'])]) ? base64_decode($pac['defaultsingtemplate']) : 'origin') . ')'; $v2raytemplate = $c['v2raytemplate'] ? base64_decode($c['v2raytemplate']) : 'default(' . ($pac['defaultv2raytemplate'] && !empty($pac['v2raytemplates'][base64_decode($pac['defaultv2raytemplate'])]) ? base64_decode($pac['defaultv2raytemplate']) : 'origin') . ')'; $clashtemplate = $c['clashtemplate'] ? base64_decode($c['clashtemplate']) : 'default(' . ($pac['defaultclashtemplate'] && !empty($pac['clashtemplates'][base64_decode($pac['defaultclashtemplate'])]) ? base64_decode($pac['defaultclashtemplate']) : 'origin') . ')'; $data[] = [ [ 'text' => $this->i18n('v2ray') . ": $v2raytemplate", 'callback_data' => "/templateUser v2ray $i", ], [ 'text' => $this->i18n('singbox') . ": $singtemplate", 'callback_data' => "/templateUser sing $i", ], [ 'text' => $this->i18n('mihomo') . ": $clashtemplate", 'callback_data' => "/templateUser clash $i", ], ]; $data[] = [ [ 'text' => $this->i18n('qr short'), 'callback_data' => "/qrXray $i", ], [ 'text' => $this->i18n('qr v2ray'), 'callback_data' => "/qrXray {$i}_1", ], [ 'text' => $this->i18n('qr singbox'), 'callback_data' => "/qrXray {$i}_2", ], ]; $data[] = [ [ 'text' => $this->i18n('hwid limit') . ': ' . ($hwidEnabled ? $hwidLimit : $this->i18n('off')) . ' (' . count($devices) . ')', 'callback_data' => "/hwidUser $i", ], ]; $data[] = [ [ 'text' => $this->i18n('rename'), 'callback_data' => "/renameXrUser $i", ], [ 'text' => $this->i18n('delete'), 'callback_data' => "/delxr $i", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/xray", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function hwidUser($i, $page = 0) { $xray = $this->getXray(); $client = $xray['inbounds'][0]['settings']['clients'][$i]; $pac = $this->getPacConf(); $devices = $this->getHwidDevicesByUser($client['id']); $scope = $this->getHwidTokenScope($i); if (!isset($_SESSION['hwidTokens'])) { $_SESSION['hwidTokens'] = []; } $_SESSION['hwidTokens'][$scope] = []; uasort($devices, fn($a, $b) => ($b['time'] ?? 0) <=> ($a['time'] ?? 0)); $hwids = array_keys($devices); $perPage = max(1, $this->limit ?: 5); $total = count($hwids); $pages = max(1, (int) ceil($total / $perPage)); $page = min(max((int) $page, 0), $pages - 1); $hwidsPage = array_slice($hwids, $page * $perPage, $perPage); $defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1)); $text[] = "Menu -> " . $this->i18n('xray') . " -> {$client['email']} -> " . $this->i18n('hwid devices'); $text[] = $this->i18n('hwid notice'); if (empty($pac['hwid_limit_enabled'])) { $status = $this->i18n('off'); } elseif (!empty($client['hwid_disabled'])) { $status = $this->i18n('off'); } elseif (!empty($client['hwid_limit'])) { $status = (int) $client['hwid_limit']; } else { $status = "default($defaultHwid)"; } $text[] = $this->i18n('hwid limit') . ': ' . $status; $text[] = $this->i18n('hwid devices') . ': ' . $total; $data[] = [ [ 'text' => $this->i18n(!empty($client['hwid_disabled']) ? 'off' : 'on'), 'callback_data' => "/hwidUserToggle $i", ], ]; $data[] = [ [ 'text' => $this->i18n('set hwid devices count'), 'callback_data' => "/setHwidUserLimit $i", ], ]; if (!empty($client['hwid_limit'])) { $data[] = [ [ 'text' => $this->i18n('use default hwid limit'), 'callback_data' => "/hwidUserDefault $i", ], ]; } if ($total == 0) { $text[] = $this->i18n('no devices'); } foreach ($hwidsPage as $index => $hwid) { $info = $devices[$hwid]; $number = $page * $perPage + $index + 1; $details = array_filter([ $info['device_os'] ?? '', $info['os_version'] ?? '', $info['device_model'] ?? '', ], fn($v) => $v !== ''); $line = $number . '. ' . htmlspecialchars($hwid, ENT_HTML5, 'UTF-8') . ''; if (!empty($details)) { $line .= ' - ' . htmlspecialchars(implode(' ', $details), ENT_HTML5, 'UTF-8'); } if (!empty($info['time'])) { $line .= ' (' . date('d.m.Y H:i', $info['time']) . ')'; } $text[] = $line; if (!empty($info['user_agent'])) { $text[] = 'UA: ' . htmlspecialchars($info['user_agent'], ENT_HTML5, 'UTF-8'); } $token = $this->rememberHwidToken($scope, $hwid); $data[] = [ [ 'text' => '🗑 ' . $number, 'callback_data' => "/hwidUserDel {$i}_{$page} $token", ], ]; } if ($pages > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/hwidUser {$i}_" . ($page - 1 >= 0 ? $page - 1 : $pages - 1), ], [ 'text' => ($page + 1) . '/' . $pages, 'callback_data' => "/hwidUser {$i}_$page", ], [ 'text' => '>>', 'callback_data' => "/hwidUser {$i}_" . (($page + 1) % $pages), ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/userXr $i", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function hwidUserToggle($i) { $xray = $this->getXray(); if (!empty($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'])) { unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled']); } else { $xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'] = 1; } file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true); $this->hwidUser($i); } public function hwidUserDefault($i) { $xray = $this->getXray(); unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']); file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->hwidUser($i); } public function setHwidUserLimit($i) { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter hwid devices count", $this->input['message_id'], reply: 'enter hwid devices count', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'callback' => 'saveHwidUserLimit', 'args' => [$i], ]; } public function saveHwidUserLimit($count, $i) { $xray = $this->getXray(); $count = (int) $count; if ($count > 0) { $xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit'] = $count; } else { unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']); } file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']); $this->hwidUser($i); } public function hwidUserDel($i, $page, $hwid) { $xray = $this->getXray(); $uid = $xray['inbounds'][0]['settings']['clients'][$i]['id']; $scope = $this->getHwidTokenScope($i); $decoded = $this->resolveHwidToken($scope, $hwid); if ($decoded !== '') { $this->deleteHwidDevice($uid, $decoded); } $this->hwidUser($i, $page); } public function getDomain($cdn = false) { $c = $this->getPacConf(); if ($cdn && $c['linkdomain']) { return $c['linkdomain']; } return $c['domain'] ?: $this->ip; } public function sub() { $xr = $this->getXray(); $pac = $this->getPacConf(); $st = $this->getXrayStats(); $domain = $_GET['cdn'] ?: ($_SERVER['SERVER_NAME'] ?: $this->getDomain($pac['transport'] != 'Reality')); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); $flag = true; $client = null; foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) { if ($v['id'] == $_GET['id']) { if (empty($v['off'])) { $flag = false; } $uid = $v['id']; $email = $v['email']; $expire = $v['time']; $client = $v; break; } } if (!$flag && !$this->processHwidRequest($client)) { exit; } $suburl = "subscription"; $download = $this->getBytes($st['users'][$k]['global']['download'] + $st['users'][$k]['session']['download']); $upload = $this->getBytes($st['users'][$k]['global']['upload'] + $st['users'][$k]['session']['upload']); $singbox = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $uid, ])); $xray = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', 's' => $uid, ])); $clash = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'cl', 's' => $uid, ])); $vless = $this->linkXray($k); $windows = "$scheme://{$domain}/pac$hash?t=si&r=w&s=$uid"; $_GET['s'] = $uid; foreach ([ 'xray' => 's', 'singbox' => 'si', 'clash' => 'cl' ] as $k => $v) { $_GET['t'] = $v; $configs[$k] = $this->subscription(1); } require __DIR__ . '/subscription.php'; } public function subscription($return = false) { switch ($_GET['t']) { case 's': $type = 'v2ray'; break; case 'si': $type = 'sing'; break; case 'cl': $type = 'clash'; break; } $pac = $this->getPacConf(); $domain = $_GET['cdn'] ?: ($_SERVER['SERVER_NAME'] ?: $this->getDomain($pac['transport'] != 'Reality')); $xr = $this->getXray(); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); $flag = true; $client = null; foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) { if ($v['id'] == $_GET['s']) { if (empty($v['off'])) { $flag = false; } $template = base64_decode($v["{$type}template"]); $uid = $v['id']; $email = $v['email']; $client = $v; break; } } if ($flag) { header('500', true, 500); exit; } if (!$return && !$this->processHwidRequest($client)) { exit; } if (!empty($_GET['r'])) { $si = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $uid, ])); $v2 = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', 's' => $uid, ])); $cl = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'cl', 's' => $uid, ])); switch ($_GET['r']) { case 'si': header("Location: sing-box://import-remote-profile/?url=$si"); exit; case 'st': header("Location: streisand://import/$v2"); exit; case 'v': header("Location: v2rayng://install-config?url=$v2"); exit; case 'k': header("Location: karing://install-config?url=$si"); exit; case 'h': header("Location: hiddify://install-config/?url=$si"); exit; case 'c': header("Location: clash://install-config/?url=$cl&overwrite=no&name=$email"); exit; case 'w': $link = htmlspecialchars($si, ENT_XML1, 'UTF-8'); $n = "singbox_$uid.zip"; copy('/singbox/singbox.zip', $n); $zip = new ZipArchive(); $zip->open($n, ZipArchive::CREATE); $zip->addFromString('winsw3.xml', preg_replace('#~url~#', $link, file_get_contents('/singbox/winsw3.xml'))); $zip->close(); header('Content-Disposition: attachment; filename="singbox.zip"'); echo file_get_contents($n); unlink($n); exit; } } switch (true) { case !empty($template) && $template == 'origin': case empty($template) && empty($pac["default{$type}template"]): case empty($template) && empty($pac["{$type}templates"][base64_decode($pac["default{$type}template"])]): case !empty($template) && empty($pac["{$type}templates"][$template]): $c = json_decode(file_get_contents("/config/{$type}.json"), true); break; case !empty($template): $c = $pac["{$type}templates"][$template]; break; default: $c = $pac["{$type}templates"][base64_decode($pac["default{$type}template"])]; break; } $outbound = $pac['outbound'] ?: 'proxy'; $c = json_decode($this->replaceTags(json_encode($c), [ '~outbound~' => $outbound, ]), true); foreach ($c['outbounds'] as $k => $v) { if ($v['tag'] == $outbound) { $index = $k; break; } } if (!isset($index)) { foreach ($c['proxies'] as $k => $v) { if ($v['name'] == $outbound) { $index = $k; break; } } } switch ($_GET['t']) { case 's': $c['outbounds'][$index]['settings']['vnext'][0]['address'] = '~domain~'; $c['outbounds'][$index]['settings']['vnext'][0]['users'][0] = [ 'id' => '~uid~', 'encryption' => 'none', ]; $fingerprint = $c['outbounds'][$index]['streamSettings']['realitySettings']['fingerprint'] ?? $c['outbounds'][$index]['streamSettings']['tlsSettings']['fingerprint'] ?? 'chrome'; if ($pac['transport'] != 'Reality') { $c['outbounds'][$index]['streamSettings'] = [ "network" => "ws", "security" => "tls", "wsSettings" => [ "path" => "/ws$hash?ed=2560" ], "tlsSettings" => [ "allowInsecure" => false, "serverName" => '~domain~', "fingerprint" => $fingerprint ] ]; unset($c['outbounds'][$index]['mux']); } else { $c['outbounds'][$index]['settings']['vnext'][0]['users'][0]["flow"] = "xtls-rprx-vision"; $c['outbounds'][$index]['streamSettings'] = [ "network" => "tcp", "security" => "reality", "realitySettings" => [ "serverName" => '~server_name~', "fingerprint" => $fingerprint, "publicKey" => '~public_key~', "shortId" => '~short_id~', ] ]; $c['outbounds'][$index]['mux'] = [ "enabled" => false, "concurrency" => -1 ]; } break; case 'si': $c['outbounds'][$index]['uuid'] = '~uid~'; if ($pac['transport'] != 'Reality') { unset($c['outbounds'][$index]['tls']['reality']); unset($c['outbounds'][$index]['flow']); $c['outbounds'][$index]["transport"] = [ "type" => "ws", "path" => "/ws$hash" ]; $c['outbounds'][$index]['tls']['server_name'] = '~domain~'; } else { unset($c['outbounds'][$index]["transport"]); $c['outbounds'][$index]['flow'] = 'xtls-rprx-vision'; $c['outbounds'][$index]['tls']['reality']['public_key'] = '~public_key~'; $c['outbounds'][$index]['tls']['server_name'] = '~server_name~'; $c['outbounds'][$index]['tls']['reality']['short_id'] = '~short_id~'; } break; case 'cl': $c['proxies'][$index]['server'] = '~domain~'; $c['proxies'][$index]['uuid'] = '~uid~'; if ($pac['transport'] != 'Reality') { unset($c['proxies'][$index]['flow']); unset($c['proxies'][$index]['reality-opts']); $c['proxies'][$index]["network"] = "ws"; $c['proxies'][$index]["ws-opts"]['path'] = "/ws$hash"; $c['proxies'][$index]["skip-cert-verify"] = false; $c['proxies'][$index]['servername'] = '~domain~'; } else { unset($c['proxies'][$index]["ws-opts"]); unset($c['proxies'][$index]["skip-cert-verify"]); $c['proxies'][$index]["network"] = "tcp"; $c['proxies'][$index]['flow'] = 'xtls-rprx-vision'; $c['proxies'][$index]['servername'] = '~server_name~'; $c['proxies'][$index]['reality-opts'] = [ 'public-key' => '~public_key~', 'short-id' => '~short_id~', ]; } break; } $c = json_decode($this->replaceTags(json_encode($c), [ '"~pac~"' => json_encode(array_keys(array_filter($pac['includelist'] ?: []))), '"~block~"' => json_encode(array_keys(array_filter($pac['blocklist'] ?: []))), '"~warp~"' => json_encode(array_keys(array_filter($pac['warplist'] ?: []))), '"~process~"' => json_encode(array_keys(array_filter($pac['processlist'] ?: []))), '"~package~"' => json_encode(array_keys(array_filter($pac['packagelist'] ?: []))), '"~subnet~"' => json_encode(array_keys(array_filter($pac['subnetlist'] ?: []))), '~dns~' => "https://$domain/dns-query$hash/$uid", '~dnspath~' => "/dns-query$hash/$uid", '~uid~' => $uid, '~domain~' => $domain, '~directdomain~' => $pac['domain'], '~cdndomain~' => $pac['linkdomain'], '~short_id~' => $xr['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0], '~email~' => $email, '~public_key~' => $pac['xray'], '~server_name~' => $xr['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0], '~ip~' => $this->ip, ]), true); switch ($_GET['t']) { case 's': if (!empty($c['routing']['rules'])) { $ips = $domains = []; foreach ($c['routing']['rules'] as $k => $v) { if (array_key_exists('domain', $v) && !empty($v['domain'])) { foreach ($v['domain'] as $j) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $j)) { $domains[$v['outboundTag']][] = $j; } else { $ips[$v['outboundTag']][] = $j; } } } if (array_key_exists('ip', $v) && !empty($v['ip'])) { foreach ($v['domain'] as $j) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $j)) { $domains[$v['outboundTag']][] = $j; } else { $ips[$v['outboundTag']][] = $j; } } } } $c['routing']['rules'] = []; if (!empty($domains)) { foreach ($domains as $k => $v) { $c['routing']['rules'][] = [ "type" => "field", "outboundTag" => $k, "domain" => $v ]; } } if (!empty($ips)) { foreach ($ips as $k => $v) { $c['routing']['rules'][] = [ "type" => "field", "outboundTag" => $k, "ip" => $v ]; } } } break; case 'si': $c['route'] = $this->addRuleSet($c['route']); $c['route'] = $this->createRuleSet($c['route'], $uid, $domain); if (!empty($c['route']['rules'])) { foreach ($c['route']['rules'] as $k => $v) { if (count($v) == 1 && array_key_exists('outbound', $v)) { unset($c['route']['rules'][$k]); } } $c['route']['rules'] = array_values($c['route']['rules']); } if (empty($c['route'])) { unset($c['route']); } break; case 'cl': $c = $this->addClashRuleSet($c); if (!empty($c['rules'])) { $c = $this->clashRules($c, $uid, $domain); if (count($c['rules']) == 1) { unset($c['rules']); } } break; } if (!empty($return)) { if ($_GET['t'] == 'cl') { return yaml_emit($c); } return json_encode($c); } if ($_GET['t'] == 'cl') { header('Content-type: text/yaml'); echo yaml_emit($c); return; } header('Content-type: application/json'); echo json_encode($c); } public function addClashRuleSet($c) { $p = $this->getPacConf(); if (!empty($p['rulessetlist']) && $c['add-rule-providers']) { foreach ($p['rulessetlist'] as $k => $v) { if (!empty($v)) { [$type, $behavior, $time, $url] = explode(':', $k, 4); if (preg_match('~\.(mrs|yaml|yml)$~', $url, $m)) { $c['rule-providers'][$url] = [ 'type' => 'http', 'url' => $url, 'interval' => (int) $time, 'behavior' => $behavior, 'format' => $m[1], ]; switch ($type) { case 'reject': case 'REJECT': array_unshift($c['rules'], [ 'RULE-SET', $url, strtoupper($type) ]); break; default: array_splice($c['rules'], count($c['rules']) - 1, 0, [[ 'RULE-SET', $url, strtoupper($type) ]]); break; } } } } } unset($c['add-rule-providers']); if (empty($c['rule-providers'])) { unset($c['rule-providers']); } return $c; } public function clashRules($c, $uid, $domain) { $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); foreach ($c['rules'] as $v) { if (array_key_exists('list', $v)) { if ($v['type'] == 'RULE-SET') { if (!empty($_GET['r']) && $v['name'] == $_GET['r']) { header("Content-Disposition: attachment; filename={$v['name']}.yaml"); header('Content-Type: text/yaml'); switch ($v['name']) { case 'process': case 'package': echo yaml_emit(['payload' => array_map(fn($e) => "PROCESS-NAME,$e", $v['list'])]); break; case 'pac': echo yaml_emit(['payload' => array_map(fn($e) => "+.$e", $v['list'])]); break; case 'subnet': echo yaml_emit(['payload' => array_map(fn($e) => $e, $v['list'])]); break; default: echo yaml_emit(['payload' => array_map(function($e) { if (preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $e)) { return "IP-CIDR,$e"; } else { return "DOMAIN-SUFFIX,$e"; } }, $v['list'])]); break; } exit; } $c['rule-providers'][$v['name']] = [ 'type' => 'http', 'url' => "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'cl', 's' => $uid, 'r' => $v['name'], ])), 'interval' => $v['interval'], 'behavior' => $v['behavior'], 'format' => 'yaml', ]; $tmp[] = "{$v['type']}, {$v['name']}, {$v['action']}"; } else { if (!empty($v['list'])) { foreach ($v['list'] as $j) { $tmp[] = "{$v['type']}, $j, {$v['action']}"; } } } } else { $tmp[] = implode(', ', $v); } } $c['rules'] = $tmp; return $c; } public function replaceTags($subject, $tags) { return str_replace(array_keys($tags), array_values($tags), $subject); } public function addRuleSet($route) { if (!empty($route['rules'])) { foreach ($route['rules'] as $k => $v) { if (!empty($v['addruleset'])) { $t[$v['outbound'] ?: 'block'] = $k; } } $p = $this->getPacConf(); if (!empty($p['rulessetlist'])) { foreach ($p['rulessetlist'] as $k => $v) { if (!empty($v)) { [$type, $time, $url] = explode(':', $k, 3); if (preg_match('~\.srs$~', $url) && !empty($route['rules'][$t[$type]])) { $route['rule_set'][] = [ "tag" => $k, "type" => "remote", "format" => "binary", "url" => $url, "download_detour" => "direct", "update_interval" => $time ]; $route['rules'][$t[$type]]['rule_set'][] = $k; } } } } foreach ($route['rules'] as $k => $v) { unset($route['rules'][$k]['addruleset']); } } return $route; } public function cleanEmptyKeys(array $arr) { foreach ($arr as $k => $v) { if (empty($v)) { unset($arr[$k]); } elseif (is_array($v)) { $arr[$k] = $this->cleanEmptyKeys($v); if (empty($arr[$k])) { unset($arr[$k]); } } } return $arr; } public function createSrs(string $name, array $rules) { $rules = $this->cleanEmptyKeys($rules); header("Content-Disposition: attachment; filename=$name.srs"); header('Content-Type: application/binary'); $f = "/tmp/$name" . time() . rand(1, 100); foreach ($rules as $k => $v) { if (array_key_exists('domain_suffix', $v)) { foreach ($v['domain_suffix'] as $j) { if (!preg_match('~^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(/\d{1,2})?$~', $j)) { $domains[] = $j; } else { $ips[] = $j; } } unset($rules[$k]['domain_suffix']); if (!empty($domains)) { $rules[$k]['domain_suffix'] = $domains; } if (!empty($ips)) { $rules[$k]['ip_cidr'] = $ips; } } } file_put_contents($f, json_encode([ 'version' => 1, 'rules' => $rules ?: [], ])); exec("sing-box rule-set compile $f"); echo file_get_contents("$f.srs"); unlink($f); unlink("$f.srs"); exit; } public function createRuleSet($route, $uid, $domain) { $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = $this->getHashBot(); foreach ($route['rules'] as $k => $v) { if (!empty($v['createruleset'])) { foreach ($v['createruleset'] as $r) { if (!empty($_GET['r']) && $r['name'] == $_GET['r']) { $this->createSrs($r['name'], $r['rules']); } $ruleset[] = [ "tag" => $r['name'], "url" => "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $uid, 'r' => $r['name'], ])), "update_interval" => $r['interval'], "type" => "remote", "format" => "binary", "download_detour" => "direct", ]; $route['rules'][$k]['rule_set'][] = $r['name']; } unset($route['rules'][$k]['createruleset']); if (empty($route['rules'][$k]['rule_set'])) { unset($route['rules'][$k]); } } } if (!empty($route['rules'])) { $route['rules'] = array_values($route['rules']); } $route['rule_set'] = array_merge($route['rule_set'] ?: [], $ruleset ?: []); if (empty($route['rule_set'])) { unset($route['rule_set']); } return $route; } public function getXray() { return json_decode(file_get_contents('/config/xray.json'), true); } public function setUpstreamDomain($domain) { $nginx = file_get_contents('/config/upstream.conf'); $t = preg_replace('~#domain.+#domain~s', "#domain\n$domain reality;\n#domain", $nginx); file_put_contents('/config/upstream.conf', $t); $this->ssh("nginx -s reload 2>&1", 'up'); } public function setUpstreamDomainOcserv($domain) { $sub = $this->getHashSubdomain('oc'); $nginx = file_get_contents('/config/upstream.conf'); $t = preg_replace('~#ocserv.+#ocserv~s', $domain ? "#ocserv\n$sub.$domain ocserv;\n#ocserv" : "#ocserv\n#$sub.\$domain ocserv;\n#ocserv", $nginx); file_put_contents('/config/upstream.conf', $t); $this->ssh("nginx -s reload 2>&1", 'up'); } public function setUpstreamDomainNaive($domain) { $sub = $this->getHashSubdomain('np'); $nginx = file_get_contents('/config/upstream.conf'); $t = preg_replace('~#naive.+#naive~s', $domain ? "#naive\n$sub.$domain naive;\n#naive" : "#naive\n#$sub.\$domain naive;\n#naive", $nginx); file_put_contents('/config/upstream.conf', $t); $this->ssh("nginx -s reload 2>&1", 'up'); } public function getHashBot($notset = false) { $p = $this->getPacConf(); if (!empty($p['hashbot'])) { return $p['hashbot']; } $p['hashbot'] = substr(hash('sha256', $this->key), 0, 8); if (empty($notset)) { $this->setPacConf($p); } return $p['hashbot']; } public function cloakNginx() { $conf = $this->getPacConf(); $template = file_get_contents('/config/nginx_default.conf'); // $template = preg_replace('~server_name ip~', "server_name {$this->ip}", $template); $template = preg_replace('~server_name domain~', "server_name " . ($conf['domain'] ? " *.{$conf['domain']} {$conf['domain']}" : '_'), $template); if ($conf['domain'] && $conf['letsencrypt']) { $template = preg_replace('/#~([^\n]+)?/', "#~{$conf['letsencrypt']}", $template); preg_match_all('~#-domain.+?#-domain~s', $template, $m); foreach ($m[0] as $v) { $template = preg_replace('~#-domain.+?#-domain~s', $this->uncomment($v, 'domain'), $template, 1); } } $h = $this->getHashBot(); $s = empty($conf['adgbrowser']) ? '' : '#'; $r = <<getXray(); if (!empty($x['inbounds'][0]['streamSettings']['wsSettings']['path'])) { $x['inbounds'][0]['streamSettings']['wsSettings']['path'] = "/ws$h"; $this->restartXray($x); } return $this->ssh('nginx -s reload', 'ng'); } public function getHashSubdomain($subdomain) { $p = $this->getPacConf(); if (!empty($p["{$subdomain}_domain"])) { return $p["{$subdomain}_domain"]; } $p["{$subdomain}_domain"] = substr(hash('sha256', "$subdomain{$this->key}"), 0, 8); $this->setPacConf($p); return $p["{$subdomain}_domain"]; } public function addWg($page) { $text = "Menu -> {$this->getTitleWG()} -> Add peer\n\n"; $data[] = [ [ 'text' => $this->i18n('all traffic'), 'callback_data' => "/add", ] ]; $data[] = [ [ 'text' => $this->i18n('subnet'), 'callback_data' => "/add_ips", ] ]; if ($this->getPacConf()['subnets']) { $data[] = [ [ 'text' => $this->i18n('listSubnet'), 'callback_data' => "/addSubnets $page", ] ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu wg $page", ] ]; return [ 'text' => $text, 'data' => $data, ]; } public function adguardBasicAuth() { return base64_encode('admin:' . $this->getPacConf()['adpswd']); } public function adguardChBr() { $c = $this->getPacConf(); $c['adgbrowser'] = $c['adgbrowser'] ? 0 : 1; $this->setPacConf($c); $this->cloakNginx(); $this->answer($this->input['callback_id'], $this->i18n($c['adgbrowser'] ? 'browser_notify_on' : 'browser_notify_off'), true); $this->menu('adguard'); } public function adguardMenu() { $conf = $this->getPacConf(); $ip = $this->ip; $domain = $this->getDomain(); $hash = $this->getHashBot(); $scheme = empty($ssl = $this->nginxGetTypeCert()) ? 'http' : 'https'; $text = "$scheme://$domain/adguard$hash\nLogin: admin\nPass: {$conf['adpswd']}\n\n"; if ($ssl) { $text .= "DNS over HTTPS:\n$ip\n$scheme://$domain/dns-query$hash" . ($conf['adguardkey'] ? "/{$conf['adguardkey']}" : '') . "\n\n"; $text .= "DNS over TLS:\ntls://" . ($conf['adguardkey'] ? "{$conf['adguardkey']}." : '') . "$domain"; } $status = $this->i18n(exec("JSON=1 timeout 2 dnslookup google.com ad") ? 'on' : 'off'); $safesearch = yaml_parse_file($this->adguard)['filtering']['safe_search']['enabled']; $text .= "\n\nstatus: $status\t\tsafesearch: " . $this->i18n($safesearch ? 'on' : 'off'); $allowedClients = yaml_parse_file($this->adguard)['dns']['allowed_clients']; $text .= $allowedClients ? "\n\nallowed clients: \n - " . implode("\n - ", $allowedClients) : ''; $data = [ [ [ 'text' => 'web panel', 'web_app' => [ "url" => "https://$domain/adguard$hash" ], ], [ 'text' => $this->i18n('third party browser') . ': ' . $this->i18n($conf['adgbrowser'] ? 'on' : 'off'), 'callback_data' => '/adguardChBr' ], ], [ [ 'text' => $this->i18n('change password'), 'callback_data' => "/adguardpsswd", ], [ 'text' => 'ClientID' . ($conf['adguardkey'] ? ": {$conf['adguardkey']}" : ''), 'callback_data' => "/setAdguardKey", ], ], ]; $data[] = [ [ 'text' => $this->i18n('fill allowed clients'), 'callback_data' => "/adgFillAllowedClients 0", ], [ 'text' => $this->i18n('delete allowed clients'), 'callback_data' => "/adgFillAllowedClients 1", ], ]; $data[] = [ [ 'text' => $this->i18n('check DNS'), 'callback_data' => "/checkdns", ], [ 'text' => $this->i18n('reset settings'), 'callback_data' => "/adguardreset", ], ]; $data[] = [ [ 'text' => $this->i18n('add upstream'), 'callback_data' => "/addupstream", ], ]; $upstreams = yaml_parse_file($this->adguard)['dns']['upstream_dns']; if (!empty($upstreams)) { foreach ($upstreams as $k => $v) { $data[] = [ [ 'text' => $v, 'callback_data' => "/menu adguard", ], [ 'text' => $this->i18n('delete'), 'callback_data' => "/delupstream $k", ], ]; } } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => $text, 'data' => $data, ]; } public function adgFillAllowedClients($delete = false) { $pac = $this->getPacConf(); $out[] = 'Restart Adguard Home'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->stopAd(); $c = yaml_parse_file($this->adguard); if (!empty($delete)) { unset($c['dns']['allowed_clients']); } else { $c['dns']['allowed_clients'] = []; $c['dns']['allowed_clients'][] = '10.10.0.0/24'; if (!empty($pac['adguardkey'])) { $c['dns']['allowed_clients'][] = $pac['adguardkey']; } $c['dns']['allowed_clients'][] = getenv('WGADDRESS'); $c['dns']['allowed_clients'][] = getenv('WG1ADDRESS'); $c['dns']['allowed_clients'][] = '10.0.2.0/24'; // openconnect if (!empty($xr = $this->getXray())) { foreach ($xr['inbounds'][0]['settings']['clients'] as $v) { $c['dns']['allowed_clients'][] = $v['id']; } } } yaml_emit_file($this->adguard, $c); $this->startAd(); $this->menu('adguard'); } public function menuLang() { $data = []; $lang = []; foreach ($this->i18n as $k => $v) { $lang = array_merge($lang, array_keys($v)); } $lang = array_unique($lang); foreach ($lang as $v) { if ($v != $this->language) { $data[] = [ [ 'text' => $v, 'callback_data' => "/lang $v", ], ]; } } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu config", ], ]; return [ 'text' => 'Language', 'data' => $data, ]; } public function expireCert() { $c = openssl_x509_read(file_get_contents("/certs/cert_public")); return openssl_x509_parse($c)["validTo_time_t"] ?: false; } public function domainsCert() { $domains = openssl_x509_parse(openssl_x509_read(file_get_contents("/certs/cert_public")))['extensions']["subjectAltName"]; if (empty($domains)) { return false; } return array_map(fn($e) => trim($e), explode(',', str_replace('DNS:', '', $domains))); } public function updatebot() { $b = exec('git -C / rev-parse --abbrev-ref HEAD'); $track = trim(file_get_contents('/update/branch')); $data = [ [ [ 'text' => "$b => $track", 'callback_data' => "/branches", ], [ 'text' => $this->i18n('changelog'), 'web_app' => ['url' => "https://raw.githubusercontent.com/mercurykd/vpnbot/$b/version"], ], ], [ [ 'text' => $this->i18n('update bot'), 'callback_data' => "/applyupdatebot", ], ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu config", ], ]; exec("git -C / branch -vv", $mm); return [ 'text' => '
' . htmlentities(implode("\n", $mm)) . '
', 'data' => $data, ]; } public function applyupdatebot() { $this->pinBackup($this->update); $r = $this->send($this->input['from'], 'update...'); file_put_contents('/update/reload_message', "{$this->input['from']}:{$r['result']['message_id']}"); file_put_contents('/update/key', $this->key); file_put_contents('/update/curl', json_encode([ 'chat_id' => $this->input['chat'], 'message_id' => $r['result']['message_id'], 'text' => '~t~' ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); file_put_contents('/update/pipe', '1'); $this->delete($this->input['from'], $this->input['message_id']); } public function restart() { $r = $this->send($this->input['from'], 'restart...'); file_put_contents('/update/reload_message', "{$this->input['from']}:{$r['result']['message_id']}"); file_put_contents('/update/key', $this->key); file_put_contents('/update/curl', json_encode([ 'chat_id' => $this->input['chat'], 'message_id' => $r['result']['message_id'], 'text' => '~t~' ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); file_put_contents('/update/pipe', '2'); $this->delete($this->input['from'], $this->input['message_id']); } public function configMenu() { $conf = $this->getPacConf(); $oc = $this->getHashSubdomain('oc'); $np = $this->getHashSubdomain('np'); if (!empty($conf['domain'])) { $ssl_expiry = $this->expireCert(); $certs = $this->domainsCert() ?: []; $text[] = "
"; $text[] = "Domains:"; $text[] = $conf['domain'] . (in_array($conf['domain'], $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); $text[] = 'naive ' . "$np.{$conf['domain']}" . (in_array("$np.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); $text[] = 'openconnect ' . "$oc.{$conf['domain']}" . (in_array("$oc.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : ''); if (!empty($conf['adguardkey'])) { $text[] = "{$conf['adguardkey']}.{$conf['domain']}" . (in_array("{$conf['adguardkey']}.{$conf['domain']}", $certs) ? ' (ssl: ' . date('Y-m-d H:i:s', $ssl_expiry) . ')' : '') . ' adguard DOT';; } $text[] = "
"; } else { $text[] = $this->i18n('domain explain'); } $data = [ [ [ 'text' => $conf['domain'] ? "{$this->i18n('delete')} {$conf['domain']}" : $this->i18n('install domain'), 'callback_data' => $conf['domain'] ? '/deldomain' : '/domain', ], [ 'text' => $this->i18n('nip.io'), 'callback_data' => '/addNipdomain', ], ], ]; if ($conf['domain']) { if ($cert = $this->nginxGetTypeCert()) { switch ($cert) { case 'letsencrypt': $data[] = [ [ 'text' => $this->i18n('renew SSL'), 'callback_data' => "/setSSL letsencrypt", ], [ 'text' => $this->i18n('delete SSL'), 'callback_data' => "/deletessl", ], ]; break; case 'self': $data[] = [ [ 'text' => $this->i18n('delete SSL'), 'callback_data' => "/deletessl", ], ]; break; } } else { $data[] = [ [ 'text' => $this->i18n('Letsencrypt SSL'), 'callback_data' => "/setSSL letsencrypt", ], [ 'text' => $this->i18n('Self SSL'), 'callback_data' => "/selfssl", ], ]; } } $data[] = [ [ 'text' => $this->i18n('Ports'), 'callback_data' => "/ports", ], [ 'text' => $this->i18n('logs'), 'callback_data' => "/logs", ], [ 'text' => $this->i18n('IP ban'), 'callback_data' => "/ipMenu", ], ]; $data[] = [ [ 'text' => $this->i18n('lang'), 'callback_data' => "/menu lang", ], [ 'text' => "{$this->i18n('page')}: " . ($conf['limitpage'] ?: 5), 'callback_data' => "/enterPage", ], ]; $data[] = [ [ 'text' => $this->i18n('export'), 'callback_data' => "/export", ], [ 'text' => $this->i18n('import'), 'callback_data' => "/import", ], ]; $backup = array_filter(explode('/', $conf['backup'])); if (!empty($backup)) { if (!empty(strtotime($backup[0])) && !empty(strtotime($backup[1]))) { $backup = "{$backup[0]} start / {$backup[1]} period"; } else { $backup = $this->i18n('off') . " {$conf['backup']} - wrong format"; } } $data[] = [ [ 'text' => $this->i18n('backup') . ': ' . ($backup ?: $this->i18n('off')), 'callback_data' => "/backup", ], ]; $data[] = [ [ 'text' => $this->i18n('autoupdate') . ': ' . $this->i18n($conf['autoupdate'] ? 'on' : 'off'), 'callback_data' => "/autoupdate", ], ]; $data[] = [ [ 'text' => $this->i18n('branches'), 'callback_data' => "/menu update", ], [ 'text' => $this->i18n('restart'), 'callback_data' => "/restart", ], ]; $data[] = [ [ 'text' => "{$this->i18n('add')} {$this->i18n('admin')}", 'callback_data' => "/addadmin", ], ]; $file = __DIR__ . '/config.php'; opcache_invalidate($file); require $file; foreach ($c['admin'] as $k => $v) { $data[] = [ [ 'text' => $this->i18n('delete') . " $v", 'callback_data' => "/deladmin $v", ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu", ], ]; return [ 'text' => implode("\n", $text), 'data' => $data, ]; } public function ports() { $text[] = 'Settings -> Ports'; $f = '/docker/compose'; $c = yaml_parse_file($f)['services']; $pac = $this->getPacConf(); $data = [ [[ 'text' => $this->i18n($c['wg'] ? 'on' : 'off') . ' ' . getenv('WGPORT') . ' Wireguard', 'callback_data' => "/hidePort wg", ]], [[ 'text' => $this->i18n($c['wg1'] ? 'on' : 'off') . ' ' . getenv('WG1PORT') . ' Wireguard', 'callback_data' => "/hidePort wg1", ]], [[ 'text' => $this->i18n($c['tg'] ? 'on' : 'off') . ' ' . getenv('TGPORT') . ' MTProto ', 'callback_data' => "/hidePort tg", ]], [[ 'text' => $this->i18n($c['ad'] ? 'on' : 'off') . ' 853 AdguardHome DoT', 'callback_data' => "/hidePort ad", ]], [[ 'text' => $this->i18n($c['ss'] ? 'on' : 'off') . ' 8388 Shadowsocks', 'callback_data' => "/hidePort ss", ]], [[ 'text' => $this->i18n($c['io'] ? 'on' : 'off') . ' 53 Iodine', 'callback_data' => "/hidePort io", ]], ]; if (!empty($pac['restart'])) { $data[] = [ [ 'text' => $this->i18n('restart'), 'callback_data' => "/restart", ], ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu config", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function hidePort($container) { $ports = [ 'wg' => getenv('WGPORT') . ':' . getenv('WGPORT') . '/udp', 'wg1' => getenv('WG1PORT') . ':' . getenv('WG1PORT') . '/udp', 'tg' => getenv('TGPORT') . ':' . getenv('TGPORT'), 'ad' => '853:853', 'ss' => '8388:8388', 'io' => '53:53/udp', ]; $f = '/docker/compose'; $c = yaml_parse_file($f); if (!empty($c['services'][$container])) { unset($c['services'][$container]); } else { $c['services'][$container]['ports'][] = $ports[$container]; } if (empty($c['services'])) { file_put_contents($f, ''); } else { yaml_emit_file($f, $c); } $pac = $this->getPacConf(); $pac['restart'] = 1; $this->setPacConf($pac); $this->ports(); } public function branches() { exec('git -C / branch -r', $m); array_shift($m); foreach ($m as $k => $v) { $data[] = [ [ 'text' => $v, 'callback_data' => "/changeBranch $k", ] ]; } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu update", ] ]; $this->update($this->input['from'], $this->input['message_id'], 'branches', $data); } public function changeBranch($i) { exec('git -C / branch -r', $m); array_shift($m); foreach ($m as $k => $v) { if ($i == $k) { file_put_contents('/update/branch', trim(str_replace('origin/', '', $v))); } } $this->menu('update'); } public function logs() { $p = $this->getPacConf(); foreach (scandir('/logs/') as $k => $v) { if (!preg_match('~^\.~', $v)) { $size = filesize("/logs/$v"); $data[] = [ [ 'text' => "$size $v", 'callback_data' => "/getLog $k", ], [ 'text' => $this->i18n('clean'), 'callback_data' => "/clearLog $k", ], ]; } } $data[] = [ [ 'text' => $this->i18n('clean all'), 'callback_data' => "/cleanLog", ], ]; $autocleanlogs = array_filter(explode('/', $p['autocleanlogs'])); if (!empty($autocleanlogs)) { if (!empty(strtotime($autocleanlogs[0])) && !empty(strtotime($autocleanlogs[1]))) { $autocleanlogs = "{$autocleanlogs[0]} start / {$autocleanlogs[1]} period"; } else { $autocleanlogs = $this->i18n('off') . " {$p['autocleanlogs']} - wrong format"; } } $data[] = [ [ 'text' => $this->i18n('autoclean'). ': ' . ($autocleanlogs ?: $this->i18n('off')), 'callback_data' => "/autoCleanLogs", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu config", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], implode("\n", $text ?: ['...']), $data ?: false, ); } public function getLog($i) { foreach (scandir('/logs/') as $k => $v) { if (!preg_match('~^\.~', $v)) { $logs[$k] = $v; } } $this->sendFile( $this->input['chat'], curl_file_create("/logs/{$logs[$i]}"), ); } public function clearLog($i) { foreach (scandir('/logs/') as $k => $v) { if ($i == $k) { file_put_contents("/logs/$v", ''); break; } } $this->logs(); } public function cleanLog() { foreach (scandir('/logs/') as $k => $v) { file_put_contents("/logs/$v", ''); } $this->logs(); } public function delLog($i) { foreach (scandir('/logs/') as $k => $v) { if ($i == $k) { unlink("/logs/$v"); break; } } $this->logs(); } public function selfUpdate() { $ip = getenv('IP'); $rm = explode(':', trim(file_get_contents('/update/reload_message'))); $m = file_get_contents('/update/message'); $this->input['chat'] = $rm[0]; $this->input['message_id'] = $rm[1]; $this->input['callback_id'] = $rm[1]; if (file_exists($this->update)) { $this->selfupdate = true; if (!empty($m)) { $this->send($this->input['chat'], "
$m
", $rm[1]); } $r = $this->send($this->input['chat'], "import settings"); $this->input['message_id'] = $r['result']['message_id']; $this->input['callback_id'] = $r['result']['message_id']; $this->importFile($this->update); unlink($this->update); } file_put_contents('/update/message', ''); file_put_contents('/update/reload_message', ''); $pac = $this->getPacConf(); unset($pac['restart']); $this->setPacConf($pac); } public function backup() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter like: start / period", $this->input['message_id'], reply: 'enter like: now / 12 hours', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setBackup', 'args' => [], ]; } public function autoCleanLogs() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter like: start / period", $this->input['message_id'], reply: 'enter like: now / 12 hours', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setAutoCleanLogs', 'args' => [], ]; } public function changeFakeDomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter domain", $this->input['message_id'], reply: 'enter domain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setFakeDomain', 'args' => [], ]; } public function changeTGDomain() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter domain", $this->input['message_id'], reply: 'enter domain', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'setTelegramDomain', 'args' => [], ]; } public function setFakeDomain($domain, $self = false) { $c = $this->getXray(); $p = $this->getPacConf(); $c['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0] = $domain; $c['inbounds'][0]['streamSettings']['realitySettings']['dest'] = $self ? "10.10.1.2:443" : "$domain:443"; $p['reality']['domain'] = $domain; $p['reality']['destination'] = $self ? "10.10.1.2:443" : "$domain:443"; $this->setPacConf($p); $this->restartXray($c); $this->setUpstreamDomain($domain); $this->xray(); } public function selfFakeDomain() { $c = $this->getPacConf(); if (!empty($c['domain'])) { $this->setFakeDomain($c['domain'], 1); } else{ $this->answer($this->input['callback_id'], 'empty domain', true); } } public function changeTransport($ws = null) { $p = $this->getPacConf(); $x = $this->getXray(); $h = $this->getHashBot(); $p['reality']['domain'] = $p['reality']['domain'] ?: 'web.telegram.org'; $p['reality']['destination'] = $p['reality']['destination'] ?: $p['reality']['domain'] . ':443'; $p['transport'] = $ws ? 'Websocket' : 'Reality'; if (empty($p['xray'])) { $shortId = trim($this->ssh('openssl rand -hex 8', 'xr')); $keys = $this->ssh('xray x25519', 'xr'); preg_match('~^PrivateKey:\s([^\s]+)~m', $keys, $m); $private = trim($m[1]); preg_match('~^Password:\s([^\s]+)~m', $keys, $m); $public = trim($m[1]); $p['xray'] = $public; $p['reality']['shortId'] = $shortId; $p['reality']['privateKey'] = $private; } if (!empty($ws)) { $p['reality']['domain'] = $x['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0] ?: $p['reality']['domain']; $p['reality']['destination'] = $x['inbounds'][0]['streamSettings']['realitySettings']['dest'] ?: $p['reality']['destination']; $p['reality']['shortId'] = $x['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0] ?: $p['reality']['shortId']; foreach ($x['inbounds'][0]['settings']['clients'] as $k => $v) { unset($x['inbounds'][0]['settings']['clients'][$k]['flow']); } $x['inbounds'][0]['streamSettings'] = [ "network" => "ws", "wsSettings" => [ "path" => "/ws$h" ] ]; } else { foreach ($x['inbounds'][0]['settings']['clients'] as $k => $v) { $x['inbounds'][0]['settings']['clients'][$k]['flow'] = 'xtls-rprx-vision'; } $x['inbounds'][0]['streamSettings'] = [ "network" => "tcp", "realitySettings" => [ "dest" => $p['reality']['destination'] ?: $x['inbounds'][0]['streamSettings']['realitySettings']['dest'], "maxClientVer" => "", "maxTimeDiff" => 0, "minClientVer" => "", "privateKey" => $p['reality']['privateKey'], "serverNames" => [ $p['reality']['domain'] ?: $x['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0] ], "shortIds" => [$p['reality']['shortId']] ?: $x['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0], "show" => false, "xver" => 0 ], "tcpSettings" => [ "acceptProxyProtocol" => true ], "sockopt" => [ "acceptProxyProtocol" => true ], "security" => "reality" ]; } $this->setUpstreamDomain($ws ? 't' : ($p['reality']['domain'] ?: $x['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0])); $this->setPacConf($p); $this->restartXray($x); $this->xray(); } public function setBackup($text) { $text = trim($text); $c = $this->getPacConf(); if (empty($text)) { $c['backup'] = ''; } else { [$start, $period] = explode('/', $text); if (!empty(strtotime($start)) && !empty(strtotime($period))) { $c['backup'] = implode(' / ', [date('Y-m-d H:i', strtotime($start)), trim($period)]); } else { $this->send($this->input['from'], $this->input['message'] . ' - wrong format'); } } if ($c['pinbackup']) { $this->pinAdmin($c['pinbackup'], 1); $c['pinbackup'] = ''; } $this->setPacConf($c); $this->menu('config'); } public function setAutoCleanLogs($text) { $text = trim($text); $c = $this->getPacConf(); if (empty($text)) { $c['autocleanlogs'] = ''; } else { [$start, $period] = explode('/', $text); if (!empty(strtotime($start)) && !empty(strtotime($period))) { $c['autocleanlogs'] = implode(' / ', [date('Y-m-d H:i', strtotime($start)), trim($period)]); } else { $this->send($this->input['from'], $this->input['message'] . ' - wrong format'); } } $this->setPacConf($c); $this->logs(); } public function debug() { $file = __DIR__ . '/config.php'; require $file; $c['debug'] = !$c['debug']; file_put_contents($file, "menu('config'); } public function getStatusPeer(string $publickey, array $peers) { foreach ($peers as $k => $v) { if ($v['peer'] == $publickey) { return $v; } } } public function getInstanceWG($k = false) { if (!empty($k)) { return ($this->wg ?? $this->getPacConf()['wg_instance']) ? 'wg1_' : ''; } return ($this->wg ?? $this->getPacConf()['wg_instance']) ? 'wg1' : 'wg'; } public function readConfig() { $r = $this->ssh('cat /etc/wireguard/wg0.conf', $this->getInstanceWG()); $r = explode(PHP_EOL, $r); $r = array_filter($r); $i = 0; foreach ($r as $k => $v) { if (preg_match('~\[(.+)\]~', $v, $m)) { $i++; if ($m[1] == 'Interface') { $data[$i]['type'] = 'interface'; } else { $data[$i]['type'] = 'peer'; } } else { $t = explode('=', $v, 2); $data[$i][trim($t[0])] = trim($t[1]); } } foreach ($data as $v) { $type = $v['type']; unset($v['type']); if ($type == 'interface') { $d['interface'] = $v; } else { $d['peers'][] = $v; } } return $d; } public function nginxGetTypeCert() { $conf = $this->ssh('cat /etc/nginx/nginx.conf', 'ng'); preg_match("/#~([^\s]+)/", $conf, $m); return $m[1]; } public function readStatus() { $r = $this->ssh($this->getWGType(), $this->getInstanceWG()); $r = explode(PHP_EOL, $r); $r = array_filter($r); $i = 0; foreach ($r as $k => $v) { if (preg_match('~^(interface|peer):~', $v, $m)) { $i++; if ($m[1] == 'interface') { $data[$i]['type'] = 'interface'; } else { $data[$i]['type'] = 'peer'; } } $t = explode(':', $v, 2); $data[$i][trim($t[0])] = trim($t[1]); } foreach ($data as $v) { $type = $v['type']; unset($v['type']); if ($type == 'interface') { $d['interface'] = $v; } else { $d['peers'][] = $v; } } return $d; } public function getName(array $a): string { $name = ''; foreach ($a as $k => $v) { if (preg_match('~^#.*name$~', $k)) { $name = $v; } } $name = $name ?: $a['AllowedIPs'] ?: $a['Address']; return $name; } public function createConfig($data) { $pac = $this->getPacConf(); $conf[] = "[Interface]"; if (empty($data['interface']['ListenPort'])) { if (empty($data['interface']['DNS'])) { $data['interface']['DNS'] = $pac[$this->getInstanceWG(1) . 'dns'] ?: $this->dns; } if (empty($data['interface']['MTU'])) { $data['interface']['MTU'] = $pac[$this->getInstanceWG(1) . 'mtu'] ?: $this->mtu; } } foreach ($data['interface'] as $k => $v) { $conf[] = "$k = $v"; } if (!empty($data['peers'])) { foreach ($data['peers'] as $peer) { $conf[] = ''; $conf[] = $peer['# PublicKey'] ? '# [Peer]' : '[Peer]'; if (!empty($peer['Endpoint'])) { $peer['Endpoint'] = ($pac[$this->getInstanceWG(1) . 'endpoint'] ? $this->ip : $this->getDomain()) . ":" . getenv($this->getInstanceWG(1) ? 'WG1PORT' : 'WGPORT'); } foreach ($peer as $k => $v) { $conf[] = "$k = $v"; } } } return implode(PHP_EOL, $conf); } public function presharedKey() { $c = $this->getPacConf(); if (empty($c[$this->getInstanceWG(1) . 'presharedkey'])) { $c[$this->getInstanceWG(1) . 'presharedkey'] = trim($this->ssh("{$this->getWGType()} genpsk", $this->getInstanceWG())); $this->setPacConf($c); } return $c[$this->getInstanceWG(1) . 'presharedkey']; } public function amneziaKeys() { $c = $this->getPacConf(); if (empty($c[$this->getInstanceWG(1) . 'amnezia_keys'])) { $c[$this->getInstanceWG(1) . 'amnezia_keys'] = [ 'Jc' => rand(3, 10), 'Jmin' => 50, 'Jmax' => 1000, 'S1' => rand(15, 150), 'S2' => rand(15, 150), 'H1' => rand(1, 2_147_483_647), 'H2' => rand(1, 2_147_483_647), 'H3' => rand(1, 2_147_483_647), 'H4' => rand(1, 2_147_483_647), ]; $this->setPacConf($c); } return $c[$this->getInstanceWG(1) . 'amnezia_keys']; } public function createPeer($ips_user = false, $name = false) { $conf = $this->readConfig(); $ipnet = explode('/', $conf['interface']['Address']); $server_ip = ip2long($ipnet[0]); $ips = [$server_ip]; $bitmask = $ipnet[1]; if (!empty($conf['peers'])) { foreach ($conf['peers'] as $k => $v) { $ips[] = ip2long(explode('/', $v['AllowedIPs'] ?: $v['# AllowedIPs'])[0]); } } $ip_count = (1 << (32 - $bitmask)) - count($ips) - 1; for ($i = 1; $i < $ip_count; $i++) { $ip = $i + $server_ip; if (!in_array($ip, $ips)) { $client_ip = long2ip($ip); break; } } $public_server_key = trim($this->ssh("echo {$conf['interface']['PrivateKey']} | {$this->getWGType()} pubkey", $this->getInstanceWG())); $private_peer_key = trim($this->ssh("{$this->getWGType()} genkey", $this->getInstanceWG())); $public_peer_key = trim($this->ssh("echo $private_peer_key | {$this->getWGType()} pubkey", $this->getInstanceWG())); $name = ($name ? "$name" : '') . time(); $conf['peers'][] = array_merge([ '## name' => $name, 'PublicKey' => $public_peer_key, 'AllowedIPs' => "$client_ip/32", ], $this->getPacConf()[$this->getInstanceWG(1) . 'amnezia'] ? ['PresharedKey' => $this->presharedKey()] : [] ); $client_conf = [ 'interface' => array_merge( [ '## name' => $name, 'PrivateKey' => $private_peer_key, 'Address' => "$client_ip/32", ], $this->getPacConf()[$this->getInstanceWG(1) . 'amnezia'] ? $this->amneziaKeys() : [] ), 'peers' => [ array_merge( [ 'PublicKey' => $public_server_key, 'AllowedIPs' => $ips_user ?: "0.0.0.0/0", 'PersistentKeepalive' => 20, ], $this->getPacConf()[$this->getInstanceWG(1) . 'amnezia'] ? ['PresharedKey' => $this->presharedKey()] : [] ), ], ]; $k = $this->saveClient($client_conf); $this->restartWG($this->createConfig($conf)); $this->menu('client', "{$k}_-2"); } public function deleteClient(int $client) { $clients = $this->readClients(); unset($clients[$client]); $this->saveClients(array_values($clients)); } public function saveClient(array $client) { $r = array_merge($this->readClients(), [$client]); $this->saveClients($r); return count($r) - 1; } public function syncPortClients() { $endpoint = [ $this->ip . ':' . getenv('WGPORT'), $this->ip . ':' . getenv('WG1PORT'), ]; for ($i=0; $i < 2; $i++) { $this->wg = $i; $clients = $this->readClients(); foreach ($clients as $k => $v) { foreach ($v['peers'] as $n => $j) { $clients[$k]['peers'][$n]['Endpoint'] = $endpoint[$i]; } } $this->saveClients($clients); } unset($this->wg); } public function saveClients(array $clients) { $c = $this->getPacConf(); $domain = ($c['domain'] ?: $this->ip) . ":" . getenv($this->getInstanceWG(1) ? 'WG1PORT' : 'WGPORT'); foreach ($clients as $k => $v) { $clients[$k]['peers'][0]['Endpoint'] = $domain; } file_put_contents($this->getInstanceWG(1) ? $this->clients1 : $this->clients, json_encode($clients, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } public function getWGType($revert = 0) { $wg = $this->getPacConf()[$this->getInstanceWG(1) . 'amnezia']; return ($revert ? !$wg : $wg) ? 'awg' : 'wg'; } public function restartWG($conf_str, $switch = false) { $this->ssh("echo '$conf_str' > /etc/wireguard/wg0.conf", $this->getInstanceWG()); if (!empty($switch)) { $this->ssh("{$this->getWGType(1)}-quick down wg0", $this->getInstanceWG()); $this->ssh("{$this->getWGType()}-quick up wg0", $this->getInstanceWG()); } else { $this->ssh("{$this->getWGType()} syncconf wg0 <({$this->getWGType()}-quick strip wg0)", $this->getInstanceWG()); } return true; } public function autoupdate() { $p = $this->getPacConf(); $p['autoupdate'] = !$p['autoupdate']; $this->setPacConf($p); $this->menu('config'); } public function disconnect(...$args) { $this->send($this->input['chat'], "disconnect: \n" . var_export($args, true) . "\n", $this->input['message_id']); } public function ssh($cmd, $service = 'wg', $wait = true) { try { $c = ssh2_connect($service, 22); if (empty($c)) { throw new Exception("no connection to $service: \n$cmd\n" . var_export($c, true)); } $a = ssh2_auth_pubkey_file($c, 'root', '/ssh/key.pub', '/ssh/key'); if (empty($a)) { throw new Exception("auth fail: \n$cmd\n" . var_export($a, true)); } $s = ssh2_exec($c, $cmd); if (empty($s)) { throw new Exception("exec fail: \n$cmd\n" . var_export($s, true)); } stream_set_blocking($s, $wait); $data = ""; while ($buf = fread($s, 4096)) { $data .= $buf; } fclose($s); ssh2_disconnect($c); } catch (Exception | Error $e) { if (!empty($GLOBALS['debug'])) { $this->send($this->input['chat'], $e->getMessage(), $this->input['message_id']); } } return $data; } public function request($method, $data, $json_header = 0) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $this->api . $method, CURLOPT_CUSTOMREQUEST => 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $json_header ? [ 'Content-Type: application/json' ] : [], CURLOPT_POSTFIELDS => $data, ]); $res = curl_exec($ch); $r = json_decode($res, true); if (!empty($res['description']) || is_null($res)) { file_put_contents('/logs/requests_error', var_export([ 'r' => [ 'method' => $method, 'data' => $data, ], 'a' => $res, ], true) . "\n", FILE_APPEND); } return $r; } public function setwebhook() { $ip = $this->ip; if (empty($ip)) { die('нет айпи'); } echo "$ip\n"; var_dump($r = $this->request('setWebhook', [ 'url' => "https://$ip/tlgrm?k={$this->key}", 'certificate' => curl_file_create('/certs/self_public'), 'allowed_updates' => json_encode(['*']), ])); if (!empty($r['result']) && $r['result'] == true) { file_put_contents('/start', 1); } else { die("set webhook fail\n"); } } public function setcommands() { $data = [ 'commands' => [ [ 'command' => 'menu', 'description' => '...', ], [ 'command' => 'id', 'description' => 'your id telegram', ], ] ]; var_dump($this->request('setMyCommands', json_encode($data), 1)); } public function send($chat, $text, ?int $to = 0, $button = false, $reply = false, $mode = 'HTML', $disable_notification = false) { if ($button) { $extra = ['inline_keyboard' => $button]; } if (false !== $reply) { $extra = [ 'force_reply' => true, 'input_field_placeholder' => $reply, 'selective' => true, ]; } $length = 3096; if (mb_strlen($text, 'utf-8') > $length) { $tails = $this->splitText($text, $length); foreach ($tails as $k => $v) { $data = [ 'chat_id' => $chat, 'text' => "$v\n", 'parse_mode' => $mode, // 'disable_web_page_preview' => true, 'disable_notification' => $disable_notification, 'reply_to_message_id' => 0 == $k && $to > 0 ? $to : false, ]; if ($k == array_key_last($tails)) { if ($extra) { $data['reply_markup'] = json_encode($extra); } } $r = $this->request('sendMessage', $data); } } else { $data = [ 'chat_id' => $chat, 'text' => $text, 'parse_mode' => $mode, // 'disable_web_page_preview' => true, 'disable_notification' => $disable_notification, 'reply_to_message_id' => $to, ]; if (!empty($extra)) { $data['reply_markup'] = json_encode($extra); } $r = $this->request('sendMessage', $data); } return $r; } public function splitText($text, $size = 4096) { $tails = preg_split('~\n~', $text); if (!empty($tails)) { foreach ($tails as $v) { $lines[] = [ 'length' => mb_strlen($v, 'utf-8'), 'text' => $v, ]; } $i = 0; foreach ($lines as $v) { $i += $v['length']; $output[ceil($i / $size)] .= $v['text'] . "\n"; } return array_values($output); } else { return [$text]; } } public function image($chat, $id_url_cFile, $caption = false, $to = false) { return $this->request('sendPhoto', [ 'chat_id' => $chat, 'photo' => $id_url_cFile, 'caption' => $caption, 'reply_to_message_id' => $to, ]); } public function sendPhoto($chat, $id_url_cFile, $caption = false, $to = false) { return $this->request('sendPhoto', [ 'chat_id' => $chat, 'photo' => $id_url_cFile, 'caption' => $caption, 'reply_to_message_id' => $to, 'parse_mode' => 'html', ]); } public function sendFile($chat, $id_url_cFile, $caption = false, $to = false) { return $this->request('sendDocument', [ 'chat_id' => $chat, 'document' => $id_url_cFile, 'caption' => $caption, 'reply_to_message_id' => $to, 'parse_mode' => 'html', ]); } public function update($chat, $message_id, $text, $button = false, $reply = false, $mode = 'HTML') { if ($button) { $extra = ['inline_keyboard' => $button]; } if ($reply !== false) { $extra = [ 'force_reply' => true, 'input_field_placeholder' => $reply ]; } $data = [ 'chat_id' => $chat, 'message_id' => $message_id, 'text' => $text, 'parse_mode' => $mode, 'disable_web_page_preview' => true, ]; if (!empty($extra)) { $data['reply_markup'] = json_encode($extra); } return $this->request('editMessageText', $data); } public function answer($callback_id, $textNotify = false, $notify = false) { return $this->callback = $this->request('answerCallbackQuery', [ 'callback_query_id' => $callback_id, 'show_alert' => $notify, 'text' => $textNotify, ]); } public function delete($chat, $message_id) { $data = [ 'chat_id' => $chat, 'message_id' => $message_id, ]; return $this->request('deleteMessage', $data); } public function pin($chat, $message_id, $notnotify = true) { $data = [ 'chat_id' => $chat, 'message_id' => $message_id, 'disable_notification' => $notnotify, ]; return $this->request('pinChatMessage', $data); } public function unpin($chat, $message_id) { $data = [ 'chat_id' => $chat, 'message_id' => $message_id, ]; return $this->request('unpinChatMessage', $data); } }