mirror of
https://github.com/itdoginfo/podkop.git
synced 2026-01-27 04:40:37 +03:00
Merge pull request #212 from itdoginfo/fix/dashboard
fix: correct vless/trojan validation on some browsers
This commit is contained in:
@@ -9,3 +9,9 @@ export function getClashWsUrl(): string {
|
|||||||
|
|
||||||
return `ws://${hostname}:9090`;
|
return `ws://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getClashUIUrl(): string {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
|
||||||
|
return `http://${hostname}:9090/ui`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ export * from './onMount';
|
|||||||
export * from './getClashApiUrl';
|
export * from './getClashApiUrl';
|
||||||
export * from './splitProxyString';
|
export * from './splitProxyString';
|
||||||
export * from './preserveScrollForPage';
|
export * from './preserveScrollForPage';
|
||||||
|
export * from './parseQueryString';
|
||||||
|
|||||||
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
22
fe-app-podkop/src/helpers/parseQueryString.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export function parseQueryString(query: string): Record<string, string> {
|
||||||
|
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<string, string>,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export async function getPodkopStatus(): Promise<{
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: '/usr/bin/podkop',
|
command: '/usr/bin/podkop',
|
||||||
args: ['get_status'],
|
args: ['get_status'],
|
||||||
timeout: 1000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export async function getSingboxStatus(): Promise<{
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: '/usr/bin/podkop',
|
command: '/usr/bin/podkop',
|
||||||
args: ['get_sing_box_status'],
|
args: ['get_sing_box_status'],
|
||||||
timeout: 1000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
getSingboxStatus,
|
getSingboxStatus,
|
||||||
} from '../../methods';
|
} from '../../methods';
|
||||||
import {
|
import {
|
||||||
|
getClashApiUrl,
|
||||||
getClashWsUrl,
|
getClashWsUrl,
|
||||||
onMount,
|
onMount,
|
||||||
preserveScrollForPage,
|
preserveScrollForPage,
|
||||||
@@ -33,6 +34,10 @@ async function fetchDashboardSections() {
|
|||||||
|
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
console.log('[fetchDashboardSections]: failed to fetch', getClashApiUrl());
|
||||||
|
}
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
latencyFetching: false,
|
latencyFetching: false,
|
||||||
@@ -44,18 +49,30 @@ async function fetchDashboardSections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const [podkop, singbox] = await Promise.all([
|
try {
|
||||||
getPodkopStatus(),
|
const [podkop, singbox] = await Promise.all([
|
||||||
getSingboxStatus(),
|
getPodkopStatus(),
|
||||||
]);
|
getSingboxStatus(),
|
||||||
|
]);
|
||||||
|
|
||||||
store.set({
|
store.set({
|
||||||
servicesInfoWidget: {
|
servicesInfoWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
failed: false,
|
failed: false,
|
||||||
data: { singbox: singbox.running, podkop: podkop.enabled },
|
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() {
|
async function connectToClashSockets() {
|
||||||
@@ -73,6 +90,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
'[fetchDashboardSections]: failed to connect',
|
||||||
|
getClashWsUrl(),
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
bandwidthWidget: {
|
bandwidthWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -108,6 +129,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
'[fetchDashboardSections]: failed to connect',
|
||||||
|
getClashWsUrl(),
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
trafficTotalWidget: {
|
trafficTotalWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Podkop } from '../../types';
|
import { Podkop } from '../../types';
|
||||||
|
import { getClashApiUrl } from '../../../helpers';
|
||||||
|
|
||||||
interface IRenderSectionsProps {
|
interface IRenderSectionsProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -16,7 +17,10 @@ function renderFailedState() {
|
|||||||
class: 'pdk_dashboard-page__outbound-section centered',
|
class: 'pdk_dashboard-page__outbound-section centered',
|
||||||
style: 'height: 127px',
|
style: 'height: 127px',
|
||||||
},
|
},
|
||||||
E('span', {}, _('Dashboard currently unavailable')),
|
E('span', {}, [
|
||||||
|
E('span', {}, _('Dashboard currently unavailable')),
|
||||||
|
E('div', { style: 'text-align: center;' }, `API: ${getClashApiUrl()}`),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
131
fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js
Normal file
131
fe-app-podkop/src/validators/tests/validateTrojanUrl.test.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,32 +1,57 @@
|
|||||||
import { ValidationResult } from './types';
|
import { ValidationResult } from './types';
|
||||||
|
|
||||||
// TODO refactor current validation and add tests
|
|
||||||
export function validateTrojanUrl(url: string): ValidationResult {
|
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 {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
if (!url.startsWith('trojan://')) {
|
||||||
|
|
||||||
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: _('Invalid Trojan URL: must start with trojan://'),
|
||||||
'Invalid Trojan URL: must contain username, hostname and port',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
} catch (_e) {
|
||||||
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
|
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +1,63 @@
|
|||||||
import { ValidationResult } from './types';
|
import { ValidationResult } from './types';
|
||||||
|
import { parseQueryString } from '../helpers';
|
||||||
|
|
||||||
export function validateVlessUrl(url: string): ValidationResult {
|
export function validateVlessUrl(url: string): ValidationResult {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url);
|
if (!url.startsWith('vless://'))
|
||||||
|
|
||||||
if (!url || /\s/.test(url)) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _('Invalid VLESS URL: must not contain spaces'),
|
message: 'Invalid VLESS URL: must start with vless://',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedUrl.protocol !== 'vless:') {
|
if (/\s/.test(url))
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _('Invalid VLESS URL: must start with vless://'),
|
message: 'Invalid VLESS URL: must not contain spaces',
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.username) {
|
const body = url.slice('vless://'.length);
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.hostname) {
|
const [mainPart] = body.split('#');
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing server') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parsedUrl.port) {
|
const [userHostPort, queryString] = mainPart.split('?');
|
||||||
return { valid: false, message: _('Invalid VLESS URL: missing port') };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (!userHostPort)
|
||||||
isNaN(+parsedUrl.port) ||
|
|
||||||
+parsedUrl.port < 1 ||
|
|
||||||
+parsedUrl.port > 65535
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing host and UUID',
|
||||||
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
valid: false,
|
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 = parseQueryString(queryString);
|
||||||
|
|
||||||
const type = params.get('type');
|
|
||||||
const validTypes = [
|
const validTypes = [
|
||||||
'tcp',
|
'tcp',
|
||||||
'raw',
|
'raw',
|
||||||
@@ -64,45 +69,31 @@ export function validateVlessUrl(url: string): ValidationResult {
|
|||||||
'ws',
|
'ws',
|
||||||
'kcp',
|
'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'];
|
const validSecurities = ['tls', 'reality', 'none'];
|
||||||
|
|
||||||
if (!security || !validSecurities.includes(security)) {
|
if (!params.type || !validTypes.includes(params.type))
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: unsupported or missing type',
|
||||||
'Invalid VLESS URL: security must be one of tls, reality, none',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (security === 'reality') {
|
if (!params.security || !validSecurities.includes(params.security))
|
||||||
if (!params.get('pbk')) {
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid VLESS URL: unsupported or missing security',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.security === 'reality') {
|
||||||
|
if (!params.pbk)
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing pbk for reality',
|
||||||
'Invalid VLESS URL: missing pbk parameter for reality security',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
if (!params.fp)
|
||||||
if (!params.get('fp')) {
|
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: 'Invalid VLESS URL: missing fp for reality',
|
||||||
'Invalid VLESS URL: missing fp parameter for reality security',
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { valid: true, message: _('Valid') };
|
return { valid: true, message: _('Valid') };
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function createAdditionalSection(mainSection) {
|
|||||||
form.Flag,
|
form.Flag,
|
||||||
'yacd',
|
'yacd',
|
||||||
_('Yacd enable'),
|
_('Yacd enable'),
|
||||||
`<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`,
|
`<a href="${main.getClashUIUrl()}" target="_blank">${main.getClashUIUrl()}</a>`,
|
||||||
);
|
);
|
||||||
o.default = '0';
|
o.default = '0';
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
|
|||||||
@@ -210,165 +210,6 @@ function validateShadowsocksUrl(url) {
|
|||||||
return { valid: true, message: _("Valid") };
|
return { valid: true, message: _("Valid") };
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/validators/validateVlessUrl.ts
|
|
||||||
function validateVlessUrl(url) {
|
|
||||||
try {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
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://")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!parsedUrl.search) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _("Invalid VLESS URL: missing query parameters")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const params = new URLSearchParams(parsedUrl.search);
|
|
||||||
const type = params.get("type");
|
|
||||||
const validTypes = [
|
|
||||||
"tcp",
|
|
||||||
"raw",
|
|
||||||
"udp",
|
|
||||||
"grpc",
|
|
||||||
"http",
|
|
||||||
"httpupgrade",
|
|
||||||
"xhttp",
|
|
||||||
"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)) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: security must be one of tls, reality, none"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (security === "reality") {
|
|
||||||
if (!params.get("pbk")) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: missing pbk parameter for reality security"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (!params.get("fp")) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid VLESS URL: missing fp parameter for reality security"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
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 (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
message: _(
|
|
||||||
"Invalid Trojan URL: must contain username, hostname and port"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} 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
|
// src/helpers/getBaseUrl.ts
|
||||||
function getBaseUrl() {
|
function getBaseUrl() {
|
||||||
const { protocol, hostname } = window.location;
|
const { protocol, hostname } = window.location;
|
||||||
@@ -771,6 +612,10 @@ function getClashWsUrl() {
|
|||||||
const { hostname } = window.location;
|
const { hostname } = window.location;
|
||||||
return `ws://${hostname}:9090`;
|
return `ws://${hostname}:9090`;
|
||||||
}
|
}
|
||||||
|
function getClashUIUrl() {
|
||||||
|
const { hostname } = window.location;
|
||||||
|
return `http://${hostname}:9090/ui`;
|
||||||
|
}
|
||||||
|
|
||||||
// src/helpers/splitProxyString.ts
|
// src/helpers/splitProxyString.ts
|
||||||
function splitProxyString(str) {
|
function splitProxyString(str) {
|
||||||
@@ -786,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
|
// src/clash/methods/createBaseApiRequest.ts
|
||||||
async function createBaseApiRequest(fetchFn) {
|
async function createBaseApiRequest(fetchFn) {
|
||||||
try {
|
try {
|
||||||
@@ -1021,7 +1049,7 @@ async function getPodkopStatus() {
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: "/usr/bin/podkop",
|
command: "/usr/bin/podkop",
|
||||||
args: ["get_status"],
|
args: ["get_status"],
|
||||||
timeout: 1e3
|
timeout: 1e4
|
||||||
});
|
});
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
||||||
@@ -1034,7 +1062,7 @@ async function getSingboxStatus() {
|
|||||||
const response = await executeShellCommand({
|
const response = await executeShellCommand({
|
||||||
command: "/usr/bin/podkop",
|
command: "/usr/bin/podkop",
|
||||||
args: ["get_sing_box_status"],
|
args: ["get_sing_box_status"],
|
||||||
timeout: 1e3
|
timeout: 1e4
|
||||||
});
|
});
|
||||||
if (response.stdout) {
|
if (response.stdout) {
|
||||||
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
return JSON.parse(response.stdout.replace(/\n/g, ""));
|
||||||
@@ -1246,7 +1274,10 @@ function renderFailedState() {
|
|||||||
class: "pdk_dashboard-page__outbound-section centered",
|
class: "pdk_dashboard-page__outbound-section centered",
|
||||||
style: "height: 127px"
|
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() {
|
function renderLoadingState() {
|
||||||
@@ -1583,6 +1614,9 @@ async function fetchDashboardSections() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { data, success } = await getDashboardSections();
|
const { data, success } = await getDashboardSections();
|
||||||
|
if (!success) {
|
||||||
|
console.log("[fetchDashboardSections]: failed to fetch", getClashApiUrl());
|
||||||
|
}
|
||||||
store.set({
|
store.set({
|
||||||
sectionsWidget: {
|
sectionsWidget: {
|
||||||
latencyFetching: false,
|
latencyFetching: false,
|
||||||
@@ -1593,17 +1627,28 @@ async function fetchDashboardSections() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function fetchServicesInfo() {
|
async function fetchServicesInfo() {
|
||||||
const [podkop, singbox] = await Promise.all([
|
try {
|
||||||
getPodkopStatus(),
|
const [podkop, singbox] = await Promise.all([
|
||||||
getSingboxStatus()
|
getPodkopStatus(),
|
||||||
]);
|
getSingboxStatus()
|
||||||
store.set({
|
]);
|
||||||
servicesInfoWidget: {
|
store.set({
|
||||||
loading: false,
|
servicesInfoWidget: {
|
||||||
failed: false,
|
loading: false,
|
||||||
data: { singbox: singbox.running, podkop: podkop.enabled }
|
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() {
|
async function connectToClashSockets() {
|
||||||
socket.subscribe(
|
socket.subscribe(
|
||||||
@@ -1619,6 +1664,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
"[fetchDashboardSections]: failed to connect",
|
||||||
|
getClashWsUrl()
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
bandwidthWidget: {
|
bandwidthWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -1652,6 +1701,10 @@ async function connectToClashSockets() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
(_err) => {
|
(_err) => {
|
||||||
|
console.log(
|
||||||
|
"[fetchDashboardSections]: failed to connect",
|
||||||
|
getClashWsUrl()
|
||||||
|
);
|
||||||
store.set({
|
store.set({
|
||||||
trafficTotalWidget: {
|
trafficTotalWidget: {
|
||||||
loading: false,
|
loading: false,
|
||||||
@@ -1928,6 +1981,7 @@ return baseclass.extend({
|
|||||||
getClashConfig,
|
getClashConfig,
|
||||||
getClashGroupDelay,
|
getClashGroupDelay,
|
||||||
getClashProxies,
|
getClashProxies,
|
||||||
|
getClashUIUrl,
|
||||||
getClashVersion,
|
getClashVersion,
|
||||||
getClashWsUrl,
|
getClashWsUrl,
|
||||||
getConfigSections,
|
getConfigSections,
|
||||||
@@ -1939,6 +1993,7 @@ return baseclass.extend({
|
|||||||
injectGlobalStyles,
|
injectGlobalStyles,
|
||||||
maskIP,
|
maskIP,
|
||||||
onMount,
|
onMount,
|
||||||
|
parseQueryString,
|
||||||
parseValueList,
|
parseValueList,
|
||||||
preserveScrollForPage,
|
preserveScrollForPage,
|
||||||
renderDashboard,
|
renderDashboard,
|
||||||
|
|||||||
Reference in New Issue
Block a user