fix: switch pairing setup codes to bootstrap tokens
This commit is contained in:
@@ -116,6 +116,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
runtime.setGatewayToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayBootstrapToken(value: String) {
|
||||
runtime.setGatewayBootstrapToken(value)
|
||||
}
|
||||
|
||||
fun setGatewayPassword(value: String) {
|
||||
runtime.setGatewayPassword(value)
|
||||
}
|
||||
|
||||
@@ -503,6 +503,7 @@ class NodeRuntime(context: Context) {
|
||||
val gatewayToken: StateFlow<String> = prefs.gatewayToken
|
||||
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
|
||||
fun setGatewayToken(value: String) = prefs.setGatewayToken(value)
|
||||
fun setGatewayBootstrapToken(value: String) = prefs.setGatewayBootstrapToken(value)
|
||||
fun setGatewayPassword(value: String) = prefs.setGatewayPassword(value)
|
||||
fun setOnboardingCompleted(value: Boolean) = prefs.setOnboardingCompleted(value)
|
||||
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
|
||||
@@ -698,10 +699,25 @@ class NodeRuntime(context: Context) {
|
||||
operatorStatusText = "Connecting…"
|
||||
updateStatus()
|
||||
val token = prefs.loadGatewayToken()
|
||||
val bootstrapToken = prefs.loadGatewayBootstrapToken()
|
||||
val password = prefs.loadGatewayPassword()
|
||||
val tls = connectionManager.resolveTlsParams(endpoint)
|
||||
operatorSession.connect(endpoint, token, password, connectionManager.buildOperatorConnectOptions(), tls)
|
||||
nodeSession.connect(endpoint, token, password, connectionManager.buildNodeConnectOptions(), tls)
|
||||
operatorSession.connect(
|
||||
endpoint,
|
||||
token,
|
||||
bootstrapToken,
|
||||
password,
|
||||
connectionManager.buildOperatorConnectOptions(),
|
||||
tls,
|
||||
)
|
||||
nodeSession.connect(
|
||||
endpoint,
|
||||
token,
|
||||
bootstrapToken,
|
||||
password,
|
||||
connectionManager.buildNodeConnectOptions(),
|
||||
tls,
|
||||
)
|
||||
operatorSession.reconnect()
|
||||
nodeSession.reconnect()
|
||||
}
|
||||
@@ -726,9 +742,24 @@ class NodeRuntime(context: Context) {
|
||||
nodeStatusText = "Connecting…"
|
||||
updateStatus()
|
||||
val token = prefs.loadGatewayToken()
|
||||
val bootstrapToken = prefs.loadGatewayBootstrapToken()
|
||||
val password = prefs.loadGatewayPassword()
|
||||
operatorSession.connect(endpoint, token, password, connectionManager.buildOperatorConnectOptions(), tls)
|
||||
nodeSession.connect(endpoint, token, password, connectionManager.buildNodeConnectOptions(), tls)
|
||||
operatorSession.connect(
|
||||
endpoint,
|
||||
token,
|
||||
bootstrapToken,
|
||||
password,
|
||||
connectionManager.buildOperatorConnectOptions(),
|
||||
tls,
|
||||
)
|
||||
nodeSession.connect(
|
||||
endpoint,
|
||||
token,
|
||||
bootstrapToken,
|
||||
password,
|
||||
connectionManager.buildNodeConnectOptions(),
|
||||
tls,
|
||||
)
|
||||
}
|
||||
|
||||
fun acceptGatewayTrustPrompt() {
|
||||
|
||||
@@ -15,7 +15,10 @@ import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import java.util.UUID
|
||||
|
||||
class SecurePrefs(context: Context) {
|
||||
class SecurePrefs(
|
||||
context: Context,
|
||||
private val securePrefsOverride: SharedPreferences? = null,
|
||||
) {
|
||||
companion object {
|
||||
val defaultWakeWords: List<String> = listOf("openclaw", "claude")
|
||||
private const val displayNameKey = "node.displayName"
|
||||
@@ -35,7 +38,7 @@ class SecurePrefs(context: Context) {
|
||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||
.build()
|
||||
}
|
||||
private val securePrefs: SharedPreferences by lazy { createSecurePrefs(appContext, securePrefsName) }
|
||||
private val securePrefs: SharedPreferences by lazy { securePrefsOverride ?: createSecurePrefs(appContext, securePrefsName) }
|
||||
|
||||
private val _instanceId = MutableStateFlow(loadOrCreateInstanceId())
|
||||
val instanceId: StateFlow<String> = _instanceId
|
||||
@@ -76,6 +79,9 @@ class SecurePrefs(context: Context) {
|
||||
private val _gatewayToken = MutableStateFlow("")
|
||||
val gatewayToken: StateFlow<String> = _gatewayToken
|
||||
|
||||
private val _gatewayBootstrapToken = MutableStateFlow("")
|
||||
val gatewayBootstrapToken: StateFlow<String> = _gatewayBootstrapToken
|
||||
|
||||
private val _onboardingCompleted =
|
||||
MutableStateFlow(plainPrefs.getBoolean("onboarding.completed", false))
|
||||
val onboardingCompleted: StateFlow<Boolean> = _onboardingCompleted
|
||||
@@ -165,6 +171,10 @@ class SecurePrefs(context: Context) {
|
||||
saveGatewayPassword(value)
|
||||
}
|
||||
|
||||
fun setGatewayBootstrapToken(value: String) {
|
||||
saveGatewayBootstrapToken(value)
|
||||
}
|
||||
|
||||
fun setOnboardingCompleted(value: Boolean) {
|
||||
plainPrefs.edit { putBoolean("onboarding.completed", value) }
|
||||
_onboardingCompleted.value = value
|
||||
@@ -193,6 +203,26 @@ class SecurePrefs(context: Context) {
|
||||
securePrefs.edit { putString(key, token.trim()) }
|
||||
}
|
||||
|
||||
fun loadGatewayBootstrapToken(): String? {
|
||||
val key = "gateway.bootstrapToken.${_instanceId.value}"
|
||||
val stored =
|
||||
_gatewayBootstrapToken.value.trim().ifEmpty {
|
||||
val persisted = securePrefs.getString(key, null)?.trim().orEmpty()
|
||||
if (persisted.isNotEmpty()) {
|
||||
_gatewayBootstrapToken.value = persisted
|
||||
}
|
||||
persisted
|
||||
}
|
||||
return stored.takeIf { it.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun saveGatewayBootstrapToken(token: String) {
|
||||
val key = "gateway.bootstrapToken.${_instanceId.value}"
|
||||
val trimmed = token.trim()
|
||||
securePrefs.edit { putString(key, trimmed) }
|
||||
_gatewayBootstrapToken.value = trimmed
|
||||
}
|
||||
|
||||
fun loadGatewayPassword(): String? {
|
||||
val key = "gateway.password.${_instanceId.value}"
|
||||
val stored = securePrefs.getString(key, null)?.trim()
|
||||
|
||||
@@ -95,6 +95,7 @@ class GatewaySession(
|
||||
private data class DesiredConnection(
|
||||
val endpoint: GatewayEndpoint,
|
||||
val token: String?,
|
||||
val bootstrapToken: String?,
|
||||
val password: String?,
|
||||
val options: GatewayConnectOptions,
|
||||
val tls: GatewayTlsParams?,
|
||||
@@ -107,11 +108,12 @@ class GatewaySession(
|
||||
fun connect(
|
||||
endpoint: GatewayEndpoint,
|
||||
token: String?,
|
||||
bootstrapToken: String?,
|
||||
password: String?,
|
||||
options: GatewayConnectOptions,
|
||||
tls: GatewayTlsParams? = null,
|
||||
) {
|
||||
desired = DesiredConnection(endpoint, token, password, options, tls)
|
||||
desired = DesiredConnection(endpoint, token, bootstrapToken, password, options, tls)
|
||||
if (job == null) {
|
||||
job = scope.launch(Dispatchers.IO) { runLoop() }
|
||||
}
|
||||
@@ -219,6 +221,7 @@ class GatewaySession(
|
||||
private inner class Connection(
|
||||
private val endpoint: GatewayEndpoint,
|
||||
private val token: String?,
|
||||
private val bootstrapToken: String?,
|
||||
private val password: String?,
|
||||
private val options: GatewayConnectOptions,
|
||||
private val tls: GatewayTlsParams?,
|
||||
@@ -346,9 +349,18 @@ class GatewaySession(
|
||||
val identity = identityStore.loadOrCreate()
|
||||
val storedToken = deviceAuthStore.loadToken(identity.deviceId, options.role)
|
||||
val trimmedToken = token?.trim().orEmpty()
|
||||
val trimmedBootstrapToken = bootstrapToken?.trim().orEmpty()
|
||||
// QR/setup/manual shared token must take precedence; stale role tokens can survive re-onboarding.
|
||||
val authToken = if (trimmedToken.isNotBlank()) trimmedToken else storedToken.orEmpty()
|
||||
val payload = buildConnectParams(identity, connectNonce, authToken, password?.trim())
|
||||
val authBootstrapToken = if (authToken.isBlank()) trimmedBootstrapToken else ""
|
||||
val payload =
|
||||
buildConnectParams(
|
||||
identity = identity,
|
||||
connectNonce = connectNonce,
|
||||
authToken = authToken,
|
||||
authBootstrapToken = authBootstrapToken,
|
||||
authPassword = password?.trim(),
|
||||
)
|
||||
val res = request("connect", payload, timeoutMs = CONNECT_RPC_TIMEOUT_MS)
|
||||
if (!res.ok) {
|
||||
val msg = res.error?.message ?: "connect failed"
|
||||
@@ -381,6 +393,7 @@ class GatewaySession(
|
||||
identity: DeviceIdentity,
|
||||
connectNonce: String,
|
||||
authToken: String,
|
||||
authBootstrapToken: String,
|
||||
authPassword: String?,
|
||||
): JsonObject {
|
||||
val client = options.client
|
||||
@@ -404,6 +417,10 @@ class GatewaySession(
|
||||
buildJsonObject {
|
||||
put("token", JsonPrimitive(authToken))
|
||||
}
|
||||
authBootstrapToken.isNotEmpty() ->
|
||||
buildJsonObject {
|
||||
put("bootstrapToken", JsonPrimitive(authBootstrapToken))
|
||||
}
|
||||
password.isNotEmpty() ->
|
||||
buildJsonObject {
|
||||
put("password", JsonPrimitive(password))
|
||||
@@ -420,7 +437,12 @@ class GatewaySession(
|
||||
role = options.role,
|
||||
scopes = options.scopes,
|
||||
signedAtMs = signedAtMs,
|
||||
token = if (authToken.isNotEmpty()) authToken else null,
|
||||
token =
|
||||
when {
|
||||
authToken.isNotEmpty() -> authToken
|
||||
authBootstrapToken.isNotEmpty() -> authBootstrapToken
|
||||
else -> null
|
||||
},
|
||||
nonce = connectNonce,
|
||||
platform = client.platform,
|
||||
deviceFamily = client.deviceFamily,
|
||||
@@ -622,7 +644,15 @@ class GatewaySession(
|
||||
}
|
||||
|
||||
private suspend fun connectOnce(target: DesiredConnection) = withContext(Dispatchers.IO) {
|
||||
val conn = Connection(target.endpoint, target.token, target.password, target.options, target.tls)
|
||||
val conn =
|
||||
Connection(
|
||||
target.endpoint,
|
||||
target.token,
|
||||
target.bootstrapToken,
|
||||
target.password,
|
||||
target.options,
|
||||
target.tls,
|
||||
)
|
||||
currentConnection = conn
|
||||
try {
|
||||
conn.connect()
|
||||
|
||||
@@ -200,8 +200,11 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
|
||||
viewModel.setManualHost(config.host)
|
||||
viewModel.setManualPort(config.port)
|
||||
viewModel.setManualTls(config.tls)
|
||||
viewModel.setGatewayBootstrapToken(config.bootstrapToken)
|
||||
if (config.token.isNotBlank()) {
|
||||
viewModel.setGatewayToken(config.token)
|
||||
} else if (config.bootstrapToken.isNotBlank()) {
|
||||
viewModel.setGatewayToken("")
|
||||
}
|
||||
viewModel.setGatewayPassword(config.password)
|
||||
viewModel.connectManual()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package ai.openclaw.app.ui
|
||||
|
||||
import androidx.core.net.toUri
|
||||
import java.util.Base64
|
||||
import java.util.Locale
|
||||
import java.net.URI
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
@@ -18,6 +18,7 @@ internal data class GatewayEndpointConfig(
|
||||
|
||||
internal data class GatewaySetupCode(
|
||||
val url: String,
|
||||
val bootstrapToken: String?,
|
||||
val token: String?,
|
||||
val password: String?,
|
||||
)
|
||||
@@ -26,6 +27,7 @@ internal data class GatewayConnectConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val tls: Boolean,
|
||||
val bootstrapToken: String,
|
||||
val token: String,
|
||||
val password: String,
|
||||
)
|
||||
@@ -44,12 +46,26 @@ internal fun resolveGatewayConnectConfig(
|
||||
if (useSetupCode) {
|
||||
val setup = decodeGatewaySetupCode(setupCode) ?: return null
|
||||
val parsed = parseGatewayEndpoint(setup.url) ?: return null
|
||||
val setupBootstrapToken = setup.bootstrapToken?.trim().orEmpty()
|
||||
val sharedToken =
|
||||
when {
|
||||
!setup.token.isNullOrBlank() -> setup.token.trim()
|
||||
setupBootstrapToken.isNotEmpty() -> ""
|
||||
else -> fallbackToken.trim()
|
||||
}
|
||||
val sharedPassword =
|
||||
when {
|
||||
!setup.password.isNullOrBlank() -> setup.password.trim()
|
||||
setupBootstrapToken.isNotEmpty() -> ""
|
||||
else -> fallbackPassword.trim()
|
||||
}
|
||||
return GatewayConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
token = setup.token ?: fallbackToken.trim(),
|
||||
password = setup.password ?: fallbackPassword.trim(),
|
||||
bootstrapToken = setupBootstrapToken,
|
||||
token = sharedToken,
|
||||
password = sharedPassword,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,6 +75,7 @@ internal fun resolveGatewayConnectConfig(
|
||||
host = parsed.host,
|
||||
port = parsed.port,
|
||||
tls = parsed.tls,
|
||||
bootstrapToken = "",
|
||||
token = fallbackToken.trim(),
|
||||
password = fallbackPassword.trim(),
|
||||
)
|
||||
@@ -69,7 +86,7 @@ internal fun parseGatewayEndpoint(rawInput: String): GatewayEndpointConfig? {
|
||||
if (raw.isEmpty()) return null
|
||||
|
||||
val normalized = if (raw.contains("://")) raw else "https://$raw"
|
||||
val uri = normalized.toUri()
|
||||
val uri = runCatching { URI(normalized) }.getOrNull() ?: return null
|
||||
val host = uri.host?.trim().orEmpty()
|
||||
if (host.isEmpty()) return null
|
||||
|
||||
@@ -104,9 +121,10 @@ internal fun decodeGatewaySetupCode(rawInput: String): GatewaySetupCode? {
|
||||
val obj = parseJsonObject(decoded) ?: return null
|
||||
val url = jsonField(obj, "url").orEmpty()
|
||||
if (url.isEmpty()) return null
|
||||
val bootstrapToken = jsonField(obj, "bootstrapToken")
|
||||
val token = jsonField(obj, "token")
|
||||
val password = jsonField(obj, "password")
|
||||
GatewaySetupCode(url = url, token = token, password = password)
|
||||
GatewaySetupCode(url = url, bootstrapToken = bootstrapToken, token = token, password = password)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -772,8 +772,18 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
return@Button
|
||||
}
|
||||
gatewayUrl = parsedSetup.url
|
||||
parsedSetup.token?.let { viewModel.setGatewayToken(it) }
|
||||
gatewayPassword = parsedSetup.password.orEmpty()
|
||||
viewModel.setGatewayBootstrapToken(parsedSetup.bootstrapToken.orEmpty())
|
||||
val sharedToken = parsedSetup.token.orEmpty().trim()
|
||||
val password = parsedSetup.password.orEmpty().trim()
|
||||
if (sharedToken.isNotEmpty()) {
|
||||
viewModel.setGatewayToken(sharedToken)
|
||||
} else if (!parsedSetup.bootstrapToken.isNullOrBlank()) {
|
||||
viewModel.setGatewayToken("")
|
||||
}
|
||||
gatewayPassword = password
|
||||
if (password.isEmpty() && !parsedSetup.bootstrapToken.isNullOrBlank()) {
|
||||
viewModel.setGatewayPassword("")
|
||||
}
|
||||
} else {
|
||||
val manualUrl = composeGatewayManualUrl(manualHost, manualPort, manualTls)
|
||||
val parsedGateway = manualUrl?.let(::parseGatewayEndpoint)
|
||||
@@ -782,6 +792,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
return@Button
|
||||
}
|
||||
gatewayUrl = parsedGateway.displayUrl
|
||||
viewModel.setGatewayBootstrapToken("")
|
||||
}
|
||||
step = OnboardingStep.Permissions
|
||||
},
|
||||
@@ -850,8 +861,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
viewModel.setManualHost(parsed.host)
|
||||
viewModel.setManualPort(parsed.port)
|
||||
viewModel.setManualTls(parsed.tls)
|
||||
if (gatewayInputMode == GatewayInputMode.Manual) {
|
||||
viewModel.setGatewayBootstrapToken("")
|
||||
}
|
||||
if (token.isNotEmpty()) {
|
||||
viewModel.setGatewayToken(token)
|
||||
} else {
|
||||
viewModel.setGatewayToken("")
|
||||
}
|
||||
viewModel.setGatewayPassword(password)
|
||||
viewModel.connectManual()
|
||||
|
||||
@@ -20,4 +20,19 @@ class SecurePrefsTest {
|
||||
assertEquals(LocationMode.WhileUsing, prefs.locationMode.value)
|
||||
assertEquals("whileUsing", plainPrefs.getString("location.enabledMode", null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val securePrefs = context.getSharedPreferences("openclaw.node.secure.test", Context.MODE_PRIVATE)
|
||||
securePrefs.edit().clear().commit()
|
||||
val prefs = SecurePrefs(context, securePrefsOverride = securePrefs)
|
||||
|
||||
prefs.setGatewayToken("shared-token")
|
||||
prefs.setGatewayBootstrapToken("bootstrap-token")
|
||||
|
||||
assertEquals("shared-token", prefs.loadGatewayToken())
|
||||
assertEquals("bootstrap-token", prefs.loadGatewayBootstrapToken())
|
||||
assertEquals("bootstrap-token", prefs.gatewayBootstrapToken.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
|
||||
private data class NodeHarness(
|
||||
val session: GatewaySession,
|
||||
val sessionJob: Job,
|
||||
val deviceAuthStore: InMemoryDeviceAuthStore,
|
||||
)
|
||||
|
||||
private data class InvokeScenarioResult(
|
||||
@@ -56,6 +57,93 @@ private data class InvokeScenarioResult(
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class GatewaySessionInvokeTest {
|
||||
@Test
|
||||
fun connect_usesBootstrapTokenWhenSharedAndDeviceTokensAreAbsent() = runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val connectAuth = CompletableDeferred<JsonObject?>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
val server =
|
||||
startGatewayServer(json) { webSocket, id, method, frame ->
|
||||
when (method) {
|
||||
"connect" -> {
|
||||
if (!connectAuth.isCompleted) {
|
||||
connectAuth.complete(frame["params"]?.jsonObject?.get("auth")?.jsonObject)
|
||||
}
|
||||
webSocket.send(connectResponseFrame(id))
|
||||
webSocket.close(1000, "done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
connectNodeSession(
|
||||
session = harness.session,
|
||||
port = server.port,
|
||||
token = null,
|
||||
bootstrapToken = "bootstrap-token",
|
||||
)
|
||||
awaitConnectedOrThrow(connected, lastDisconnect, server)
|
||||
|
||||
val auth = withTimeout(TEST_TIMEOUT_MS) { connectAuth.await() }
|
||||
assertEquals("bootstrap-token", auth?.get("bootstrapToken")?.jsonPrimitive?.content)
|
||||
assertNull(auth?.get("token"))
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connect_prefersStoredDeviceTokenOverBootstrapToken() = runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val connectAuth = CompletableDeferred<JsonObject?>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
val server =
|
||||
startGatewayServer(json) { webSocket, id, method, frame ->
|
||||
when (method) {
|
||||
"connect" -> {
|
||||
if (!connectAuth.isCompleted) {
|
||||
connectAuth.complete(frame["params"]?.jsonObject?.get("auth")?.jsonObject)
|
||||
}
|
||||
webSocket.send(connectResponseFrame(id))
|
||||
webSocket.close(1000, "done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
val deviceId = DeviceIdentityStore(RuntimeEnvironment.getApplication()).loadOrCreate().deviceId
|
||||
harness.deviceAuthStore.saveToken(deviceId, "node", "device-token")
|
||||
|
||||
connectNodeSession(
|
||||
session = harness.session,
|
||||
port = server.port,
|
||||
token = null,
|
||||
bootstrapToken = "bootstrap-token",
|
||||
)
|
||||
awaitConnectedOrThrow(connected, lastDisconnect, server)
|
||||
|
||||
val auth = withTimeout(TEST_TIMEOUT_MS) { connectAuth.await() }
|
||||
assertEquals("device-token", auth?.get("token")?.jsonPrimitive?.content)
|
||||
assertNull(auth?.get("bootstrapToken"))
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nodeInvokeRequest_roundTripsInvokeResult() = runBlocking {
|
||||
val handshakeOrigin = AtomicReference<String?>(null)
|
||||
@@ -182,11 +270,12 @@ class GatewaySessionInvokeTest {
|
||||
): NodeHarness {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
val sessionJob = SupervisorJob()
|
||||
val deviceAuthStore = InMemoryDeviceAuthStore()
|
||||
val session =
|
||||
GatewaySession(
|
||||
scope = CoroutineScope(sessionJob + Dispatchers.Default),
|
||||
identityStore = DeviceIdentityStore(app),
|
||||
deviceAuthStore = InMemoryDeviceAuthStore(),
|
||||
deviceAuthStore = deviceAuthStore,
|
||||
onConnected = { _, _, _ ->
|
||||
if (!connected.isCompleted) connected.complete(Unit)
|
||||
},
|
||||
@@ -197,10 +286,15 @@ class GatewaySessionInvokeTest {
|
||||
onInvoke = onInvoke,
|
||||
)
|
||||
|
||||
return NodeHarness(session = session, sessionJob = sessionJob)
|
||||
return NodeHarness(session = session, sessionJob = sessionJob, deviceAuthStore = deviceAuthStore)
|
||||
}
|
||||
|
||||
private suspend fun connectNodeSession(session: GatewaySession, port: Int) {
|
||||
private suspend fun connectNodeSession(
|
||||
session: GatewaySession,
|
||||
port: Int,
|
||||
token: String? = "test-token",
|
||||
bootstrapToken: String? = null,
|
||||
) {
|
||||
session.connect(
|
||||
endpoint =
|
||||
GatewayEndpoint(
|
||||
@@ -210,7 +304,8 @@ class GatewaySessionInvokeTest {
|
||||
port = port,
|
||||
tlsEnabled = false,
|
||||
),
|
||||
token = "test-token",
|
||||
token = token,
|
||||
bootstrapToken = bootstrapToken,
|
||||
password = null,
|
||||
options =
|
||||
GatewayConnectOptions(
|
||||
|
||||
@@ -8,7 +8,8 @@ import org.junit.Test
|
||||
class GatewayConfigResolverTest {
|
||||
@Test
|
||||
fun resolveScannedSetupCodeAcceptsRawSetupCode() {
|
||||
val setupCode = encodeSetupCode("""{"url":"wss://gateway.example:18789","token":"token-1"}""")
|
||||
val setupCode =
|
||||
encodeSetupCode("""{"url":"wss://gateway.example:18789","bootstrapToken":"bootstrap-1"}""")
|
||||
|
||||
val resolved = resolveScannedSetupCode(setupCode)
|
||||
|
||||
@@ -17,7 +18,8 @@ class GatewayConfigResolverTest {
|
||||
|
||||
@Test
|
||||
fun resolveScannedSetupCodeAcceptsQrJsonPayload() {
|
||||
val setupCode = encodeSetupCode("""{"url":"wss://gateway.example:18789","password":"pw-1"}""")
|
||||
val setupCode =
|
||||
encodeSetupCode("""{"url":"wss://gateway.example:18789","bootstrapToken":"bootstrap-1"}""")
|
||||
val qrJson =
|
||||
"""
|
||||
{
|
||||
@@ -53,6 +55,43 @@ class GatewayConfigResolverTest {
|
||||
assertNull(resolved)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodeGatewaySetupCodeParsesBootstrapToken() {
|
||||
val setupCode =
|
||||
encodeSetupCode("""{"url":"wss://gateway.example:18789","bootstrapToken":"bootstrap-1"}""")
|
||||
|
||||
val decoded = decodeGatewaySetupCode(setupCode)
|
||||
|
||||
assertEquals("wss://gateway.example:18789", decoded?.url)
|
||||
assertEquals("bootstrap-1", decoded?.bootstrapToken)
|
||||
assertNull(decoded?.token)
|
||||
assertNull(decoded?.password)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveGatewayConnectConfigPrefersBootstrapTokenFromSetupCode() {
|
||||
val setupCode =
|
||||
encodeSetupCode("""{"url":"wss://gateway.example:18789","bootstrapToken":"bootstrap-1"}""")
|
||||
|
||||
val resolved =
|
||||
resolveGatewayConnectConfig(
|
||||
useSetupCode = true,
|
||||
setupCode = setupCode,
|
||||
manualHost = "",
|
||||
manualPort = "",
|
||||
manualTls = true,
|
||||
fallbackToken = "shared-token",
|
||||
fallbackPassword = "shared-password",
|
||||
)
|
||||
|
||||
assertEquals("gateway.example", resolved?.host)
|
||||
assertEquals(18789, resolved?.port)
|
||||
assertEquals(true, resolved?.tls)
|
||||
assertEquals("bootstrap-1", resolved?.bootstrapToken)
|
||||
assertNull(resolved?.token?.takeIf { it.isNotEmpty() })
|
||||
assertNull(resolved?.password?.takeIf { it.isNotEmpty() })
|
||||
}
|
||||
|
||||
private fun encodeSetupCode(payloadJson: String): String {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(payloadJson.toByteArray(Charsets.UTF_8))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user