1363 lines
51 KiB
PHP
1363 lines
51 KiB
PHP
<?php
|
|
include './cfg.php';
|
|
|
|
$str_cfg = substr($selected_config, strlen("$neko_dir/config") + 1);
|
|
$_IMG = '/luci-static/ssr/';
|
|
$singbox_bin = '/usr/bin/sing-box';
|
|
$singbox_log = '/var/log/singbox_log.txt';
|
|
$singbox_config_dir = '/etc/neko/config';
|
|
$log = '/etc/neko/tmp/log.txt';
|
|
$start_script_path = '/etc/neko/core/start.sh';
|
|
|
|
$log_dir = dirname($log);
|
|
if (!file_exists($log_dir)) {
|
|
mkdir($log_dir, 0755, true);
|
|
}
|
|
|
|
$start_script_template = <<<'EOF'
|
|
#!/bin/bash
|
|
|
|
export ENABLE_DEPRECATED_TUN_ADDRESS_X=true
|
|
|
|
SINGBOX_LOG="%s"
|
|
CONFIG_FILE="%s"
|
|
SINGBOX_BIN="%s"
|
|
FIREWALL_LOG="%s"
|
|
|
|
mkdir -p "$(dirname "$SINGBOX_LOG")"
|
|
mkdir -p "$(dirname "$FIREWALL_LOG")"
|
|
touch "$SINGBOX_LOG"
|
|
touch "$FIREWALL_LOG"
|
|
chmod 644 "$SINGBOX_LOG"
|
|
chmod 644 "$FIREWALL_LOG"
|
|
|
|
exec >> "$SINGBOX_LOG" 2>&1
|
|
|
|
log() {
|
|
echo "[$(date)] $1" >> "$FIREWALL_LOG"
|
|
}
|
|
|
|
log "Starting Sing-box with config: $CONFIG_FILE"
|
|
|
|
log "Restarting firewall..."
|
|
/etc/init.d/firewall restart
|
|
sleep 2
|
|
|
|
if command -v fw4 > /dev/null; then
|
|
log "FW4 Detected. Starting nftables."
|
|
|
|
nft flush ruleset
|
|
|
|
nft -f - <<'NFTABLES'
|
|
flush ruleset
|
|
|
|
table inet singbox {
|
|
set local_ipv4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
elements = {
|
|
10.0.0.0/8,
|
|
127.0.0.0/8,
|
|
169.254.0.0/16,
|
|
172.16.0.0/12,
|
|
192.168.0.0/16,
|
|
240.0.0.0/4
|
|
}
|
|
}
|
|
|
|
set local_ipv6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
elements = {
|
|
::ffff:0.0.0.0/96,
|
|
64:ff9b::/96,
|
|
100::/64,
|
|
2001::/32,
|
|
2001:10::/28,
|
|
2001:20::/28,
|
|
2001:db8::/32,
|
|
2002::/16,
|
|
fc00::/7,
|
|
fe80::/10
|
|
}
|
|
}
|
|
|
|
chain singbox-tproxy {
|
|
fib daddr type { unspec, local, anycast, multicast } return
|
|
ip daddr @local_ipv4 return
|
|
ip6 daddr @local_ipv6 return
|
|
udp dport { 123 } return
|
|
meta l4proto { tcp, udp } meta mark set 1 tproxy to :9888 accept
|
|
}
|
|
|
|
chain singbox-mark {
|
|
fib daddr type { unspec, local, anycast, multicast } return
|
|
ip daddr @local_ipv4 return
|
|
ip6 daddr @local_ipv6 return
|
|
udp dport { 123 } return
|
|
meta mark set 1
|
|
}
|
|
|
|
chain mangle-output {
|
|
type route hook output priority mangle; policy accept;
|
|
meta l4proto { tcp, udp } skgid != 1 ct direction original goto singbox-mark
|
|
}
|
|
|
|
chain mangle-prerouting {
|
|
type filter hook prerouting priority mangle; policy accept;
|
|
iifname { lo, eth0 } meta l4proto { tcp, udp } ct direction original goto singbox-tproxy
|
|
}
|
|
}
|
|
NFTABLES
|
|
|
|
elif command -v fw3 > /dev/null; then
|
|
log "FW3 Detected. Starting iptables."
|
|
|
|
iptables -t mangle -F
|
|
iptables -t mangle -X
|
|
iptables -t mangle -N singbox-mark
|
|
iptables -t mangle -A singbox-mark -m addrtype --dst-type UNSPEC,LOCAL,ANYCAST,MULTICAST -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 10.0.0.0/8 -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 127.0.0.0/8 -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 169.254.0.0/16 -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 172.16.0.0/12 -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 192.168.0.0/16 -j RETURN
|
|
iptables -t mangle -A singbox-mark -d 240.0.0.0/4 -j RETURN
|
|
iptables -t mangle -A singbox-mark -p udp --dport 123 -j RETURN
|
|
iptables -t mangle -A singbox-mark -j MARK --set-mark 1
|
|
|
|
iptables -t mangle -N singbox-tproxy
|
|
iptables -t mangle -A singbox-tproxy -m addrtype --dst-type UNSPEC,LOCAL,ANYCAST,MULTICAST -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 10.0.0.0/8 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 127.0.0.0/8 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 169.254.0.0/16 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 172.16.0.0/12 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 192.168.0.0/16 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -d 240.0.0.0/4 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -p udp --dport 123 -j RETURN
|
|
iptables -t mangle -A singbox-tproxy -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 9888
|
|
iptables -t mangle -A singbox-tproxy -p udp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 9888
|
|
|
|
iptables -t mangle -A OUTPUT -p tcp -m cgroup ! --cgroup 1 -j singbox-mark
|
|
iptables -t mangle -A OUTPUT -p udp -m cgroup ! --cgroup 1 -j singbox-mark
|
|
iptables -t mangle -A PREROUTING -i lo -p tcp -j singbox-tproxy
|
|
iptables -t mangle -A PREROUTING -i lo -p udp -j singbox-tproxy
|
|
iptables -t mangle -A PREROUTING -i eth0 -p tcp -j singbox-tproxy
|
|
iptables -t mangle -A PREROUTING -i eth0 -p udp -j singbox-tproxy
|
|
|
|
ip6tables -t mangle -N singbox-mark
|
|
ip6tables -t mangle -A singbox-mark -m addrtype --dst-type UNSPEC,LOCAL,ANYCAST,MULTICAST -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d ::ffff:0.0.0.0/96 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 64:ff9b::/96 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 100::/64 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 2001::/32 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 2001:10::/28 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 2001:20::/28 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 2001:db8::/32 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d 2002::/16 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d fc00::/7 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -d fe80::/10 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -p udp --dport 123 -j RETURN
|
|
ip6tables -t mangle -A singbox-mark -j MARK --set-mark 1
|
|
|
|
ip6tables -t mangle -N singbox-tproxy
|
|
ip6tables -t mangle -A singbox-tproxy -m addrtype --dst-type UNSPEC,LOCAL,ANYCAST,MULTICAST -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d ::ffff:0.0.0.0/96 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 64:ff9b::/96 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 100::/64 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 2001::/32 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 2001:10::/28 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 2001:20::/28 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 2001:db8::/32 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d 2002::/16 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d fc00::/7 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -d fe80::/10 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -p udp --dport 123 -j RETURN
|
|
ip6tables -t mangle -A singbox-tproxy -p tcp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 9888
|
|
ip6tables -t mangle -A singbox-tproxy -p udp -j TPROXY --tproxy-mark 0x1/0x1 --on-port 9888
|
|
|
|
ip6tables -t mangle -A OUTPUT -p tcp -m cgroup ! --cgroup 1 -j singbox-mark
|
|
ip6tables -t mangle -A OUTPUT -p udp -m cgroup ! --cgroup 1 -j singbox-mark
|
|
ip6tables -t mangle -A PREROUTING -i lo -p tcp -j singbox-tproxy
|
|
ip6tables -t mangle -A PREROUTING -i lo -p udp -j singbox-tproxy
|
|
ip6tables -t mangle -A PREROUTING -i eth0 -p tcp -j singbox-tproxy
|
|
ip6tables -t mangle -A PREROUTING -i eth0 -p udp -j singbox-tproxy
|
|
|
|
else
|
|
log "Neither fw3 nor fw4 detected, unable to configure firewall rules."
|
|
exit 1
|
|
fi
|
|
|
|
log "Firewall rules applied successfully"
|
|
log "Starting sing-box with config: $CONFIG_FILE"
|
|
exec "$SINGBOX_BIN" run -c "$CONFIG_FILE"
|
|
EOF;
|
|
|
|
function createStartScript($configFile) {
|
|
global $start_script_template, $singbox_bin, $singbox_log, $log;
|
|
$script = sprintf($start_script_template, $singbox_log, $configFile, $singbox_bin, $log);
|
|
|
|
$dir = dirname('/etc/neko/core/start.sh');
|
|
if (!file_exists($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
|
|
file_put_contents('/etc/neko/core/start.sh', $script);
|
|
chmod('/etc/neko/core/start.sh', 0755);
|
|
|
|
//writeToLog("Created start script with config: $configFile");
|
|
//writeToLog("Singbox binary: $singbox_bin");
|
|
//writeToLog("Log file: $singbox_log");
|
|
//writeToLog("Firewall log file: $log");
|
|
}
|
|
|
|
function writeToLog($message) {
|
|
global $log;
|
|
$dateTime = new DateTime();
|
|
$time = $dateTime->format('H:i:s');
|
|
$logMessage = "[ $time ] $message\n";
|
|
if (file_put_contents($log, $logMessage, FILE_APPEND) === false) {
|
|
error_log("Failed to write to log file: $log");
|
|
}
|
|
}
|
|
|
|
function createCronScript() {
|
|
$log_file = '/var/log/singbox_log.txt';
|
|
$tmp_log_file = '/etc/neko/tmp/neko_log.txt';
|
|
$additional_log_file = '/etc/neko/tmp/log.txt';
|
|
$max_size = 1048576;
|
|
$cron_schedule = "0 */4 * * * /bin/bash /etc/neko/core/set_cron.sh";
|
|
$cronScriptContent = <<<EOL
|
|
#!/bin/bash
|
|
|
|
LOG_FILE="$log_file"
|
|
TMP_LOG_FILE="$tmp_log_file"
|
|
ADDITIONAL_LOG_FILE="$additional_log_file"
|
|
MAX_SIZE=$max_size
|
|
LOG_PATH="/etc/neko/tmp/log.txt"
|
|
|
|
crontab -l | grep -v "/etc/neko/core/set_cron.sh" | crontab -
|
|
(crontab -l 2>/dev/null; echo "$cron_schedule") | crontab -
|
|
|
|
timestamp() {
|
|
date "+[ %H:%M:%S ]"
|
|
}
|
|
|
|
if [ -f "\$LOG_FILE" ] && [ \$(stat -c %s "\$LOG_FILE") -gt \$MAX_SIZE ]; then
|
|
echo "\$(timestamp) Sing-box log file (\$LOG_FILE) exceeded \$MAX_SIZE bytes. Clearing log..." >> \$LOG_PATH 2>&1
|
|
> "\$LOG_FILE"
|
|
echo "\$(timestamp) Sing-box log file (\$LOG_FILE) has been cleared." >> \$LOG_PATH 2>&1
|
|
else
|
|
echo "\$(timestamp) Sing-box log file (\$LOG_FILE) is within the size limit. No action needed." >> \$LOG_PATH 2>&1
|
|
fi
|
|
|
|
if [ -f "\$TMP_LOG_FILE" ] && [ \$(stat -c %s "\$TMP_LOG_FILE") -gt \$MAX_SIZE ]; then
|
|
echo "\$(timestamp) Mihomo log file (\$TMP_LOG_FILE) exceeded \$MAX_SIZE bytes. Clearing log..." >> \$LOG_PATH 2>&1
|
|
> "\$TMP_LOG_FILE"
|
|
echo "\$(timestamp) Mihomo log file (\$TMP_LOG_FILE) has been cleared." >> \$LOG_PATH 2>&1
|
|
else
|
|
echo "\$(timestamp) Mihomo log file (\$TMP_LOG_FILE) is within the size limit. No action needed." >> \$LOG_PATH 2>&1
|
|
fi
|
|
|
|
if [ -f "\$ADDITIONAL_LOG_FILE" ] && [ \$(stat -c %s "\$ADDITIONAL_LOG_FILE") -gt \$MAX_SIZE ]; then
|
|
echo "\$(timestamp) NeKoBox log file (\$ADDITIONAL_LOG_FILE) exceeded \$MAX_SIZE bytes. Clearing log..." >> \$LOG_PATH 2>&1
|
|
> "\$ADDITIONAL_LOG_FILE"
|
|
echo "\$(timestamp) NeKoBox log file (\$ADDITIONAL_LOG_FILE) has been cleared." >> \$LOG_PATH 2>&1
|
|
else
|
|
echo "\$(timestamp) NeKoBox log file (\$ADDITIONAL_LOG_FILE) is within the size limit. No action needed." >> \$LOG_PATH 2>&1
|
|
fi
|
|
|
|
echo "\$(timestamp) Log rotation completed." >> \$LOG_PATH 2>&1
|
|
EOL;
|
|
|
|
$cronScriptPath = '/etc/neko/core/set_cron.sh';
|
|
file_put_contents($cronScriptPath, $cronScriptContent);
|
|
chmod($cronScriptPath, 0755);
|
|
shell_exec("sh $cronScriptPath");
|
|
}
|
|
|
|
function rotateLogs($logFile, $maxSize = 1048576) {
|
|
if (file_exists($logFile) && filesize($logFile) > $maxSize) {
|
|
file_put_contents($logFile, '');
|
|
chmod($logFile, 0644);
|
|
//echo "Log file cleared successfully.\n";
|
|
}
|
|
}
|
|
|
|
function isSingboxRunning() {
|
|
global $singbox_bin;
|
|
$command = "pgrep -f " . escapeshellarg($singbox_bin);
|
|
exec($command, $output);
|
|
return !empty($output);
|
|
}
|
|
|
|
function isNekoBoxRunning() {
|
|
global $neko_dir;
|
|
$pid = trim(shell_exec("cat $neko_dir/tmp/neko.pid 2>/dev/null"));
|
|
return !empty($pid) && file_exists("/proc/$pid");
|
|
}
|
|
|
|
function getSingboxPID() {
|
|
global $singbox_bin;
|
|
$command = "pgrep -f " . escapeshellarg($singbox_bin);
|
|
exec($command, $output);
|
|
return isset($output[0]) ? $output[0] : null;
|
|
}
|
|
|
|
function getRunningConfigFile() {
|
|
global $singbox_bin;
|
|
$command = "ps w | grep '$singbox_bin' | grep -v grep";
|
|
exec($command, $output);
|
|
foreach ($output as $line) {
|
|
if (strpos($line, '-c') !== false) {
|
|
$parts = explode('-c', $line);
|
|
if (isset($parts[1])) {
|
|
$configPath = trim(explode(' ', trim($parts[1]))[0]);
|
|
return $configPath;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getAvailableConfigFiles() {
|
|
global $singbox_config_dir;
|
|
return glob("$singbox_config_dir/*.json");
|
|
}
|
|
|
|
$availableConfigs = getAvailableConfigFiles();
|
|
|
|
//writeToLog("Script started");
|
|
|
|
if(isset($_POST['neko'])){
|
|
$dt = $_POST['neko'];
|
|
writeToLog("Received neko action: $dt");
|
|
if ($dt == 'start') {
|
|
if (isSingboxRunning()) {
|
|
writeToLog("Cannot start NekoBox: Sing-box is running");
|
|
} else {
|
|
shell_exec("$neko_dir/core/neko -s");
|
|
writeToLog("Mihomo started successfully");
|
|
}
|
|
}
|
|
if ($dt == 'disable') {
|
|
shell_exec("$neko_dir/core/neko -k");
|
|
writeToLog("Mihomo stopped");
|
|
}
|
|
if ($dt == 'restart') {
|
|
if (isSingboxRunning()) {
|
|
writeToLog("Cannot restart NekoBox: Sing-box is running");
|
|
} else {
|
|
shell_exec("$neko_dir/core/neko -r");
|
|
writeToLog("Mihomo restarted successfully");
|
|
}
|
|
}
|
|
if ($dt == 'clear') {
|
|
shell_exec("echo \"Logs has been cleared...\" > $neko_dir/tmp/neko_log.txt");
|
|
writeToLog("Mihomo logs cleared");
|
|
}
|
|
writeToLog("Neko action completed: $dt");
|
|
}
|
|
|
|
if (isset($_POST['singbox'])) {
|
|
$action = $_POST['singbox'];
|
|
$config_file = isset($_POST['config_file']) ? $_POST['config_file'] : '';
|
|
|
|
writeToLog("Received singbox action: $action");
|
|
//writeToLog("Config file: $config_file");
|
|
|
|
switch ($action) {
|
|
case 'start':
|
|
if (isNekoBoxRunning()) {
|
|
writeToLog("Cannot start Sing-box: NekoBox is running");
|
|
} else {
|
|
writeToLog("Starting Sing-box");
|
|
|
|
$singbox_version = trim(shell_exec("$singbox_bin version"));
|
|
writeToLog("Sing-box version: $singbox_version");
|
|
|
|
shell_exec("mkdir -p " . dirname($singbox_log));
|
|
shell_exec("touch $singbox_log && chmod 644 $singbox_log");
|
|
rotateLogs($singbox_log);
|
|
|
|
createStartScript($config_file);
|
|
createCronScript();
|
|
$output = shell_exec("sh $start_script_path >> $singbox_log 2>&1 &");
|
|
//writeToLog("Shell output: " . ($output ?: "No output"));
|
|
|
|
sleep(3);
|
|
$pid = getSingboxPID();
|
|
if ($pid) {
|
|
writeToLog("Sing-box Started successfully. PID: $pid");
|
|
} else {
|
|
writeToLog("Failed to start Sing-box");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'disable':
|
|
writeToLog("Stopping Sing-box");
|
|
$pid = getSingboxPID();
|
|
if ($pid) {
|
|
writeToLog("Killing Sing-box PID: $pid");
|
|
shell_exec("kill $pid");
|
|
if (file_exists('/usr/sbin/fw4')) {
|
|
shell_exec("nft flush ruleset");
|
|
} else {
|
|
shell_exec("iptables -t mangle -F");
|
|
shell_exec("iptables -t mangle -X");
|
|
}
|
|
shell_exec("/etc/init.d/firewall restart");
|
|
writeToLog("Cleared firewall rules and restarted firewall");
|
|
sleep(1);
|
|
if (!isSingboxRunning()) {
|
|
writeToLog("Sing-box has been stopped successfully");
|
|
} else {
|
|
writeToLog("Force killing Sing-box");
|
|
shell_exec("kill -9 $pid");
|
|
writeToLog("Sing-box has been force stopped");
|
|
}
|
|
} else {
|
|
writeToLog("Sing-box is not running");
|
|
}
|
|
break;
|
|
|
|
case 'restart':
|
|
if (isNekoBoxRunning()) {
|
|
writeToLog("Cannot restart Sing-box: NekoBox is running");
|
|
} else {
|
|
writeToLog("Restarting Sing-box");
|
|
|
|
$pid = getSingboxPID();
|
|
if ($pid) {
|
|
writeToLog("Killing Sing-box PID: $pid");
|
|
shell_exec("kill $pid");
|
|
sleep(1);
|
|
}
|
|
|
|
shell_exec("mkdir -p " . dirname($singbox_log));
|
|
shell_exec("touch $singbox_log && chmod 644 $singbox_log");
|
|
rotateLogs($singbox_log);
|
|
|
|
createStartScript($config_file);
|
|
shell_exec("sh $start_script_path >> $singbox_log 2>&1 &");
|
|
|
|
sleep(3);
|
|
$new_pid = getSingboxPID();
|
|
if ($new_pid) {
|
|
writeToLog("Sing-box Restarted successfully. New PID: $new_pid");
|
|
} else {
|
|
writeToLog("Failed to restart Sing-box");
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
sleep(2);
|
|
|
|
$singbox_status = isSingboxRunning() ? '1' : '0';
|
|
exec("uci set neko.cfg.singbox_enabled='$singbox_status'");
|
|
exec("uci commit neko");
|
|
//writeToLog("Singbox status set to: $singbox_status");
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['cronTime'])) {
|
|
$cronTime = $_POST['cronTime'];
|
|
|
|
if (empty($cronTime)) {
|
|
$logMessage = "Please provide a valid Cron time format!";
|
|
file_put_contents('/etc/neko/tmp/log.txt', date('Y-m-d H:i:s') . " - ERROR: $logMessage\n", FILE_APPEND);
|
|
echo $logMessage;
|
|
exit;
|
|
}
|
|
|
|
$startScriptPath = '/etc/neko/core/start.sh';
|
|
|
|
if (!file_exists('/etc/neko/tmp')) {
|
|
mkdir('/etc/neko/tmp', 0755, true);
|
|
}
|
|
|
|
$restartScriptContent = <<<EOL
|
|
#!/bin/bash
|
|
LOG_PATH="/etc/neko/tmp/log.txt"
|
|
|
|
timestamp() {
|
|
date "+%Y-%m-%d %H:%M:%S"
|
|
}
|
|
|
|
MAX_RETRIES=5
|
|
RETRY_INTERVAL=5
|
|
|
|
start_singbox() {
|
|
sh /etc/neko/core/start.sh
|
|
}
|
|
|
|
check_singbox() {
|
|
pgrep -x "singbox" > /dev/null
|
|
return $?
|
|
}
|
|
|
|
if pgrep -x "singbox" > /dev/null
|
|
then
|
|
echo "$(timestamp) Sing-box is already running, restarting..." >> \$LOG_PATH
|
|
kill $(pgrep -x "singbox")
|
|
sleep 2
|
|
start_singbox
|
|
|
|
RETRY_COUNT=0
|
|
while ! check_singbox && [ \$RETRY_COUNT -lt \$MAX_RETRIES ]; do
|
|
echo "$(timestamp) Sing-box restart failed, retrying... (\$((RETRY_COUNT + 1))/\$MAX_RETRIES)" >> \$LOG_PATH
|
|
sleep \$RETRY_INTERVAL
|
|
start_singbox
|
|
((RETRY_COUNT++))
|
|
done
|
|
|
|
if check_singbox; then
|
|
echo "$(timestamp) Sing-box restarted successfully!" >> \$LOG_PATH
|
|
else
|
|
echo "$(timestamp) Sing-box restart failed, max retries reached!" >> \$LOG_PATH
|
|
fi
|
|
else
|
|
echo "$(timestamp) Sing-box is not running, starting Sing-box..." >> \$LOG_PATH
|
|
start_singbox
|
|
|
|
RETRY_COUNT=0
|
|
while ! check_singbox && [ \$RETRY_COUNT -lt \$MAX_RETRIES ]; do
|
|
echo "$(timestamp) Sing-box start failed, retrying... (\$((RETRY_COUNT + 1))/\$MAX_RETRIES)" >> \$LOG_PATH
|
|
sleep \$RETRY_INTERVAL
|
|
start_singbox
|
|
((RETRY_COUNT++))
|
|
done
|
|
|
|
if check_singbox; then
|
|
echo "$(timestamp) Sing-box started successfully!" >> \$LOG_PATH
|
|
else
|
|
echo "$(timestamp) Sing-box start failed, max retries reached!" >> \$LOG_PATH
|
|
fi
|
|
fi
|
|
EOL;
|
|
|
|
$scriptPath = '/etc/neko/core/restart_singbox.sh';
|
|
file_put_contents($scriptPath, $restartScriptContent);
|
|
chmod($scriptPath, 0755);
|
|
|
|
$cronSchedule = $cronTime . " /bin/bash $scriptPath";
|
|
exec("crontab -l | grep -v '$scriptPath' | crontab -");
|
|
exec("(crontab -l 2>/dev/null; echo \"$cronSchedule\") | crontab -");
|
|
|
|
$logMessage = "Cron job successfully set. Sing-box will restart automatically at $cronTime.";
|
|
file_put_contents('/etc/neko/tmp/log.txt', date('[ H:i:s ] ') . "$logMessage\n", FILE_APPEND);
|
|
echo json_encode(['success' => true, 'message' => 'Cron job successfully set.']);
|
|
exit;
|
|
}
|
|
|
|
if (isset($_POST['clear_singbox_log'])) {
|
|
file_put_contents($singbox_log, '');
|
|
writeToLog("Singbox log cleared");
|
|
}
|
|
|
|
if (isset($_POST['clear_plugin_log'])) {
|
|
$plugin_log_file = "$neko_dir/tmp/log.txt";
|
|
file_put_contents($plugin_log_file, '');
|
|
writeToLog("Nekobox log cleared");
|
|
}
|
|
|
|
|
|
$neko_status = exec("uci -q get neko.cfg.enabled");
|
|
$singbox_status = isSingboxRunning() ? '1' : '0';
|
|
exec("uci set neko.cfg.singbox_enabled='$singbox_status'");
|
|
exec("uci commit neko");
|
|
|
|
//writeToLog("Final neko status: $neko_status");
|
|
//writeToLog("Final singbox status: $singbox_status");
|
|
|
|
if ($singbox_status == '1') {
|
|
$runningConfigFile = getRunningConfigFile();
|
|
if ($runningConfigFile) {
|
|
$str_cfg = htmlspecialchars(basename($runningConfigFile));
|
|
//writeToLog("Running config file: $str_cfg");
|
|
} else {
|
|
$str_cfg = 'Sing-box configuration file: No running configuration file found';
|
|
writeToLog("No running config file found");
|
|
}
|
|
}
|
|
|
|
function readRecentLogLines($filePath, $lines = 1000) {
|
|
if (!file_exists($filePath)) {
|
|
return "The log file does not exist: $filePath";
|
|
}
|
|
if (!is_readable($filePath)) {
|
|
return "Unable to read the log file: $filePath";
|
|
}
|
|
$command = "tail -n $lines " . escapeshellarg($filePath);
|
|
$output = shell_exec($command);
|
|
return $output ?: "The log is empty";
|
|
}
|
|
|
|
function readLogFile($filePath) {
|
|
if (file_exists($filePath)) {
|
|
return nl2br(htmlspecialchars(readRecentLogLines($filePath, 1000), ENT_NOQUOTES));
|
|
} else {
|
|
return 'The log file does not exist';
|
|
}
|
|
}
|
|
|
|
$neko_log_content = readLogFile("$neko_dir/tmp/neko_log.txt");
|
|
$singbox_log_content = readLogFile($singbox_log);
|
|
?>
|
|
|
|
<?php
|
|
$isNginx = false;
|
|
if (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') !== false) {
|
|
$isNginx = true;
|
|
}
|
|
?>
|
|
|
|
<?php
|
|
if (isset($_GET['ajax'])) {
|
|
$dt = json_decode(shell_exec("ubus call system board"), true);
|
|
$devices = $dt['model'];
|
|
|
|
$kernelv = exec("cat /proc/sys/kernel/ostype");
|
|
$osrelease = exec("cat /proc/sys/kernel/osrelease");
|
|
$OSVer = $dt['release']['distribution'] . ' ' . $dt['release']['version'];
|
|
$kernelParts = explode('.', $osrelease, 3);
|
|
$kernelv = 'Linux ' .
|
|
(isset($kernelParts[0]) ? $kernelParts[0] : '') . '.' .
|
|
(isset($kernelParts[1]) ? $kernelParts[1] : '') . '.' .
|
|
(isset($kernelParts[2]) ? $kernelParts[2] : '');
|
|
$kernelv = strstr($kernelv, '-', true) ?: $kernelv;
|
|
$fullOSInfo = $kernelv . ' ' . $OSVer;
|
|
|
|
$tmpramTotal = exec("cat /proc/meminfo | grep MemTotal | awk '{print $2}'");
|
|
$tmpramAvailable = exec("cat /proc/meminfo | grep MemAvailable | awk '{print $2}'");
|
|
|
|
$ramTotal = number_format(($tmpramTotal / 1000), 1);
|
|
$ramAvailable = number_format(($tmpramAvailable / 1000), 1);
|
|
$ramUsage = number_format((($tmpramTotal - $tmpramAvailable) / 1000), 1);
|
|
|
|
$raw_uptime = exec("cat /proc/uptime | awk '{print $1}'");
|
|
$days = floor($raw_uptime / 86400);
|
|
$hours = floor(($raw_uptime / 3600) % 24);
|
|
$minutes = floor(($raw_uptime / 60) % 60);
|
|
$seconds = $raw_uptime % 60;
|
|
|
|
$cpuLoad = shell_exec("cat /proc/loadavg");
|
|
$cpuLoad = explode(' ', $cpuLoad);
|
|
$cpuLoadAvg1Min = round($cpuLoad[0], 2);
|
|
$cpuLoadAvg5Min = round($cpuLoad[1], 2);
|
|
$cpuLoadAvg15Min = round($cpuLoad[2], 2);
|
|
|
|
echo json_encode([
|
|
'systemInfo' => "$devices - $fullOSInfo",
|
|
'ramUsage' => "$ramUsage/$ramTotal MB",
|
|
'cpuLoad' => "$cpuLoadAvg1Min $cpuLoadAvg5Min $cpuLoadAvg15Min",
|
|
'uptime' => "{$days} days {$hours} hours {$minutes} minutes {$seconds} seconds",
|
|
'cpuLoadAvg1Min' => $cpuLoadAvg1Min,
|
|
'ramTotal' => $ramTotal,
|
|
'ramUsageOnly' => $ramUsage,
|
|
]);
|
|
exit;
|
|
}
|
|
?>
|
|
<?php
|
|
$default_config = '/etc/neko/config/mihomo.yaml';
|
|
|
|
$current_config = file_exists('/www/nekobox/lib/selected_config.txt')
|
|
? trim(file_get_contents('/www/nekobox/lib/selected_config.txt'))
|
|
: $default_config;
|
|
|
|
if (!file_exists($current_config)) {
|
|
$default_config_content = "external-controller: 0.0.0.0:9090\n";
|
|
$default_config_content .= "secret: Akun\n";
|
|
$default_config_content .= "external-ui: ui\n";
|
|
$default_config_content .= "# Please edit this file as needed\n";
|
|
|
|
file_put_contents($current_config, $default_config_content);
|
|
file_put_contents('/www/nekobox/lib/selected_config.txt', $current_config);
|
|
|
|
$logMessage = "The configuration file is missing; a default configuration file has been created.";
|
|
} else {
|
|
$config_content = file_get_contents($current_config);
|
|
|
|
$missing_config = false;
|
|
$default_config_content = [
|
|
"external-controller" => "0.0.0.0:9090",
|
|
"secret" => "Akun",
|
|
"external-ui" => "ui"
|
|
];
|
|
|
|
foreach ($default_config_content as $key => $value) {
|
|
if (strpos($config_content, "$key:") === false) {
|
|
$config_content .= "$key: $value\n";
|
|
$missing_config = true;
|
|
}
|
|
}
|
|
|
|
if ($missing_config) {
|
|
file_put_contents($current_config, $config_content);
|
|
$logMessage = "The configuration file is missing some options; the missing configuration items have been added automatically";
|
|
}
|
|
}
|
|
|
|
if (isset($logMessage)) {
|
|
echo "<script>alert('$logMessage');</script>";
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['selected_config'])) {
|
|
$selected_file = $_POST['selected_config'];
|
|
$config_dir = '/etc/neko/config';
|
|
$selected_file_path = $config_dir . '/' . $selected_file;
|
|
|
|
if (file_exists($selected_file_path) && pathinfo($selected_file, PATHINFO_EXTENSION) == 'yaml') {
|
|
file_put_contents('/www/nekobox/lib/selected_config.txt', $selected_file_path);
|
|
} else {
|
|
echo "<script>alert('Invalid configuration file');</script>";
|
|
}
|
|
}
|
|
?>
|
|
|
|
<!doctype html>
|
|
<html lang="en" data-bs-theme="<?php echo substr($neko_theme,0,-4) ?>">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Home - Nekobox</title>
|
|
<link rel="icon" href="./assets/img/nekobox.png">
|
|
<link href="./assets/css/bootstrap.min.css" rel="stylesheet">
|
|
<link href="./assets/css/custom.css" rel="stylesheet">
|
|
<link href="./assets/theme/<?php echo $neko_theme ?>" rel="stylesheet">
|
|
<link href="./assets/bootstrap/bootstrap-icons.css" rel="stylesheet">
|
|
<script type="text/javascript" src="./assets/js/feather.min.js"></script>
|
|
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
|
<script type="text/javascript" src="./assets/js/neko.js"></script>
|
|
<script type="text/javascript" src="./assets/bootstrap/bootstrap.min.js"></script>
|
|
<script src="./assets/js/bootstrap.bundle.min.js"></script>
|
|
<?php include './ping.php'; ?>
|
|
</head>
|
|
<body>
|
|
<?php if ($isNginx): ?>
|
|
<div id="nginxWarning" class="alert alert-warning alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1050;">
|
|
<strong data-translate="nginxWarningStrong"></strong>
|
|
<span data-translate="nginxWarning"></span>
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
<script>
|
|
setTimeout(function() {
|
|
var warningAlert = document.getElementById('nginxWarning');
|
|
if (warningAlert) {
|
|
warningAlert.classList.remove('show');
|
|
setTimeout(function() {
|
|
warningAlert.remove();
|
|
}, 300);
|
|
}
|
|
}, 5000);
|
|
</script>
|
|
<?php endif; ?>
|
|
<div class="container-sm container-bg callout border border-3 rounded-4 col-11">
|
|
<div class="row">
|
|
<a href="./index.php" class="col btn btn-lg text-nowrap"><i class="bi bi-house-door"></i> <span data-translate="home">Home</span></a>
|
|
<a href="./dashboard.php" class="col btn btn-lg text-nowrap"><i class="bi bi-bar-chart"></i> <span data-translate="panel">Panel</span></a>
|
|
<a href="./singbox.php" class="col btn btn-lg text-nowrap"><i class="bi bi-box"></i> <span data-translate="document">Document</span></a>
|
|
<a href="./settings.php" class="col btn btn-lg text-nowrap"><i class="bi bi-gear"></i> <span data-translate="settings">Settings</span></a>
|
|
<div class="container-sm text-center col-8">
|
|
<img src="./assets/img/nekobox.png">
|
|
<div id="version-info">
|
|
<a id="version-link" href="https://github.com/Thaolga/openwrt-nekobox/releases" target="_blank">
|
|
<img id="current-version" src="./assets/img/curent.svg" alt="Current Version" style="max-width: 100%; height: auto;" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
$(document).ready(function() {
|
|
$.ajax({
|
|
url: 'check_update.php',
|
|
method: 'GET',
|
|
dataType: 'json',
|
|
success: function(data) {
|
|
if (data.hasUpdate) {
|
|
$('#current-version').attr('src', 'https://raw.githubusercontent.com/Thaolga/openwrt-nekobox/refs/heads/nekobox/luci-app-nekobox/htdocs/nekobox/assets/img/Latest.svg');
|
|
}
|
|
console.log('Current Version:', data.currentVersion);
|
|
console.log('Latest Version:', data.latestVersion);
|
|
console.log('Has Update:', data.hasUpdate);
|
|
},
|
|
error: function(jqXHR, textStatus, errorThrown) {
|
|
//$('#version-info').text('Error fetching version information');
|
|
console.error('AJAX Error:', textStatus, errorThrown);
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
<h2 class="royal-style">NekoBox</h2>
|
|
<style>
|
|
|
|
.nav-pills .nav-link {
|
|
background-color: transparent !important;
|
|
color: inherit;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.nav-pills .nav-link.active {
|
|
background-color: transparent !important;
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.section-container {
|
|
padding-left: 42px;
|
|
padding-right: 42px;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
width: 120%;
|
|
}
|
|
|
|
.log-container {
|
|
height: 270px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
|
|
.log-card {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.custom-icon {
|
|
width: 20px !important;
|
|
height: 20px !important;
|
|
vertical-align: middle !important;
|
|
margin-right: 5px !important;
|
|
stroke: #FF00FF !important;
|
|
fill: none !important;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.section-container {
|
|
padding-left: 15px;
|
|
padding-right: 15px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
tr {
|
|
margin-bottom: 15px;
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.section-container .table {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
|
|
.section-container .table tbody,
|
|
.section-container .table thead,
|
|
.section-container .table tr {
|
|
display: block;
|
|
}
|
|
|
|
.section-container .table td {
|
|
display: block;
|
|
width: 100%;
|
|
padding: 10px;
|
|
border: 1px solid #ddd;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.section-container .table td:first-child {
|
|
font-weight: bold;
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.section-container .btn-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
|
|
.section-container .form-select,
|
|
.section-container .form-control,
|
|
.section-container .input-group {
|
|
width: 100%;
|
|
}
|
|
|
|
.section-container .btn {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.section-container .table td {
|
|
background-color: #fff;
|
|
border-radius: 5px;
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.section-container .table td:first-child {
|
|
background-color: #f0f0f0;
|
|
font-size: 1.1em;
|
|
}
|
|
|
|
.section-container .btn {
|
|
border-radius: 5px;
|
|
}
|
|
|
|
.section-container .btn-group {
|
|
gap: 15px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<div class="section-container">
|
|
<table class="table table-borderless mb-2">
|
|
<tbody>
|
|
<tr>
|
|
<td style="width:150px" data-translate="status">Status</td>
|
|
<td class="d-grid">
|
|
<div class="btn-group w-100" role="group" aria-label="ctrl">
|
|
<?php
|
|
if ($neko_status == 1) {
|
|
echo "<button type=\"button\" class=\"btn btn-success\" data-translate=\"mihomoRunning\">Mihomo Running</button>\n";
|
|
} else {
|
|
echo "<button type=\"button\" class=\"btn btn-outline-danger\" data-translate=\"mihomoNotRunning\">Mihomo Not Running</button>\n";
|
|
}
|
|
echo "<button type=\"button\" class=\"btn btn-deepskyblue\">$str_cfg</button>\n";
|
|
if ($singbox_status == 1) {
|
|
echo "<button type=\"button\" class=\"btn btn-success\" data-translate=\"singboxRunning\">Sing-box Running</button>\n";
|
|
} else {
|
|
echo "<button type=\"button\" class=\"btn btn-outline-danger\" data-translate=\"singboxNotRunning\">Sing-box Not Running</button>\n";
|
|
}
|
|
?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px" data-translate="mihomoControl">Mihomo Control</td>
|
|
<td class="d-grid">
|
|
<form action="index.php" method="post" style="display: inline-block; width: 100%; margin-bottom: 10px;">
|
|
<div class="form-group">
|
|
<select id="configSelect" class="form-select" name="selected_config" onchange="saveConfigToLocalStorage(); this.form.submit()">
|
|
<option value="" data-translate="selectConfig">Please select a configuration file</option>
|
|
<?php
|
|
$config_dir = '/etc/neko/config';
|
|
$files = array_diff(scandir($config_dir), array('..', '.'));
|
|
foreach ($files as $file) {
|
|
if (pathinfo($file, PATHINFO_EXTENSION) == 'yaml') {
|
|
$selected = (realpath($config_dir . '/' . $file) == realpath($current_config)) ? 'selected' : '';
|
|
echo "<option value='$file' $selected>$file</option>";
|
|
}
|
|
}
|
|
?>
|
|
</select>
|
|
</div>
|
|
</form>
|
|
<form action="index.php" method="post" style="display: inline-block; width: 100%;">
|
|
<div class="btn-group w-100">
|
|
<button type="submit" name="neko" value="start" class="btn btn<?php if ($neko_status == 1) echo "-outline" ?>-success <?php if ($neko_status == 1) echo "disabled" ?>" data-translate="enableMihomo">Enable Mihomo</button>
|
|
<button type="submit" name="neko" value="disable" class="btn btn<?php if ($neko_status == 0) echo "-outline" ?>-danger <?php if ($neko_status == 0) echo "disabled" ?>" data-translate="disableMihomo">Disable Mihomo</button>
|
|
<button type="submit" name="neko" value="restart" class="btn btn<?php if ($neko_status == 0) echo "-outline" ?>-warning <?php if ($neko_status == 0) echo "disabled" ?>" data-translate="restartMihomo">Restart Mihomo</button>
|
|
</div>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px" data-translate="singboxControl">Singbox Control</td>
|
|
<td class="d-grid">
|
|
<form action="index.php" method="post">
|
|
<div class="input-group mb-2">
|
|
<select name="config_file" id="config_file" class="form-select" onchange="saveConfigSelection()">
|
|
<option value="" data-translate="selectConfig">Please select a configuration file</option>
|
|
<?php foreach ($availableConfigs as $config): ?>
|
|
<option value="<?= htmlspecialchars($config) ?>" <?= isset($_POST['config_file']) && $_POST['config_file'] === $config ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars(basename($config)) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div class="btn-group w-100">
|
|
<button type="submit" name="singbox" value="start" class="btn btn<?php echo ($singbox_status == 1) ? "-outline" : "" ?>-success <?php echo ($singbox_status == 1) ? "disabled" : "" ?>" data-translate="enableSingbox">Enable Sing-box</button>
|
|
<button type="submit" name="singbox" value="disable" class="btn btn<?php echo ($singbox_status == 0) ? "-outline" : "" ?>-danger <?php echo ($singbox_status == 0) ? "disabled" : "" ?>" data-translate="disableSingbox">Disable Sing-box</button>
|
|
<button type="submit" name="singbox" value="restart" class="btn btn<?php echo ($singbox_status == 0) ? "-outline" : "" ?>-warning <?php echo ($singbox_status == 0) ? "disabled" : "" ?>" data-translate="restartSingbox">Restart Sing-box</button>
|
|
</div>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px" data-translate="runningMode">Running Mode</td>
|
|
<td class="d-grid">
|
|
<?php
|
|
$mode_placeholder = '';
|
|
if ($neko_status == 1) {
|
|
$mode_placeholder = $neko_cfg['echanced'] . " | " . $neko_cfg['mode'];
|
|
} elseif ($singbox_status == 1) {
|
|
$mode_placeholder = "Rule Mode";
|
|
} else {
|
|
$mode_placeholder = "Not Running";
|
|
}
|
|
?>
|
|
<input class="form-control text-center" name="mode" type="text" placeholder="<?php echo $mode_placeholder; ?>" disabled>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|
const savedConfig = localStorage.getItem("configSelection");
|
|
if (savedConfig) {
|
|
document.getElementById("config_file").value = savedConfig;
|
|
}
|
|
});
|
|
function saveConfigSelection() {
|
|
const selectedConfig = document.getElementById("config_file").value;
|
|
localStorage.setItem("configSelection", selectedConfig);
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
const lastShownTime = localStorage.getItem('lastCronMessageShownTime');
|
|
const currentTime = new Date().getTime();
|
|
|
|
if (!lastShownTime || (currentTime - lastShownTime) > 12 * 60 * 60 * 1000) {
|
|
document.getElementById('cron-success-message').style.display = 'block';
|
|
localStorage.setItem('lastCronMessageShownTime', currentTime);
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
function saveConfigToLocalStorage() {
|
|
const selectedConfig = document.getElementById('configSelect').value;
|
|
if (selectedConfig) {
|
|
localStorage.setItem('selected_config', selectedConfig);
|
|
}
|
|
}
|
|
|
|
window.onload = function() {
|
|
const savedConfig = localStorage.getItem('selected_config');
|
|
if (savedConfig) {
|
|
const configSelect = document.getElementById('configSelect');
|
|
configSelect.value = savedConfig;
|
|
}
|
|
};
|
|
</script>
|
|
<div id="collapsibleHeader" style="cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
|
<i id="toggleIcon" class="triangle-icon"></i>
|
|
<h2 id="systemTitle" class="text-center" style="display: none; margin-top: 0;" data-translate="systemInfo">System Status</h2>
|
|
</div>
|
|
|
|
<div id="collapsible" style="display: none; margin-top: 5px;">
|
|
<table class="table table-borderless rounded-4 mb-2">
|
|
<tbody>
|
|
<tr>
|
|
<td style="width:150px"><span data-translate="systemInfo">System Info</span></td>
|
|
<td id="systemInfo"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px"><span data-translate="systemMemory">System Memory</span></td>
|
|
<td id="ramUsage"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px"><span data-translate="avgLoad">Average Load</span></td>
|
|
<td id="cpuLoad"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px"><span data-translate="uptime">Uptime</span></td>
|
|
<td id="uptime"></td>
|
|
</tr>
|
|
<tr>
|
|
<td style="width:150px"><span data-translate="trafficStats">Traffic Stats</span></td>
|
|
<td>⬇️ <span id="downtotal"></span> | ⬆️ <span id="uptotal"></span></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
const collapsible = document.getElementById('collapsible');
|
|
const collapsibleHeader = document.getElementById('collapsibleHeader');
|
|
const toggleIcon = document.getElementById('toggleIcon');
|
|
const systemTitle = document.getElementById('systemTitle');
|
|
|
|
let isCollapsed = true;
|
|
|
|
if (localStorage.getItem('isCollapsed') === 'false') {
|
|
isCollapsed = false;
|
|
collapsible.style.display = 'block';
|
|
systemTitle.style.display = 'block';
|
|
toggleIcon.classList.add('rotated');
|
|
}
|
|
|
|
collapsibleHeader.addEventListener('click', () => {
|
|
if (isCollapsed) {
|
|
collapsible.style.display = 'block';
|
|
systemTitle.style.display = 'block';
|
|
toggleIcon.classList.add('rotated');
|
|
} else {
|
|
collapsible.style.display = 'none';
|
|
systemTitle.style.display = 'none';
|
|
toggleIcon.classList.remove('rotated');
|
|
}
|
|
isCollapsed = !isCollapsed;
|
|
localStorage.setItem('isCollapsed', isCollapsed);
|
|
});
|
|
|
|
function fetchSystemStatus() {
|
|
fetch('?ajax=1')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('systemInfo').innerText = data.systemInfo;
|
|
document.getElementById('ramUsage').innerText = data.ramUsage;
|
|
document.getElementById('cpuLoad').innerText = data.cpuLoad;
|
|
document.getElementById('uptime').innerText = data.uptime;
|
|
document.getElementById('cpuLoadAvg1Min').innerText = data.cpuLoadAvg1Min;
|
|
document.getElementById('ramUsageOnly').innerText = data.ramUsageOnly + ' / ' + data.ramTotal + ' MB';
|
|
})
|
|
.catch(error => console.error('Error fetching data:', error));
|
|
}
|
|
|
|
setInterval(fetchSystemStatus, 1000);
|
|
fetchSystemStatus();
|
|
</script>
|
|
|
|
<style>
|
|
.triangle-icon {
|
|
width: 0;
|
|
height: 0;
|
|
border-left: 12px solid transparent;
|
|
border-right: 12px solid transparent;
|
|
border-top: 12px solid blue;
|
|
display: inline-block;
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
|
|
.rotated {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
|
|
.form-inline {
|
|
display: inline-block;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
.form-inline {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
}
|
|
|
|
.form-check-inline, .btn {
|
|
font-size: 10px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
#logTabs .nav-item {
|
|
display: block;
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
<h2 class="text-center" data-translate="log"></h2>
|
|
<ul class="nav nav-pills mb-3" id="logTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" id="pluginLogTab" data-bs-toggle="pill" href="#pluginLog" role="tab" aria-controls="pluginLog" aria-selected="true"><span data-translate="nekoBoxLog"></span></a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" id="mihomoLogTab" data-bs-toggle="pill" href="#mihomoLog" role="tab" aria-controls="mihomoLog" aria-selected="false"><span data-translate="mihomoLog"></span></a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" id="singboxLogTab" data-bs-toggle="pill" href="#singboxLog" role="tab" aria-controls="singboxLog" aria-selected="false"><span data-translate="singboxLog"></span></a>
|
|
</li>
|
|
</ul>
|
|
<div class="tab-content" id="logTabsContent">
|
|
<div class="tab-pane fade" id="pluginLog" role="tabpanel" aria-labelledby="pluginLogTab">
|
|
<div class="card log-card">
|
|
<div class="card-body">
|
|
<pre id="plugin_log" class="log-container form-control" style="resize: vertical; overflow: auto; height: 370px; white-space: pre-wrap;" contenteditable="true"></pre>
|
|
</div>
|
|
<div class="card-footer text-center">
|
|
<form action="index.php" method="post">
|
|
<button type="submit" name="clear_plugin_log" class="btn btn-danger"><i class="bi bi-trash"></i> <span data-translate="clearLog"></span></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="mihomoLog" role="tabpanel" aria-labelledby="mihomoLogTab">
|
|
<div class="card log-card">
|
|
<div class="card-body">
|
|
<pre id="bin_logs" class="log-container form-control" style="resize: vertical; overflow: auto; height: 370px; white-space: pre-wrap;" contenteditable="true"></pre>
|
|
</div>
|
|
<div class="card-footer text-center">
|
|
<form action="index.php" method="post">
|
|
<button type="submit" name="neko" value="clear" class="btn btn-danger"><i class="bi bi-trash"></i> <span data-translate="clearLog"></span></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="singboxLog" role="tabpanel" aria-labelledby="singboxLogTab">
|
|
<div class="card log-card">
|
|
<div class="card-body">
|
|
<pre id="singbox_log" class="log-container form-control" style="resize: vertical; overflow: auto; height: 370px; white-space: pre-wrap;" contenteditable="true"></pre>
|
|
</div>
|
|
<div class="card-footer text-center">
|
|
<form action="index.php" method="post" class="form-inline">
|
|
<div class="form-check form-check-inline mb-2">
|
|
<input class="form-check-input" type="checkbox" id="autoRefresh" checked>
|
|
<label class="form-check-label" for="autoRefresh"><span data-translate="autoRefresh"></span></label>
|
|
</div>
|
|
<button type="submit" name="clear_singbox_log" class="btn btn-danger me-2"><i class="bi bi-trash"></i> <span data-translate="clearLog"></span></button>
|
|
<button type="button" class="btn btn-primary me-2" data-toggle="modal" data-target="#cronModal"><i class="bi bi-clock"></i> <span data-translate="scheduledRestart"></span></button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="cronModal" tabindex="-1" role="dialog" aria-labelledby="cronModalLabel" aria-hidden="true" data-backdrop="static" data-keyboard="false">
|
|
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="cronModalLabel" data-translate="setCronTitle"></h5>
|
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="cronForm" method="POST">
|
|
<div class="form-group ">
|
|
<label for="cronTime" data-translate="setRestartTime"></label>
|
|
<input type="text" class="form-control mt-3" id="cronTime" name="cronTime" value="0 3 * * *" required>
|
|
</div>
|
|
<div class="alert alert-info mt-3">
|
|
<strong><?= $langData[$currentLang]['tip'] ?>:</strong> <?= $langData[$currentLang]['cronFormat'] ?>:
|
|
<ul>
|
|
<li><code>分钟 小时 日 月 星期</code></li>
|
|
<li><?= $langData[$currentLang]['example1'] ?>: <code>0 2 * * *</code></li>
|
|
<li><?= $langData[$currentLang]['example2'] ?>: <code>0 3 * * 1</code></li>
|
|
<li><?= $langData[$currentLang]['example3'] ?>: <code>0 9 * * 1-5</code></li>
|
|
</ul>
|
|
</div>
|
|
</form>
|
|
<div id="resultMessage" class="mt-3"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-dismiss="modal" data-translate="cancel"></button>
|
|
<button type="submit" class="btn btn-primary" form="cronForm" data-translate="save"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
$('#cronForm').submit(function(event) {
|
|
event.preventDefault();
|
|
var cronTime = $('#cronTime').val();
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: '',
|
|
data: { cronTime: cronTime },
|
|
dataType: 'json',
|
|
success: function(response) {
|
|
if (response.success) {
|
|
$('#resultMessage').html('<div class="alert alert-success">' + response.message + '</div>');
|
|
setTimeout(function() {
|
|
$('#cronModal').modal('hide');
|
|
}, 2000);
|
|
}
|
|
},
|
|
error: function() {
|
|
$('#resultMessage').html('<div class="alert alert-danger">设置 Cron 任务失败,请重试!</div>');
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
function scrollToBottom(elementId) {
|
|
var logElement = document.getElementById(elementId);
|
|
logElement.scrollTop = logElement.scrollHeight;
|
|
}
|
|
function fetchLogs() {
|
|
if (!document.getElementById('autoRefresh').checked) {
|
|
return;
|
|
}
|
|
Promise.all([
|
|
fetch('fetch_logs.php?file=plugin_log'),
|
|
fetch('fetch_logs.php?file=mihomo_log'),
|
|
fetch('fetch_logs.php?file=singbox_log')
|
|
])
|
|
.then(responses => Promise.all(responses.map(res => res.text())))
|
|
.then(data => {
|
|
document.getElementById('plugin_log').textContent = data[0];
|
|
document.getElementById('bin_logs').textContent = data[1];
|
|
document.getElementById('singbox_log').textContent = data[2];
|
|
scrollToBottom('plugin_log');
|
|
scrollToBottom('bin_logs');
|
|
scrollToBottom('singbox_log');
|
|
})
|
|
.catch(err => console.error('Error fetching logs:', err));
|
|
}
|
|
fetchLogs();
|
|
let intervalId = setInterval(fetchLogs, 5000);
|
|
document.getElementById('autoRefresh').addEventListener('change', function() {
|
|
if (this.checked) {
|
|
intervalId = setInterval(fetchLogs, 5000);
|
|
} else {
|
|
clearInterval(intervalId);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const autoRefreshCheckbox = document.getElementById('autoRefresh');
|
|
const isChecked = localStorage.getItem('autoRefresh') === 'true';
|
|
autoRefreshCheckbox.checked = isChecked;
|
|
|
|
if (isChecked) {
|
|
intervalId = setInterval(fetchLogs, 5000);
|
|
}
|
|
});
|
|
|
|
document.getElementById('autoRefresh').addEventListener('change', function() {
|
|
localStorage.setItem('autoRefresh', this.checked);
|
|
if (this.checked) {
|
|
intervalId = setInterval(fetchLogs, 5000);
|
|
} else {
|
|
clearInterval(intervalId);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
window.addEventListener('load', function() {
|
|
const activeTab = localStorage.getItem('activeTab') || 'pluginLogTab';
|
|
const activeTabLink = document.getElementById(activeTab);
|
|
const activeTabPane = document.getElementById(activeTab.replace('Tab', ''));
|
|
activeTabLink.classList.add('active');
|
|
activeTabPane.classList.add('show', 'active');
|
|
});
|
|
|
|
document.querySelectorAll('.nav-link').forEach(tab => {
|
|
tab.addEventListener('click', function() {
|
|
const selectedTab = this.id;
|
|
localStorage.setItem('activeTab', selectedTab);
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
<footer class="text-center">
|
|
<p><?php echo isset($message) ? $message : ''; ?></p>
|
|
<p><?php echo $footer; ?></p>
|
|
</footer>
|
|
</body>
|
|
</html>
|