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) => {
+
+
+
+