summaryrefslogtreecommitdiff
path: root/src/modules/backend/utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/modules/backend/utils.ts')
-rw-r--r--src/modules/backend/utils.ts98
1 files changed, 74 insertions, 24 deletions
diff --git a/src/modules/backend/utils.ts b/src/modules/backend/utils.ts
index 47d2bd1..7d63251 100644
--- a/src/modules/backend/utils.ts
+++ b/src/modules/backend/utils.ts
@@ -1,41 +1,88 @@
import { fetchConfigs } from '~/api/configs';
import type { ClashAPIConfig } from '~/types';
-export const DEFAULT_API_BASE_URL = 'http://127.0.0.1:9090';
+export type Protocol = 'http' | 'https';
-const Ok = 0;
+/** 后端地址在表单里被拆成三段独立编辑 */
+export type BackendFields = {
+ protocol: Protocol;
+ host: string;
+ port: string;
+};
-export function normalizeAPIBaseURL(baseURL: string, currentProtocol: string) {
- let normalizedBaseURL = baseURL || DEFAULT_API_BASE_URL;
+export const DEFAULT_BACKEND_FIELDS: BackendFields = {
+ protocol: 'http',
+ host: '127.0.0.1',
+ port: '9090',
+};
- if (normalizedBaseURL) {
- const prefix = normalizedBaseURL.substring(0, 7);
- if (prefix.includes(':/')) {
- if (prefix !== 'http://' && prefix !== 'https:/') {
- return { error: 'Must starts with http:// or https://' };
- }
- } else if (currentProtocol) {
- normalizedBaseURL = `${currentProtocol}//${normalizedBaseURL}`;
- }
- }
+const Ok = 0;
- return { baseURL: normalizedBaseURL };
+/** IPv6 在 URL 里带方括号,表单里只展示裸地址 */
+function stripBrackets(host: string) {
+ return host.replace(/^\[/, '').replace(/\]$/, '');
}
-export async function verifyAPIConfig(apiConfig: ClashAPIConfig): Promise<[number, string?]> {
+function isIPv6(host: string) {
+ return host.includes(':');
+}
+
+/** 把完整 baseURL 拆成协议 / 主机 / 端口,解析失败返回 null */
+export function splitAPIBaseURL(baseURL: string): BackendFields | null {
+ let url: URL;
try {
- new URL(apiConfig.baseURL);
+ url = new URL(baseURL);
} catch (e) {
- if (apiConfig.baseURL) {
- const prefix = apiConfig.baseURL.substring(0, 7);
- if (prefix !== 'http://' && prefix !== 'https:/') {
- return [1, 'Must starts with http:// or https://'];
- }
- }
+ return null;
+ }
- return [1, 'Invalid URL'];
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
+ const protocol: Protocol = url.protocol === 'https:' ? 'https' : 'http';
+
+ return {
+ protocol,
+ host: stripBrackets(url.hostname),
+ port: url.port || (protocol === 'https' ? '443' : '80'),
+ };
+}
+
+/**
+ * 允许把一整条地址粘贴进 Host 输入框,自动拆到对应字段。
+ * 返回 null 表示这就是个普通主机名,按原样填入 Host 即可。
+ */
+export function splitPastedHost(value: string): Partial<BackendFields> | null {
+ const text = value.trim();
+ const hasProtocol = text.includes('://');
+ if (!hasProtocol && !/^[^\s/:]+:\d+$/.test(text)) return null;
+
+ const fields = splitAPIBaseURL(hasProtocol ? text : `http://${text}`);
+ if (!fields) return null;
+
+ // 没写协议时不要凭空替用户选一个
+ return hasProtocol ? fields : { host: fields.host, port: fields.port };
+}
+
+/** 三段拼回 baseURL,同时做基本校验 */
+export function buildAPIBaseURL({
+ protocol,
+ host,
+ port,
+}: BackendFields): { baseURL: string } | { error: string } {
+ const trimmedHost = stripBrackets(host.trim());
+ const trimmedPort = port.trim();
+
+ if (!trimmedHost) return { error: 'Host is required' };
+ if (/[\s/?#]/.test(trimmedHost)) return { error: 'Invalid host' };
+ if (!trimmedPort) return { error: 'Port is required' };
+ if (!/^\d{1,5}$/.test(trimmedPort) || Number(trimmedPort) < 1 || Number(trimmedPort) > 65535) {
+ return { error: 'Port must be a number between 1 and 65535' };
}
+ const hostname = isIPv6(trimmedHost) ? `[${trimmedHost}]` : trimmedHost;
+ return { baseURL: `${protocol}://${hostname}:${trimmedPort}` };
+}
+
+export async function verifyAPIConfig(apiConfig: ClashAPIConfig): Promise<[number, string?]> {
try {
const res = await fetchConfigs(apiConfig);
if (res.status > 399) {
@@ -50,6 +97,9 @@ export async function verifyAPIConfig(apiConfig: ClashAPIConfig): Promise<[numbe
export async function detectEmbeddedAPIBaseURL() {
try {
const res = await fetch('/');
+ // 内核设置了 secret 时根路径返回 401,这同样说明当前 origin 就是 API 地址
+ // (面板被内核自己托管在 /ui/ 下的常见情况)
+ if (res.status === 401) return window.location.origin;
if (res.headers.get('content-type')?.includes('application/json')) {
const data = await res.json();
if (data.hello === 'clash') {