diff --git a/fe-app-podkop/locales/calls.json b/fe-app-podkop/locales/calls.json index f1bb11d..970e80e 100644 --- a/fe-app-podkop/locales/calls.json +++ b/fe-app-podkop/locales/calls.json @@ -3,35 +3,35 @@ "call": "✔ Enabled", "key": "✔ Enabled", "places": [ - "src/podkop/tabs/dashboard/initController.ts:342" + "src/podkop/tabs/dashboard/initController.ts:345" ] }, { "call": "✔ Running", "key": "✔ Running", "places": [ - "src/podkop/tabs/dashboard/initController.ts:353" + "src/podkop/tabs/dashboard/initController.ts:356" ] }, { "call": "✘ Disabled", "key": "✘ Disabled", "places": [ - "src/podkop/tabs/dashboard/initController.ts:343" + "src/podkop/tabs/dashboard/initController.ts:346" ] }, { "call": "✘ Stopped", "key": "✘ Stopped", "places": [ - "src/podkop/tabs/dashboard/initController.ts:354" + "src/podkop/tabs/dashboard/initController.ts:357" ] }, { "call": "Active Connections", "key": "Active Connections", "places": [ - "src/podkop/tabs/dashboard/initController.ts:304" + "src/podkop/tabs/dashboard/initController.ts:307" ] }, { @@ -379,8 +379,8 @@ "call": "Downlink", "key": "Downlink", "places": [ - "src/podkop/tabs/dashboard/initController.ts:238", - "src/podkop/tabs/dashboard/initController.ts:272" + "src/podkop/tabs/dashboard/initController.ts:241", + "src/podkop/tabs/dashboard/initController.ts:275" ] }, { @@ -637,6 +637,97 @@ "src/validators/validateSubnet.ts:11" ] }, + { + "call": "Invalid HY2 URL: insecure must be 0 or 1", + "key": "Invalid HY2 URL: insecure must be 0 or 1", + "places": [ + "src/validators/validateHysteriaUrl.ts:76" + ] + }, + { + "call": "Invalid HY2 URL: invalid port number", + "key": "Invalid HY2 URL: invalid port number", + "places": [ + "src/validators/validateHysteriaUrl.ts:62" + ] + }, + { + "call": "Invalid HY2 URL: missing credentials/server", + "key": "Invalid HY2 URL: missing credentials/server", + "places": [ + "src/validators/validateHysteriaUrl.ts:32" + ] + }, + { + "call": "Invalid HY2 URL: missing host", + "key": "Invalid HY2 URL: missing host", + "places": [ + "src/validators/validateHysteriaUrl.ts:49" + ] + }, + { + "call": "Invalid HY2 URL: missing host & port", + "key": "Invalid HY2 URL: missing host & port", + "places": [ + "src/validators/validateHysteriaUrl.ts:43" + ] + }, + { + "call": "Invalid HY2 URL: missing password", + "key": "Invalid HY2 URL: missing password", + "places": [ + "src/validators/validateHysteriaUrl.ts:38" + ] + }, + { + "call": "Invalid HY2 URL: missing port", + "key": "Invalid HY2 URL: missing port", + "places": [ + "src/validators/validateHysteriaUrl.ts:53" + ] + }, + { + "call": "Invalid HY2 URL: must not contain spaces", + "key": "Invalid HY2 URL: must not contain spaces", + "places": [ + "src/validators/validateHysteriaUrl.ts:19" + ] + }, + { + "call": "Invalid HY2 URL: must start with hysteria2:// or hy2://", + "key": "Invalid HY2 URL: must start with hysteria2:// or hy2://", + "places": [ + "src/validators/validateHysteriaUrl.ts:12" + ] + }, + { + "call": "Invalid HY2 URL: obfs-password required when obfs is set", + "key": "Invalid HY2 URL: obfs-password required when obfs is set", + "places": [ + "src/validators/validateHysteriaUrl.ts:99" + ] + }, + { + "call": "Invalid HY2 URL: parsing failed", + "key": "Invalid HY2 URL: parsing failed", + "places": [ + "src/validators/validateHysteriaUrl.ts:113" + ] + }, + { + "call": "Invalid HY2 URL: sni cannot be empty", + "key": "Invalid HY2 URL: sni cannot be empty", + "places": [ + "src/validators/validateHysteriaUrl.ts:106" + ] + }, + { + "call": "Invalid HY2 URL: unsupported obfs type", + "key": "Invalid HY2 URL: unsupported obfs type", + "places": [ + "src/validators/validateHysteriaUrl.ts:88" + ] + }, { "call": "Invalid IP address", "key": "Invalid IP address", @@ -880,7 +971,7 @@ "call": "Memory Usage", "key": "Memory Usage", "places": [ - "src/podkop/tabs/dashboard/initController.ts:308" + "src/podkop/tabs/dashboard/initController.ts:311" ] }, { @@ -1023,7 +1114,7 @@ "call": "Podkop", "key": "Podkop", "places": [ - "src/podkop/tabs/dashboard/initController.ts:340" + "src/podkop/tabs/dashboard/initController.ts:343" ] }, { @@ -1290,7 +1381,7 @@ "call": "Services info", "key": "Services info", "places": [ - "src/podkop/tabs/dashboard/initController.ts:337" + "src/podkop/tabs/dashboard/initController.ts:340" ] }, { @@ -1312,7 +1403,7 @@ "call": "Sing-box", "key": "Sing-box", "places": [ - "src/podkop/tabs/dashboard/initController.ts:351" + "src/podkop/tabs/dashboard/initController.ts:354" ] }, { @@ -1425,7 +1516,7 @@ "call": "System info", "key": "System info", "places": [ - "src/podkop/tabs/dashboard/initController.ts:301" + "src/podkop/tabs/dashboard/initController.ts:304" ] }, { @@ -1453,13 +1544,7 @@ "call": "Text List", "key": "Text List", "places": [ - "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:368" - ] - }, - { - "call": "Text List (comma/space/newline separated)", - "key": "Text List (comma/space/newline separated)", - "places": [ + "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:368", "../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:448" ] }, @@ -1502,14 +1587,14 @@ "call": "Traffic", "key": "Traffic", "places": [ - "src/podkop/tabs/dashboard/initController.ts:235" + "src/podkop/tabs/dashboard/initController.ts:238" ] }, { "call": "Traffic Total", "key": "Traffic Total", "places": [ - "src/podkop/tabs/dashboard/initController.ts:265" + "src/podkop/tabs/dashboard/initController.ts:268" ] }, { @@ -1572,15 +1657,15 @@ "call": "Uplink", "key": "Uplink", "places": [ - "src/podkop/tabs/dashboard/initController.ts:237", - "src/podkop/tabs/dashboard/initController.ts:268" + "src/podkop/tabs/dashboard/initController.ts:240", + "src/podkop/tabs/dashboard/initController.ts:271" ] }, { - "call": "URL must start with vless://, ss://, trojan://, or socks4/5://", - "key": "URL must start with vless://, ss://, trojan://, or socks4/5://", + "call": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", + "key": "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://", "places": [ - "src/validators/validateProxyUrl.ts:29" + "src/validators/validateProxyUrl.ts:37" ] }, { @@ -1675,6 +1760,7 @@ "src/validators/validateDns.ts:18", "src/validators/validateDomain.ts:13", "src/validators/validateDomain.ts:30", + "src/validators/validateHysteriaUrl.ts:111", "src/validators/validateIp.ts:8", "src/validators/validateOutboundJson.ts:7", "src/validators/validatePath.ts:16", diff --git a/fe-app-podkop/locales/podkop.pot b/fe-app-podkop/locales/podkop.pot index 0bf57c8..542bfbe 100644 --- a/fe-app-podkop/locales/podkop.pot +++ b/fe-app-podkop/locales/podkop.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-06 14:19+0200\n" -"PO-Revision-Date: 2025-11-06 14:19+0200\n" +"POT-Creation-Date: 2025-12-01 14:30+0200\n" +"PO-Revision-Date: 2025-12-01 14:30+0200\n" "Last-Translator: divocat \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -16,23 +16,23 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/podkop/tabs/dashboard/initController.ts:342 +#: src/podkop/tabs/dashboard/initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:353 +#: src/podkop/tabs/dashboard/initController.ts:356 msgid "✔ Running" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:343 +#: src/podkop/tabs/dashboard/initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:354 +#: src/podkop/tabs/dashboard/initController.ts:357 msgid "✘ Stopped" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:304 +#: src/podkop/tabs/dashboard/initController.ts:307 msgid "Active Connections" msgstr "" @@ -236,8 +236,8 @@ msgstr "" msgid "Dont Touch My DHCP!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:238 -#: src/podkop/tabs/dashboard/initController.ts:272 +#: src/podkop/tabs/dashboard/initController.ts:241 +#: src/podkop/tabs/dashboard/initController.ts:275 msgid "Downlink" msgstr "" @@ -390,6 +390,58 @@ msgstr "" msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "" +#: src/validators/validateHysteriaUrl.ts:76 +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:62 +msgid "Invalid HY2 URL: invalid port number" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:32 +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:49 +msgid "Invalid HY2 URL: missing host" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:43 +msgid "Invalid HY2 URL: missing host & port" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:38 +msgid "Invalid HY2 URL: missing password" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:53 +msgid "Invalid HY2 URL: missing port" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:19 +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:12 +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:99 +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:113 +msgid "Invalid HY2 URL: parsing failed" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:106 +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:88 +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "" + #: src/validators/validateIp.ts:11 msgid "Invalid IP address" msgstr "" @@ -527,7 +579,7 @@ msgstr "" msgid "Main DNS" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:308 +#: src/podkop/tabs/dashboard/initController.ts:311 msgid "Memory Usage" msgstr "" @@ -613,7 +665,7 @@ msgstr "" msgid "Pending" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:340 +#: src/podkop/tabs/dashboard/initController.ts:343 msgid "Podkop" msgstr "" @@ -766,7 +818,7 @@ msgstr "" msgid "Select the WAN interfaces to be monitored" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:337 +#: src/podkop/tabs/dashboard/initController.ts:340 msgid "Services info" msgstr "" @@ -779,7 +831,7 @@ msgstr "" msgid "Show sing-box config" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:351 +#: src/podkop/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" @@ -844,7 +896,7 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:301 +#: src/podkop/tabs/dashboard/initController.ts:304 msgid "System info" msgstr "" @@ -861,11 +913,8 @@ msgid "Test latency" msgstr "" #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:368 -msgid "Text List" -msgstr "" - #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:448 -msgid "Text List (comma/space/newline separated)" +msgid "Text List" msgstr "" #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 @@ -888,11 +937,11 @@ msgstr "" msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:235 +#: src/podkop/tabs/dashboard/initController.ts:238 msgid "Traffic" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:265 +#: src/podkop/tabs/dashboard/initController.ts:268 msgid "Traffic Total" msgstr "" @@ -931,13 +980,13 @@ msgstr "" msgid "Unknown error" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:237 -#: src/podkop/tabs/dashboard/initController.ts:268 +#: src/podkop/tabs/dashboard/initController.ts:240 +#: src/podkop/tabs/dashboard/initController.ts:271 msgid "Uplink" msgstr "" -#: src/validators/validateProxyUrl.ts:29 -msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +#: src/validators/validateProxyUrl.ts:37 +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" #: src/validators/validateUrl.ts:17 @@ -992,6 +1041,7 @@ msgstr "" #: src/validators/validateDns.ts:18 #: src/validators/validateDomain.ts:13 #: src/validators/validateDomain.ts:30 +#: src/validators/validateHysteriaUrl.ts:111 #: src/validators/validateIp.ts:8 #: src/validators/validateOutboundJson.ts:7 #: src/validators/validatePath.ts:16 diff --git a/fe-app-podkop/locales/podkop.ru.po b/fe-app-podkop/locales/podkop.ru.po index 25971ad..fab7459 100644 --- a/fe-app-podkop/locales/podkop.ru.po +++ b/fe-app-podkop/locales/podkop.ru.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-06 16:19+0200\n" -"PO-Revision-Date: 2025-11-06 16:19+0200\n" +"POT-Creation-Date: 2025-12-01 16:30+0200\n" +"PO-Revision-Date: 2025-12-01 16:30+0200\n" "Last-Translator: divocat\n" "Language-Team: none\n" "Language: ru\n" @@ -281,6 +281,45 @@ msgstr "Неверный домен" msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1" + +msgid "Invalid HY2 URL: invalid port number" +msgstr "Неверный URL Hysteria2: неверный номер порта" + +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер" + +msgid "Invalid HY2 URL: missing host" +msgstr "Неверный URL Hysteria2: отсутствует хост" + +msgid "Invalid HY2 URL: missing host & port" +msgstr "Неверный URL Hysteria2: отсутствуют хост и порт" + +msgid "Invalid HY2 URL: missing password" +msgstr "Неверный URL Hysteria2: отсутствует пароль" + +msgid "Invalid HY2 URL: missing port" +msgstr "Неверный URL Hysteria2: отсутствует порт" + +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "Неверный URL Hysteria2: не должен содержать пробелов" + +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://" + +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs" + +msgid "Invalid HY2 URL: parsing failed" +msgstr "Неверный URL Hysteria2: ошибка разбора" + +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "Неверный URL Hysteria2: sni не может быть пустым" + +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs" + msgid "Invalid IP address" msgstr "Неверный IP-адрес" @@ -626,9 +665,6 @@ msgstr "Тестирование задержки" msgid "Text List" msgstr "Текстовый список" -msgid "Text List (comma/space/newline separated)" -msgstr "Текстовый список (через запятую, пробел или новую строку)" - msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" @@ -674,8 +710,8 @@ msgstr "Неизвестная ошибка" msgid "Uplink" msgstr "Исходящий" -msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" -msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://" +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" msgid "URL must use one of the following protocols:" msgstr "URL должен использовать один из следующих протоколов:" diff --git a/fe-app-podkop/src/validators/tests/validateHysteriaUrl.test.js b/fe-app-podkop/src/validators/tests/validateHysteriaUrl.test.js new file mode 100644 index 0000000..b759fd2 --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateHysteriaUrl.test.js @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { validateHysteria2Url } from '../validateHysteriaUrl.js'; + +const validUrls = [ + // Basic password-only + ['password basic', 'hysteria2://pass@example.com:443/#hy2-basic'], + + // insecure=1 + [ + 'insecure allowed', + 'hysteria2://pass@example.com:443/?insecure=1#hy2-insecure', + ], + + // SNI + ['SNI param', 'hysteria2://pass@example.com:443/?sni=google.com#hy2-sni'], + + // Obfuscation + [ + 'Obfs + password', + 'hysteria2://mypassword@1.1.1.1:8443/?obfs=salamander&obfs-password=abc123#hy2-obfs', + ], + + // All params + [ + 'All options combined', + 'hysteria2://pw@8.8.8.8:8443/?sni=example.com&obfs=salamander&obfs-password=hello&insecure=1#hy2-full', + ], + + // Explicit obfs=none (valid) + ['obfs none = ok', 'hysteria2://pw@example.com:443/?obfs=none#hy2-none'], +]; + +const invalidUrls = [ + ['No prefix', 'pw@example.com:443'], + ['Missing password', 'hysteria2://@example.com:443/'], + ['Missing host', 'hysteria2://pw@:443/'], + ['Missing port', 'hysteria2://pw@example.com/'], + ['Non-numeric port', 'hysteria2://pw@example.com:port/'], + ['Port out of range', 'hysteria2://pw@example.com:99999/'], + + // Obfuscation errors + ['Unknown obfs type', 'hysteria2://pw@example.com:443/?obfs=weird'], + [ + 'obfs without obfs-password', + 'hysteria2://pw@example.com:443/?obfs=salamander', + ], + + // insecure only accepts 0/1 + ['invalid insecure', 'hysteria2://pw@example.com:443/?insecure=5'], + + // SNI empty + ['empty sni', 'hysteria2://pw@example.com:443/?sni='], +]; + +describe('validateHysteria2Url', () => { + describe.each(validUrls)('Valid HY2 URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateHysteria2Url(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid HY2 URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateHysteria2Url(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range', () => { + const res = validateHysteria2Url('hysteria2://pw@example.com:70000/'); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/validateHysteriaUrl.ts b/fe-app-podkop/src/validators/validateHysteriaUrl.ts new file mode 100644 index 0000000..464f16d --- /dev/null +++ b/fe-app-podkop/src/validators/validateHysteriaUrl.ts @@ -0,0 +1,117 @@ +import { ValidationResult } from './types'; +import { parseQueryString } from '../helpers/parseQueryString'; + +export function validateHysteria2Url(url: string): ValidationResult { + try { + const isHY2 = url.startsWith('hysteria2://'); + const isHY2Short = url.startsWith('hy2://'); + + if (!isHY2 && !isHY2Short) { + return { + valid: false, + message: _('Invalid HY2 URL: must start with hysteria2:// or hy2://'), + }; + } + + if (/\s/.test(url)) { + return { + valid: false, + message: _('Invalid HY2 URL: must not contain spaces'), + }; + } + + const prefix = isHY2 ? 'hysteria2://' : 'hy2://'; + const body = url.slice(prefix.length); + + const [mainPart] = body.split('#'); + const [authHostPort, queryString] = mainPart.split('?'); + + if (!authHostPort) + return { + valid: false, + message: _('Invalid HY2 URL: missing credentials/server'), + }; + + const [passwordPart, hostPortPart] = authHostPort.split('@'); + + if (!passwordPart) + return { valid: false, message: _('Invalid HY2 URL: missing password') }; + + if (!hostPortPart) + return { + valid: false, + message: _('Invalid HY2 URL: missing host & port'), + }; + + const [host, port] = hostPortPart.split(':'); + + if (!host) { + return { valid: false, message: _('Invalid HY2 URL: missing host') }; + } + + if (!port) { + return { valid: false, message: _('Invalid HY2 URL: missing port') }; + } + + const cleanedPort = port.replace('/', ''); + const portNum = Number(cleanedPort); + + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _('Invalid HY2 URL: invalid port number'), + }; + } + + if (queryString) { + const params = parseQueryString(queryString); + const paramsKeys = Object.keys(params); + + if ( + paramsKeys.includes('insecure') && + !['0', '1'].includes(params.insecure) + ) { + return { + valid: false, + message: _('Invalid HY2 URL: insecure must be 0 or 1'), + }; + } + + const validObfsTypes = ['none', 'salamander']; + + if ( + paramsKeys.includes('obfs') && + !validObfsTypes.includes(params.obfs) + ) { + return { + valid: false, + message: _('Invalid HY2 URL: unsupported obfs type'), + }; + } + + if ( + paramsKeys.includes('obfs') && + params.obfs !== 'none' && + !params['obfs-password'] + ) { + return { + valid: false, + message: _( + 'Invalid HY2 URL: obfs-password required when obfs is set', + ), + }; + } + + if (paramsKeys.includes('sni') && !params.sni) { + return { + valid: false, + message: _('Invalid HY2 URL: sni cannot be empty'), + }; + } + } + + return { valid: true, message: _('Valid') }; + } catch (_e) { + return { valid: false, message: _('Invalid HY2 URL: parsing failed') }; + } +} diff --git a/fe-app-podkop/src/validators/validateProxyUrl.ts b/fe-app-podkop/src/validators/validateProxyUrl.ts index 912bbca..d0be83d 100644 --- a/fe-app-podkop/src/validators/validateProxyUrl.ts +++ b/fe-app-podkop/src/validators/validateProxyUrl.ts @@ -3,6 +3,7 @@ import { validateShadowsocksUrl } from './validateShadowsocksUrl'; import { validateVlessUrl } from './validateVlessUrl'; import { validateTrojanUrl } from './validateTrojanUrl'; import { validateSocksUrl } from './validateSocksUrl'; +import { validateHysteria2Url } from './validateHysteriaUrl'; // TODO refactor current validation and add tests export function validateProxyUrl(url: string): ValidationResult { @@ -24,10 +25,17 @@ export function validateProxyUrl(url: string): ValidationResult { return validateSocksUrl(trimmedUrl); } + if ( + trimmedUrl.startsWith('hysteria2://') || + trimmedUrl.startsWith('hy2://') + ) { + return validateHysteria2Url(trimmedUrl); + } + return { valid: false, message: _( - 'URL must start with vless://, ss://, trojan://, or socks4/5://', + 'URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://', ), }; } 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 638867e..f5204dc 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 @@ -448,6 +448,92 @@ function validateSocksUrl(url) { return { valid: true, message: _("Valid") }; } +// src/validators/validateHysteriaUrl.ts +function validateHysteria2Url(url) { + try { + const isHY2 = url.startsWith("hysteria2://"); + const isHY2Short = url.startsWith("hy2://"); + if (!isHY2 && !isHY2Short) { + return { + valid: false, + message: _("Invalid HY2 URL: must start with hysteria2:// or hy2://") + }; + } + if (/\s/.test(url)) { + return { + valid: false, + message: _("Invalid HY2 URL: must not contain spaces") + }; + } + const prefix = isHY2 ? "hysteria2://" : "hy2://"; + const body = url.slice(prefix.length); + const [mainPart] = body.split("#"); + const [authHostPort, queryString] = mainPart.split("?"); + if (!authHostPort) + return { + valid: false, + message: _("Invalid HY2 URL: missing credentials/server") + }; + const [passwordPart, hostPortPart] = authHostPort.split("@"); + if (!passwordPart) + return { valid: false, message: _("Invalid HY2 URL: missing password") }; + if (!hostPortPart) + return { + valid: false, + message: _("Invalid HY2 URL: missing host & port") + }; + const [host, port] = hostPortPart.split(":"); + if (!host) { + return { valid: false, message: _("Invalid HY2 URL: missing host") }; + } + if (!port) { + return { valid: false, message: _("Invalid HY2 URL: missing port") }; + } + const cleanedPort = port.replace("/", ""); + const portNum = Number(cleanedPort); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) { + return { + valid: false, + message: _("Invalid HY2 URL: invalid port number") + }; + } + if (queryString) { + const params = parseQueryString(queryString); + const paramsKeys = Object.keys(params); + if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) { + return { + valid: false, + message: _("Invalid HY2 URL: insecure must be 0 or 1") + }; + } + const validObfsTypes = ["none", "salamander"]; + if (paramsKeys.includes("obfs") && !validObfsTypes.includes(params.obfs)) { + return { + valid: false, + message: _("Invalid HY2 URL: unsupported obfs type") + }; + } + if (paramsKeys.includes("obfs") && params.obfs !== "none" && !params["obfs-password"]) { + return { + valid: false, + message: _( + "Invalid HY2 URL: obfs-password required when obfs is set" + ) + }; + } + if (paramsKeys.includes("sni") && !params.sni) { + return { + valid: false, + message: _("Invalid HY2 URL: sni cannot be empty") + }; + } + } + return { valid: true, message: _("Valid") }; + } catch (_e) { + return { valid: false, message: _("Invalid HY2 URL: parsing failed") }; + } +} + // src/validators/validateProxyUrl.ts function validateProxyUrl(url) { const trimmedUrl = url.trim(); @@ -463,10 +549,13 @@ function validateProxyUrl(url) { if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } + if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { + return validateHysteria2Url(trimmedUrl); + } return { valid: false, message: _( - "URL must start with vless://, ss://, trojan://, or socks4/5://" + "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" ) }; } diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js index 5d522fe..0529ac5 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js @@ -87,7 +87,7 @@ function createSectionContent(section) { _("URLTest Proxy Links"), ); o.depends("proxy_config_type", "urltest"); - o.placeholder = "vless://, ss://, trojan://, socks4/5:// links"; + o.placeholder = "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"; o.rmempty = false; o.validate = function (section_id, value) { // Optional diff --git a/luci-app-podkop/po/ru/podkop.po b/luci-app-podkop/po/ru/podkop.po index 25971ad..fab7459 100644 --- a/luci-app-podkop/po/ru/podkop.po +++ b/luci-app-podkop/po/ru/podkop.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-06 16:19+0200\n" -"PO-Revision-Date: 2025-11-06 16:19+0200\n" +"POT-Creation-Date: 2025-12-01 16:30+0200\n" +"PO-Revision-Date: 2025-12-01 16:30+0200\n" "Last-Translator: divocat\n" "Language-Team: none\n" "Language: ru\n" @@ -281,6 +281,45 @@ msgstr "Неверный домен" msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y" +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1" + +msgid "Invalid HY2 URL: invalid port number" +msgstr "Неверный URL Hysteria2: неверный номер порта" + +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер" + +msgid "Invalid HY2 URL: missing host" +msgstr "Неверный URL Hysteria2: отсутствует хост" + +msgid "Invalid HY2 URL: missing host & port" +msgstr "Неверный URL Hysteria2: отсутствуют хост и порт" + +msgid "Invalid HY2 URL: missing password" +msgstr "Неверный URL Hysteria2: отсутствует пароль" + +msgid "Invalid HY2 URL: missing port" +msgstr "Неверный URL Hysteria2: отсутствует порт" + +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "Неверный URL Hysteria2: не должен содержать пробелов" + +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://" + +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs" + +msgid "Invalid HY2 URL: parsing failed" +msgstr "Неверный URL Hysteria2: ошибка разбора" + +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "Неверный URL Hysteria2: sni не может быть пустым" + +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs" + msgid "Invalid IP address" msgstr "Неверный IP-адрес" @@ -626,9 +665,6 @@ msgstr "Тестирование задержки" msgid "Text List" msgstr "Текстовый список" -msgid "Text List (comma/space/newline separated)" -msgstr "Текстовый список (через запятую, пробел или новую строку)" - msgid "The DNS server used to look up the IP address of an upstream DNS server" msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера" @@ -674,8 +710,8 @@ msgstr "Неизвестная ошибка" msgid "Uplink" msgstr "Исходящий" -msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" -msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://" +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" +msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://" msgid "URL must use one of the following protocols:" msgstr "URL должен использовать один из следующих протоколов:" diff --git a/luci-app-podkop/po/templates/podkop.pot b/luci-app-podkop/po/templates/podkop.pot index 0bf57c8..542bfbe 100644 --- a/luci-app-podkop/po/templates/podkop.pot +++ b/luci-app-podkop/po/templates/podkop.pot @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: PODKOP\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-11-06 14:19+0200\n" -"PO-Revision-Date: 2025-11-06 14:19+0200\n" +"POT-Creation-Date: 2025-12-01 14:30+0200\n" +"PO-Revision-Date: 2025-12-01 14:30+0200\n" "Last-Translator: divocat \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -16,23 +16,23 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: src/podkop/tabs/dashboard/initController.ts:342 +#: src/podkop/tabs/dashboard/initController.ts:345 msgid "✔ Enabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:353 +#: src/podkop/tabs/dashboard/initController.ts:356 msgid "✔ Running" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:343 +#: src/podkop/tabs/dashboard/initController.ts:346 msgid "✘ Disabled" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:354 +#: src/podkop/tabs/dashboard/initController.ts:357 msgid "✘ Stopped" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:304 +#: src/podkop/tabs/dashboard/initController.ts:307 msgid "Active Connections" msgstr "" @@ -236,8 +236,8 @@ msgstr "" msgid "Dont Touch My DHCP!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:238 -#: src/podkop/tabs/dashboard/initController.ts:272 +#: src/podkop/tabs/dashboard/initController.ts:241 +#: src/podkop/tabs/dashboard/initController.ts:275 msgid "Downlink" msgstr "" @@ -390,6 +390,58 @@ msgstr "" msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y" msgstr "" +#: src/validators/validateHysteriaUrl.ts:76 +msgid "Invalid HY2 URL: insecure must be 0 or 1" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:62 +msgid "Invalid HY2 URL: invalid port number" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:32 +msgid "Invalid HY2 URL: missing credentials/server" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:49 +msgid "Invalid HY2 URL: missing host" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:43 +msgid "Invalid HY2 URL: missing host & port" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:38 +msgid "Invalid HY2 URL: missing password" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:53 +msgid "Invalid HY2 URL: missing port" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:19 +msgid "Invalid HY2 URL: must not contain spaces" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:12 +msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:99 +msgid "Invalid HY2 URL: obfs-password required when obfs is set" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:113 +msgid "Invalid HY2 URL: parsing failed" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:106 +msgid "Invalid HY2 URL: sni cannot be empty" +msgstr "" + +#: src/validators/validateHysteriaUrl.ts:88 +msgid "Invalid HY2 URL: unsupported obfs type" +msgstr "" + #: src/validators/validateIp.ts:11 msgid "Invalid IP address" msgstr "" @@ -527,7 +579,7 @@ msgstr "" msgid "Main DNS" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:308 +#: src/podkop/tabs/dashboard/initController.ts:311 msgid "Memory Usage" msgstr "" @@ -613,7 +665,7 @@ msgstr "" msgid "Pending" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:340 +#: src/podkop/tabs/dashboard/initController.ts:343 msgid "Podkop" msgstr "" @@ -766,7 +818,7 @@ msgstr "" msgid "Select the WAN interfaces to be monitored" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:337 +#: src/podkop/tabs/dashboard/initController.ts:340 msgid "Services info" msgstr "" @@ -779,7 +831,7 @@ msgstr "" msgid "Show sing-box config" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:351 +#: src/podkop/tabs/dashboard/initController.ts:354 msgid "Sing-box" msgstr "" @@ -844,7 +896,7 @@ msgstr "" msgid "Successfully copied!" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:301 +#: src/podkop/tabs/dashboard/initController.ts:304 msgid "System info" msgstr "" @@ -861,11 +913,8 @@ msgid "Test latency" msgstr "" #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:368 -msgid "Text List" -msgstr "" - #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js:448 -msgid "Text List (comma/space/newline separated)" +msgid "Text List" msgstr "" #: ../luci-app-podkop/htdocs/luci-static/resources/view/podkop/settings.js:46 @@ -888,11 +937,11 @@ msgstr "" msgid "Time in seconds for DNS record caching (default: 60)" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:235 +#: src/podkop/tabs/dashboard/initController.ts:238 msgid "Traffic" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:265 +#: src/podkop/tabs/dashboard/initController.ts:268 msgid "Traffic Total" msgstr "" @@ -931,13 +980,13 @@ msgstr "" msgid "Unknown error" msgstr "" -#: src/podkop/tabs/dashboard/initController.ts:237 -#: src/podkop/tabs/dashboard/initController.ts:268 +#: src/podkop/tabs/dashboard/initController.ts:240 +#: src/podkop/tabs/dashboard/initController.ts:271 msgid "Uplink" msgstr "" -#: src/validators/validateProxyUrl.ts:29 -msgid "URL must start with vless://, ss://, trojan://, or socks4/5://" +#: src/validators/validateProxyUrl.ts:37 +msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://" msgstr "" #: src/validators/validateUrl.ts:17 @@ -992,6 +1041,7 @@ msgstr "" #: src/validators/validateDns.ts:18 #: src/validators/validateDomain.ts:13 #: src/validators/validateDomain.ts:30 +#: src/validators/validateHysteriaUrl.ts:111 #: src/validators/validateIp.ts:8 #: src/validators/validateOutboundJson.ts:7 #: src/validators/validatePath.ts:16 diff --git a/podkop/files/usr/lib/helpers.sh b/podkop/files/usr/lib/helpers.sh index 93dcf6d..c25edb8 100644 --- a/podkop/files/usr/lib/helpers.sh +++ b/podkop/files/usr/lib/helpers.sh @@ -125,6 +125,12 @@ url_decode() { printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')" } +# Returns the scheme (protocol) part of a URL +url_get_scheme() { + local url="$1" + echo "${url%%://*}" +} + # Extracts the userinfo (username[:password]) part from a URL url_get_userinfo() { local url="$1" @@ -134,13 +140,23 @@ url_get_userinfo() { # Extracts the host part from a URL url_get_host() { local url="$1" - echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p' + + url="${url#*://}" + url="${url#*@}" + url="${url%%[/?#]*}" + + echo "${url%%:*}" } # Extracts the port number from a URL url_get_port() { local url="$1" - echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p' + + url="${url#*://}" + url="${url#*@}" + url="${url%%[/?#]*}" + + [[ "$url" == *:* ]] && echo "${url#*:}" || echo "" } # Extracts the path from a URL (without query or fragment; returns "/" if empty) diff --git a/podkop/files/usr/lib/sing_box_config_facade.sh b/podkop/files/usr/lib/sing_box_config_facade.sh index 4686332..6887e20 100644 --- a/podkop/files/usr/lib/sing_box_config_facade.sh +++ b/podkop/files/usr/lib/sing_box_config_facade.sh @@ -64,7 +64,8 @@ sing_box_cf_add_proxy_outbound() { url=$(url_decode "$url") url=$(url_strip_fragment "$url") - local scheme="${url%%://*}" + local scheme + scheme="$(url_get_scheme "$url")" case "$scheme" in socks4 | socks4a | socks5) local tag host port version userinfo username password udp_over_tcp @@ -146,6 +147,21 @@ sing_box_cf_add_proxy_outbound() { config=$(_add_outbound_security "$config" "$tag" "$url") config=$(_add_outbound_transport "$config" "$tag" "$url") ;; + hysteria2 | hy2) + local tag host port password obfuscator_type obfuscator_password upload_mbps download_mbps + tag=$(get_outbound_tag_by_section "$section") + host=$(url_get_host "$url") + port="$(url_get_port "$url")" + password=$(url_get_userinfo "$url") + obfuscator_type=$(url_get_query_param "$url" "obfs") + obfuscator_password=$(url_get_query_param "$url" "obfs-password") + upload_mbps=$(url_get_query_param "$url" "upmbps") + download_mbps=$(url_get_query_param "$url" "downmbps") + + config=$(sing_box_cm_add_hysteria2_outbound "$config" "$tag" "$host" "$port" "$password" "$obfuscator_type" \ + "$obfuscator_password" "$upload_mbps" "$download_mbps") + config=$(_add_outbound_security "$config" "$tag" "$url") + ;; *) log "Unsupported proxy $scheme type. Aborted." "fatal" exit 1 @@ -160,13 +176,20 @@ _add_outbound_security() { local outbound_tag="$2" local url="$3" - local security + local security scheme security=$(url_get_query_param "$url" "security") + if [ -z "$security" ]; then + scheme="$(url_get_scheme "$url")" + if [ "$scheme" = "hysteria2" ] || [ "$scheme" = "hy2" ]; then + security="tls" + fi + fi + case "$security" in tls | reality) local sni insecure alpn fingerprint public_key short_id sni=$(url_get_query_param "$url" "sni") - insecure=$(url_get_query_param "$url" "allowInsecure") + insecure=$(_get_insecure_query_param_from_url "$url") alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")") fingerprint=$(url_get_query_param "$url" "fp") public_key=$(url_get_query_param "$url" "pbk") @@ -193,6 +216,18 @@ _add_outbound_security() { echo "$config" } +_get_insecure_query_param_from_url() { + local url="$1" + + local insecure + insecure=$(url_get_query_param "$url" "allowInsecure") + if [ -z "$insecure" ]; then + insecure=$(url_get_query_param "$url" "insecure") + fi + + echo "$insecure" +} + _add_outbound_transport() { local config="$1" local outbound_tag="$2" diff --git a/podkop/files/usr/lib/sing_box_config_manager.sh b/podkop/files/usr/lib/sing_box_config_manager.sh index 9a575d4..9a0af62 100644 --- a/podkop/files/usr/lib/sing_box_config_manager.sh +++ b/podkop/files/usr/lib/sing_box_config_manager.sh @@ -21,9 +21,9 @@ SERVICE_TAG="__service_tag" ####################################### # Configure the logging section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string, JSON configuration # disabled: boolean, true to disable logging -# level: string, e.g., "info", "debug", "warn" +# level: string, log level. One of: trace debug info warn error fatal panic. # timestamp: boolean, true to include timestamps # Outputs: # Writes updated JSON configuration to stdout @@ -50,7 +50,7 @@ sing_box_cm_configure_log() { ####################################### # Configure the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # final: string, default dns server tag # strategy: string, default domain strategy for resolving the domain names # independent_cache: boolean, whether to use an independent DNS cache @@ -82,12 +82,12 @@ sing_box_cm_configure_dns() { ####################################### # Add a UDP DNS server to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the DNS server # server_address: string, IP address or hostname of the DNS server -# server_port: string or number, port of the DNS server -# domain_resolver: string, domain resolver to use for resolving domain names -# detour: string, tag of the upstream outbound +# server_port: string or integer, port of the DNS server +# domain_resolver: string, domain resolver to use for resolving domain names (optional) +# detour: string, tag of the upstream outbound (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -122,12 +122,12 @@ sing_box_cm_add_udp_dns_server() { ####################################### # Add a TLS DNS server to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the DNS server # server_address: string, IP address or hostname of the DNS server -# server_port: string or number, port of the DNS server -# domain_resolver: string, domain resolver to use for resolving domain names -# detour: string, tag of the upstream outbound +# server_port: string or integer, port of the DNS server +# domain_resolver: string, domain resolver to use for resolving domain names (optional) +# detour: string, tag of the upstream outbound (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -162,14 +162,14 @@ sing_box_cm_add_tls_dns_server() { ####################################### # Add an HTTPS DNS server to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the DNS server # server_address: string, IP address or hostname of the DNS server -# server_port: string or number, port of the DNS server -# path: string, optional URL path for HTTPS DNS requests -# headers: string, optional additional headers for HTTPS DNS requests -# domain_resolver: string, domain resolver to use for resolving domain names -# detour: string, tag of the upstream outbound +# server_port: string or integer, port of the DNS server +# path: string, URL path for HTTPS DNS requests (optional) +# headers: string, additional headers for HTTPS DNS requests (optional) +# domain_resolver: string, domain resolver to use for resolving domain names (optional) +# detour: string, tag of the upstream outbound (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -210,7 +210,7 @@ sing_box_cm_add_https_dns_server() { ####################################### # Add a FakeIP DNS server to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the DNS server # inet4_range: string, IPv4 range used for fake IP mapping # Outputs: @@ -236,7 +236,7 @@ sing_box_cm_add_fakeip_dns_server() { ####################################### # Add a DNS routing rule to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # server: string, target DNS server for the rule # tag: string, identifier for the route rule # Outputs: @@ -263,10 +263,10 @@ sing_box_cm_add_dns_route_rule() { ####################################### # Patch a DNS routing rule in the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the rule to patch # key: string, the key in the rule to update or add -# value: JSON value to assign to the key +# value: string, JSON value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -304,9 +304,9 @@ sing_box_cm_patch_dns_route_rule() { ####################################### # Add a DNS reject rule to the DNS section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # key: string, the key to set for the reject rule -# value: JSON value to assign to the key +# value: string, JSON value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -331,10 +331,10 @@ sing_box_cm_add_dns_reject_rule() { ####################################### # Add a TProxy inbound to the inbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the inbound # listen_address: string, IP address to listen on -# listen_port: number, port to listen on +# listen_port: integer, port to listen on # tcp_fast_open: boolean, enable or disable TCP Fast Open # udp_fragment: boolean, enable or disable UDP fragmentation # Outputs: @@ -369,10 +369,10 @@ sing_box_cm_add_tproxy_inbound() { ####################################### # Add a Direct inbound to the inbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the inbound # listen_address: string, IP address to listen on -# listen_port: number, port to listen on +# listen_port: integer, port to listen on # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -399,10 +399,10 @@ sing_box_cm_add_direct_inbound() { ####################################### # Add a Mixed inbound to the inbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the inbound # listen_address: string, IP address to listen on -# listen_port: number, port to listen on +# listen_port: integer, port to listen on # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -429,7 +429,7 @@ sing_box_cm_add_mixed_inbound() { ####################################### # Add a Direct outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # Outputs: # Writes updated JSON configuration to stdout @@ -451,15 +451,15 @@ sing_box_cm_add_direct_outbound() { ####################################### # Add a SOCKS outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # server_address: string, IP address or hostname of the SOCKS server -# server_port: number, port of the SOCKS server -# version: string, optional SOCKS version -# username: string, optional username for authentication -# password: string, optional password for authentication -# network: string, optional network type (e.g., "tcp") -# udp_over_tcp: number, optional version for UDP over TCP +# server_port: integer, port of the SOCKS server +# version: string, SOCKS version (optional) +# username: string, username for authentication (optional) +# password: string, password for authentication (optional) +# network: string, network type (e.g., "tcp") (optional) +# udp_over_tcp: integer, version for UDP over TCP (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -509,16 +509,16 @@ sing_box_cm_add_socks_outbound() { ####################################### # Add a Shadowsocks outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # server_address: string, IP address or hostname of the Shadowsocks server -# server_port: number, port of the Shadowsocks server +# server_port: integer, port of the Shadowsocks server # method: string, encryption method # password: string, password for encryption -# network: string, optional network type (e.g., "tcp") -# udp_over_tcp: number, optional version for UDP over TCP -# plugin: string, optional plugin name -# plugin_opts: string, optional plugin options +# network: string, network type (e.g., "tcp") (optional) +# udp_over_tcp: integer, version for UDP over TCP (optional) +# plugin: string, plugin name (optional) +# plugin_opts: string, plugin options (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -573,14 +573,14 @@ sing_box_cm_add_shadowsocks_outbound() { ####################################### # Add a VLESS outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # server_address: string, IP address or hostname of the VLESS server -# server_port: number, port of the VLESS server +# server_port: integer, port of the VLESS server # uuid: string, user UUID -# flow: string, optional flow setting -# network: string, optional network type (e.g., "tcp") -# packet_encoding: string, optional packet encoding method +# flow: string, flow setting (optional) +# network: string, network type (e.g., "tcp") (optional) +# packet_encoding: string, packet encoding method (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -624,12 +624,12 @@ sing_box_cm_add_vless_outbound() { ####################################### # Add a Trojan outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: string, JSON configuration +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # server_address: string, IP address or hostname of the Trojan server -# server_port: number, port of the Trojan server +# server_port: integer, port of the Trojan server # password: string, password for authentication -# network: string, optional network type (e.g., "tcp") +# network: string, network type (e.g., "tcp") (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -661,15 +661,76 @@ sing_box_cm_add_trojan_outbound() { )]' } +####################################### +# Add a Hysteria2 outbound to the outbounds section of a sing-box JSON configuration. +# Arguments: +# config: string (JSON), sing-box configuration to modify +# tag: string, identifier for the outbound +# server_address: string, IP address or hostname of the Hysteria2 server +# server_port: integer, port of the Hysteria2 server +# password: string, password for authentication +# obfuscator_type: string, obfuscation type (optional) +# obfuscator_password: string, obfuscation password (optional) +# upload_mbps: integer, upload bandwidth limit in Mbps (optional) +# download_mbps: integer, download bandwidth limit in Mbps (optional) +# network: string, network type (e.g., "udp") (optional) +# Outputs: +# Writes updated JSON configuration to stdout +# Example: +# CONFIG=$(sing_box_cm_add_hysteria2_outbound "$CONFIG" "hysteria2-out" "example.com" 443 "supersecret" \ +# "salamander" "obfs-pass" "50" "200" "udp") +####################################### +sing_box_cm_add_hysteria2_outbound() { + local config="$1" + local tag="$2" + local server_address="$3" + local server_port="$4" + local password="$5" + local obfuscator_type="$6" + local obfuscator_password="$7" + local upload_mbps="$8" + local download_mbps="$9" + local network="${10}" + + echo "$config" | jq \ + --arg tag "$tag" \ + --arg server_address "$server_address" \ + --arg server_port "$server_port" \ + --arg password "$password" \ + --arg obfuscator_type "$obfuscator_type" \ + --arg obfuscator_password "$obfuscator_password" \ + --arg upload_mbps "$upload_mbps" \ + --arg download_mbps "$download_mbps" \ + --arg network "$network" \ + '.outbounds += [( + { + type: "hysteria2", + tag: $tag, + server: $server_address, + server_port: ($server_port | tonumber), + password: $password + } + + (if $obfuscator_type != "" and $obfuscator_password != "" then { + obfs: { + type: $obfuscator_type, + password: $obfuscator_password + } + } else {} end) + + (if $upload_mbps != "" then {up_mbps: ($upload_mbps | tonumber)} else {} end) + + (if $download_mbps != "" then {down_mbps: ($download_mbps | tonumber)} else {} end) + + (if $network != "" then {network: $network} else {} end) + )]' +} + ####################################### # Set gRPC transport settings for an outbound in a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the outbound to modify -# service_name: string, optional gRPC service name -# idle_timeout: string or number, optional idle timeout -# ping_timeout: string or number, optional ping timeout -# permit_without_stream: boolean, optional flag for permitting without stream +# service_name: string, gRPC service name (optional) +# idle_timeout: string or integer, idle timeout (optional) +# ping_timeout: string or integer, ping timeout (optional) +# permit_without_stream: boolean, flag for permitting without stream (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -709,12 +770,12 @@ sing_box_cm_set_grpc_transport_for_outbound() { ####################################### # Set WebSocket transport settings for an outbound in a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the outbound to modify # path: string, WebSocket path -# host: string, optional Host header for WebSocket -# max_early_data: number, optional maximum early data -# early_data_header_name: string, optional header name for early data +# host: string, Host header for WebSocket (optional) +# max_early_data: integer, maximum early data (optional) +# early_data_header_name: string, header name for early data (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -759,14 +820,14 @@ sing_box_cm_set_ws_transport_for_outbound() { ####################################### # Set TLS settings for an outbound in a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the outbound to modify -# server_name: string, optional, used to verify the hostname on the returned certificates -# insecure: boolean, accept any server certificate -# alpn: JSON value or null, optional supported application level protocols -# utls_fingerprint: string, optional uTLS fingerprint -# reality_public_key: string, optional Reality public key -# reality_short_id: string, optional Reality short ID +# server_name: string, used to verify the hostname on the returned certificates (optional) +# insecure: boolean, accept any server certificate (optional) +# alpn: string, JSON value, supported application level protocols (optional) +# utls_fingerprint: string, uTLS fingerprint (optional) +# reality_public_key: string, Reality public key (optional) +# reality_short_id: string, Reality short ID (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -825,7 +886,7 @@ sing_box_cm_set_tls_for_outbound() { ####################################### # Add a Direct outbound with a specific network interface to a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound # interface: string, network interface to bind the outbound # domain_resolver: string, tag of the domain resolver to be used for this outbound (optional) @@ -857,9 +918,9 @@ sing_box_cm_add_interface_outbound() { ####################################### # Add a raw outbound JSON object to the outbounds section of a sing-box configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the outbound -# raw_outbound: JSON object, the raw outbound configuration to add +# raw_outbound: string, JSON object, the raw outbound configuration to add # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -881,14 +942,14 @@ sing_box_cm_add_raw_outbound() { ####################################### # Add a URLTest outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the URLTest outbound -# outbounds: JSON array of outbound tags to test -# url: URL to probe (optional) -# interval: test interval (e.g., "10s") (optional) -# tolerance: max latency difference tolerated (optional) -# idle_timeout: idle timeout duration (optional) -# interrupt_exist_connections: flag to interrupt existing connections ("true"/"false") (optional) +# outbounds: string, JSON array of outbound tags to test +# url: string, URL to probe (optional) +# interval: string, test interval (e.g., "10s") (optional) +# tolerance: string or integer, max latency difference tolerated (optional) +# idle_timeout: string or integer, idle timeout duration (optional) +# interrupt_exist_connections: boolean, flag to interrupt existing connections ("true"/"false") (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -929,11 +990,11 @@ sing_box_cm_add_urltest_outbound() { ####################################### # Add a Selector outbound to the outbounds section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the Selector outbound -# outbounds: JSON array of outbound tags to choose from -# default: default outbound tag if none selected (optional) -# interrupt_exist_connections: flag to interrupt existing connections ("true"/"false") (optional) +# outbounds: string (JSON), array of outbound tags to choose from +# default: string, default outbound tag if none selected +# interrupt_exist_connections: boolean, flag to interrupt existing connections ("true"/"false") (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -965,11 +1026,11 @@ sing_box_cm_add_selector_outbound() { ####################################### # Configure the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # final: string, final outbound tag for unmatched traffic # auto_detect_interface: boolean, enable or disable automatic interface detection # default_domain_resolver: string, default DNS resolver for domain-based routing -# default_interface: string, default network interface to use when auto detection is disabled +# default_interface: string, default network interface to use when auto detection is disabled (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1001,7 +1062,7 @@ sing_box_cm_configure_route() { ####################################### # Add a routing rule to the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the route rule # inbound: string, inbound tag to match # outbound: string, outbound tag to route matched traffic to @@ -1032,10 +1093,10 @@ sing_box_cm_add_route_rule() { ####################################### # Patch a routing rule in the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the route rule to patch # key: string, the key in the rule to update or add -# value: JSON value to assign to the key +# value: string (JSON), value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1071,9 +1132,9 @@ sing_box_cm_patch_route_rule() { ####################################### # Add a reject rule to the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # key: string, the key to set for the reject rule -# value: JSON value to assign to the key +# value: string (JSON), value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1098,9 +1159,9 @@ sing_box_cm_add_reject_route_rule() { ####################################### # Add a hijack-dns rule to the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # key: string, the key to set for the hijack-dns rule -# value: JSON value to assign to the key +# value: string (JSON), value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1125,7 +1186,7 @@ sing_box_cm_add_hijack_dns_route_rule() { ####################################### # Add a route-options rule to the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the route-options rule # Outputs: # Writes updated JSON configuration to stdout @@ -1148,9 +1209,9 @@ sing_box_cm_add_options_route_rule() { ####################################### # Add a sniff rule to the route section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # key: string, the key to set for the sniff rule -# value: JSON value to assign to the key +# value: string (JSON), value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1176,7 +1237,7 @@ sing_box_cm_sniff_route_rule() { ####################################### # Add an inline ruleset to the route.rule_set section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the inline ruleset # Outputs: # Writes updated JSON configuration to stdout @@ -1198,10 +1259,10 @@ sing_box_cm_add_inline_ruleset() { ####################################### # Add or update a rule in an inline ruleset within the route.rule_set section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier of the inline ruleset # key: string, the key in the ruleset to update or add -# value: JSON value to assign to the key +# value: string (JSON), value to assign to the key # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1238,7 +1299,7 @@ sing_box_cm_add_inline_ruleset_rule() { ####################################### # Add a local ruleset to the route.rule_set section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the local ruleset # format: string, format of the local ruleset ("source" or "binary") # path: string, file path to the local ruleset @@ -1269,12 +1330,12 @@ sing_box_cm_add_local_ruleset() { ####################################### # Add a remote ruleset to the route.rule_set section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # tag: string, identifier for the remote ruleset # format: string, format of the remote ruleset ("source" or "binary") # url: string, URL to download the ruleset from -# download_detour: string, optional detour tag for downloading -# update_interval: string, optional update interval for the ruleset +# download_detour: string, detour tag for downloading (optional) +# update_interval: string, update interval for the ruleset (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1310,7 +1371,7 @@ sing_box_cm_add_remote_ruleset() { ####################################### # Configure the experimental cache_file section of a sing-box JSON configuration. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # enabled: boolean, enable or disable file caching # path: string, file path for cache storage # store_fakeip: boolean, whether to store fake IPs in the cache @@ -1339,10 +1400,10 @@ sing_box_cm_configure_cache_file() { ####################################### # Configure the experimental clash_api section of a sing-box JSON configuration. # Arguments: -# config: string, JSON configuration +# config: string (JSON), sing-box configuration to modify # external_controller: string, API listening address; Clash API will be disabled if empty -# external_ui: string, Optional path to static web resources to serve at http://{{external-controller}}/ui -# secret: string, Optional secret for the RESTful API Authenticate by specifying HTTP header +# external_ui: string, path to static web resources to serve at http://{{external-controller}}/ui (optional) +# secret: string, secret for the RESTful API Authenticate by specifying HTTP header (optional) # Outputs: # Writes updated JSON configuration to stdout # Example: @@ -1368,7 +1429,7 @@ sing_box_cm_configure_clash_api() { ####################################### # Save a sing-box JSON configuration to a file, removing service-specific tags. # Arguments: -# config: JSON configuration (string) +# config: string (JSON), sing-box configuration to modify # file_path: string, path to save the configuration file # Outputs: # Writes the cleaned JSON configuration to the specified file