Merge pull request #103 from itdoginfo/fix/comments

♻️ refactor(podkop): improve diagnostics and error handling
This commit is contained in:
itdoginfo
2025-05-12 16:48:15 +03:00
committed by GitHub
4 changed files with 258 additions and 90 deletions

View File

@@ -12,9 +12,14 @@ const STATUS_COLORS = {
WARNING: '#ff9800'
};
const ERROR_POLL_INTERVAL = 5000; // 5 seconds
const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
const ERROR_POLL_INTERVAL = 10000; // 10 seconds
const COMMAND_TIMEOUT = 10000; // 10 seconds
const FETCH_TIMEOUT = 10000; // 10 seconds
const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
async function safeExec(command, args = [], timeout = 7000) {
async function safeExec(command, args = [], timeout = COMMAND_TIMEOUT) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
@@ -397,14 +402,18 @@ function createConfigSection(section, map, network) {
const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidDomain = false;
for (const line of lines) {
// Skip empty lines or lines that start with //
if (!line || line.startsWith('//')) continue;
// Skip empty lines
if (!line) continue;
// Extract domain part (before any //)
const domainPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!domainPart) continue;
// Process each domain in the line (separated by comma or space)
const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0);
@@ -412,8 +421,14 @@ function createConfigSection(section, map, network) {
if (!domainRegex.test(domain)) {
return _('Invalid domain format: %s. Enter domain without protocol').format(domain);
}
hasValidDomain = true;
}
}
if (!hasValidDomain) {
return _('At least one valid domain must be specified. Comments-only content is not allowed.');
}
return true;
};
@@ -492,14 +507,18 @@ function createConfigSection(section, map, network) {
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidSubnet = false;
for (const line of lines) {
// Skip empty lines or lines that start with //
if (!line || line.startsWith('//')) continue;
// Skip empty lines
if (!line) continue;
// Extract subnet part (before any //)
const subnetPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!subnetPart) continue;
// Process each subnet in the line (separated by comma or space)
const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0);
@@ -523,8 +542,14 @@ function createConfigSection(section, map, network) {
return _('CIDR must be between 0 and 32 in: %s').format(subnet);
}
}
hasValidSubnet = true;
}
}
if (!hasValidSubnet) {
return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.');
}
return true;
};
@@ -576,7 +601,7 @@ const copyToClipboard = (text, button) => {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = _('Copied!');
setTimeout(() => button.textContent = originalText, 1000);
setTimeout(() => button.textContent = originalText, BUTTON_FEEDBACK_TIMEOUT);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
@@ -631,53 +656,100 @@ const maskIP = (ip) => {
};
const showConfigModal = async (command, title) => {
const res = await safeExec('/usr/bin/podkop', [command]);
let formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
// Create and show modal immediately with loading state
const modalContent = E('div', { 'class': 'panel-body' }, [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', {
'id': 'modal-content-pre',
style: 'margin: 0;'
}, _('Loading...'))
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'id': 'copy-button',
'click': ev => copyToClipboard(document.getElementById('modal-content-pre').innerText, ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
if (command === 'global_check') {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
ui.showModal(_(title), modalContent);
const response = await fetch('https://fakeip.podkop.fyi/check', { signal: controller.signal });
const data = await response.json();
clearTimeout(timeoutId);
if (data.fakeip === true) {
formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n';
} else {
formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n';
formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n';
formattedOutput += _('Its must be router!') + '\n';
}
// Bypass check
const bypassResponse = await fetch('https://fakeip.podkop.fyi/check', { signal: controller.signal });
const bypassData = await bypassResponse.json();
const bypassResponse2 = await fetch('https://ip.podkop.fyi/check', { signal: controller.signal });
const bypassData2 = await bypassResponse2.json();
formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) {
formattedOutput += '✅ ' + _('Proxy working correctly') + '\n';
formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
} else if (bypassData.IP === bypassData2.IP) {
formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n';
formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n';
} else {
formattedOutput += '❌ ' + _('Proxy check failed') + '\n';
}
} catch (error) {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
// Function to update modal content
const updateModalContent = (content) => {
const pre = document.getElementById('modal-content-pre');
if (pre) {
pre.textContent = content;
}
}
};
ui.showModal(_(title), createModalContent(_(title), formattedOutput));
try {
let formattedOutput = '';
if (command === 'global_check') {
const res = await safeExec('/usr/bin/podkop', [command]);
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
const response = await fetch('https://fakeip.podkop.fyi/check', { signal: controller.signal });
const data = await response.json();
clearTimeout(timeoutId);
if (data.fakeip === true) {
formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n';
} else {
formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n';
formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n';
formattedOutput += _('Its must be router!') + '\n';
}
// Bypass check
const bypassResponse = await fetch('https://fakeip.podkop.fyi/check', { signal: controller.signal });
const bypassData = await bypassResponse.json();
const bypassResponse2 = await fetch('https://ip.podkop.fyi/check', { signal: controller.signal });
const bypassData2 = await bypassResponse2.json();
formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) {
formattedOutput += '✅ ' + _('Proxy working correctly') + '\n';
formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
} else if (bypassData.IP === bypassData2.IP) {
formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n';
formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n';
} else {
formattedOutput += '❌ ' + _('Proxy check failed') + '\n';
}
updateModalContent(formattedOutput);
} catch (error) {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
}
} else {
const res = await safeExec('/usr/bin/podkop', [command]);
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
updateModalContent(formattedOutput);
}
} catch (error) {
updateModalContent(_('Error: ') + error.message);
}
};
// Button Factory
@@ -783,8 +855,8 @@ const createStatusPanel = (title, status, buttons, extraData = {}) => {
title: _('Lists Update Results')
})
] : title === _('FakeIP Status') ? [
E('div', { style: 'margin-bottom: 10px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('span', { style: `color: ${extraData.fakeipStatus?.color}` }, [
extraData.fakeipStatus?.state === 'working' ? '✔' : extraData.fakeipStatus?.state === 'not_working' ? '✘' : '!',
' ',
@@ -799,8 +871,8 @@ const createStatusPanel = (title, status, buttons, extraData = {}) => {
])
])
]),
E('div', { style: 'margin-bottom: 10px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', {}, _('DNS Status')),
E('br'),
E('span', { style: `color: ${extraData.dnsStatus?.remote?.color}` }, [
@@ -816,8 +888,8 @@ const createStatusPanel = (title, status, buttons, extraData = {}) => {
])
])
]),
E('div', { style: 'margin-bottom: 10px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', {}, extraData.configName),
E('br'),
E('span', { style: `color: ${extraData.bypassStatus?.color}` }, [
@@ -843,8 +915,14 @@ let createStatusSection = function (podkopStatus, singboxStatus, podkop, luci, s
action: 'restart',
reload: true
}),
ButtonFactory.createActionButton({
label: 'Stop Podkop',
type: 'apply',
action: 'stop',
reload: true
}),
ButtonFactory.createInitActionButton({
label: podkopStatus.enabled ? 'Disable Podkop' : 'Enable Podkop',
label: podkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
type: podkopStatus.enabled ? 'remove' : 'apply',
action: podkopStatus.enabled ? 'disable' : 'enable',
reload: true
@@ -1275,7 +1353,7 @@ return view.extend({
}
updateDiagnostics();
diagnosticsUpdateTimer = setInterval(updateDiagnostics, 10000);
diagnosticsUpdateTimer = setInterval(updateDiagnostics, DIAGNOSTICS_UPDATE_INTERVAL);
}
function stopDiagnosticsUpdates() {
@@ -1311,7 +1389,7 @@ return view.extend({
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
try {
const response = await fetch('https://fakeip.podkop.fyi/check', { signal: controller.signal });
@@ -1400,7 +1478,7 @@ return view.extend({
let ip1 = null;
try {
const controller1 = new AbortController();
const timeoutId1 = setTimeout(() => controller1.abort(), 10000);
const timeoutId1 = setTimeout(() => controller1.abort(), FETCH_TIMEOUT);
const response1 = await fetch('https://fakeip.podkop.fyi/check', { signal: controller1.signal });
const data1 = await response1.json();
@@ -1415,7 +1493,7 @@ return view.extend({
let ip2 = null;
try {
const controller2 = new AbortController();
const timeoutId2 = setTimeout(() => controller2.abort(), 10000);
const timeoutId2 = setTimeout(() => controller2.abort(), FETCH_TIMEOUT);
const response2 = await fetch('https://ip.podkop.fyi/check', { signal: controller2.signal });
const data2 = await response2.json();
@@ -1494,8 +1572,8 @@ return view.extend({
checkDNSAvailability()
.then(result => results.dnsStatus = result)
.catch(() => results.dnsStatus = {
remote: { state: 'error', message: 'check error', color: STATUS_COLORS.WARNING },
local: { state: 'error', message: 'check error', color: STATUS_COLORS.WARNING }
remote: createStatus('error', 'DNS check error', 'WARNING'),
local: createStatus('error', 'DNS check error', 'WARNING')
}),
checkBypass()
@@ -1665,7 +1743,7 @@ return view.extend({
}
}
}
}, 100);
}, DIAGNOSTICS_INITIAL_DELAY);
node.classList.add('fade-in');
return node;

View File

@@ -842,4 +842,34 @@ msgid "Its must be router!"
msgstr "Это должен быть роутер!"
msgid "Global check"
msgstr "Глобальная проверка"
msgstr "Глобальная проверка"
msgid "Starting lists update..."
msgstr "Начало обновления списков..."
msgid "DNS check passed"
msgstr "Проверка DNS пройдена"
msgid "DNS check failed after 60 attempts"
msgstr "Проверка DNS не удалась после 60 попыток"
msgid "GitHub connection check passed"
msgstr "Проверка подключения к GitHub пройдена"
msgid "GitHub connection check passed (via proxy)"
msgstr "Проверка подключения к GitHub пройдена (через прокси)"
msgid "GitHub connection check failed after 60 attempts"
msgstr "Проверка подключения к GitHub не удалась после 60 попыток"
msgid "Downloading and processing lists..."
msgstr "Загрузка и обработка списков..."
msgid "Lists update completed successfully"
msgstr "Обновление списков успешно завершено"
msgid "Lists update failed"
msgstr "Обновление списков не удалось"
msgid "Error: "
msgstr "Ошибка: "

View File

@@ -1193,4 +1193,37 @@ msgid "Its must be router!"
msgstr ""
msgid "Global check"
msgstr ""
msgid "Starting lists update..."
msgstr ""
msgid "DNS check passed"
msgstr ""
msgid "DNS check failed after 60 attempts"
msgstr ""
msgid "GitHub connection check passed"
msgstr ""
msgid "GitHub connection check passed (via proxy)"
msgstr ""
msgid "GitHub connection check failed after 60 attempts"
msgstr ""
msgid "Downloading and processing lists..."
msgstr ""
msgid "Lists update completed successfully"
msgstr ""
msgid "Lists update failed"
msgstr ""
msgid "Loading..."
msgstr ""
msgid "Error: "
msgstr ""

View File

@@ -26,6 +26,11 @@ SRC_INTERFACE=""
RESOLV_CONF="/etc/resolv.conf"
CLOUDFLARE_OCTETS="103.21 103.22 103.31 104.16 104.17 104.18 104.19 104.20 104.21 104.22 104.23 104.24 104.25 104.26 104.27 104.28 108.162 131.0 141.101 162.158 162.159 172.64 172.65 172.66 172.67 172.68 172.69 172.70 172.71 173.245 188.114 190.93 197.234 198.41"
# Color constants
COLOR_CYAN="\033[0;36m"
COLOR_GREEN="\033[0;32m"
COLOR_RESET="\033[0m"
log() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
@@ -36,11 +41,16 @@ log() {
nolog() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
local CYAN="\033[0;36m"
local GREEN="\033[0;32m"
local RESET="\033[0m"
echo -e "${CYAN}[$timestamp]${RESET} ${GREEN}$message${RESET}"
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
echolog() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
logger -t "podkop" "$timestamp $message"
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
start_main() {
@@ -557,13 +567,13 @@ prepare_custom_ruleset() {
}
list_update() {
log "Update remote lists"
echolog "🔄 Starting lists update..."
local i
for i in $(seq 1 60); do
if nslookup -timeout=1 openwrt.org >/dev/null 2>&1; then
log "DNS is available"
echolog "DNS check passed"
break
fi
log "DNS is unavailable [$i/60]"
@@ -571,7 +581,7 @@ list_update() {
done
if [ "$i" -eq 60 ]; then
log "Error: DNS check failed after 10 attempts"
echolog " DNS check failed after 60 attempts"
return 1
fi
@@ -579,28 +589,36 @@ list_update() {
config_get_bool detour "main" "detour" "0"
if [ "$detour" -eq 1 ]; then
if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com >/dev/null; then
log "GitHub is available"
echolog "GitHub connection check passed (via proxy)"
break
fi
else
if curl -s -m 3 https://github.com >/dev/null; then
log "GitHub is available"
echolog "GitHub connection check passed"
break
fi
fi
log "GitHub is unavailable [$i/60]"
echolog "GitHub is unavailable [$i/60]"
sleep 3
done
if [ "$i" -eq 60 ]; then
log "Error: Cannot connect to GitHub after 10 attempts"
echolog "❌ GitHub connection check failed after 60 attempts"
return 1
fi
echolog "📥 Downloading and processing lists..."
config_foreach process_remote_ruleset_subnet
config_foreach process_domains_list_url
config_foreach process_subnet_for_section_remote
if [ $? -eq 0 ]; then
echolog "✅ Lists update completed successfully"
else
echolog "❌ Lists update failed"
fi
}
find_working_resolver() {
@@ -2195,24 +2213,33 @@ check_dns_available() {
fi
if [ "$dns_type" = "doh" ]; then
local result=""
# Generate random DNS query ID (2 bytes)
local random_id=$(head -c2 /dev/urandom | hexdump -ve '1/1 "%.2x"')
# Create DNS wire format query for google.com A record with random ID
local dns_query=$(printf "\x${random_id:0:2}\x${random_id:2:2}\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x06google\x03com\x00\x00\x01\x00\x01" | base64)
if echo "$dns_server" | grep -q "quad9.net" || \
echo "$dns_server" | grep -qE "^9\.9\.9\.(9|10|11)$|^149\.112\.112\.(112|10|11)$|^2620:fe::(fe|9|10|11)$|^2620:fe::fe:(10|11)$"; then
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server:5053/dns-query?name=itdog.info&type=A")
else
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/dns-query?name=itdog.info&type=A")
if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then
is_available=1
status="available"
else
result=$(curl --connect-timeout 5 -s -H "accept: application/dns-json" "https://$dns_server/resolve?name=itdog.info&type=A")
fi
fi
# Try POST method first (RFC 8484 compliant)
local result=$(echo "$dns_query" | base64 -d | curl -H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @- \
--connect-timeout 5 -s -w "%{size_download}" \
-o /dev/null \
"https://$dns_server/dns-query" 2>/dev/null)
if [ $? -eq 0 ] && echo "$result" | grep -q "data"; then
if [ $? -eq 0 ] && [ -n "$result" ] && [ "$result" -ge 40 ] && [ "$result" -le 100 ]; then
is_available=1
status="available"
else
# Try GET method as fallback
result=$(curl -H "accept: application/dns-message" \
--connect-timeout 5 -s -w "%{size_download}" \
-o /dev/null \
"https://$dns_server/dns-query?dns=$(echo "$dns_query" | tr -d '\n')" 2>/dev/null)
if [ $? -eq 0 ] && [ -n "$result" ] && [ "$result" -ge 40 ] && [ "$result" -le 100 ]; then
is_available=1
status="available"
fi
fi
elif [ "$dns_type" = "dot" ]; then
(nc "$dns_server" 853 </dev/null >/dev/null 2>&1) & pid=$!