From 0fba5769a39ec2bef5b6984ca06a72c513707fa5 Mon Sep 17 00:00:00 2001 From: Mg Pig Date: Wed, 9 Sep 2026 22:55:44 +0800 Subject: [PATCH] feat(web): add URL path support for listener addresses (#1967) --- .../frontend-lib/src/components/Config.vue | 9 +++- .../frontend-lib/src/components/UrlInput.vue | 52 +++++++++++++++---- easytier-web/frontend-lib/src/locales/cn.yaml | 1 + easytier-web/frontend-lib/src/locales/en.yaml | 1 + 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/easytier-web/frontend-lib/src/components/Config.vue b/easytier-web/frontend-lib/src/components/Config.vue index 3428a8b1..7aca4deb 100644 --- a/easytier-web/frontend-lib/src/components/Config.vue +++ b/easytier-web/frontend-lib/src/components/Config.vue @@ -46,6 +46,11 @@ const protos: { [proto: string]: number } = { srv: 0, } +const listenerExcludedProtos = new Set(['http', 'https', 'txt', 'srv']) +const listenerProtos: { [proto: string]: number } = Object.fromEntries( + Object.entries(protos).filter(([proto]) => !listenerExcludedProtos.has(proto)) +) + const inetSuggestions = ref(['']) function searchInetSuggestions(e: { query: string }) { @@ -402,8 +407,8 @@ function removeVpnPortalClient(index: number) {
- +
diff --git a/easytier-web/frontend-lib/src/components/UrlInput.vue b/easytier-web/frontend-lib/src/components/UrlInput.vue index 060cfaa6..f5737096 100644 --- a/easytier-web/frontend-lib/src/components/UrlInput.vue +++ b/easytier-web/frontend-lib/src/components/UrlInput.vue @@ -15,12 +15,19 @@ const url = defineModel({ required: true }) const editing = ref(false) const hostFocused = ref(false) -const parseUrl = (val: string | null | undefined): { proto: string; host: string; port: number | null } => { +type ParsedUrlValue = { + proto: string + host: string + port: number | null + path: string +} + +const parseUrl = (val: string | null | undefined): ParsedUrlValue => { const getValidPort = (portStr: string, proto: string) => { const p = parseInt(portStr) return isNaN(p) ? (props.protos[proto] ?? 11010) : p } - const parseByPattern = (input: string) => { + const parseByPattern = (input: string): ParsedUrlValue | null => { const trimmed = input.trim() if (!trimmed) { return null @@ -28,7 +35,9 @@ const parseUrl = (val: string | null | undefined): { proto: string; host: string const match = trimmed.match(/^(\w+):\/\/(.*)$/) const proto = match ? match[1] : 'tcp' const rest = match ? match[2] : trimmed - const authority = rest.split(/[/?#]/)[0] + const pathStart = rest.search(/[/?#]/) + const authority = pathStart >= 0 ? rest.slice(0, pathStart) : rest + const path = pathStart >= 0 ? rest.slice(pathStart) : '' if (!authority) { return null } @@ -40,7 +49,7 @@ const parseUrl = (val: string | null | undefined): { proto: string; host: string const remain = hostAndMaybePort.slice(ipv6End + 1) // null = no explicit port in URL; do not fabricate a default const port: number | null = remain.startsWith(':') ? getValidPort(remain.slice(1), proto) : null - return { proto, host, port } + return { proto, host, port, path } } } const portMatch = hostAndMaybePort.match(/^(.*):(\d+)$/) @@ -48,26 +57,27 @@ const parseUrl = (val: string | null | undefined): { proto: string; host: string // null = no explicit port in URL; buildUrlValue will omit the port entirely, // preserving the protocol's implied standard port (e.g. 443 for wss://). const port: number | null = portMatch ? parseInt(portMatch[2]) : null - return { proto, host, port } + return { proto, host, port, path } } if (!val) { - return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010 } + return { proto: 'tcp', host: '', port: props.protos['tcp'] ?? 11010, path: '' } } const parsedByPattern = parseByPattern(val) if (parsedByPattern) { return parsedByPattern } - return { proto: 'tcp', host: '', port: null } + return { proto: 'tcp', host: '', port: null, path: '' } } const internalValue = ref(parseUrl(url.value)) const defaultHost = '0.0.0.0' -const buildUrlValue = (value: { proto: string, host: string, port: number | null }, forceDefaultHost = false) => { +const buildUrlValue = (value: ParsedUrlValue, forceDefaultHost = false) => { const proto = value.proto || 'tcp' const rawHost = (value.host ?? '').trim() const host = rawHost || (forceDefaultHost ? defaultHost : '') + const path = supportsPath.value ? normalizePath(value.path) : '' if (!host) { return null } @@ -75,9 +85,9 @@ const buildUrlValue = (value: { proto: string, host: string, port: number | null // original URL had no explicit port (port === null) – avoids overwriting an // implicit standard port (e.g. 443 for wss) with an EasyTier default (11012). if (props.protos[proto] === 0 || value.port === null) { - return `${proto}://${host}` + return `${proto}://${host}${path}` } - return `${proto}://${host}:${value.port}` + return `${proto}://${host}:${value.port}${path}` } const syncUrlFromInternal = (forceDefaultHost = false) => { @@ -106,6 +116,18 @@ const isNoPortProto = computed(() => { return props.protos[internalValue.value.proto] === 0 }) +const supportsPath = computed(() => { + const proto = internalValue.value.proto + return proto === 'ws' || proto === 'wss' || proto === 'http' || proto === 'https' +}) + +const normalizePath = (path: string) => { + const trimmed = path.trim() + if (!trimmed) + return '' + return trimmed.startsWith('/') ? trimmed : `/${trimmed}` +} + // Sync from external watch(() => url.value, (newVal) => { if (hostFocused.value) { @@ -116,7 +138,8 @@ watch(() => url.value, (newVal) => { const sameHost = parsed.host === internalHost || (!internalHost.trim() && parsed.host === defaultHost) if (parsed.proto !== internalValue.value.proto || !sameHost || - parsed.port !== internalValue.value.port) { + parsed.port !== internalValue.value.port || + parsed.path !== internalValue.value.path) { internalValue.value = parsed } }) @@ -166,6 +189,9 @@ const onProtoChange = (newProto: string) => { + @@ -197,6 +223,10 @@ const onProtoChange = (newProto: string) => { +
+ + +