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->logs = [ 'nginx_default_access', 'nginx_domain_access', 'upstream_access', 'xray', ]; } 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('~^/switchBanIp$~', $this->input['callback'], $m): $this->switchBanIp(); break; case preg_match('~^/searchLogs (.+)$~', $this->input['message'], $m): $this->searchLogs($m[1]); 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['message'], $m): $this->autoupdate(); break; case preg_match('~^/ports$~', $this->input['callback'], $m): $this->ports(); break; case preg_match('~^/ip$~', $this->input['message'], $m): case preg_match('~^/analysisIp$~', $this->input['callback'], $m): $this->analysisIp(); 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+\.\d+\.\d+\.\d+) (\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('~^/denyIp (.+?)(?:\s(\d)\s(\d+?)\s(\d))?$~', $this->input['callback'], $m): $this->denyIp($m[1], $m[2], $m[3], $m[4]); break; case preg_match('~^/whiteIp (.+?)(?:\s(\d)\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'], $this->input['from'], $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('~^/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('~^/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['callback'], $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+)?)$~', $this->input['callback'], $m): $this->subnet(...explode('_', $m['arg'])); break; case preg_match('~^/subnetAdd (?P-?\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m): $this->subnetAdd(...explode('_', $m['arg'])); break; case preg_match('~^/subnetDelete (?P-?\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('~^/changeOcPass$~', $this->input['callback'], $m): $this->changeOcPass(); break; case preg_match('~^/changeNaiveUser$~', $this->input['callback'], $m): $this->changeNaiveUser(); 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('~^/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('~^/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('~^/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('~^/generateSecretXray$~', $this->input['callback'], $m): $this->generateSecretXray(); break; case preg_match('~^/changeFakeDomain$~', $this->input['callback'], $m): $this->changeFakeDomain(); 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->exportManual(); 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) { $c['inbounds'][0]['settings']['clients'] = array_values($c['inbounds'][0]['settings']['clients']); $c['log']['access'] = '/logs/xray'; $this->ssh('pkill xray', 'xr'); file_put_contents('/config/xray.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->ssh('xray run -config /xray.json > /dev/null 2>&1 &', 'xr'); } 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 changeCamouflage() { $r = $this->send( $this->input['chat'], "@{$this->input['username']} enter camouflage key", $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' => 'chockey', '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 password', ); $_SESSION['reply'][$r['result']['message_id']] = [ 'start_message' => $this->input['message_id'], 'start_callback' => $this->input['callback_id'], 'callback' => 'chnplogin', 'args' => [], ]; } public function changeNaivePass() { $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' => 'chnppass', 'args' => [], ]; } public function addOcUser() { $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' => 'addocus', 'args' => [], ]; } public function addXrUser() { $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' => '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'); $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); $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 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) { $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->menu('oc'); } public function sspwdch($pass) { $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'); $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($period); $this->checkCert(); $this->autoAnalyzeLogs(); sleep($period); } } public function autoAnalyzeLogs() { try { $pac = $this->getPacConf(); if (!empty($pac['autoscan'])) { $r = $this->analysisIp(1); require __DIR__ . '/config.php'; if (!empty($c['admin']) && (empty($this->time3) || ((time() - $this->time3) > $pac['autoscan_timeout']))) { $this->time3 = time(); if (!empty($r)) { foreach ($r as $k => $v) { $tmp = array_unique($v); foreach ($tmp as $i) { $t[$i]++; } } foreach ($t as $k => $v) { $text .= "\n$v $k"; } if (!empty($pac['autodeny'])) { $this->denyIp(array_keys($r)); $ban = count(array_keys($r)); foreach (array_keys($r) as $v) { $ips[] = [[ 'text' => "logs $v", 'callback_data' => "/searchIp $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) { } } public function checkBackup($delta) { $c = $this->getPacConf(); if (!empty($c['backup'])) { $now = strtotime(date('Y-m-d H:i:s')); [$start, $period] = explode('/', $c['backup']); $start = strtotime(trim($start)); $period = strtotime(trim($period), 0); if ( !empty($start) && !empty($period) && empty($this->backup) && $now - $start >= 0 && (($now - $start) % $period < $delta) ) { if (!empty($c['pinbackup'])) { $this->pinAdmin($c['pinbackup'], 1); } $this->pinBackup(); } } } 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() { require __DIR__ . '/config.php'; $conf = $this->getPacConf(); $bot = preg_replace('~[\W]~iu', '_', $this->request('getMyName', [])['result']['name']); $conf['pinbackup'] = $this->upload("{$bot}_export_" . date('d_m_Y_H_i') . '.json', $this->export(), $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)) { 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"], ] ] ]); } 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, 'ss' => $this->getSSConfig(), 'sl' => $this->getSSLocalConfig(), 'ad' => yaml_parse_file($this->adguard), 'pac' => $this->getPacConf(), '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'), ]; return json_encode($conf, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } public function exportManual($file = false) { $json = $this->export(); if (!empty($file)) { file_put_contents($file, $json); } $bot = preg_replace('~[\W]~iu', '_', $this->request('getMyName', [])['result']['name']); return $this->upload("{$bot}_export_" . date('d_m_Y_H_i') . '.json', $json); } 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(); } // 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'] == 'Websocket' ? 't' : ($json['pac']['reality']['domain'] ?: $json['xray']['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0])); } // 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)); $t = file_get_contents('/config/nginx_default.conf'); if (!empty($json['pac']['domain'])) { $t = preg_replace('/server_name ([^\n]+)?/', "server_name *.{$json['pac']['domain']} {$json['pac']['domain']};", $t); preg_match_all('~#-domain.+?#-domain~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~#-domain.+?#-domain~s', $this->uncomment($v, 'domain'), $t, 1); } } if (!empty($json['ssl'])) { $name = $json['pac']['letsencrypt'] ? 'letsencrypt' : 'self'; $t = preg_replace('/#~([^\n]+)?/', "#~$name", $t); preg_match_all('~#-ssl.+?#-ssl~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~#-ssl.+?#-ssl~s', $this->uncomment($v, 'ssl'), $t, 1); } } file_put_contents('/config/nginx.conf', $t); $this->adguardProtect(); $out[] = $this->ssh("nginx -s reload 2>&1", 'ng'); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $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 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); $nginx = file_get_contents('/config/nginx.conf'); $t = preg_replace('/server_name ([^\n]+)?/', "server_name {$conf['domain']} *.{$conf['domain']};", $nginx); preg_match_all('~#-domain.+?#-domain~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~#-domain.+?#-domain~s', $this->uncomment($v, 'domain'), $t, 1); } file_put_contents('/config/nginx.conf', $t); $this->adguardProtect(); $u = $this->ssh("nginx -t 2>&1", 'ng'); $out[] = $u; if (empty($nomenu)) { $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); } if (preg_match('~test is successful~', $u)) { $out[] = $this->ssh("nginx -s reload 2>&1", 'ng'); if (empty($nomenu)) { $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); } $this->setPacConf($conf); $this->chocdomain($domain); $this->setUpstreamDomainOcserv($domain); $this->setUpstreamDomainNaive($domain); } else { file_put_contents('/config/nginx.conf', $nginx); } } 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) { $nginx = file_get_contents('/config/nginx.conf'); $t = preg_replace("/#~[^\s]+/", '#~', $nginx); preg_match_all('~##ssl.+?##ssl~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~##ssl.+?##ssl~s', $this->comment($v, 'ssl'), $t, 1); } file_put_contents('/config/nginx.conf', $t); $u = $this->ssh("nginx -t 2>&1", 'ng'); $this->update($this->input['chat'], $this->input['message_id'], $u); if (preg_match('~test is successful~', $u)) { $u .= $this->ssh("nginx -s reload 2>&1", 'ng'); $this->update($this->input['chat'], $this->input['message_id'], $u); $u .= $this->stopAd(); $this->update($this->input['chat'], $this->input['message_id'], $u); $c = yaml_parse_file($this->adguard); $c['tls']['enabled'] = false; $c['tls']['server_name'] = ''; yaml_emit_file($this->adguard, $c); $u .= $this->startAd(); $this->update($this->input['chat'], $this->input['message_id'], $u); unlink('/certs/cert_private'); unlink('/certs/cert_public'); sleep(3); } else { file_put_contents('/config/nginx.conf', $nginx); } 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']}" : ''; if (!empty($conf['subdomain'])) { foreach ($conf['subdomain'] as $v) { $custom .= " -d $v"; } } 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 $custom --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'] = 1; $this->setPacConf($conf); break; case 'self': $r = $this->request('getFile', ['file_id' => $this->input['file_id']]); $bundle = file_get_contents($this->file . $r['result']['file_path']); break; } if (preg_match('~[^\s]+BEGIN PRIVATE KEY.+?END PRIVATE KEY[^\s]+~s', $bundle, $m)) { 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)); $nginx = file_get_contents('/config/nginx.conf'); $t = preg_replace('/#~([^\n]+)?/', "#~$name", $nginx); preg_match_all('~#-ssl.+?#-ssl~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~#-ssl.+?#-ssl~s', $this->uncomment($v, 'ssl'), $t, 1); } file_put_contents('/config/nginx.conf', $t); $u = $this->ssh("nginx -t 2>&1", 'ng'); $out[] = $u; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); if (preg_match('~test is successful~', $u)) { $out[] = $this->ssh("nginx -s reload 2>&1", 'ng'); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $out[] = 'Restart ocserv'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->restartOcserv(file_get_contents('/config/ocserv.conf')); $out[] = 'Restart NaiveProxy'; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->restartNaive(); $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['tls']['enabled'] = true; $c['tls']['server_name'] = $conf['domain']; yaml_emit_file($this->adguard, $c); $out[] = $this->startAd(); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); } else { file_put_contents('/config/nginx.conf', $nginx); } } 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']); $nginx = $t = file_get_contents('/config/nginx.conf'); preg_match_all('~##domain.+?##domain~s', $t, $m); foreach ($m[0] as $k => $v) { $t = preg_replace('~##domain.+?##domain~s', $this->comment($v, 'domain'), $t, 1); } file_put_contents('/config/nginx.conf', $t); $u = $this->ssh("nginx -t 2>&1", 'ng'); $out[] = $u; $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); if (preg_match('~test is successful~', $u)) { $out[] = $this->ssh("nginx -s reload 2>&1", 'ng'); $this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out)); $this->setPacConf($conf); $this->setUpstreamDomainOcserv(''); $this->chocdomain(''); } else { file_put_contents('/config/nginx.conf', $nginx); } $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(); $this->adguardProtect(); } 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 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(); 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]); 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']} [direct | block | proxy]:time:URL", $this->input['message_id'], reply: '[direct | block | proxy]:time:URL', ); break; default: $r = $this->send( $this->input['chat'], "@{$this->input['username']} list domains separated by commas", $this->input['message_id'], reply: 'list domains 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) { $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->xrayUpdateRules(); $this->xtlsprocess(); break; case 'packagelist': $this->xrayUpdateRules(); $this->xtlsapp(); break; case 'rulessetlist': $this->xrayUpdateRules(); $this->xtlsrulesset(); 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']))) { $rules[] = [ "type" => "field", "outboundTag" => "block", "domain" => array_keys(array_filter($c['blocklist'])), ]; } if (!empty($c['warplist']) && !empty(array_filter($c['warplist']))) { $rules[] = [ "type" => "field", "outboundTag" => "warp", "domain" => array_keys(array_filter($c['warplist'])), ]; } $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) { $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], ]; } public function subnetSave($text, $wgpage, $page) { $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); } $this->subnet($wgpage, $page); } public function subnetDelete($wgpage, $k, $page = 0) { $c = $this->getPacConf(); unset($c['subnets'][$k]); $this->setPacConf($c); $this->subnet($wgpage, $page); } 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, $count = 5) { $count = $this->limit; $text = "Menu -> Wireguard -> " . $this->i18n('listSubnet') . "\n"; $data[] = [ [ 'text' => $this->i18n('calc'), 'callback_data' => "/calc", ], ]; $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/subnetAdd {$wgpage}_$page", ], ]; $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", ], ]; } if ($page != -1 && $all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/subnet {$wgpage}_" . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ 'text' => '>>', 'callback_data' => "/subnet {$wgpage}_" . ($page < $all - 1 ? $page + 1 : 0), ] ]; } } $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/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' => 'all', // 'callback_data' => "/menu wg -1", // ], [ '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 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 = substr(md5($this->key), 0, 8); $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?h=$hash&a=127.0.0.1&p=1080 text; $urls[0][] = [ 'text' => "PAC", 'web_app' => ['url' => "https://$ip/pac?h=$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?h=$hash&t=mpac text; $urls[0][] = [ 'text' => "PAC ShadowSocks(Android)", 'web_app' => ['url' => "https://$ip/pac?h=$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?h=$hash&t=rpac&a=127.0.0.1&p=1080 text; $urls[0][] = [ 'text' => "Reverse PAC", 'url' => "$scheme://$ip/pac?h=$hash&t=rpac", ]; $urls[1][] = [ 'text' => "Reverse PAC Wireguard proxy", 'url' => "$scheme://$ip/pac?h=$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?h=$hash&t=rmpac text; $urls[2][] = [ 'text' => "Reverse PAC SS(Android)", 'url' => "$scheme://$ip/pac?h=$hash&t=rmpac", ]; } if ($urls) { $data = $urls; } $data[] = [ [ 'text' => $this->i18n('add') . ' community antifilter', 'callback_data' => "/addCommunityFilter", ], ]; $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 '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 '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' => 'set to ' . ($p['domains_outbound'] ? 'proxy' : 'direct'), 'callback_data' => "/domainsOutbound", ], ]; $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' => 'set to ' . ($p['app_outbound'] ? 'proxy' : 'direct'), 'callback_data' => "/appOutbound", ], ]; $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'); $p = $this->getPacConf(); $data[] = [ [ 'text' => 'set to ' . ($p['process_outbound'] ? 'proxy' : 'direct'), 'callback_data' => "/processOutbound", ], ]; $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']) ? $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' => '>>', '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() { $conf = $this->getPacConf(); $ip = $this->ip; $domain = $this->getDomain(); $scheme = empty($ssl = $this->nginxGetTypeCert()) ? 'http' : 'https'; $ss = $this->getSSConfig(); $v2ray = !empty($ss['plugin']) ? 'ON' : 'OFF'; $port = !empty($ss['plugin']) ? (!empty($ssl) ? 443 : 80) : getenv('SSPORT'); $options = !empty($ssl) && !empty($ss['plugin']) ? "tls;fast-open;path=/v2ray;host=$domain" : "path=/v2ray;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;host=$domain" . (!empty($ssl) ? ';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 menu($type = false, $arg = false, $return = false) { $domain = $this->getPacConf()['domain'] ?: $this->ip; $cron = $this->ssh('pgrep -f cron.php', 'service'); $menu = [ 'main' => [ 'text' => 'v' . getenv('VER') . ($this->dontshowcron ? '' : "\ncron: " . $this->i18n($cron ? 'on' : 'off') . ($cron ? '' : ' show logs/php_error')), '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('sh_title'), 'callback_data' => "/menu ss", ], [ 'text' => $this->i18n('warp') . ': ' . $this->warpStatus(), 'callback_data' => "/warp", ], ], [ [ 'text' => $this->i18n('ad_title'), 'callback_data' => "/menu adguard", ], [ 'text' => $this->i18n('pac'), 'callback_data' => "/pacMenu 0", ], ], [ [ 'text' => $this->i18n('IP ban & Logs'), 'callback_data' => "/ipMenu", ], ], [ [ 'text' => $this->i18n('config'), 'callback_data' => "/menu config", ], ], [ [ 'text' => $this->i18n('chat'), 'url' => "https://t.me/+Wfxg6-nrokBlMmYy", ], [ 'text' => $this->i18n('donate'), 'web_app' => [ 'url' => "https://$domain/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 ($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 switchSilence() { $c = $this->getPacConf(); $c['silence'] = (($c['silence'] ?: 0) + 1) % 3; $this->setPacConf($c); $this->ipMenu(); } public function ipMenu() { $text = 'Menu -> IP'; $pac = $this->getPacConf(); $d = count($pac['deny'] ?: []); $w = count($pac['white'] ?: []); $data[] = [ [ 'text' => $this->i18n('logs'), 'callback_data' => "/logs", ], ]; $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('silence') . ': ' . ((function ($pac) { switch ($pac['silence']) { case 0: return $this->i18n('off'); case 1: return '🟡'; case 2: return $this->i18n('on'); } })($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", ], ]; $this->update( $this->input['chat'], $this->input['message_id'], $text, $data ?: false, ); } public function analysisIp($return = false) { $pac = $this->getPacConf(); foreach (array_merge($pac['white'] ?: [], $pac['deny'] ?: [], [ '10.10.0.1' ,'10.10.1.1' , '10.10.0.2' ,'10.10.1.2' , '10.10.0.3' ,'10.10.1.3' , '10.10.0.4' ,'10.10.1.4' , '10.10.0.5' ,'10.10.1.5' , '10.10.0.6' ,'10.10.1.6' , '10.10.0.7' ,'10.10.1.7' , '10.10.0.8' ,'10.10.1.8' , '10.10.0.9' ,'10.10.1.9' , '10.10.0.10','10.10.1.10', '10.10.0.11','10.10.1.11', '10.10.0.12','10.10.1.12', '10.10.0.13','10.10.1.13', '10.10.0.14','10.10.1.14', '10.10.0.15','10.10.1.15', ]) as $v) { $xr[$v] = true; } 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 ($r = fopen('/logs/xray', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+)(?=.+accepted)~', $l, $m)) { $xr[$m[1]] = true; } } fclose($r); } if ($r = fopen('/logs/upstream_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (preg_match('~(\d+\.\d+\.\d+\.\d+).+200\s\d+\s0$~', $l, $m)) { if (empty($xr[$m[1]])) { $ip[$m[1]][] = 'possibly a Reality Degenerate'; } } } fclose($r); } $reg = [ 'GET /ws(?:.+)? HTTP', 'GET /adguard/(?:.+)? HTTP', 'GET /webapp(?:.+)? HTTP', 'GET /pac(?:.+)? HTTP', 'GET \.well-known(?:.+)? HTTP', 'GET /v2ray(?:.+)? HTTP', 'GET /dns-query(?:.+)? HTTP', 'GET / HTTP', 'GET /tlgrm(?:.+)? HTTP', 'GET /jsoneditor.min.css HTTP', 'GET /jsoneditor.min.js HTTP', 'GET /jquery-3.7.1.min.js HTTP', 'GET /img/jsoneditor-icons.svg HTTP', 'GET /favicon.ico HTTP', ]; if ($r = fopen('/logs/nginx_default_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (!preg_match('~' . implode('|', $reg) . '~', $l)) { if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { if (empty($xr[$m[1]])) { $ip[$m[1]][] = 'possibly a scanner'; } } } } fclose($r); } if ($r = fopen('/logs/nginx_domain_access', 'r')) { while (feof($r) === false) { $l = fgets($r); if (!preg_match('~' . implode('|', $reg) . '~', $l)) { if (preg_match('~(\d+\.\d+\.\d+\.\d+)~', $l, $m)) { if (empty($xr[$m[1]])) { $ip[$m[1]][] = 'possibly a scanner'; } } } } fclose($r); } if (!empty($ip)) { if (!empty($return)) { return $ip; } foreach ($ip as $k => $v) { $file .= "/searchLogs $k\n"; } $this->sendFile($this->input['from'], new CURLStringFile($file, 'analyze_ip_' . date('Y_m_d_H_i_s'))); } else { $this->answer($this->input['callback_id'], 'empty'); } } public function searchLogs($search) { if (preg_match('~^\d+\.\d+\.\d+\.\d+$~', $search)) { $this->send($this->input['from'], $search, button: [[ [ 'text' => $this->i18n('block'), 'callback_data' => "/denyIp $search", ], [ 'text' => $this->i18n('ignore'), 'callback_data' => "/whiteIp $search", ], [ 'text' => $this->i18n('logs'), 'callback_data' => "/searchIp $search", ], [ 'text' => $this->i18n('clean logs'), 'callback_data' => "/cleanLogs $search", ], ]]); } } 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 denyList($page = 0, $white = 0) { $text = 'Menu -> IP -> ' . ($white ? 'white' : 'deny') . ' 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($domains)) { foreach (array_slice($domains, $page * $this->limit, $this->limit) as $v) { $data[] = [ [ 'text' => $this->i18n('delete') . " $v", 'callback_data' => "/allowIp $v $page" . ($white ? " 1" : ''), ], [ 'text' => $this->i18n($white ? 'block' : 'ignore'), 'callback_data' => ($white ? "/denyIp" : "/whiteIp") . " $v 1 $page $white", ], [ 'text' => $this->i18n('logs'), 'callback_data' => "/searchIp $v", ], [ 'text' => $this->i18n('clean logs'), 'callback_data' => "/cleanLogs $v 1", ], ]; } if ($all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/denyList " . ($page - 1 >= 0 ? $page - 1 : $all - 1) . ($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, $nodelete = 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($nodelete)) { $this->delete($this->input['from'], $this->input['message_id']); } $this->syncDeny(); if (!empty($nodelete)) { $this->denyList($page, $white); } } public function whiteIp($ip, $nodelete = 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($nodelete)) { $this->delete($this->input['from'], $this->input['message_id']); } $this->syncDeny(); if (!empty($nodelete)) { $this->denyList($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 (!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 $v) { $text .= "deny $v;\n"; } } $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'] == 'Websocket'); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = substr(md5($this->key), 0, 8); $si = "$scheme://{$domain}/pac/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $c['inbounds'][0]['settings']['clients'][$i]['id'], ])); $v2 = "$scheme://{$domain}/pac/" . 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'] == 'Websocket') { return "vless://{$c['inbounds'][0]['settings']['clients'][$i]['id']}@$domain:443?flow=&path=%2Fws&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"; $text[] = "https://{$pac['naive']['user']}:{$pac['naive']['pass']}@np.$domain"; $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; $hash = substr(md5($this->key), 0, 8); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $text[] = "Menu -> Mirror"; $text[] = <<client -> intermediate VPS -> vpnbot ^ | | install | | mirror | ----------- PNG; $text[] = "$scheme://$ip/pac?h=$hash&t=mirror"; $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() { unlink('/config/mirror.tar.gz'); unlink('/config/mirror.tar'); $s = file_get_contents('/mirror/start_socat.sh'); $t = preg_replace([ '~{ip}~', '~{tg}~', '~{ss}~', '~{wg}~', ], [ getenv('IP'), getenv('TGPORT'), getenv('SSPORT'), getenv('WGPORT'), ], $s); file_put_contents('/mirror/start_socat.sh', $t); $a = new PharData('/config/mirror.tar'); $a->buildFromDirectory('/mirror'); $a->compress(Phar::GZ); if (!empty($this->input)) { $this->sendFile($this->input['from'], curl_file_create('/config/mirror.tar.gz')); } else { header('Content-Disposition: attachment; filename=mirror.tar.gz'); header('Content-Type: application/tar+gzip'); echo file_get_contents('/config/mirror.tar.gz'); exit; } unlink('/config/mirror.tar.gz'); unlink('/config/mirror.tar'); file_put_contents('/mirror/start_socat.sh', $s); } 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)) { $text[] = "https://oc.$domain/?$cs"; } $text[] = "password: $pass"; $data[] = [ [ 'text' => $this->i18n('change secret'), 'callback_data' => "/changeCamouflage", ], [ 'text' => $this->i18n('change password'), 'callback_data' => "/changeOcPass", ], [ 'text' => $this->i18n('dns') . ": $dns", 'callback_data' => "/changeOcDns", ], ]; $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(); foreach ($r['inbounds'][0]['settings']['clients'] as $k => $v) { if ($i == $k) { unset($r['inbounds'][0]['settings']['clients'][$k]); $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($user) { $c = $this->getXray(); $p = $this->getPacConf(); $uuid = trim($this->ssh('xray uuid', 'xr')); $c['inbounds'][0]['settings']['clients'][] = $p['transport'] == 'Websocket' ? [ 'id' => $uuid, 'email' => $user, ] : [ 'id' => $uuid, 'flow' => 'xtls-rprx-vision', 'email' => $user, ]; $this->restartXray($c); $this->adguardXrayClients(); $this->userXr(count($c['inbounds'][0]['settings']['clients']) - 1); } public function setTimerXr($time, $i) { $time = strtotime($time); if ($time === false) { $this->send($this->input['chat'], 'wrong format'); return; } $c = $this->getXray(); if (empty($time)) { unset($c['inbounds'][0]['settings']['clients'][$i]['time']); } else { if (!empty($c['inbounds'][0]['settings']['clients'][$i]['off'])) { $this->switchXr($i, 1); $c = $this->getXray(); } $c['inbounds'][0]['settings']['clients'][$i]['time'] = $time; } file_put_contents('/config/xray.json', json_encode($c, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); $this->userXr($i); } public function switchXr($i, $nm = 0) { $c = $this->getXray(); 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 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) { switch ($type) { case 'sing': $f = new \CURLFile('/config/sing.json', 'application/json', 'origin.json'); break; case 'v2ray': $f = new \CURLFile('/config/v2ray.json', 'application/json', 'origin.json'); break; } $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 = substr(md5($this->key), 0, 8); $text[] = "Menu -> " . $this->i18n('xray') . " -> $type templates"; $templates = $pac["{$type}templates"]; $data[] = [ [ 'text' => $this->i18n('add'), 'callback_data' => "/templateAdd $type", ], ]; $data[] = [ [ 'text' => "origin", 'web_app' => ['url' => "https://$domain/pac?h=$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?h=$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 xray($page = 0) { if (!$this->ssh('pgrep xray', 'xr')) { $this->generateSecretXray(); } $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'] ?: 'Reality'); $data[] = [ [ 'text' => $p['linkdomain'] ?: $this->i18n('cdn'), 'callback_data' => '/addLinkDomain', ], ]; $data[] = [ [ 'text' => $this->i18n('Reality') . ' ' . ($p['transport'] != 'Websocket' ? $this->i18n('on') : $this->i18n('off')), 'callback_data' => "/changeTransport", ], [ 'text' => $this->i18n('Websocket') . ' ' . ($p['transport'] == 'Websocket' ? $this->i18n('on') : $this->i18n('off')), 'callback_data' => "/changeTransport 1", ], ]; if ($p['transport'] != 'Websocket') { $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", ], ]; $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) { $time = $v['time'] ? $this->getTime($v['time']) : ''; $data[] = [ [ 'text' => "{$v['email']}" . ($time ? ": $time" : ''), 'callback_data' => "/userXr $k", ], ]; } if ($page != -1 && $all > 1) { $data[] = [ [ 'text' => '<<', 'callback_data' => "/xray " . ($page - 1 >= 0 ? $page - 1 : $all - 1), ], [ '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'; $p = $this->getPacConf(); $outbound = $p['outbound'] ?: 'proxy'; $data = [ [[ 'text' => $this->i18n('block'), 'callback_data' => "/xtlsblock", ]], [[ 'text' => $this->i18n('warp'), 'callback_data' => "/xtlswarp", ]], [[ 'text' => $this->i18n('rulesset'), 'callback_data' => "/xtlsrulesset", ]], [[ 'text' => 'domains: ' . ($p['domains_outbound'] ? 'direct' : $outbound), 'callback_data' => "/xtlsproxy", ]], [[ 'text' => 'process: ' . ($p['process_outbound'] ? 'direct' : $outbound), 'callback_data' => "/xtlsprocess", ]], [[ 'text' => 'package: ' . ($p['app_outbound'] ? 'direct' : $outbound), 'callback_data' => "/xtlsapp", ]], [[ 'text' => 'final: ' . ($p['final_outbound'] ? $outbound : 'direct'), 'callback_data' => "/finalOutbound", ]], ]; $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 $m[1]; } return 'off'; } 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) { $c = $this->getXray()['inbounds'][0]['settings']['clients'][$i]; $pac = $this->getPacConf(); $domain = $this->getDomain($pac['transport'] == 'Websocket'); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = substr(md5($this->key), 0, 8); $text[] = "Menu -> " . $this->i18n('xray') . " -> {$c['email']}\n"; $text[] = "
{$this->linkXray($i)}
\n"; $text[] = "import subscribe:"; $text[] = "v2rayng"; $text[] = "sing-box"; $text[] = "streisand"; $text[] = "hiddify"; $text[] = "karing"; $si = "$scheme://{$domain}/pac/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $c['id'], ])); $xr = "$scheme://{$domain}/pac/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', 's' => $c['id'], ])); $text[] = "\nxray config:
$xr
"; $text[] = "sing-box config:
$si
"; $text[] = "sing-box windows: windows service"; $data[] = [ [ 'text' => 'xray', 'web_app' => ['url' => "https://{$domain}/pac?h=$hash&t=s&s={$c['id']}"], ], [ 'text' => 'sing-box', 'web_app' => ['url' => "https://{$domain}/pac?h=$hash&t=si&s={$c['id']}"], ], ]; $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') . ')'; $data[] = [ [ 'text' => $this->i18n('v2ray') . ": $v2raytemplate", 'callback_data' => "/templateUser v2ray $i", ], [ 'text' => $this->i18n('singbox') . ": $singtemplate", 'callback_data' => "/templateUser sing $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('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 getDomain($cdn = false) { $c = $this->getPacConf(); if ($cdn && $c['linkdomain']) { return $c['linkdomain']; } return $c['domain'] ?: $this->ip; } public function subscription() { $type = $_GET['t'] == 's' ? 'v2ray' : 'sing'; $pac = $this->getPacConf(); $domain = $_GET['cdn'] ?: ($_SERVER['SERVER_NAME'] ?: $this->getDomain($pac['transport'] == 'Websocket')); $xr = $this->getXray(); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = substr(md5($this->key), 0, 8); $flag = true; 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']; break; } } if ($flag) { return false; } if (!empty($_GET['r'])) { $si = "$scheme://{$domain}/pac/" . base64_encode(serialize([ 'h' => $hash, 't' => 'si', 's' => $uid, ])); $v2 = "$scheme://{$domain}/pac/" . base64_encode(serialize([ 'h' => $hash, 't' => 's', '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 '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; } } 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', ]; if ($pac['transport'] == 'Websocket') { $c['outbounds'][$index]['streamSettings'] = [ "network" => "ws", "security" => "tls", "wsSettings" => [ "path" => "/ws?ed=2560" ], "tlsSettings" => [ "allowInsecure" => false, "serverName" => '~domain~', "fingerprint" => "chrome" ] ]; 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" => "chrome", "publicKey" => '~public_key~', "shortId" => '~short_id~', ] ]; $c['outbounds'][$index]['mux'] = [ "enabled" => false, "concurrency" => -1 ]; } break; case 'si': $c['outbounds'][$index]['server'] = '~domain~'; $c['outbounds'][$index]['uuid'] = '~uid~'; if ($pac['transport'] == 'Websocket') { unset($c['outbounds'][$index]['tls']['reality']); unset($c['outbounds'][$index]['flow']); $c['outbounds'][$index]["transport"] = [ "type" => "ws", "path" => "/ws" ]; $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~'; } $c['route'] = $this->addRuleSet($c['route']); $c['route'] = $this->createRuleSet($c['route'], $uid, $domain); break; } $json = $this->replaceTags(json_encode($c), [ '"~pac~"' => json_encode(array_keys(array_filter($pac['includelist'] ?: []))), '~dns~' => "https://$domain/dns-query/$uid", '~uid~' => $uid, '~domain~' => $domain, '~short_id~' => $xr['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0], '~public_key~' => $pac['xray'], '~server_name~' => $xr['inbounds'][0]['streamSettings']['realitySettings']['serverNames'][0], '~app_outbound~' => $pac['app_outbound'] ? 'direct' : $outbound, '~process_outbound~' => $pac['process_outbound'] ? 'direct' : $outbound, '~domains_outbound~' => $pac['domains_outbound'] ? 'direct' : $outbound, '~final_outbound~' => $pac['final_outbound'] ? $outbound : 'direct', '~ip~' => $this->ip, ]); $json = $this->clearEmptyRules($json); header('Content-type: application/json'); echo $json; } public function replaceTags($subject, $tags) { return str_replace(array_keys($tags), array_values($tags), $subject); } public function clearEmptyRules($json) { $json = json_decode($json, 1); if (!empty($json['routing']['rules'])) { foreach ($json['routing']['rules'] as $k => $v) { if (array_key_exists('domain', $v) && empty($v['domain'])) { unset($json['routing']['rules'][$k]); } } $json['routing']['rules'] = array_values($json['routing']['rules']); } if (!empty($json['route']['rules'])) { foreach ($json['route']['rules'] as $k => $v) { if (count($v) < 2) { unset($json['route']['rules'][$k]); } } $json['route']['rules'] = array_values($json['route']['rules']); } return json_encode($json); } public function addRuleSet($route) { foreach ($route['rules'] as $k => $v) { $t[$v['outbound']] = $k; } $p = $this->getPacConf(); if (!empty($p['rulessetlist'])) { foreach ($p['rulessetlist'] as $k => $v) { if (!empty($v)) { [$type, $time, $url] = explode(':', $k, 3); $route['rule_set'][] = [ "tag" => $k, "type" => "remote", "format" => "binary", "url" => $url, "download_detour" => "direct", "update_interval" => $time ]; $route['rules'][$t[$type]]['rule_set'][] = $k; } } } return $route; } public function createRuleSet($route, $uid, $domain) { $pac = $this->getPacConf(); $scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https'; $hash = substr(md5($this->key), 0, 8); foreach ($route['rules'] as $k => $v) { if (!empty($v['createruleset'])) { foreach ($v['createruleset'] as $r) { foreach ($r['rules'] as $l => $n) { switch (true) { case array_key_exists('domain_suffix', $n): switch ($n['domain_suffix']) { case '~pac~': $t = 'includelist'; break; case '~warp~': $t = 'warplist'; break; case '~block~': $t = 'blocklist'; break; } $r['rules'][$l]['domain_suffix'] = array_keys(array_filter($pac[$t] ?: [])); if (empty($r['rules'][$l]['domain_suffix'])) { unset($r['rules'][$l]); } break; case array_key_exists('package_name', $n): $r['rules'][$l]['package_name'] = array_keys(array_filter($pac['packagelist'] ?: [])); if (empty($r['rules'][$l]['package_name'])) { unset($r['rules'][$l]); } break; case array_key_exists('process_name', $n): $r['rules'][$l]['process_name'] = array_keys(array_filter($pac['processlist'] ?: [])); if (empty($r['rules'][$l]['process_name'])) { unset($r['rules'][$l]); } break; } } if (!empty($_GET['r']) && $r['name'] == $_GET['r']) { header("Content-Disposition: attachment; filename={$r['name']}.srs"); header('Content-Type: application/binary'); $f = "/tmp/{$r['name']}" . time() . rand(1, 100); file_put_contents($f, json_encode([ 'version' => 1, 'rules' => $r['rules'] ?: [], ])); exec("sing-box rule-set compile $f"); echo file_get_contents("$f.srs"); unlink($f); unlink("$f.srs"); exit; } $ruleset[] = [ "tag" => $r['name'], "url" => "$scheme://{$domain}/pac/" . 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]); } } } $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 generateSecretXray() { $c = $this->getXray(); $shortId = trim($this->ssh('openssl rand -hex 8', 'xr')); $keys = $this->ssh('xray x25519', 'xr'); preg_match('~^Private key:\s([^\s]+)~m', $keys, $m); $private = trim($m[1]); preg_match('~^Public key:\s([^\s]+)~m', $keys, $m); $public = trim($m[1]); $c['inbounds'][0]['streamSettings']['realitySettings']['privateKey'] = $private; $c['inbounds'][0]['streamSettings']['realitySettings']['shortIds'][0] = $shortId; $pac = $this->getPacConf(); $pac['xray'] = $public; $pac['reality']['shortId'] = $shortId; $pac['reality']['privateKey'] = $private; $this->setPacConf($pac); $this->restartXray($c); } 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) { $nginx = file_get_contents('/config/upstream.conf'); $t = preg_replace('~#ocserv.+#ocserv~s', $domain ? "#ocserv\noc.$domain ocserv;\n#ocserv" : "#ocserv\n#oc.\$domain ocserv;\n#ocserv", $nginx); file_put_contents('/config/upstream.conf', $t); $this->ssh("nginx -s reload 2>&1", 'up'); } public function setUpstreamDomainNaive($domain) { $nginx = file_get_contents('/config/upstream.conf'); $t = preg_replace('~#naive.+#naive~s', $domain ? "#naive\nnp.$domain naive;\n#naive" : "#naive\n#np.\$domain naive;\n#naive", $nginx); file_put_contents('/config/upstream.conf', $t); $this->ssh("nginx -s reload 2>&1", 'up'); } 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('proxy ip'), 'callback_data' => "/proxy", ] ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/menu wg $page", ] ]; return [ 'text' => $text, 'data' => $data, ]; } public function adguardProtect() { $h = substr(hash('sha256', $this->key), 0, 8); $s = empty($this->getPacConf()['adgbrowser']) ? '' : '#'; $r = <<getPacConf()['adpswd']); } public function adguardChBr() { $c = $this->getPacConf(); $c['adgbrowser'] = $c['adgbrowser'] ? 0 : 1; $this->setPacConf($c); $this->adguardProtect(); $this->ssh('nginx -s reload', 'ng'); $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(); $scheme = empty($ssl = $this->nginxGetTypeCert()) ? 'http' : 'https'; $text = "$scheme://$domain/adguard\nLogin: admin\nPass: {$conf['adpswd']}\n\n"; if ($ssl) { $text .= "DNS over HTTPS:\n$ip\n$scheme://$domain/dns-query" . ($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" ], ], [ '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 implode("\n", 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->exportManual($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() { exec('git -C / fetch'); $conf = $this->getPacConf(); if (!empty($conf['subdomain'])) { $custom = "\ncustom:\n"; foreach ($conf['subdomain'] as $v) { $custom .= "$v\n"; } } $text[] = $conf['domain'] ? "Domains:\n{$conf['domain']}\nnp.{$conf['domain']} (for naiveproxy)\noc.{$conf['domain']} (for openconnect)" . ($conf['adguardkey'] ? "\n{$conf['adguardkey']}.{$conf['domain']} (for adguard DoT)" : '') . $custom : $this->i18n('domain explain'); $ssl = $this->expireCert(); $text[] = $conf['domain'] ? "\nSSL: " . ($ssl ? date('Y-m-d H:i:s', $this->expireCert()) . "\n" . $this->domainsCert() : 'none') : ''; $data = [ [ [ 'text' => $conf['domain'] ? "{$this->i18n('delete')} {$conf['domain']}" : $this->i18n('install domain'), 'callback_data' => $conf['domain'] ? '/deldomain' : '/domain', ], [ 'text' => $this->i18n('+ subdomain'), 'callback_data' => '/addSubdomain', ], [ '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('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('lang'), 'callback_data' => "/menu lang", ], [ 'text' => "{$this->i18n('page')}: " . ($conf['limitpage'] ?: 5), 'callback_data' => "/enterPage", ], [ 'text' => $this->i18n($conf['blinkmenu'] ? 'blinkmenuon' : 'blinkmenuoff'), 'callback_data' => "/blinkmenuswitch", ], ]; $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('fake html'), 'callback_data' => "/addOverrideHtml", ], [ 'text' => $this->i18n('ports'), 'callback_data' => "/ports", ], ]; $data[] = [ [ 'text' => $this->i18n('debug') . ': ' . $this->i18n($c['debug'] ? 'on' : 'off'), 'callback_data' => "/debug", ], ]; $data[] = [ [ 'text' => $this->i18n(exec('git -C / rev-list --count HEAD..@{u}') ? 'have updates' : 'no updates'), 'callback_data' => "/menu update", ], [ 'text' => $this->i18n('restart'), 'callback_data' => "/restart", ], ]; $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']; $data = [ [[ 'text' => 'Letsencrypt 80 ' . $this->i18n($c['ng'] ? 'off' : 'on'), 'callback_data' => "/hidePort ng", ]], [[ 'text' => 'Shadowsocks ' . getenv('SSPORT') . ' ' . $this->i18n($c['ss'] ? 'off' : 'on'), 'callback_data' => "/hidePort ss", ]], [[ 'text' => 'DoT 853 ' . $this->i18n($c['ad'] ? 'off' : 'on'), 'callback_data' => "/hidePort ad", ]], [[ 'text' => 'Wireguard-1 ' . getenv('WGPORT') . ' ' . $this->i18n($c['wg'] ? 'off' : 'on'), 'callback_data' => "/hidePort wg", ]], [[ 'text' => 'Wireguard-2 ' . getenv('WG1PORT') . ' ' . $this->i18n($c['wg1'] ? 'off' : 'on'), 'callback_data' => "/hidePort wg1", ]], [[ 'text' => 'MTProto ' . getenv('TGPORT') . ' ' . $this->i18n($c['tg'] ? 'off' : 'on'), 'callback_data' => "/hidePort tg", ]], ]; $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) { $f = '/docker/compose'; $c = yaml_parse_file($f); if (!empty($c['services'][$container])) { unset($c['services'][$container]); } else { $c['services'][$container]['ports'] = []; } if (empty($c['services'])) { file_put_contents($f, ''); } else { yaml_emit_file($f, $c); file_put_contents($f, str_replace('ports:', 'ports: !override', file_get_contents($f))); } $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() { 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", ], ]; $data[] = [ [ 'text' => $this->i18n('back'), 'callback_data' => "/ipMenu", ], ]; $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', ''); } 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 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(); $p['transport'] = $ws ? 'Websocket' : 'Reality'; 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" ] ]; } 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 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->send($this->input['from'], $this->i18n('autoupdate') . ' ' . $this->i18n($p['autoupdate'] ? 'on' : 'off')); } 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'), ])); 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); } }