From d6481675e0fd8228d9d0e896f99b12de82c440e1 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 7 Oct 2025 20:12:14 +0500 Subject: [PATCH 1/6] fix: update shebang to env bash and add strict mode for safer script execution in xgettext.sh --- luci-app-podkop/msgmerge.sh | 2 +- luci-app-podkop/xgettext.sh | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/luci-app-podkop/msgmerge.sh b/luci-app-podkop/msgmerge.sh index fd1e7e6..06e5706 100644 --- a/luci-app-podkop/msgmerge.sh +++ b/luci-app-podkop/msgmerge.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -euo pipefail PODIR="po" diff --git a/luci-app-podkop/xgettext.sh b/luci-app-podkop/xgettext.sh index 6826874..7f5d13b 100644 --- a/luci-app-podkop/xgettext.sh +++ b/luci-app-podkop/xgettext.sh @@ -1,4 +1,5 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail SRC_DIR="htdocs/luci-static/resources/view/podkop" OUT_POT="po/templates/podkop.pot" From a8b2001cc1b94c6f8719345bc4b6cef3ca95b03f Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 7 Oct 2025 20:12:46 +0500 Subject: [PATCH 2/6] fix: sort input files before processing in xgettext.sh to ensure consistent POT generation --- luci-app-podkop/xgettext.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/luci-app-podkop/xgettext.sh b/luci-app-podkop/xgettext.sh index 7f5d13b..0db78fb 100644 --- a/luci-app-podkop/xgettext.sh +++ b/luci-app-podkop/xgettext.sh @@ -12,6 +12,7 @@ if [ ${#FILES[@]} -eq 0 ]; then exit 1 fi +mapfile -t FILES < <(printf '%s\n' "${FILES[@]}" | sort) mkdir -p "$(dirname "$OUT_POT")" echo "Generating POT template from JS files in $SRC_DIR" From 1bce7c0c98c8a584613f61bc28f745dfd2e21202 Mon Sep 17 00:00:00 2001 From: divocat Date: Tue, 7 Oct 2025 18:26:59 +0300 Subject: [PATCH 3/6] fix: migrate test latency to locales --- fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts | 2 +- .../htdocs/luci-static/resources/view/podkop/main.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts index 4501ae7..1acdb26 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts @@ -101,7 +101,7 @@ export function renderDefaultState({ class: 'btn dashboard-sections-grid-item-test-latency', click: () => testLatency(), }, - 'Test latency', + _('Test latency'), ), ]), E( diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index dae6acf..99ffd3d 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -1294,7 +1294,7 @@ function renderDefaultState({ class: "btn dashboard-sections-grid-item-test-latency", click: () => testLatency() }, - "Test latency" + _("Test latency") ) ]), E( From ae4a3781e6292f75399186bf0d156dd6f631e502 Mon Sep 17 00:00:00 2001 From: Andrey Petelin Date: Tue, 7 Oct 2025 20:42:34 +0500 Subject: [PATCH 4/6] i18n: update Russian translations for additional settings and related messages --- luci-app-podkop/po/ru/podkop.po | 145 +++++++++++++++++++++++++++++--- 1 file changed, 133 insertions(+), 12 deletions(-) diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index ef1d859..7cfb6bc 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -51,9 +51,11 @@ msgid "Config without description" msgstr "Конфигурация без описания" msgid "" -"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs" +"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup " +"configs" msgstr "" -"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для резервных конфигураций" +"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для " +"резервных конфигураций" msgid "No active configuration found. One configuration is required." msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка." @@ -124,14 +126,18 @@ msgstr "Выберите предустановленные сервисы дл msgid "Regional options cannot be used together" msgstr "Нельзя использовать несколько региональных опций одновременно" +#, javascript-format msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." msgid "Russia inside restrictions" msgstr "Ограничения Russia inside" -msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." -msgstr "Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора." +#, javascript-format +msgid "" +"Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection." +msgstr "" +"Внимание: «Russia inside» может использоваться только с %s. %s уже находится в «Russia inside» и был удалён из выбора." msgid "User Domain List Type" msgstr "Тип пользовательского списка доменов" @@ -214,8 +220,12 @@ msgstr "Введите подсети в нотации CIDR (например: msgid "User Subnets List" msgstr "Список пользовательских подсетей" -msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //" -msgstr "Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии после //" +msgid "" +"Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments " +"after //" +msgstr "" +"Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии " +"после //" msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed." msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы." @@ -568,11 +578,122 @@ msgstr "Конфигурация: " msgid "Diagnostics" msgstr "Диагностика" -msgid "Podkop" -msgstr "Podkop" +msgid "Additional Settings" +msgstr "Дополнительные настройки" -msgid "Extra configurations" -msgstr "Дополнительные конфигурации" +msgid "Yacd enable" +msgstr "Включить YACD" -msgid "Add Section" -msgstr "Добавить раздел" +msgid "Exclude NTP" +msgstr "Исключить NTP" + +msgid "Allows you to exclude NTP protocol traffic from the tunnel" +msgstr "Позволяет исключить направление трафика NTP-протокола в туннель" + +msgid "QUIC disable" +msgstr "Отключить QUIC" + +msgid "For issues with the video stream" +msgstr "Для проблем с видеопотоком" + +msgid "List Update Frequency" +msgstr "Частота обновления списков" + +msgid "Select how often the lists will be updated" +msgstr "Выберите как часто будут обновляться списки" + +msgid "Select DNS protocol to use" +msgstr "Выберите протокол DNS" + +msgid "Bootstrap DNS server" +msgstr "Bootstrap DNS-сервер" + +msgid "The DNS server used to look up the IP address of an upstream DNS server" +msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" + +msgid "DNS Rewrite TTL" +msgstr "Перезапись TTL для DNS" + +msgid "Time in seconds for DNS record caching (default: 60)" +msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)" + +msgid "TTL value cannot be empty" +msgstr "Значение TTL не может быть пустым" + +msgid "TTL must be a positive number" +msgstr "TTL должно быть положительным числом" + +msgid "Config File Path" +msgstr "Путь к файлу конфигурации" + +msgid "Select path for sing-box config file. Change this ONLY if you know what you are doing" +msgstr "Выберите путь к файлу конфигурации sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Cache File Path" +msgstr "Путь к файлу кэша" + +msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" +msgstr "Выберите или введите путь к файлу кеша sing-box. Изменяйте это, ТОЛЬКО если вы знаете, что делаете" + +msgid "Cache file path cannot be empty" +msgstr "Путь к файлу кэша не может быть пустым" + +msgid "Path must be absolute (start with /)" +msgstr "Путь должен быть абсолютным (начинаться с /)" + +msgid "Path must end with cache.db" +msgstr "Путь должен заканчиваться на cache.db" + +msgid "Path must contain at least one directory (like /tmp/cache.db)" +msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" + +msgid "Source Network Interface" +msgstr "Сетевой интерфейс источника" + +msgid "Select the network interface from which the traffic will originate" +msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" + +msgid "Interface monitoring" +msgstr "Мониторинг интерфейсов" + +msgid "Interface monitoring for bad WAN" +msgstr "Мониторинг интерфейсов для плохого WAN" + +msgid "Interface for monitoring" +msgstr "Интерфейс для мониторинга" + +msgid "Select the WAN interfaces to be monitored" +msgstr "Выберите WAN интерфейсы для мониторинга" + +msgid "Interface Monitoring Delay" +msgstr "Задержка при мониторинге интерфейсов" + +msgid "Delay in milliseconds before reloading podkop after interface UP" +msgstr "Задержка в миллисекундах перед перезагрузкой podkop после поднятия интерфейса" + +msgid "Delay value cannot be empty" +msgstr "Значение задержки не может быть пустым" + +msgid "Dont touch my DHCP!" +msgstr "Не трогать мой DHCP!" + +msgid "Podkop will not change the DHCP config" +msgstr "Podkop не будет изменять конфигурацию DHCP" + +msgid "Proxy download of lists" +msgstr "Загрузка списков через прокси" + +msgid "Downloading all lists via main Proxy/VPN" +msgstr "Загрузка всех списков через основной прокси/VPN" + +msgid "IP for exclusion" +msgstr "IP для исключения" + +msgid "Specify local IP addresses that will never use the configured route" +msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут" + +msgid "Mixed enable" +msgstr "Включить смешанный режим" + +msgid "Browser port: 2080" +msgstr "Порт браузера: 2080" From 72b2a34af9dd7729b70a6299161268b28b7115e6 Mon Sep 17 00:00:00 2001 From: divocat Date: Tue, 7 Oct 2025 19:19:10 +0300 Subject: [PATCH 5/6] fix: allow .tld for user_domains_text & user_domains --- .../src/validators/tests/validateDomain.test.js | 17 +++++++++++++++++ fe-app-podkop/src/validators/validateDomain.ts | 14 ++++++++++++-- .../resources/view/podkop/configSection.js | 4 ++-- .../luci-static/resources/view/podkop/main.js | 8 +++++++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/fe-app-podkop/src/validators/tests/validateDomain.test.js b/fe-app-podkop/src/validators/tests/validateDomain.test.js index a2c312a..b26bc23 100644 --- a/fe-app-podkop/src/validators/tests/validateDomain.test.js +++ b/fe-app-podkop/src/validators/tests/validateDomain.test.js @@ -29,6 +29,13 @@ export const invalidDomains = [ ['Too long domain (>253 chars)', Array(40).fill('abcdef').join('.') + '.com'], ]; +export const dotTLDTests = [ + ['Dot TLD allowed (.net)', '.net', true, true], + ['Dot TLD not allowed (.net)', '.net', false, false], + ['Invalid with double dot', '..net', true, false], + ['Invalid single word TLD (net)', 'net', true, false], +]; + describe('validateDomain', () => { describe.each(validDomains)('Valid domain: %s', (_desc, domain) => { it(`returns valid=true for "${domain}"`, () => { @@ -43,4 +50,14 @@ describe('validateDomain', () => { expect(res.valid).toBe(false); }); }); + + describe.each(dotTLDTests)( + 'Dot TLD toggle: %s', + (_desc, domain, allowDotTLD, expected) => { + it(`"${domain}" with allowDotTLD=${allowDotTLD} → valid=${expected}`, () => { + const res = validateDomain(domain, allowDotTLD); + expect(res.valid).toBe(expected); + }); + }, + ); }); diff --git a/fe-app-podkop/src/validators/validateDomain.ts b/fe-app-podkop/src/validators/validateDomain.ts index fdebefa..d4ff863 100644 --- a/fe-app-podkop/src/validators/validateDomain.ts +++ b/fe-app-podkop/src/validators/validateDomain.ts @@ -1,8 +1,18 @@ import { ValidationResult } from './types'; -export function validateDomain(domain: string): ValidationResult { +export function validateDomain( + domain: string, + allowDotTLD = false +): ValidationResult { const domainRegex = - /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + + if (allowDotTLD) { + const dotTLD = /^\.[a-zA-Z]{2,}$/; + if (dotTLD.test(domain)) { + return { valid: true, message: _('Valid') }; + } + } if (!domainRegex.test(domain)) { return { valid: false, message: _('Invalid domain address') }; diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index cde7914..bc49dc7 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -455,7 +455,7 @@ function createConfigSection(section) { return true; } - const validation = main.validateDomain(value); + const validation = main.validateDomain(value, true); if (validation.valid) { return true; @@ -493,7 +493,7 @@ function createConfigSection(section) { ); } - const { valid, results } = main.bulkValidate(domains, main.validateDomain); + const { valid, results } = main.bulkValidate(domains, row => main.validateDomain(row, true)); if (!valid) { const errors = results diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index 99ffd3d..d417fca 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -14,8 +14,14 @@ function validateIPV4(ip) { } // src/validators/validateDomain.ts -function validateDomain(domain) { +function validateDomain(domain, allowDotTLD = false) { const domainRegex = /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + if (allowDotTLD) { + const dotTLD = /^\.[a-zA-Z]{2,}$/; + if (dotTLD.test(domain)) { + return { valid: true, message: _("Valid") }; + } + } if (!domainRegex.test(domain)) { return { valid: false, message: _("Invalid domain address") }; } From 48c8f01d2fba1e4ff7ca814bca66aeb1eeee35db Mon Sep 17 00:00:00 2001 From: divocat Date: Tue, 7 Oct 2025 20:34:38 +0300 Subject: [PATCH 6/6] fix: correct proxy string label displaying on dashboard --- fe-app-podkop/src/helpers/index.ts | 1 + fe-app-podkop/src/helpers/splitProxyString.ts | 7 +++++++ .../src/podkop/methods/getDashboardSections.ts | 12 +++++++----- .../src/validators/tests/validateDomain.test.js | 14 +++++++------- fe-app-podkop/src/validators/validateDomain.ts | 6 +++--- .../resources/view/podkop/configSection.js | 6 +----- .../luci-static/resources/view/podkop/main.js | 10 +++++++++- 7 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 fe-app-podkop/src/helpers/splitProxyString.ts diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index 242f2e7..43c8879 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -7,3 +7,4 @@ export * from './maskIP'; export * from './getProxyUrlName'; export * from './onMount'; export * from './getClashApiUrl'; +export * from './splitProxyString'; diff --git a/fe-app-podkop/src/helpers/splitProxyString.ts b/fe-app-podkop/src/helpers/splitProxyString.ts new file mode 100644 index 0000000..9426e8b --- /dev/null +++ b/fe-app-podkop/src/helpers/splitProxyString.ts @@ -0,0 +1,7 @@ +export function splitProxyString(str: string) { + return str + .split('\n') + .map((line) => line.trim()) + .filter((line) => !line.startsWith('//')) + .filter(Boolean); +} diff --git a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts index c101926..6507775 100644 --- a/fe-app-podkop/src/podkop/methods/getDashboardSections.ts +++ b/fe-app-podkop/src/podkop/methods/getDashboardSections.ts @@ -1,7 +1,7 @@ import { Podkop } from '../types'; import { getConfigSections } from './getConfigSections'; import { getClashProxies } from '../../clash'; -import { getProxyUrlName } from '../../helpers'; +import { getProxyUrlName, splitProxyString } from '../../helpers'; interface IGetDashboardSectionsResponse { success: boolean; @@ -35,6 +35,11 @@ export async function getDashboardSections(): Promise proxy.code === `${section['.name']}-out`, ); + const activeConfigs = splitProxyString(section.proxy_string); + + const proxyDisplayName = + getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ''; + return { withTagSelect: false, code: outbound?.code || section['.name'], @@ -42,10 +47,7 @@ export async function getDashboardSections(): Promise { }); describe.each(dotTLDTests)( - 'Dot TLD toggle: %s', - (_desc, domain, allowDotTLD, expected) => { - it(`"${domain}" with allowDotTLD=${allowDotTLD} → valid=${expected}`, () => { - const res = validateDomain(domain, allowDotTLD); - expect(res.valid).toBe(expected); - }); - }, + 'Dot TLD toggle: %s', + (_desc, domain, allowDotTLD, expected) => { + it(`"${domain}" with allowDotTLD=${allowDotTLD} → valid=${expected}`, () => { + const res = validateDomain(domain, allowDotTLD); + expect(res.valid).toBe(expected); + }); + }, ); }); diff --git a/fe-app-podkop/src/validators/validateDomain.ts b/fe-app-podkop/src/validators/validateDomain.ts index d4ff863..14e1f24 100644 --- a/fe-app-podkop/src/validators/validateDomain.ts +++ b/fe-app-podkop/src/validators/validateDomain.ts @@ -1,11 +1,11 @@ import { ValidationResult } from './types'; export function validateDomain( - domain: string, - allowDotTLD = false + domain: string, + allowDotTLD = false, ): ValidationResult { const domainRegex = - /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; + /^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/; if (allowDotTLD) { const dotTLD = /^\.[a-zA-Z]{2,}$/; diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js index bc49dc7..c44e389 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js @@ -130,11 +130,7 @@ function createConfigSection(section) { } try { - const activeConfigs = value - .split('\n') - .map((line) => line.trim()) - .filter((line) => !line.startsWith('//')) - .filter(Boolean); + const activeConfigs = main.splitProxyString(value); if (!activeConfigs.length) { return _( diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js index d417fca..7d09f99 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/main.js @@ -771,6 +771,11 @@ function getClashWsUrl() { return `ws://${hostname}:9090`; } +// src/helpers/splitProxyString.ts +function splitProxyString(str) { + return str.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("//")).filter(Boolean); +} + // src/clash/methods/createBaseApiRequest.ts async function createBaseApiRequest(fetchFn) { try { @@ -899,6 +904,8 @@ async function getDashboardSections() { const outbound = proxies.find( (proxy) => proxy.code === `${section[".name"]}-out` ); + const activeConfigs = splitProxyString(section.proxy_string); + const proxyDisplayName = getProxyUrlName(activeConfigs?.[0]) || outbound?.value?.name || ""; return { withTagSelect: false, code: outbound?.code || section[".name"], @@ -906,7 +913,7 @@ async function getDashboardSections() { outbounds: [ { code: outbound?.code || section[".name"], - displayName: getProxyUrlName(section.proxy_string) || outbound?.value?.name || "", + displayName: proxyDisplayName, latency: outbound?.value?.history?.[0]?.delay || 0, type: outbound?.value?.type || "", selected: true @@ -1897,6 +1904,7 @@ return baseclass.extend({ onMount, parseValueList, renderDashboard, + splitProxyString, triggerLatencyGroupTest, triggerLatencyProxyTest, triggerProxySelector,