From 9d89258c0c827ddffa617c01deb5e3388ae73b76 Mon Sep 17 00:00:00 2001 From: divocat Date: Thu, 9 Oct 2025 17:31:10 +0300 Subject: [PATCH 1/4] fix: correct vless/trojan validation on some browsers --- .../src/validators/validateTrojanUrl.ts | 31 +++++++------- .../src/validators/validateVlessUrl.ts | 15 +++---- .../luci-static/resources/view/podkop/main.js | 42 ++++++++++--------- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/fe-app-podkop/src/validators/validateTrojanUrl.ts b/fe-app-podkop/src/validators/validateTrojanUrl.ts index 8e9e627..811d76c 100644 --- a/fe-app-podkop/src/validators/validateTrojanUrl.ts +++ b/fe-app-podkop/src/validators/validateTrojanUrl.ts @@ -2,22 +2,23 @@ import { ValidationResult } from './types'; // TODO refactor current validation and add tests export function validateTrojanUrl(url: string): ValidationResult { - if (!url.startsWith('trojan://')) { - return { - valid: false, - message: _('Invalid Trojan URL: must start with trojan://'), - }; - } - - if (!url || /\s/.test(url)) { - return { - valid: false, - message: _('Invalid Trojan URL: must not contain spaces'), - }; - } - try { - const parsedUrl = new URL(url); + if (!url.startsWith('trojan://')) { + return { + valid: false, + message: _('Invalid Trojan URL: must start with trojan://'), + }; + } + + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _('Invalid Trojan URL: must not contain spaces'), + }; + } + + const refinedURL = url.replace('trojan://', 'https://'); + const parsedUrl = new URL(refinedURL); if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { return { diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index 73746e4..3956306 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -2,7 +2,12 @@ import { ValidationResult } from './types'; export function validateVlessUrl(url: string): ValidationResult { try { - const parsedUrl = new URL(url); + if (!url.startsWith('vless://')) { + return { + valid: false, + message: _('Invalid VLESS URL: must start with vless://'), + }; + } if (!url || /\s/.test(url)) { return { @@ -11,12 +16,8 @@ export function validateVlessUrl(url: string): ValidationResult { }; } - if (parsedUrl.protocol !== 'vless:') { - return { - valid: false, - message: _('Invalid VLESS URL: must start with vless://'), - }; - } + const refinedURL = url.replace('vless://', 'https://'); + const parsedUrl = new URL(refinedURL); if (!parsedUrl.username) { return { valid: false, message: _('Invalid VLESS URL: missing UUID') }; 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 31db43d..3018ad4 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 @@ -213,19 +213,20 @@ function validateShadowsocksUrl(url) { // src/validators/validateVlessUrl.ts function validateVlessUrl(url) { try { - const parsedUrl = new URL(url); + if (!url.startsWith("vless://")) { + return { + valid: false, + message: _("Invalid VLESS URL: must start with vless://") + }; + } if (!url || /\s/.test(url)) { return { valid: false, message: _("Invalid VLESS URL: must not contain spaces") }; } - if (parsedUrl.protocol !== "vless:") { - return { - valid: false, - message: _("Invalid VLESS URL: must start with vless://") - }; - } + const refinedURL = url.replace("vless://", "https://"); + const parsedUrl = new URL(refinedURL); if (!parsedUrl.username) { return { valid: false, message: _("Invalid VLESS URL: missing UUID") }; } @@ -324,20 +325,21 @@ function validateOutboundJson(value) { // src/validators/validateTrojanUrl.ts function validateTrojanUrl(url) { - if (!url.startsWith("trojan://")) { - return { - valid: false, - message: _("Invalid Trojan URL: must start with trojan://") - }; - } - if (!url || /\s/.test(url)) { - return { - valid: false, - message: _("Invalid Trojan URL: must not contain spaces") - }; - } try { - const parsedUrl = new URL(url); + if (!url.startsWith("trojan://")) { + return { + valid: false, + message: _("Invalid Trojan URL: must start with trojan://") + }; + } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _("Invalid Trojan URL: must not contain spaces") + }; + } + const refinedURL = url.replace("trojan://", "https://"); + const parsedUrl = new URL(refinedURL); if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { return { valid: false, From 9bc2b5ffef9f57c943f545c955d425d7a81722c3 Mon Sep 17 00:00:00 2001 From: divocat Date: Thu, 9 Oct 2025 18:17:43 +0300 Subject: [PATCH 2/4] fix: correct link validation & some points on dash --- .../src/podkop/methods/getPodkopStatus.ts | 2 +- .../src/podkop/methods/getSingboxStatus.ts | 2 +- .../tabs/dashboard/initDashboardController.ts | 47 +++-- .../podkop/tabs/dashboard/renderSections.ts | 6 +- .../tests/validateShadowsocksUrl.test.js | 49 +++++ .../tests/validateTrojanUrl.test.js | 131 +++++++++++++ .../src/validators/validateTrojanUrl.ts | 40 +++- .../src/validators/validateVlessUrl.ts | 117 ++++++----- .../luci-static/resources/view/podkop/main.js | 182 +++++++++++------- 9 files changed, 422 insertions(+), 154 deletions(-) create mode 100644 fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js create mode 100644 fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js diff --git a/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts b/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts index 9286dda..666c41e 100644 --- a/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts +++ b/fe-app-podkop/src/podkop/methods/getPodkopStatus.ts @@ -7,7 +7,7 @@ export async function getPodkopStatus(): Promise<{ const response = await executeShellCommand({ command: '/usr/bin/podkop', args: ['get_status'], - timeout: 1000, + timeout: 10000, }); if (response.stdout) { diff --git a/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts b/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts index d65221e..41735f5 100644 --- a/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts +++ b/fe-app-podkop/src/podkop/methods/getSingboxStatus.ts @@ -8,7 +8,7 @@ export async function getSingboxStatus(): Promise<{ const response = await executeShellCommand({ command: '/usr/bin/podkop', args: ['get_sing_box_status'], - timeout: 1000, + timeout: 10000, }); if (response.stdout) { diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts b/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts index f07a15c..d5c9526 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/initDashboardController.ts @@ -4,6 +4,7 @@ import { getSingboxStatus, } from '../../methods'; import { + getClashApiUrl, getClashWsUrl, onMount, preserveScrollForPage, @@ -33,6 +34,10 @@ async function fetchDashboardSections() { const { data, success } = await getDashboardSections(); + if (!success) { + console.log('[fetchDashboardSections]: failed to fetch', getClashApiUrl()); + } + store.set({ sectionsWidget: { latencyFetching: false, @@ -44,18 +49,30 @@ async function fetchDashboardSections() { } async function fetchServicesInfo() { - const [podkop, singbox] = await Promise.all([ - getPodkopStatus(), - getSingboxStatus(), - ]); + try { + const [podkop, singbox] = await Promise.all([ + getPodkopStatus(), + getSingboxStatus(), + ]); - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { singbox: singbox.running, podkop: podkop.enabled }, - }, - }); + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.running, podkop: podkop.enabled }, + }, + }); + } catch (err) { + console.log('[fetchServicesInfo]: failed to fetchServices', err); + + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, podkop: 0 }, + }, + }); + } } async function connectToClashSockets() { @@ -73,6 +90,10 @@ async function connectToClashSockets() { }); }, (_err) => { + console.log( + '[fetchDashboardSections]: failed to connect', + getClashWsUrl(), + ); store.set({ bandwidthWidget: { loading: false, @@ -108,6 +129,10 @@ async function connectToClashSockets() { }); }, (_err) => { + console.log( + '[fetchDashboardSections]: failed to connect', + getClashWsUrl(), + ); store.set({ trafficTotalWidget: { loading: false, diff --git a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts index 640a813..b8b77b0 100644 --- a/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts +++ b/fe-app-podkop/src/podkop/tabs/dashboard/renderSections.ts @@ -1,4 +1,5 @@ import { Podkop } from '../../types'; +import { getClashApiUrl } from '../../../helpers'; interface IRenderSectionsProps { loading: boolean; @@ -16,7 +17,10 @@ function renderFailedState() { class: 'pdk_dashboard-page__outbound-section centered', style: 'height: 127px', }, - E('span', {}, _('Dashboard currently unavailable')), + E('span', {}, [ + E('span', {}, _('Dashboard currently unavailable')), + E('div', { style: 'text-align: center;' }, `API: ${getClashApiUrl()}`), + ]), ); } diff --git a/fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js b/fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js new file mode 100644 index 0000000..039be2d --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateShadowsocksUrl.test.js @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest'; +import { validateShadowsocksUrl } from '../validateShadowsocksUrl'; + +const validUrls = [ + [ + 'no-client', + 'ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client', + ], + [ + 'client', + 'ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client', + ], + [ + 'plain-user', + 'ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user', + ], +]; + +const invalidUrls = [ + ['No prefix', 'uuid@127.0.0.1:443?type=tcp'], + ['No host', 'ss://password@:443?type=tcp'], + ['No port', 'ss://password@127.0.0.1?type=tcp'], + ['Invalid port', 'ss://password@127.0.0.1:abc?type=tcp'], + ['Missing type', 'ss://password@127.0.0.1:443'], + ['Contains space', 'ss://password@127.0.0.1:443?type=tcp #extra'], +]; + +describe('validateShadowsocksUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateShadowsocksUrl(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateShadowsocksUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range', () => { + const res = validateShadowsocksUrl( + 'ss://password@127.0.0.1:99999?type=tcp', + ); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js b/fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js new file mode 100644 index 0000000..7f1281d --- /dev/null +++ b/fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js @@ -0,0 +1,131 @@ +import { describe, it, expect } from 'vitest'; +import { validateTrojanUrl } from '../validateTrojanUrl'; + +const validUrls = [ + // TCP + [ + 'tcp + none', + 'trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none', + ], + [ + 'tcp + reality', + 'trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality', + ], + [ + 'tcp + tls', + 'trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls', + ], + [ + 'tcp + tls + insecure', + 'trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure', + ], + [ + 'tcp + tls + ech', + 'trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech', + ], + + // mKCP + [ + 'mKCP + none', + 'trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP', + ], + + // WebSocket + [ + 'ws + none', + 'trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none', + ], + [ + 'ws + tls', + 'trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls', + ], + [ + 'ws + tls + insecure', + 'trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecure', + ], + [ + 'ws + tls + ech', + 'trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech', + ], + + // gRPC + [ + 'grpc + none', + 'trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none', + ], + [ + 'grpc + reality', + 'trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality', + ], + [ + 'grpc + tls', + 'trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls', + ], + [ + 'grpc + tls + insecure', + 'trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure', + ], + [ + 'grpc + tls + ech', + 'trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech', + ], + + // HTTPUpgrade + [ + 'httpupgrade + none', + 'trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none', + ], + [ + 'httpupgrade + tls', + 'trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls', + ], + [ + 'httpupgrade + tls + insecure', + 'trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure', + ], + [ + 'httpupgrade + tls + ech', + 'trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech', + ], + + // XHTTP + [ + 'xhttp + none', + 'trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp', + ], +]; + +const invalidUrls = [ + ['No prefix', 'uuid@host:443?type=tcp&security=tls'], + ['No password', 'trojan://@127.0.0.1:443?type=tcp&security=tls'], + ['No host', 'trojan://pass@:443?type=tcp&security=tls'], + ['No port', 'trojan://pass@127.0.0.1?type=tcp&security=tls'], + ['Invalid port', 'trojan://pass@127.0.0.1:abc?type=tcp&security=tls'], + [ + 'tcp + reality + unexpected spaces', + 'trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni= google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality', + ], +]; + +describe('validateTrojanUrl', () => { + describe.each(validUrls)('Valid URL: %s', (_desc, url) => { + it(`returns valid=true for "${url}"`, () => { + const res = validateTrojanUrl(url); + expect(res.valid).toBe(true); + }); + }); + + describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => { + it(`returns valid=false for "${url}"`, () => { + const res = validateTrojanUrl(url); + expect(res.valid).toBe(false); + }); + }); + + it('detects invalid port range', () => { + const res = validateTrojanUrl( + 'trojan://pass@127.0.0.1:99999?type=tcp&security=tls', + ); + expect(res.valid).toBe(false); + }); +}); diff --git a/fe-app-podkop/src/validators/validateTrojanUrl.ts b/fe-app-podkop/src/validators/validateTrojanUrl.ts index 811d76c..052aef8 100644 --- a/fe-app-podkop/src/validators/validateTrojanUrl.ts +++ b/fe-app-podkop/src/validators/validateTrojanUrl.ts @@ -1,6 +1,5 @@ import { ValidationResult } from './types'; -// TODO refactor current validation and add tests export function validateTrojanUrl(url: string): ValidationResult { try { if (!url.startsWith('trojan://')) { @@ -17,17 +16,42 @@ export function validateTrojanUrl(url: string): ValidationResult { }; } - const refinedURL = url.replace('trojan://', 'https://'); - const parsedUrl = new URL(refinedURL); + const body = url.slice('trojan://'.length); + const [mainPart] = body.split('#'); + const [userHostPort] = mainPart.split('?'); - if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { + const [userPart, hostPortPart] = userHostPort.split('@'); + + if (!userHostPort) return { valid: false, - message: _( - 'Invalid Trojan URL: must contain username, hostname and port', - ), + message: 'Invalid Trojan URL: missing credentials and host', + }; + + if (!userPart) + return { valid: false, message: 'Invalid Trojan URL: missing password' }; + + if (!hostPortPart) + return { + valid: false, + message: 'Invalid Trojan URL: missing hostname and port', + }; + + const [host, port] = hostPortPart.split(':'); + + if (!host) + return { valid: false, message: 'Invalid Trojan URL: missing hostname' }; + + if (!port) + return { valid: false, message: 'Invalid Trojan URL: missing port' }; + + const portNum = Number(port); + + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: 'Invalid Trojan URL: invalid port number', }; - } } catch (_e) { return { valid: false, message: _('Invalid Trojan URL: parsing failed') }; } diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index 3956306..56c8ac4 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -2,58 +2,71 @@ import { ValidationResult } from './types'; export function validateVlessUrl(url: string): ValidationResult { try { - if (!url.startsWith('vless://')) { + if (!url.startsWith('vless://')) return { valid: false, - message: _('Invalid VLESS URL: must start with vless://'), + message: 'Invalid VLESS URL: must start with vless://', }; - } - if (!url || /\s/.test(url)) { + if (/\s/.test(url)) return { valid: false, - message: _('Invalid VLESS URL: must not contain spaces'), + message: 'Invalid VLESS URL: must not contain spaces', }; - } - const refinedURL = url.replace('vless://', 'https://'); - const parsedUrl = new URL(refinedURL); + const body = url.slice('vless://'.length); - if (!parsedUrl.username) { - return { valid: false, message: _('Invalid VLESS URL: missing UUID') }; - } + const [mainPart] = body.split('#'); - if (!parsedUrl.hostname) { - return { valid: false, message: _('Invalid VLESS URL: missing server') }; - } + const [userHostPort, queryString] = mainPart.split('?'); - if (!parsedUrl.port) { - return { valid: false, message: _('Invalid VLESS URL: missing port') }; - } - - if ( - isNaN(+parsedUrl.port) || - +parsedUrl.port < 1 || - +parsedUrl.port > 65535 - ) { + if (!userHostPort) return { valid: false, - message: _( - 'Invalid VLESS URL: invalid port number. Must be between 1 and 65535', - ), + message: 'Invalid VLESS URL: missing host and UUID', }; - } - if (!parsedUrl.search) { + const [userPart, hostPortPart] = userHostPort.split('@'); + + if (!userPart) + return { valid: false, message: 'Invalid VLESS URL: missing UUID' }; + + if (!hostPortPart) + return { valid: false, message: 'Invalid VLESS URL: missing server' }; + + const [host, port] = hostPortPart.split(':'); + + if (!host) + return { valid: false, message: 'Invalid VLESS URL: missing hostname' }; + + if (!port) + return { valid: false, message: 'Invalid VLESS URL: missing port' }; + + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) return { valid: false, - message: _('Invalid VLESS URL: missing query parameters'), + message: 'Invalid VLESS URL: invalid port number', }; - } - const params = new URLSearchParams(parsedUrl.search); + if (!queryString) + return { + valid: false, + message: 'Invalid VLESS URL: missing query parameters', + }; + + const params = queryString + .split('&') + .filter(Boolean) + .map((pair) => pair.split('=')) + .reduce( + (acc, [key, value = '']) => { + if (key) acc[key] = value; + return acc; + }, + {} as Record, + ); - const type = params.get('type'); const validTypes = [ 'tcp', 'raw', @@ -65,45 +78,31 @@ export function validateVlessUrl(url: string): ValidationResult { 'ws', 'kcp', ]; - - if (!type || !validTypes.includes(type)) { - return { - valid: false, - message: _( - 'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws', - ), - }; - } - - const security = params.get('security'); const validSecurities = ['tls', 'reality', 'none']; - if (!security || !validSecurities.includes(security)) { + if (!params.type || !validTypes.includes(params.type)) return { valid: false, - message: _( - 'Invalid VLESS URL: security must be one of tls, reality, none', - ), + message: 'Invalid VLESS URL: unsupported or missing type', }; - } - if (security === 'reality') { - if (!params.get('pbk')) { + if (!params.security || !validSecurities.includes(params.security)) + return { + valid: false, + message: 'Invalid VLESS URL: unsupported or missing security', + }; + + if (params.security === 'reality') { + if (!params.pbk) return { valid: false, - message: _( - 'Invalid VLESS URL: missing pbk parameter for reality security', - ), + message: 'Invalid VLESS URL: missing pbk for reality', }; - } - if (!params.get('fp')) { + if (!params.fp) return { valid: false, - message: _( - 'Invalid VLESS URL: missing fp parameter for reality security', - ), + message: 'Invalid VLESS URL: missing fp for reality', }; - } } return { valid: true, message: _('Valid') }; 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 3018ad4..d715c16 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 @@ -213,45 +213,52 @@ function validateShadowsocksUrl(url) { // src/validators/validateVlessUrl.ts function validateVlessUrl(url) { try { - if (!url.startsWith("vless://")) { + if (!url.startsWith("vless://")) return { valid: false, - message: _("Invalid VLESS URL: must start with vless://") + message: "Invalid VLESS URL: must start with vless://" }; - } - if (!url || /\s/.test(url)) { + if (/\s/.test(url)) return { valid: false, - message: _("Invalid VLESS URL: must not contain spaces") + message: "Invalid VLESS URL: must not contain spaces" }; - } - const refinedURL = url.replace("vless://", "https://"); - const parsedUrl = new URL(refinedURL); - if (!parsedUrl.username) { - return { valid: false, message: _("Invalid VLESS URL: missing UUID") }; - } - if (!parsedUrl.hostname) { - return { valid: false, message: _("Invalid VLESS URL: missing server") }; - } - if (!parsedUrl.port) { - return { valid: false, message: _("Invalid VLESS URL: missing port") }; - } - if (isNaN(+parsedUrl.port) || +parsedUrl.port < 1 || +parsedUrl.port > 65535) { + const body = url.slice("vless://".length); + const [mainPart] = body.split("#"); + const [userHostPort, queryString] = mainPart.split("?"); + if (!userHostPort) return { valid: false, - message: _( - "Invalid VLESS URL: invalid port number. Must be between 1 and 65535" - ) + message: "Invalid VLESS URL: missing host and UUID" }; - } - if (!parsedUrl.search) { + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userPart) + return { valid: false, message: "Invalid VLESS URL: missing UUID" }; + if (!hostPortPart) + return { valid: false, message: "Invalid VLESS URL: missing server" }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid VLESS URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid VLESS URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) return { valid: false, - message: _("Invalid VLESS URL: missing query parameters") + message: "Invalid VLESS URL: invalid port number" }; - } - const params = new URLSearchParams(parsedUrl.search); - const type = params.get("type"); + if (!queryString) + return { + valid: false, + message: "Invalid VLESS URL: missing query parameters" + }; + const params = queryString.split("&").filter(Boolean).map((pair) => pair.split("=")).reduce( + (acc, [key, value = ""]) => { + if (key) acc[key] = value; + return acc; + }, + {} + ); const validTypes = [ "tcp", "raw", @@ -263,41 +270,28 @@ function validateVlessUrl(url) { "ws", "kcp" ]; - if (!type || !validTypes.includes(type)) { - return { - valid: false, - message: _( - "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws" - ) - }; - } - const security = params.get("security"); const validSecurities = ["tls", "reality", "none"]; - if (!security || !validSecurities.includes(security)) { + if (!params.type || !validTypes.includes(params.type)) return { valid: false, - message: _( - "Invalid VLESS URL: security must be one of tls, reality, none" - ) + message: "Invalid VLESS URL: unsupported or missing type" }; - } - if (security === "reality") { - if (!params.get("pbk")) { + if (!params.security || !validSecurities.includes(params.security)) + return { + valid: false, + message: "Invalid VLESS URL: unsupported or missing security" + }; + if (params.security === "reality") { + if (!params.pbk) return { valid: false, - message: _( - "Invalid VLESS URL: missing pbk parameter for reality security" - ) + message: "Invalid VLESS URL: missing pbk for reality" }; - } - if (!params.get("fp")) { + if (!params.fp) return { valid: false, - message: _( - "Invalid VLESS URL: missing fp parameter for reality security" - ) + message: "Invalid VLESS URL: missing fp for reality" }; - } } return { valid: true, message: _("Valid") }; } catch (_e) { @@ -338,16 +332,33 @@ function validateTrojanUrl(url) { message: _("Invalid Trojan URL: must not contain spaces") }; } - const refinedURL = url.replace("trojan://", "https://"); - const parsedUrl = new URL(refinedURL); - if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) { + const body = url.slice("trojan://".length); + const [mainPart] = body.split("#"); + const [userHostPort] = mainPart.split("?"); + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userHostPort) return { valid: false, - message: _( - "Invalid Trojan URL: must contain username, hostname and port" - ) + message: "Invalid Trojan URL: missing credentials and host" + }; + if (!userPart) + return { valid: false, message: "Invalid Trojan URL: missing password" }; + if (!hostPortPart) + return { + valid: false, + message: "Invalid Trojan URL: missing hostname and port" + }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid Trojan URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid Trojan URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: "Invalid Trojan URL: invalid port number" }; - } } catch (_e) { return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; } @@ -1023,7 +1034,7 @@ async function getPodkopStatus() { const response = await executeShellCommand({ command: "/usr/bin/podkop", args: ["get_status"], - timeout: 1e3 + timeout: 1e4 }); if (response.stdout) { return JSON.parse(response.stdout.replace(/\n/g, "")); @@ -1036,7 +1047,7 @@ async function getSingboxStatus() { const response = await executeShellCommand({ command: "/usr/bin/podkop", args: ["get_sing_box_status"], - timeout: 1e3 + timeout: 1e4 }); if (response.stdout) { return JSON.parse(response.stdout.replace(/\n/g, "")); @@ -1248,7 +1259,10 @@ function renderFailedState() { class: "pdk_dashboard-page__outbound-section centered", style: "height: 127px" }, - E("span", {}, _("Dashboard currently unavailable")) + E("span", {}, [ + E("span", {}, _("Dashboard currently unavailable")), + E("div", { style: "text-align: center;" }, `API: ${getClashApiUrl()}`) + ]) ); } function renderLoadingState() { @@ -1585,6 +1599,9 @@ async function fetchDashboardSections() { } }); const { data, success } = await getDashboardSections(); + if (!success) { + console.log("[fetchDashboardSections]: failed to fetch", getClashApiUrl()); + } store.set({ sectionsWidget: { latencyFetching: false, @@ -1595,17 +1612,28 @@ async function fetchDashboardSections() { }); } async function fetchServicesInfo() { - const [podkop, singbox] = await Promise.all([ - getPodkopStatus(), - getSingboxStatus() - ]); - store.set({ - servicesInfoWidget: { - loading: false, - failed: false, - data: { singbox: singbox.running, podkop: podkop.enabled } - } - }); + try { + const [podkop, singbox] = await Promise.all([ + getPodkopStatus(), + getSingboxStatus() + ]); + store.set({ + servicesInfoWidget: { + loading: false, + failed: false, + data: { singbox: singbox.running, podkop: podkop.enabled } + } + }); + } catch (err) { + console.log("[fetchServicesInfo]: failed to fetchServices", err); + store.set({ + servicesInfoWidget: { + loading: false, + failed: true, + data: { singbox: 0, podkop: 0 } + } + }); + } } async function connectToClashSockets() { socket.subscribe( @@ -1621,6 +1649,10 @@ async function connectToClashSockets() { }); }, (_err) => { + console.log( + "[fetchDashboardSections]: failed to connect", + getClashWsUrl() + ); store.set({ bandwidthWidget: { loading: false, @@ -1654,6 +1686,10 @@ async function connectToClashSockets() { }); }, (_err) => { + console.log( + "[fetchDashboardSections]: failed to connect", + getClashWsUrl() + ); store.set({ trafficTotalWidget: { loading: false, From 715a278af8629920cabb42bb0c6f69e7b6d7bada Mon Sep 17 00:00:00 2001 From: divocat Date: Thu, 9 Oct 2025 18:20:48 +0300 Subject: [PATCH 3/4] fix: force http for yacd enable link --- fe-app-podkop/src/helpers/getClashApiUrl.ts | 6 ++++++ .../luci-static/resources/view/podkop/additionalTab.js | 2 +- .../htdocs/luci-static/resources/view/podkop/main.js | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/fe-app-podkop/src/helpers/getClashApiUrl.ts b/fe-app-podkop/src/helpers/getClashApiUrl.ts index dd4046d..27032a0 100644 --- a/fe-app-podkop/src/helpers/getClashApiUrl.ts +++ b/fe-app-podkop/src/helpers/getClashApiUrl.ts @@ -9,3 +9,9 @@ export function getClashWsUrl(): string { return `ws://${hostname}:9090`; } + +export function getClashUIUrl(): string { + const { hostname } = window.location; + + return `http://${hostname}:9090/ui`; +} diff --git a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js index d317048..f06ae90 100644 --- a/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js +++ b/luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js @@ -12,7 +12,7 @@ function createAdditionalSection(mainSection) { form.Flag, 'yacd', _('Yacd enable'), - `${main.getBaseUrl()}:9090/ui`, + `${main.getClashUIUrl()}`, ); o.default = '0'; o.rmempty = false; 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 d715c16..1f1c56d 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 @@ -784,6 +784,10 @@ function getClashWsUrl() { const { hostname } = window.location; return `ws://${hostname}:9090`; } +function getClashUIUrl() { + const { hostname } = window.location; + return `http://${hostname}:9090/ui`; +} // src/helpers/splitProxyString.ts function splitProxyString(str) { @@ -1966,6 +1970,7 @@ return baseclass.extend({ getClashConfig, getClashGroupDelay, getClashProxies, + getClashUIUrl, getClashVersion, getClashWsUrl, getConfigSections, From 0493565c5f84d005015f7f2328c738490eb5e2d3 Mon Sep 17 00:00:00 2001 From: divocat Date: Fri, 10 Oct 2025 14:06:19 +0300 Subject: [PATCH 4/4] fix: implement query params parsing func --- fe-app-podkop/src/helpers/index.ts | 1 + fe-app-podkop/src/helpers/parseQueryString.ts | 22 ++ .../src/validators/validateVlessUrl.ts | 13 +- .../luci-static/resources/view/podkop/main.js | 356 +++++++++--------- 4 files changed, 209 insertions(+), 183 deletions(-) create mode 100644 fe-app-podkop/src/helpers/parseQueryString.ts diff --git a/fe-app-podkop/src/helpers/index.ts b/fe-app-podkop/src/helpers/index.ts index da7da80..9b48e5b 100644 --- a/fe-app-podkop/src/helpers/index.ts +++ b/fe-app-podkop/src/helpers/index.ts @@ -9,3 +9,4 @@ export * from './onMount'; export * from './getClashApiUrl'; export * from './splitProxyString'; export * from './preserveScrollForPage'; +export * from './parseQueryString'; diff --git a/fe-app-podkop/src/helpers/parseQueryString.ts b/fe-app-podkop/src/helpers/parseQueryString.ts new file mode 100644 index 0000000..5c4c0a0 --- /dev/null +++ b/fe-app-podkop/src/helpers/parseQueryString.ts @@ -0,0 +1,22 @@ +export function parseQueryString(query: string): Record { + const clean = query.startsWith('?') ? query.slice(1) : query; + + return clean + .split('&') + .filter(Boolean) + .reduce( + (acc, pair) => { + const [rawKey, rawValue = ''] = pair.split('='); + + if (!rawKey) { + return acc; + } + + const key = decodeURIComponent(rawKey); + const value = decodeURIComponent(rawValue); + + return { ...acc, [key]: value }; + }, + {} as Record, + ); +} diff --git a/fe-app-podkop/src/validators/validateVlessUrl.ts b/fe-app-podkop/src/validators/validateVlessUrl.ts index 56c8ac4..06a84ba 100644 --- a/fe-app-podkop/src/validators/validateVlessUrl.ts +++ b/fe-app-podkop/src/validators/validateVlessUrl.ts @@ -1,4 +1,5 @@ import { ValidationResult } from './types'; +import { parseQueryString } from '../helpers'; export function validateVlessUrl(url: string): ValidationResult { try { @@ -55,17 +56,7 @@ export function validateVlessUrl(url: string): ValidationResult { message: 'Invalid VLESS URL: missing query parameters', }; - const params = queryString - .split('&') - .filter(Boolean) - .map((pair) => pair.split('=')) - .reduce( - (acc, [key, value = '']) => { - if (key) acc[key] = value; - return acc; - }, - {} as Record, - ); + const params = parseQueryString(queryString); const validTypes = [ 'tcp', 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 1f1c56d..2f5f206 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 @@ -210,178 +210,6 @@ function validateShadowsocksUrl(url) { return { valid: true, message: _("Valid") }; } -// src/validators/validateVlessUrl.ts -function validateVlessUrl(url) { - try { - if (!url.startsWith("vless://")) - return { - valid: false, - message: "Invalid VLESS URL: must start with vless://" - }; - if (/\s/.test(url)) - return { - valid: false, - message: "Invalid VLESS URL: must not contain spaces" - }; - const body = url.slice("vless://".length); - const [mainPart] = body.split("#"); - const [userHostPort, queryString] = mainPart.split("?"); - if (!userHostPort) - return { - valid: false, - message: "Invalid VLESS URL: missing host and UUID" - }; - const [userPart, hostPortPart] = userHostPort.split("@"); - if (!userPart) - return { valid: false, message: "Invalid VLESS URL: missing UUID" }; - if (!hostPortPart) - return { valid: false, message: "Invalid VLESS URL: missing server" }; - const [host, port] = hostPortPart.split(":"); - if (!host) - return { valid: false, message: "Invalid VLESS URL: missing hostname" }; - if (!port) - return { valid: false, message: "Invalid VLESS URL: missing port" }; - const portNum = Number(port); - if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) - return { - valid: false, - message: "Invalid VLESS URL: invalid port number" - }; - if (!queryString) - return { - valid: false, - message: "Invalid VLESS URL: missing query parameters" - }; - const params = queryString.split("&").filter(Boolean).map((pair) => pair.split("=")).reduce( - (acc, [key, value = ""]) => { - if (key) acc[key] = value; - return acc; - }, - {} - ); - const validTypes = [ - "tcp", - "raw", - "udp", - "grpc", - "http", - "httpupgrade", - "xhttp", - "ws", - "kcp" - ]; - const validSecurities = ["tls", "reality", "none"]; - if (!params.type || !validTypes.includes(params.type)) - return { - valid: false, - message: "Invalid VLESS URL: unsupported or missing type" - }; - if (!params.security || !validSecurities.includes(params.security)) - return { - valid: false, - message: "Invalid VLESS URL: unsupported or missing security" - }; - if (params.security === "reality") { - if (!params.pbk) - return { - valid: false, - message: "Invalid VLESS URL: missing pbk for reality" - }; - if (!params.fp) - return { - valid: false, - message: "Invalid VLESS URL: missing fp for reality" - }; - } - return { valid: true, message: _("Valid") }; - } catch (_e) { - return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; - } -} - -// src/validators/validateOutboundJson.ts -function validateOutboundJson(value) { - try { - const parsed = JSON.parse(value); - if (!parsed.type || !parsed.server || !parsed.server_port) { - return { - valid: false, - message: _( - 'Outbound JSON must contain at least "type", "server" and "server_port" fields' - ) - }; - } - return { valid: true, message: _("Valid") }; - } catch { - return { valid: false, message: _("Invalid JSON format") }; - } -} - -// src/validators/validateTrojanUrl.ts -function validateTrojanUrl(url) { - try { - if (!url.startsWith("trojan://")) { - return { - valid: false, - message: _("Invalid Trojan URL: must start with trojan://") - }; - } - if (!url || /\s/.test(url)) { - return { - valid: false, - message: _("Invalid Trojan URL: must not contain spaces") - }; - } - const body = url.slice("trojan://".length); - const [mainPart] = body.split("#"); - const [userHostPort] = mainPart.split("?"); - const [userPart, hostPortPart] = userHostPort.split("@"); - if (!userHostPort) - return { - valid: false, - message: "Invalid Trojan URL: missing credentials and host" - }; - if (!userPart) - return { valid: false, message: "Invalid Trojan URL: missing password" }; - if (!hostPortPart) - return { - valid: false, - message: "Invalid Trojan URL: missing hostname and port" - }; - const [host, port] = hostPortPart.split(":"); - if (!host) - return { valid: false, message: "Invalid Trojan URL: missing hostname" }; - if (!port) - return { valid: false, message: "Invalid Trojan URL: missing port" }; - const portNum = Number(port); - if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) - return { - valid: false, - message: "Invalid Trojan URL: invalid port number" - }; - } catch (_e) { - return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; - } - return { valid: true, message: _("Valid") }; -} - -// src/validators/validateProxyUrl.ts -function validateProxyUrl(url) { - if (url.startsWith("ss://")) { - return validateShadowsocksUrl(url); - } - if (url.startsWith("vless://")) { - return validateVlessUrl(url); - } - if (url.startsWith("trojan://")) { - return validateTrojanUrl(url); - } - return { - valid: false, - message: _("URL must start with vless:// or ss:// or trojan://") - }; -} - // src/helpers/getBaseUrl.ts function getBaseUrl() { const { protocol, hostname } = window.location; @@ -803,6 +631,189 @@ function preserveScrollForPage(renderFn) { }); } +// src/helpers/parseQueryString.ts +function parseQueryString(query) { + const clean = query.startsWith("?") ? query.slice(1) : query; + return clean.split("&").filter(Boolean).reduce( + (acc, pair) => { + const [rawKey, rawValue = ""] = pair.split("="); + if (!rawKey) { + return acc; + } + const key = decodeURIComponent(rawKey); + const value = decodeURIComponent(rawValue); + return { ...acc, [key]: value }; + }, + {} + ); +} + +// src/validators/validateVlessUrl.ts +function validateVlessUrl(url) { + try { + if (!url.startsWith("vless://")) + return { + valid: false, + message: "Invalid VLESS URL: must start with vless://" + }; + if (/\s/.test(url)) + return { + valid: false, + message: "Invalid VLESS URL: must not contain spaces" + }; + const body = url.slice("vless://".length); + const [mainPart] = body.split("#"); + const [userHostPort, queryString] = mainPart.split("?"); + if (!userHostPort) + return { + valid: false, + message: "Invalid VLESS URL: missing host and UUID" + }; + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userPart) + return { valid: false, message: "Invalid VLESS URL: missing UUID" }; + if (!hostPortPart) + return { valid: false, message: "Invalid VLESS URL: missing server" }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid VLESS URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid VLESS URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: "Invalid VLESS URL: invalid port number" + }; + if (!queryString) + return { + valid: false, + message: "Invalid VLESS URL: missing query parameters" + }; + const params = parseQueryString(queryString); + const validTypes = [ + "tcp", + "raw", + "udp", + "grpc", + "http", + "httpupgrade", + "xhttp", + "ws", + "kcp" + ]; + const validSecurities = ["tls", "reality", "none"]; + if (!params.type || !validTypes.includes(params.type)) + return { + valid: false, + message: "Invalid VLESS URL: unsupported or missing type" + }; + if (!params.security || !validSecurities.includes(params.security)) + return { + valid: false, + message: "Invalid VLESS URL: unsupported or missing security" + }; + if (params.security === "reality") { + if (!params.pbk) + return { + valid: false, + message: "Invalid VLESS URL: missing pbk for reality" + }; + if (!params.fp) + return { + valid: false, + message: "Invalid VLESS URL: missing fp for reality" + }; + } + return { valid: true, message: _("Valid") }; + } catch (_e) { + return { valid: false, message: _("Invalid VLESS URL: parsing failed") }; + } +} + +// src/validators/validateOutboundJson.ts +function validateOutboundJson(value) { + try { + const parsed = JSON.parse(value); + if (!parsed.type || !parsed.server || !parsed.server_port) { + return { + valid: false, + message: _( + 'Outbound JSON must contain at least "type", "server" and "server_port" fields' + ) + }; + } + return { valid: true, message: _("Valid") }; + } catch { + return { valid: false, message: _("Invalid JSON format") }; + } +} + +// src/validators/validateTrojanUrl.ts +function validateTrojanUrl(url) { + try { + if (!url.startsWith("trojan://")) { + return { + valid: false, + message: _("Invalid Trojan URL: must start with trojan://") + }; + } + if (!url || /\s/.test(url)) { + return { + valid: false, + message: _("Invalid Trojan URL: must not contain spaces") + }; + } + const body = url.slice("trojan://".length); + const [mainPart] = body.split("#"); + const [userHostPort] = mainPart.split("?"); + const [userPart, hostPortPart] = userHostPort.split("@"); + if (!userHostPort) + return { + valid: false, + message: "Invalid Trojan URL: missing credentials and host" + }; + if (!userPart) + return { valid: false, message: "Invalid Trojan URL: missing password" }; + if (!hostPortPart) + return { + valid: false, + message: "Invalid Trojan URL: missing hostname and port" + }; + const [host, port] = hostPortPart.split(":"); + if (!host) + return { valid: false, message: "Invalid Trojan URL: missing hostname" }; + if (!port) + return { valid: false, message: "Invalid Trojan URL: missing port" }; + const portNum = Number(port); + if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) + return { + valid: false, + message: "Invalid Trojan URL: invalid port number" + }; + } catch (_e) { + return { valid: false, message: _("Invalid Trojan URL: parsing failed") }; + } + return { valid: true, message: _("Valid") }; +} + +// src/validators/validateProxyUrl.ts +function validateProxyUrl(url) { + if (url.startsWith("ss://")) { + return validateShadowsocksUrl(url); + } + if (url.startsWith("vless://")) { + return validateVlessUrl(url); + } + if (url.startsWith("trojan://")) { + return validateTrojanUrl(url); + } + return { + valid: false, + message: _("URL must start with vless:// or ss:// or trojan://") + }; +} + // src/clash/methods/createBaseApiRequest.ts async function createBaseApiRequest(fetchFn) { try { @@ -1982,6 +1993,7 @@ return baseclass.extend({ injectGlobalStyles, maskIP, onMount, + parseQueryString, parseValueList, preserveScrollForPage, renderDashboard,