mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-09-10 16:00:47 +00:00
feat(web): add URL path support for listener addresses (#1967)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user