Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8398ecf736 | |||
| 0d14061edb | |||
| 03218b974d | |||
| 84ac1200dc | |||
| aa64a48db2 | |||
| 70c83da66a | |||
| 52ff056474 | |||
| 225260044a | |||
| 050d1ee3eb | |||
| 6979cca602 | |||
| 7fc24ea4f4 |
@@ -10,6 +10,7 @@
|
||||
/sftp-config.json
|
||||
/tg.pyr
|
||||
/config/wg0.conf
|
||||
/config/hwid.json
|
||||
/todo.todo
|
||||
/app/zapretlists/*
|
||||
!/app/zapretlists/.gitkeep
|
||||
|
||||
+676
-9
@@ -14,6 +14,7 @@ class Bot
|
||||
public $logs;
|
||||
public $reg;
|
||||
public $pool;
|
||||
public $hwid;
|
||||
|
||||
public function __construct($key, $i18n)
|
||||
{
|
||||
@@ -31,6 +32,7 @@ class Bot
|
||||
$this->limit = $this->getPacConf()['limitpage'] ?: 5;
|
||||
$this->adguard = '/config/AdGuardHome.yaml';
|
||||
$this->update = '/update/json';
|
||||
$this->hwid = '/config/hwid.json';
|
||||
$this->logs = [
|
||||
'nginx_default_access',
|
||||
'nginx_domain_access',
|
||||
@@ -168,6 +170,30 @@ class Bot
|
||||
case preg_match('~^/setIpLimit$~', $this->input['callback'], $m):
|
||||
$this->setIpLimit();
|
||||
break;
|
||||
case preg_match('~^/hwidLimit$~', $this->input['callback'], $m):
|
||||
$this->hwidLimit();
|
||||
break;
|
||||
case preg_match('~^/toggleHwidLimit(?: (\w+))?$~', $this->input['callback'], $m):
|
||||
$this->toggleHwidLimit($m[1] ?? null);
|
||||
break;
|
||||
case preg_match('~^/setHwidDevices(?: (\w+))?$~', $this->input['callback'], $m):
|
||||
$this->setHwidDevices($m[1] ?? null);
|
||||
break;
|
||||
case preg_match('~^/hwidUser (\d+)(?:_(\d+))?$~', $this->input['callback'], $m):
|
||||
$this->hwidUser($m[1], $m[2] ?? 0);
|
||||
break;
|
||||
case preg_match('~^/hwidUserToggle (\d+)$~', $this->input['callback'], $m):
|
||||
$this->hwidUserToggle($m[1]);
|
||||
break;
|
||||
case preg_match('~^/hwidUserDefault (\d+)$~', $this->input['callback'], $m):
|
||||
$this->hwidUserDefault($m[1]);
|
||||
break;
|
||||
case preg_match('~^/setHwidUserLimit (\d+)$~', $this->input['callback'], $m):
|
||||
$this->setHwidUserLimit($m[1]);
|
||||
break;
|
||||
case preg_match('~^/hwidUserDel (\d+)_(\d+) (.+)$~', $this->input['callback'], $m):
|
||||
$this->hwidUserDel($m[1], $m[2], $m[3]);
|
||||
break;
|
||||
case preg_match('~^/searchLogs (.+)$~', $this->input['message'], $m):
|
||||
$this->searchLogs($m[1]);
|
||||
break;
|
||||
@@ -291,6 +317,21 @@ class Bot
|
||||
case preg_match('~^/logs$~', $this->input['callback'], $m):
|
||||
$this->logs();
|
||||
break;
|
||||
case preg_match('~^/iodine$~', $this->input['callback'], $m):
|
||||
$this->iodine();
|
||||
break;
|
||||
case preg_match('~^/iodineDomain$~', $this->input['callback'], $m):
|
||||
$this->iodineDomain();
|
||||
break;
|
||||
case preg_match('~^/iodinePassword$~', $this->input['callback'], $m):
|
||||
$this->iodinePassword();
|
||||
break;
|
||||
case preg_match('~^/setIodineDomain (\w+)$~', $this->input['callback'], $m):
|
||||
$this->setIodineDomain($m[1]);
|
||||
break;
|
||||
case preg_match('~^/setIodinePassword (\w+)$~', $this->input['callback'], $m):
|
||||
$this->setIodinePassword($m[1]);
|
||||
break;
|
||||
case preg_match('~^/getLog (?P<arg>\d+(?:_(?:-)?\d+)?)$~', $this->input['callback'], $m):
|
||||
$this->getLog(...explode('_', $m['arg']));
|
||||
break;
|
||||
@@ -432,6 +473,9 @@ class Bot
|
||||
case preg_match('~^/download (\d+)$~', $this->input['callback'], $m):
|
||||
$this->downloadPeer($m[1]);
|
||||
break;
|
||||
case preg_match('~^/dw (\w+) (\w+)$~', $this->input['callback'], $m):
|
||||
$this->dw($m[1], $m[2]);
|
||||
break;
|
||||
case preg_match('~^/deloc (\d+)$~', $this->input['callback'], $m):
|
||||
$this->deloc($m[1]);
|
||||
break;
|
||||
@@ -1691,6 +1735,7 @@ class Bot
|
||||
'wg1' => $wg1,
|
||||
'ad' => yaml_parse_file($this->adguard),
|
||||
'pac' => $this->getPacConf(),
|
||||
'hwid' => file_exists($this->hwid) ? (json_decode(file_get_contents($this->hwid), true) ?: []) : [],
|
||||
'ssl' => file_exists('/certs/cert_private') && preg_match('~BEGIN PRIVATE KEY~', file_get_contents('/certs/cert_private')) ? [
|
||||
'private' => file_get_contents('/certs/cert_private'),
|
||||
'public' => file_get_contents('/certs/cert_public'),
|
||||
@@ -1807,6 +1852,13 @@ class Bot
|
||||
file_put_contents('/config/mtprotodomain', $json['mtprotodomain'] ?: '');
|
||||
$this->restartTG();
|
||||
}
|
||||
// hwid
|
||||
if (array_key_exists('hwid', $json)) {
|
||||
$out[] = 'update hwid devices';
|
||||
$this->update($this->input['chat'], $this->input['message_id'], implode("\n", $out));
|
||||
$data = is_array($json['hwid']) ? $json['hwid'] : [];
|
||||
file_put_contents($this->hwid, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
// xray
|
||||
if (!empty($json['xray'])) {
|
||||
$out[] = 'update xray';
|
||||
@@ -1849,6 +1901,15 @@ class Bot
|
||||
}
|
||||
}
|
||||
|
||||
public function dw($u, $t)
|
||||
{
|
||||
$c = $this->getXray()['inbounds'][0]['settings']['clients'][$u];
|
||||
$_GET['s'] = $c['id'];
|
||||
$_GET['t'] = $t;
|
||||
$conf = $this->subscription(1);
|
||||
$this->sendFile($this->input['from'], new CURLStringFile($conf, $c['email'] . ($t == 'cl' ? '_mihomo.yaml' :($t == 'si' ? '_singbox.json' : '_v2ray.json'))));
|
||||
}
|
||||
|
||||
public function downloadPeer($client)
|
||||
{
|
||||
$cl = $client;
|
||||
@@ -4347,6 +4408,104 @@ DNS-over-HTTPS with IP:
|
||||
return implode("\n", $result);
|
||||
}
|
||||
|
||||
public function iodineDomain()
|
||||
{
|
||||
$r = $this->send(
|
||||
$this->input['chat'],
|
||||
"@{$this->input['username']} enter domain",
|
||||
$this->input['message_id'],
|
||||
reply: 'enter domain',
|
||||
);
|
||||
$_SESSION['reply'][$r['result']['message_id']] = [
|
||||
'start_message' => $this->input['message_id'],
|
||||
'callback' => 'setIodineDomain',
|
||||
'args' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function iodinePassword()
|
||||
{
|
||||
$r = $this->send(
|
||||
$this->input['chat'],
|
||||
"@{$this->input['username']} enter password",
|
||||
$this->input['message_id'],
|
||||
reply: 'enter password',
|
||||
);
|
||||
$_SESSION['reply'][$r['result']['message_id']] = [
|
||||
'start_message' => $this->input['message_id'],
|
||||
'callback' => 'setIodinePassword',
|
||||
'args' => [],
|
||||
];
|
||||
}
|
||||
|
||||
public function setIodinePassword($text)
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
if ($text) {
|
||||
$c['iodinePassword'] = $text;
|
||||
} else {
|
||||
unset($c['iodinePassword']);
|
||||
}
|
||||
$this->setPacConf($c);
|
||||
$this->iodineRestart();
|
||||
$this->iodine();
|
||||
}
|
||||
|
||||
public function setIodineDomain($text)
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
if ($text) {
|
||||
$c['iodineDomain'] = $text;
|
||||
} else {
|
||||
unset($c['iodineDomain']);
|
||||
}
|
||||
$this->setPacConf($c);
|
||||
$this->iodineRestart();
|
||||
$this->iodine();
|
||||
}
|
||||
|
||||
public function iodineRestart()
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
$this->ssh('pkill iodine', 'io');
|
||||
if (!empty($c['iodineDomain']) && !empty($c['iodinePassword'])) {
|
||||
$this->ssh("iodined -c -P {$c['iodinePassword']} 10.0.0.1 {$c['iodineDomain']}", 'io');
|
||||
}
|
||||
}
|
||||
|
||||
public function iodine()
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
$text[] = 'Iodine';
|
||||
$text[] = "domain: {$c['iodineDomain']}";
|
||||
$text[] = "password: {$c['iodinePassword']}";
|
||||
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('set domain'),
|
||||
'callback_data' => "/iodineDomain",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('set password'),
|
||||
'callback_data' => "/iodinePassword",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('back'),
|
||||
'callback_data' => "/menu",
|
||||
],
|
||||
];
|
||||
$this->update(
|
||||
$this->input['chat'],
|
||||
$this->input['message_id'],
|
||||
implode("\n", $text),
|
||||
$data ?: false,
|
||||
);
|
||||
}
|
||||
|
||||
public function menu($type = false, $arg = false, $return = false)
|
||||
{
|
||||
$conf = $this->getPacConf();
|
||||
@@ -4402,6 +4561,7 @@ DNS-over-HTTPS with IP:
|
||||
$this->i18n($this->ssh('pgrep mtproto-proxy', 'tg') ? 'on' : 'off') . ' ' . $this->i18n('mtproto'),
|
||||
$this->i18n(exec("JSON=1 timeout 2 dnslookup google.com ad") ? 'on' : 'off') . ' ' . $this->i18n('ad_title'),
|
||||
$this->i18n($this->ssh('pgrep ssserver', 'ss') ? 'on' : 'off') . ' ' . $this->i18n('sh_title'),
|
||||
$this->i18n($this->ssh('pgrep iodine', 'io') ? 'on' : 'off') . ' ' . $this->i18n('Iodine'),
|
||||
$this->i18n($this->warpStatus()) . ' ' . $this->i18n('warp'),
|
||||
],
|
||||
[
|
||||
@@ -4413,6 +4573,7 @@ DNS-over-HTTPS with IP:
|
||||
$this->i18n($c['tg'] ? 'on' : 'off') . ' ' . getenv('TGPORT'),
|
||||
$this->i18n($c['ad'] ? 'on' : 'off') . ' 853',
|
||||
$this->i18n($c['ss'] ? 'on' : 'off') . ' ' . getenv('SSPORT'),
|
||||
$this->i18n($c['io'] ? 'on' : 'off') . ' 53',
|
||||
'',
|
||||
],
|
||||
]);
|
||||
@@ -4486,6 +4647,12 @@ DNS-over-HTTPS with IP:
|
||||
'callback_data' => "/pacMenu 0",
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'text' => $this->i18n('Iodine'),
|
||||
'callback_data' => "/iodine",
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
'text' => $this->i18n('config'),
|
||||
@@ -4597,6 +4764,263 @@ DNS-over-HTTPS with IP:
|
||||
$this->xray();
|
||||
}
|
||||
|
||||
public function hwidLimit()
|
||||
{
|
||||
$pac = $this->getPacConf();
|
||||
$enabled = !empty($pac['hwid_limit_enabled']);
|
||||
$count = max(1, (int) ($pac['hwid_device_count'] ?: 1));
|
||||
|
||||
$text[] = 'Settings -> ' . $this->i18n('hwid limit');
|
||||
$text[] = $this->i18n('hwid notice');
|
||||
$text[] = $this->i18n('hwid limit') . ': ' . ($enabled ? $count : $this->i18n('off'));
|
||||
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n($enabled ? 'on' : 'off'),
|
||||
'callback_data' => '/toggleHwidLimit',
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('set hwid devices count') . ': ' . $count,
|
||||
'callback_data' => '/setHwidDevices',
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('back'),
|
||||
'callback_data' => '/xray',
|
||||
],
|
||||
];
|
||||
|
||||
$this->update(
|
||||
$this->input['chat'],
|
||||
$this->input['message_id'],
|
||||
implode("\n", $text ?: ['...']),
|
||||
$data ?: false,
|
||||
);
|
||||
}
|
||||
|
||||
public function toggleHwidLimit($context = null)
|
||||
{
|
||||
$pac = $this->getPacConf();
|
||||
$pac['hwid_limit_enabled'] = $pac['hwid_limit_enabled'] ? 0 : 1;
|
||||
if (!empty($pac['hwid_limit_enabled']) && empty($pac['hwid_device_count'])) {
|
||||
$pac['hwid_device_count'] = 1;
|
||||
}
|
||||
$this->setPacConf($pac);
|
||||
$this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true);
|
||||
if ($context === 'xray') {
|
||||
$this->xray();
|
||||
} else {
|
||||
$this->hwidLimit();
|
||||
}
|
||||
}
|
||||
|
||||
public function setHwidDevices($context = null)
|
||||
{
|
||||
$r = $this->send(
|
||||
$this->input['chat'],
|
||||
"@{$this->input['username']} enter hwid devices count",
|
||||
$this->input['message_id'],
|
||||
reply: 'enter hwid devices count',
|
||||
);
|
||||
$_SESSION['reply'][$r['result']['message_id']] = [
|
||||
'start_message' => $this->input['message_id'],
|
||||
'callback' => 'saveHwidDevices',
|
||||
'args' => [$context],
|
||||
];
|
||||
}
|
||||
|
||||
public function saveHwidDevices($count, $context = null)
|
||||
{
|
||||
$count = (int) $count;
|
||||
if ($count <= 0) {
|
||||
$count = 1;
|
||||
}
|
||||
$pac = $this->getPacConf();
|
||||
$pac['hwid_device_count'] = $count;
|
||||
$this->setPacConf($pac);
|
||||
$this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']);
|
||||
if ($context === 'xray') {
|
||||
$this->xray();
|
||||
} else {
|
||||
$this->hwidLimit();
|
||||
}
|
||||
}
|
||||
|
||||
public function getHwidStorage()
|
||||
{
|
||||
if (!file_exists($this->hwid)) {
|
||||
return [];
|
||||
}
|
||||
$data = json_decode(file_get_contents($this->hwid), true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
public function setHwidStorage(array $storage)
|
||||
{
|
||||
file_put_contents($this->hwid, json_encode($storage, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
public function getHwidDevicesByUser($uid)
|
||||
{
|
||||
$storage = $this->getHwidStorage();
|
||||
return $storage[$uid] ?? [];
|
||||
}
|
||||
|
||||
public function setHwidDevice($uid, $hwid, array $info)
|
||||
{
|
||||
$storage = $this->getHwidStorage();
|
||||
$storage[$uid][$hwid] = $info;
|
||||
$this->setHwidStorage($storage);
|
||||
}
|
||||
|
||||
public function deleteHwidDevice($uid, $hwid)
|
||||
{
|
||||
$storage = $this->getHwidStorage();
|
||||
if (isset($storage[$uid][$hwid])) {
|
||||
unset($storage[$uid][$hwid]);
|
||||
if (empty($storage[$uid])) {
|
||||
unset($storage[$uid]);
|
||||
}
|
||||
$this->setHwidStorage($storage);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteHwidUser($uid)
|
||||
{
|
||||
$storage = $this->getHwidStorage();
|
||||
if (isset($storage[$uid])) {
|
||||
unset($storage[$uid]);
|
||||
$this->setHwidStorage($storage);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getHwidTokenScope($index)
|
||||
{
|
||||
return ($this->input['chat'] ?? 'global') . ':' . $index;
|
||||
}
|
||||
|
||||
protected function rememberHwidToken($scope, $hwid)
|
||||
{
|
||||
if (!isset($_SESSION['hwidTokens'])) {
|
||||
$_SESSION['hwidTokens'] = [];
|
||||
}
|
||||
if (!isset($_SESSION['hwidTokens'][$scope])) {
|
||||
$_SESSION['hwidTokens'][$scope] = [];
|
||||
}
|
||||
do {
|
||||
try {
|
||||
$token = bin2hex(random_bytes(5));
|
||||
} catch (\Throwable $e) {
|
||||
$token = substr(hash('sha256', $hwid . microtime(true)), 0, 10);
|
||||
}
|
||||
} while (isset($_SESSION['hwidTokens'][$scope][$token]));
|
||||
|
||||
$_SESSION['hwidTokens'][$scope][$token] = $hwid;
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
protected function resolveHwidToken($scope, $token)
|
||||
{
|
||||
if (isset($_SESSION['hwidTokens'][$scope][$token])) {
|
||||
$hwid = $_SESSION['hwidTokens'][$scope][$token];
|
||||
unset($_SESSION['hwidTokens'][$scope][$token]);
|
||||
return $hwid;
|
||||
}
|
||||
|
||||
$decoded = base64_decode($token, true);
|
||||
|
||||
return $decoded !== false ? $decoded : '';
|
||||
}
|
||||
|
||||
public function processHwidRequest(array $client)
|
||||
{
|
||||
$pac = $this->getPacConf();
|
||||
if (empty($pac['hwid_limit_enabled']) || !empty($client['hwid_disabled'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$limit = (int) ($client['hwid_limit'] ?: ($pac['hwid_device_count'] ?: 0));
|
||||
if ($limit <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$devices = $this->getHwidDevicesByUser($client['id']);
|
||||
$hwid = trim($_SERVER['HTTP_X_HWID'] ?? '');
|
||||
$isBrowser = $this->isBrowserRequest();
|
||||
|
||||
if ($hwid === '') {
|
||||
if ($isBrowser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$message = 'HWID device limit exceeded';
|
||||
header('announce: base64:' . base64_encode($message));
|
||||
header('X-HWID-Status: ' . $message);
|
||||
header('HTTP/1.1 429 Too Many Requests', true, 429);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$isNew = !isset($devices[$hwid]);
|
||||
|
||||
if ($isNew && count($devices) >= $limit) {
|
||||
$message = 'HWID device limit exceeded';
|
||||
header('announce: base64:' . base64_encode($message));
|
||||
header('X-HWID-Status: ' . $message);
|
||||
header('HTTP/1.1 429 Too Many Requests', true, 429);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setHwidDevice($client['id'], $hwid, [
|
||||
'time' => time(),
|
||||
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||
'device_os' => $_SERVER['HTTP_X_DEVICE_OS'] ?? '',
|
||||
'os_version' => $_SERVER['HTTP_X_VER_OS'] ?? '',
|
||||
'device_model' => $_SERVER['HTTP_X_DEVICE_MODEL'] ?? '',
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isBrowserRequest()
|
||||
{
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
|
||||
|
||||
if ($userAgent === '' && $accept === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$browserPatterns = [
|
||||
'Mozilla/',
|
||||
'Chrome/',
|
||||
'Safari/',
|
||||
'Firefox/',
|
||||
'Edge/',
|
||||
'Edg/',
|
||||
'MSIE ',
|
||||
'Trident/',
|
||||
'Opera/',
|
||||
'OPR/',
|
||||
];
|
||||
|
||||
foreach ($browserPatterns as $pattern) {
|
||||
if (stripos($userAgent, $pattern) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (stripos($accept, 'text/html') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function switchSilence()
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
@@ -5432,6 +5856,7 @@ DNS-over-HTTPS with IP:
|
||||
$st = $this->getXrayStats();
|
||||
foreach ($r['inbounds'][0]['settings']['clients'] as $k => $v) {
|
||||
if ($i == $k) {
|
||||
$this->deleteHwidUser($r['inbounds'][0]['settings']['clients'][$k]['id']);
|
||||
unset($r['inbounds'][0]['settings']['clients'][$k]);
|
||||
unset($st['users'][$k]);
|
||||
$this->setXrayStats($st);
|
||||
@@ -5903,13 +6328,25 @@ DNS-over-HTTPS with IP:
|
||||
'callback_data' => "/changeTransport 1",
|
||||
],
|
||||
];
|
||||
$ip_count = $p['ip_count'] ?: 1;
|
||||
$ip_count = $p['ip_count'] ?: 1;
|
||||
$hwidEnabled = !empty($p['hwid_limit_enabled']);
|
||||
$defaultHwids = max(1, (int) ($p['hwid_device_count'] ?: 1));
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('ip limit') . ' ' . ($p['ip_limit'] ? ": {$p['ip_limit']} sec & $ip_count" : $this->i18n('off')),
|
||||
'callback_data' => "/setIpLimit",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('hwid limit') . ': ' . $this->i18n($hwidEnabled ? 'on' : 'off') . " ({$defaultHwids})",
|
||||
'callback_data' => '/toggleHwidLimit xray',
|
||||
],
|
||||
[
|
||||
'text' => $this->i18n('set hwid devices count'),
|
||||
'callback_data' => '/setHwidDevices xray',
|
||||
],
|
||||
];
|
||||
if ($p['transport'] == 'Reality') {
|
||||
$data[] = [
|
||||
[
|
||||
@@ -6295,12 +6732,18 @@ DNS-over-HTTPS with IP:
|
||||
|
||||
public function userXr($i)
|
||||
{
|
||||
$c = $this->getXray()['inbounds'][0]['settings']['clients'][$i];
|
||||
$xray = $this->getXray();
|
||||
$c = $xray['inbounds'][0]['settings']['clients'][$i];
|
||||
$pac = $this->getPacConf();
|
||||
$domain = $this->getDomain($pac['transport'] != 'Reality');
|
||||
$scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https';
|
||||
$hash = $this->getHashBot();
|
||||
|
||||
$devices = $this->getHwidDevicesByUser($c['id']);
|
||||
$hwidEnabled = !empty($pac['hwid_limit_enabled']) && empty($c['hwid_disabled']);
|
||||
$defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1));
|
||||
$hwidLimit = $c['hwid_limit'] ? (int) $c['hwid_limit'] : $defaultHwid;
|
||||
|
||||
$text[] = "Menu -> " . $this->i18n('xray') . " -> {$c['email']}\n";
|
||||
if (file_exists(__DIR__ . '/subscription.php')) {
|
||||
$text[] = "<a href='$scheme://{$domain}/pac$hash/sub?id={$c['id']}'>subscription</a>";
|
||||
@@ -6347,15 +6790,29 @@ DNS-over-HTTPS with IP:
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('v2ray'),
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=s&s={$c['id']}"],
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=s&s={$c['id']}"]
|
||||
],
|
||||
[
|
||||
'text' => $this->i18n('singbox'),
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=si&s={$c['id']}"],
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=si&s={$c['id']}"]
|
||||
],
|
||||
[
|
||||
'text' => $this->i18n('mihomo'),
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=cl&s={$c['id']}"],
|
||||
'web_app' => ['url' => "https://{$domain}/pac$hash?t=cl&s={$c['id']}"]
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('v2ray ⬇️'),
|
||||
'callback_data' => "/dw {$i} s",
|
||||
],
|
||||
[
|
||||
'text' => $this->i18n('singbox ⬇️'),
|
||||
'callback_data' => "/dw {$i} si",
|
||||
],
|
||||
[
|
||||
'text' => $this->i18n('mihomo ⬇️'),
|
||||
'callback_data' => "/dw {$i} cl",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
@@ -6399,6 +6856,12 @@ DNS-over-HTTPS with IP:
|
||||
'callback_data' => "/qrXray {$i}_2",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('hwid limit') . ': ' . ($hwidEnabled ? $hwidLimit : $this->i18n('off')) . ' (' . count($devices) . ')',
|
||||
'callback_data' => "/hwidUser $i",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('rename'),
|
||||
@@ -6423,6 +6886,188 @@ DNS-over-HTTPS with IP:
|
||||
);
|
||||
}
|
||||
|
||||
public function hwidUser($i, $page = 0)
|
||||
{
|
||||
$xray = $this->getXray();
|
||||
$client = $xray['inbounds'][0]['settings']['clients'][$i];
|
||||
$pac = $this->getPacConf();
|
||||
|
||||
$devices = $this->getHwidDevicesByUser($client['id']);
|
||||
$scope = $this->getHwidTokenScope($i);
|
||||
if (!isset($_SESSION['hwidTokens'])) {
|
||||
$_SESSION['hwidTokens'] = [];
|
||||
}
|
||||
$_SESSION['hwidTokens'][$scope] = [];
|
||||
uasort($devices, fn($a, $b) => ($b['time'] ?? 0) <=> ($a['time'] ?? 0));
|
||||
$hwids = array_keys($devices);
|
||||
$perPage = max(1, $this->limit ?: 5);
|
||||
$total = count($hwids);
|
||||
$pages = max(1, (int) ceil($total / $perPage));
|
||||
$page = min(max((int) $page, 0), $pages - 1);
|
||||
$hwidsPage = array_slice($hwids, $page * $perPage, $perPage);
|
||||
$defaultHwid = max(1, (int) ($pac['hwid_device_count'] ?: 1));
|
||||
|
||||
$text[] = "Menu -> " . $this->i18n('xray') . " -> {$client['email']} -> " . $this->i18n('hwid devices');
|
||||
$text[] = $this->i18n('hwid notice');
|
||||
if (empty($pac['hwid_limit_enabled'])) {
|
||||
$status = $this->i18n('off');
|
||||
} elseif (!empty($client['hwid_disabled'])) {
|
||||
$status = $this->i18n('off');
|
||||
} elseif (!empty($client['hwid_limit'])) {
|
||||
$status = (int) $client['hwid_limit'];
|
||||
} else {
|
||||
$status = "default($defaultHwid)";
|
||||
}
|
||||
$text[] = $this->i18n('hwid limit') . ': ' . $status;
|
||||
$text[] = $this->i18n('hwid devices') . ': ' . $total;
|
||||
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n(!empty($client['hwid_disabled']) ? 'off' : 'on'),
|
||||
'callback_data' => "/hwidUserToggle $i",
|
||||
],
|
||||
];
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('set hwid devices count'),
|
||||
'callback_data' => "/setHwidUserLimit $i",
|
||||
],
|
||||
];
|
||||
if (!empty($client['hwid_limit'])) {
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('use default hwid limit'),
|
||||
'callback_data' => "/hwidUserDefault $i",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($total == 0) {
|
||||
$text[] = $this->i18n('no devices');
|
||||
}
|
||||
|
||||
foreach ($hwidsPage as $index => $hwid) {
|
||||
$info = $devices[$hwid];
|
||||
$number = $page * $perPage + $index + 1;
|
||||
$details = array_filter([
|
||||
$info['device_os'] ?? '',
|
||||
$info['os_version'] ?? '',
|
||||
$info['device_model'] ?? '',
|
||||
], fn($v) => $v !== '');
|
||||
$line = $number . '. <code>' . htmlspecialchars($hwid, ENT_HTML5, 'UTF-8') . '</code>';
|
||||
if (!empty($details)) {
|
||||
$line .= ' - ' . htmlspecialchars(implode(' ', $details), ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
if (!empty($info['time'])) {
|
||||
$line .= ' (' . date('d.m.Y H:i', $info['time']) . ')';
|
||||
}
|
||||
$text[] = $line;
|
||||
if (!empty($info['user_agent'])) {
|
||||
$text[] = 'UA: ' . htmlspecialchars($info['user_agent'], ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
$token = $this->rememberHwidToken($scope, $hwid);
|
||||
$data[] = [
|
||||
[
|
||||
'text' => '🗑 ' . $number,
|
||||
'callback_data' => "/hwidUserDel {$i}_{$page} $token",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if ($pages > 1) {
|
||||
$data[] = [
|
||||
[
|
||||
'text' => '<<',
|
||||
'callback_data' => "/hwidUser {$i}_" . ($page - 1 >= 0 ? $page - 1 : $pages - 1),
|
||||
],
|
||||
[
|
||||
'text' => ($page + 1) . '/' . $pages,
|
||||
'callback_data' => "/hwidUser {$i}_$page",
|
||||
],
|
||||
[
|
||||
'text' => '>>',
|
||||
'callback_data' => "/hwidUser {$i}_" . (($page + 1) % $pages),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$data[] = [
|
||||
[
|
||||
'text' => $this->i18n('back'),
|
||||
'callback_data' => "/userXr $i",
|
||||
],
|
||||
];
|
||||
|
||||
$this->update(
|
||||
$this->input['chat'],
|
||||
$this->input['message_id'],
|
||||
implode("\n", $text ?: ['...']),
|
||||
$data ?: false,
|
||||
);
|
||||
}
|
||||
|
||||
public function hwidUserToggle($i)
|
||||
{
|
||||
$xray = $this->getXray();
|
||||
if (!empty($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'])) {
|
||||
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled']);
|
||||
} else {
|
||||
$xray['inbounds'][0]['settings']['clients'][$i]['hwid_disabled'] = 1;
|
||||
}
|
||||
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$this->answer($this->input['callback_id'], $this->i18n('hwid notice'), true);
|
||||
$this->hwidUser($i);
|
||||
}
|
||||
|
||||
public function hwidUserDefault($i)
|
||||
{
|
||||
$xray = $this->getXray();
|
||||
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']);
|
||||
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$this->hwidUser($i);
|
||||
}
|
||||
|
||||
public function setHwidUserLimit($i)
|
||||
{
|
||||
$r = $this->send(
|
||||
$this->input['chat'],
|
||||
"@{$this->input['username']} enter hwid devices count",
|
||||
$this->input['message_id'],
|
||||
reply: 'enter hwid devices count',
|
||||
);
|
||||
$_SESSION['reply'][$r['result']['message_id']] = [
|
||||
'start_message' => $this->input['message_id'],
|
||||
'callback' => 'saveHwidUserLimit',
|
||||
'args' => [$i],
|
||||
];
|
||||
}
|
||||
|
||||
public function saveHwidUserLimit($count, $i)
|
||||
{
|
||||
$xray = $this->getXray();
|
||||
$count = (int) $count;
|
||||
if ($count > 0) {
|
||||
$xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit'] = $count;
|
||||
} else {
|
||||
unset($xray['inbounds'][0]['settings']['clients'][$i]['hwid_limit']);
|
||||
}
|
||||
file_put_contents('/config/xray.json', json_encode($xray, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$this->send($this->input['chat'], $this->i18n('hwid notice'), $this->input['message_id']);
|
||||
$this->hwidUser($i);
|
||||
}
|
||||
|
||||
public function hwidUserDel($i, $page, $hwid)
|
||||
{
|
||||
$xray = $this->getXray();
|
||||
$uid = $xray['inbounds'][0]['settings']['clients'][$i]['id'];
|
||||
$scope = $this->getHwidTokenScope($i);
|
||||
$decoded = $this->resolveHwidToken($scope, $hwid);
|
||||
if ($decoded !== '') {
|
||||
$this->deleteHwidDevice($uid, $decoded);
|
||||
}
|
||||
$this->hwidUser($i, $page);
|
||||
}
|
||||
|
||||
public function getDomain($cdn = false)
|
||||
{
|
||||
$c = $this->getPacConf();
|
||||
@@ -6441,16 +7086,23 @@ DNS-over-HTTPS with IP:
|
||||
$scheme = empty($this->nginxGetTypeCert()) ? 'http' : 'https';
|
||||
$hash = $this->getHashBot();
|
||||
$flag = true;
|
||||
$client = null;
|
||||
foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) {
|
||||
if ($v['id'] == $_GET['id']) {
|
||||
if (empty($v['off'])) {
|
||||
$flag = false;
|
||||
}
|
||||
$uid = $v['id'];
|
||||
$email = $v['email'];
|
||||
$uid = $v['id'];
|
||||
$email = $v['email'];
|
||||
$expire = $v['time'];
|
||||
$client = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$flag && !$this->processHwidRequest($client)) {
|
||||
exit;
|
||||
}
|
||||
$suburl = "<a href='$scheme://{$domain}/pac$hash/sub?id={$uid}'>subscription</a>";
|
||||
$download = $this->getBytes($st['users'][$k]['global']['download'] + $st['users'][$k]['session']['download']);
|
||||
$upload = $this->getBytes($st['users'][$k]['global']['upload'] + $st['users'][$k]['session']['upload']);
|
||||
$singbox = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([
|
||||
@@ -6502,6 +7154,7 @@ DNS-over-HTTPS with IP:
|
||||
$hash = $this->getHashBot();
|
||||
|
||||
$flag = true;
|
||||
$client = null;
|
||||
foreach ($xr['inbounds'][0]['settings']['clients'] as $k => $v) {
|
||||
if ($v['id'] == $_GET['s']) {
|
||||
if (empty($v['off'])) {
|
||||
@@ -6510,6 +7163,7 @@ DNS-over-HTTPS with IP:
|
||||
$template = base64_decode($v["{$type}template"]);
|
||||
$uid = $v['id'];
|
||||
$email = $v['email'];
|
||||
$client = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -6518,6 +7172,10 @@ DNS-over-HTTPS with IP:
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$return && !$this->processHwidRequest($client)) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!empty($_GET['r'])) {
|
||||
$si = "$scheme://{$domain}/pac$hash/" . base64_encode(serialize([
|
||||
'h' => $hash,
|
||||
@@ -6642,7 +7300,6 @@ DNS-over-HTTPS with IP:
|
||||
}
|
||||
break;
|
||||
case 'si':
|
||||
$c['outbounds'][$index]['server'] = '~domain~';
|
||||
$c['outbounds'][$index]['uuid'] = '~uid~';
|
||||
if ($pac['transport'] != 'Reality') {
|
||||
unset($c['outbounds'][$index]['tls']['reality']);
|
||||
@@ -6725,6 +7382,9 @@ DNS-over-HTTPS with IP:
|
||||
}
|
||||
$c['route']['rules'] = array_values($c['route']['rules']);
|
||||
}
|
||||
if (empty($c['route'])) {
|
||||
unset($c['route']);
|
||||
}
|
||||
break;
|
||||
case 'cl':
|
||||
$c = $this->addClashRuleSet($c);
|
||||
@@ -6948,7 +7608,9 @@ DNS-over-HTTPS with IP:
|
||||
}
|
||||
}
|
||||
}
|
||||
$route['rules'] = array_values($route['rules']);
|
||||
if (!empty($route['rules'])) {
|
||||
$route['rules'] = array_values($route['rules']);
|
||||
}
|
||||
$route['rule_set'] = array_merge($route['rule_set'] ?: [], $ruleset ?: []);
|
||||
if (empty($route['rule_set'])) {
|
||||
unset($route['rule_set']);
|
||||
@@ -7542,6 +8204,10 @@ DNS-over-HTTPS with IP:
|
||||
'text' => $this->i18n($c['ss'] ? 'on' : 'off') . ' 8388 Shadowsocks',
|
||||
'callback_data' => "/hidePort ss",
|
||||
]],
|
||||
[[
|
||||
'text' => $this->i18n($c['io'] ? 'on' : 'off') . ' 53 Iodine',
|
||||
'callback_data' => "/hidePort io",
|
||||
]],
|
||||
];
|
||||
if (!empty($pac['restart'])) {
|
||||
$data[] = [
|
||||
@@ -7573,6 +8239,7 @@ DNS-over-HTTPS with IP:
|
||||
'tg' => getenv('TGPORT') . ':' . getenv('TGPORT'),
|
||||
'ad' => '853:853',
|
||||
'ss' => '8388:8388',
|
||||
'io' => '53:53/udp',
|
||||
];
|
||||
$f = '/docker/compose';
|
||||
$c = yaml_parse_file($f);
|
||||
|
||||
@@ -193,6 +193,30 @@ $i = [
|
||||
'en' => 'delete internal dns',
|
||||
'ru' => 'удалить внутренний dns',
|
||||
],
|
||||
'hwid limit' => [
|
||||
'en' => 'HWID limit',
|
||||
'ru' => 'HWID лимит',
|
||||
],
|
||||
'hwid devices' => [
|
||||
'en' => 'HWID devices',
|
||||
'ru' => 'Устройства HWID',
|
||||
],
|
||||
'set hwid devices count' => [
|
||||
'en' => 'set HWID devices count',
|
||||
'ru' => 'установить количество HWID устройств',
|
||||
],
|
||||
'use default hwid limit' => [
|
||||
'en' => 'use default limit',
|
||||
'ru' => 'использовать лимит по умолчанию',
|
||||
],
|
||||
'no devices' => [
|
||||
'en' => 'no devices',
|
||||
'ru' => 'нет устройств',
|
||||
],
|
||||
'hwid notice' => [
|
||||
'en' => 'HWID limit works only when subscription is refreshed or added',
|
||||
'ru' => 'HWID лимит срабатывает только в момент обновления и добавления подписки',
|
||||
],
|
||||
'on' => [
|
||||
'en' => '🟢',
|
||||
'ru' => '🟢',
|
||||
|
||||
@@ -24,3 +24,4 @@ $bot->adguardSync();
|
||||
$bot->cloakNginx();
|
||||
$bot->syncDeny();
|
||||
$bot->cleanDocker();
|
||||
$bot->iodineRestart();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
+31
-2
@@ -283,7 +283,7 @@ services:
|
||||
ipv4_address: 10.10.0.14
|
||||
logging: *default-logging
|
||||
ad:
|
||||
image: mercurykd/vpnbot-ad:1.3
|
||||
image: mercurykd/vpnbot-ad:1.4
|
||||
build:
|
||||
dockerfile: dockerfile/adguard.dockerfile
|
||||
args:
|
||||
@@ -346,7 +346,7 @@ services:
|
||||
ipv4_address: 10.10.0.8
|
||||
logging: *default-logging
|
||||
xr:
|
||||
image: mercurykd/vpnbot-xr:1.4
|
||||
image: mercurykd/vpnbot-xr:1.5
|
||||
build:
|
||||
dockerfile: dockerfile/xray.dockerfile
|
||||
args:
|
||||
@@ -532,3 +532,32 @@ services:
|
||||
default:
|
||||
ipv4_address: 10.10.0.6
|
||||
logging: *default-logging
|
||||
io:
|
||||
image: mercurykd/vpnbot-io:1.0
|
||||
build:
|
||||
dockerfile: dockerfile/iodine.dockerfile
|
||||
volumes:
|
||||
- ./config/.profile:/root/.ashrc:ro
|
||||
- ./ssh:/ssh
|
||||
- ./config/sshd_config:/etc/ssh/sshd_config
|
||||
- ./scripts/start_io.sh:/start_io.sh
|
||||
hostname: iodine
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
container_name: iodine-${VER}
|
||||
depends_on:
|
||||
php:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: true # default
|
||||
- path: ./override.env
|
||||
required: false
|
||||
stop_grace_period: 1s
|
||||
command: ["/bin/sh", "/start_io.sh"]
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 10.10.0.16
|
||||
logging: *default-logging
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM alpine:3.22
|
||||
RUN apk add iodine openssh iptables\
|
||||
&& mkdir /root/.ssh
|
||||
ENV ENV="/root/.ashrc"
|
||||
@@ -37,6 +37,8 @@ proxy: # консоль сервиса
|
||||
docker compose exec proxy /bin/sh
|
||||
tg: # консоль сервиса
|
||||
docker compose exec tg /bin/sh
|
||||
io: # консоль сервиса
|
||||
docker compose exec io /bin/sh
|
||||
xr: # консоль сервиса
|
||||
docker compose exec xr /bin/sh
|
||||
oc: # консоль сервиса
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
cat /ssh/key.pub > /root/.ssh/authorized_keys
|
||||
ssh-keygen -A
|
||||
exec /usr/sbin/sshd -D -e "$@" &
|
||||
iptables -A FORWARD -i dns0 -o eth0 -j ACCEPT
|
||||
iptables -A FORWARD -i eth0 -o dns0 -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
tail -f /dev/null
|
||||
@@ -1,3 +1,9 @@
|
||||
08.11.2025 v2.26
|
||||
- обновлено ядро xray
|
||||
- обновлен adguardHome
|
||||
- добавлен iodine(dnstt)
|
||||
- добавлены кнопки скачивания vless-конфига
|
||||
- hwid для подписки orion от legiz
|
||||
19.07.2025 v2.25
|
||||
- перед запуском обязательно запустить:
|
||||
<pre>touch ./override.env ./docker-compose.override.yml ./config/location.conf ./config/override.conf</pre>
|
||||
|
||||
Reference in New Issue
Block a user