Skip to content

Commit a51b06f

Browse files
ly85206559cursoragentsteipete
authored
fix(android): strip ws scheme prefix from manual gateway host input (#99110)
* fix(android): strip ws scheme prefix from manual gateway host input Reject bare ws:// in the Host field and normalize pasted ws:// LAN URLs so manual onboarding resolves the real hostname instead of host ws. Fixes #87216. Co-authored-by: Cursor <[email protected]> * fix(android): preserve complete manual gateway endpoints * fix(android): preserve gateway i18n inventory --------- Co-authored-by: Cursor <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent db7bfc6 commit a51b06f

2 files changed

Lines changed: 85 additions & 2 deletions

File tree

apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayConfigResolver.kt

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ internal fun parseGatewayEndpointResult(rawInput: String): GatewayEndpointParseR
164164
}
165165
val defaultPort = if (tls) 443 else 18789
166166
val displayPort = if (tls) 443 else 80
167-
val port = uri.port.takeIf { it in 1..65535 } ?: defaultPort
167+
val port = gatewayPort(uri.port, defaultPort) ?: return GatewayEndpointParseResult(error = GatewayEndpointValidationError.INVALID_URL)
168168
val displayHost = if (host.contains(":")) "[$host]" else host
169169
val displayUrl =
170170
if (port == displayPort && defaultPort == displayPort) {
@@ -244,6 +244,13 @@ internal fun gatewayEndpointValidationMessage(
244244
}
245245
}
246246

247+
private fun gatewayPort(port: Int, defaultPort: Int): Int? =
248+
when {
249+
port == -1 -> defaultPort
250+
port in 1..65535 -> port
251+
else -> null
252+
}
253+
247254
/** Builds a URL from manual host/port/tls fields for shared endpoint parsing. */
248255
internal fun composeGatewayManualUrl(
249256
hostInput: String,
@@ -252,6 +259,14 @@ internal fun composeGatewayManualUrl(
252259
): String? {
253260
val host = hostInput.trim()
254261
if (host.isEmpty()) return null
262+
// A pasted endpoint is already a complete authority; its scheme and port
263+
// must not be silently replaced by stale values from the separate controls.
264+
if (host.contains("://")) {
265+
val parsed = parseGatewayEndpointResult(host)
266+
return host.takeUnless { parsed.error == GatewayEndpointValidationError.INVALID_URL }
267+
}
268+
val bareHost = host.trimEnd('/')
269+
if (bareHost.isEmpty() || bareHost.contains('/')) return null
255270
val portTrimmed = portInput.trim()
256271
val port =
257272
if (portTrimmed.isEmpty()) {
@@ -261,7 +276,7 @@ internal fun composeGatewayManualUrl(
261276
}
262277
if (port !in 1..65535) return null
263278
val scheme = if (tls) "https" else "http"
264-
return "$scheme://${ai.openclaw.app.gateway.formatGatewayAuthority(host, port)}"
279+
return "$scheme://${ai.openclaw.app.gateway.formatGatewayAuthority(bareHost, port)}"
265280
}
266281

267282
private fun parseJsonObject(input: String): JsonObject? = runCatching { gatewaySetupJson.parseToJsonElement(input).jsonObject }.getOrNull()

apps/android/app/src/test/java/ai/openclaw/app/ui/GatewayConfigResolverTest.kt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,14 @@ class GatewayConfigResolverTest {
351351
assertEquals(GatewayEndpointValidationError.INVALID_URL, parsed.error)
352352
}
353353

354+
@Test
355+
fun parseGatewayEndpointResultRejectsInvalidExplicitPort() {
356+
val parsed = parseGatewayEndpointResult("wss://gateway.example:70000")
357+
358+
assertNull(parsed.config)
359+
assertEquals(GatewayEndpointValidationError.INVALID_URL, parsed.error)
360+
}
361+
354362
@Test
355363
fun parseGatewayEndpointResultAllowsPrivateLanCleartextGateway() {
356364
val parsed = parseGatewayEndpointResult("ws://192.168.1.20:18789")
@@ -592,6 +600,66 @@ class GatewayConfigResolverTest {
592600
assertEquals(false, resolved?.tls)
593601
}
594602

603+
@Test
604+
fun composeGatewayManualUrlRejectsBareScheme() {
605+
assertNull(composeGatewayManualUrl("ws://", "18789", tls = false))
606+
}
607+
608+
@Test
609+
fun composeGatewayManualUrlPreservesCompleteEndpoint() {
610+
val cleartextUrl = composeGatewayManualUrl("ws://192.168.178.57:18790", "18789", tls = true)
611+
val tlsUrl = composeGatewayManualUrl("wss://gateway.example:443", "18789", tls = false)
612+
613+
assertEquals("ws://192.168.178.57:18790", cleartextUrl)
614+
assertEquals("wss://gateway.example:443", tlsUrl)
615+
assertEquals("http://192.168.178.57:18790", parseGatewayEndpoint(cleartextUrl!!)?.displayUrl)
616+
assertEquals("https://gateway.example", parseGatewayEndpoint(tlsUrl!!)?.displayUrl)
617+
}
618+
619+
@Test
620+
fun composeGatewayManualUrlPreservesCompleteEndpointValidationError() {
621+
val url = composeGatewayManualUrl("ws://gateway.example:18789", "18789", tls = false)
622+
623+
assertEquals(GatewayEndpointValidationError.INSECURE_REMOTE_URL, parseGatewayEndpointResult(url!!).error)
624+
}
625+
626+
@Test
627+
fun resolveGatewayConnectConfigManualAcceptsCompleteLanEndpoint() {
628+
val resolved =
629+
resolveGatewayConnectConfig(
630+
useSetupCode = false,
631+
setupCode = "",
632+
savedManualHost = "",
633+
savedManualPort = "",
634+
savedManualTls = false,
635+
manualHostInput = "ws://192.168.178.57:18790",
636+
manualPortInput = "18789",
637+
manualTlsInput = true,
638+
fallbackBootstrapToken = "",
639+
fallbackToken = "",
640+
fallbackPassword = "",
641+
)
642+
643+
assertEquals("192.168.178.57", resolved?.host)
644+
assertEquals(18790, resolved?.port)
645+
assertEquals(false, resolved?.tls)
646+
}
647+
648+
@Test
649+
fun composeGatewayManualUrlPreservesIpv6Hosts() {
650+
for (hostInput in listOf("::1", "[::1]")) {
651+
assertEquals("http://[::1]:18789", composeGatewayManualUrl(hostInput, "18789", tls = false))
652+
}
653+
}
654+
655+
@Test
656+
fun composeGatewayManualUrlTrimsTrailingSlashFromBareHost() {
657+
assertEquals(
658+
"http://192.168.1.20:20000",
659+
composeGatewayManualUrl("192.168.1.20/", "20000", tls = false),
660+
)
661+
}
662+
595663
@Test
596664
fun composeGatewayManualUrlDefaultsPortTo443WhenTlsAndPortBlank() {
597665
val url = composeGatewayManualUrl("mydevice.tail1234.ts.net", "", tls = true)

0 commit comments

Comments
 (0)