feat(web): add URL path support for listener addresses (#1967)

This commit is contained in:
Mg Pig
2026-09-09 22:55:44 +08:00
committed by GitHub
parent 38e2a621bb
commit 0fba5769a3
4 changed files with 50 additions and 13 deletions
@@ -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) {
<div class="flex flex-row gap-x-9 flex-wrap">
<div class="flex flex-col gap-2 grow p-fluid">
<label for="listener_urls">{{ t('listener_urls') }}</label>
<UrlListInput v-model="curNetwork.listener_urls" :protos="protos" :add-label="t('add_listener_url')"
placeholder="0.0.0.0" />
<UrlListInput v-model="curNetwork.listener_urls" :protos="listenerProtos"
:add-label="t('add_listener_url')" placeholder="0.0.0.0" />
</div>
</div>
@@ -15,12 +15,19 @@ const url = defineModel<string>({ 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) => {
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="max-w-24"
:placeholder="String(protos[internalValue.proto] ?? 11010)" fluid />
</template>
<template v-if="supportsPath">
<InputText v-model="internalValue.path" placeholder="/mypath" class="grow" />
</template>
<!-- Rendered in both responsive branches; keep action slot content free of side effects and duplicate IDs. -->
<slot name="actions"></slot>
</InputGroup>
@@ -197,6 +223,10 @@ const onProtoChange = (newProto: string) => {
<InputNumber v-model="internalValue.port" :format="false" :min="1" :max="65535" class="w-full"
:placeholder="String(protos[internalValue.proto] ?? 11010)" />
</div>
<div v-if="supportsPath" class="flex flex-col gap-2">
<label>{{ t('path') }}</label>
<InputText v-model="internalValue.path" placeholder="/mypath" class="w-full" />
</div>
</div>
<template #footer>
<Button :label="t('web.common.confirm') || 'Done'" icon="pi pi-check" @click="onDialogConfirm"
@@ -38,6 +38,7 @@ basic_settings: 基础设置
listener_urls: 监听地址
rpc_port: RPC端口
port: 端口
path: 路径
rpc_portal_whitelists: RPC白名单
config_network: 配置网络
running: 运行中
@@ -38,6 +38,7 @@ basic_settings: Basic Settings
listener_urls: Listener URLs
rpc_port: RPC Port
port: Port
path: Path
rpc_portal_whitelists: RPC Whitelist
config_network: Config Network
running: Running