fix: guard global ABSDK session state
This commit is contained in:
@@ -208,13 +208,18 @@ class AbsdkBridge internal constructor(
|
|||||||
private inline fun <T> withActiveSession(
|
private inline fun <T> withActiveSession(
|
||||||
profileId: ProviderProfileId,
|
profileId: ProviderProfileId,
|
||||||
block: () -> DomainResult<T>,
|
block: () -> DomainResult<T>,
|
||||||
): DomainResult<T> = if (hasActiveSession(profileId)) {
|
): DomainResult<T> {
|
||||||
block()
|
if (!hasActiveSession(profileId)) {
|
||||||
} else {
|
return DomainResult.Failure(
|
||||||
DomainResult.Failure(
|
|
||||||
DomainError.TokenInvalid(detail = "No active ABSDK session for this provider"),
|
DomainError.TokenInvalid(detail = "No active ABSDK session for this provider"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return block().also { result ->
|
||||||
|
if (result is DomainResult.Failure && result.error is DomainError.TokenInvalid) {
|
||||||
|
activeSession.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasActiveSession(profileId: ProviderProfileId): Boolean =
|
private fun hasActiveSession(profileId: ProviderProfileId): Boolean =
|
||||||
activeSession.value?.profileId == profileId
|
activeSession.value?.profileId == profileId
|
||||||
@@ -299,11 +304,17 @@ class AbsdkBridge internal constructor(
|
|||||||
private const val DISABLED_STATUS = "0"
|
private const val DISABLED_STATUS = "0"
|
||||||
private const val ENABLED_STATUS = "1"
|
private const val ENABLED_STATUS = "1"
|
||||||
|
|
||||||
fun create(): AbsdkBridge = AbsdkBridge(
|
@OptIn(CleartextAbsdkTransport::class)
|
||||||
|
private val processInstance: AbsdkBridge by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||||
|
AbsdkBridge(
|
||||||
client = VendorAbsdkClient.create(),
|
client = VendorAbsdkClient.create(),
|
||||||
dispatcher = Dispatchers.IO,
|
dispatcher = Dispatchers.IO,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CleartextAbsdkTransport
|
||||||
|
fun create(): AbsdkBridge = processInstance
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val SUCCESS_CODE = "00000"
|
private const val SUCCESS_CODE = "00000"
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.flagship.abox.manager.absdk.bridge
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks construction of the original ABSDK 0630 client, whose base endpoint is hardcoded to HTTP.
|
||||||
|
*
|
||||||
|
* Opting in acknowledges that transport confidentiality and integrity are not provided by the
|
||||||
|
* vendor JAR. App integration must remain disabled until the deployment has an approved transport
|
||||||
|
* control and the security limitation is presented to the responsible integrator.
|
||||||
|
*/
|
||||||
|
@RequiresOptIn(
|
||||||
|
message = "ABSDK 0630 uses a hardcoded cleartext HTTP transport. " +
|
||||||
|
"Do not enable it without an approved transport control.",
|
||||||
|
level = RequiresOptIn.Level.ERROR,
|
||||||
|
)
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.CLASS,
|
||||||
|
AnnotationTarget.CONSTRUCTOR,
|
||||||
|
AnnotationTarget.FUNCTION,
|
||||||
|
AnnotationTarget.PROPERTY,
|
||||||
|
)
|
||||||
|
annotation class CleartextAbsdkTransport
|
||||||
+1
@@ -4,6 +4,7 @@ import com.dhc.absdk.ABRet
|
|||||||
import com.dhc.absdk.ABSDK
|
import com.dhc.absdk.ABSDK
|
||||||
import com.dhc.absdk.IrInfo
|
import com.dhc.absdk.IrInfo
|
||||||
|
|
||||||
|
@CleartextAbsdkTransport
|
||||||
internal class VendorAbsdkClient private constructor(
|
internal class VendorAbsdkClient private constructor(
|
||||||
private val sdk: ABSDK,
|
private val sdk: ABSDK,
|
||||||
) : AbsdkClient {
|
) : AbsdkClient {
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package com.flagship.abox.manager.absdk.bridge
|
||||||
|
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@OptIn(CleartextAbsdkTransport::class)
|
||||||
|
class AbsdkFactoryTest {
|
||||||
|
@Test
|
||||||
|
fun productionFactoryReturnsOneProcessInstance() {
|
||||||
|
assertSame(AbsdkBridge.create(), AbsdkBridge.create())
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -91,6 +91,25 @@ class AbsdkSessionBridgeTest {
|
|||||||
assertNull(bridge.observeSession(profile).first())
|
assertNull(bridge.observeSession(profile).first())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deviceTokenErrorClearsSessionAndBlocksFurtherCalls() = runTest {
|
||||||
|
val client = ScriptedAbsdkClient().apply {
|
||||||
|
socketStatusResponses = ArrayDeque(
|
||||||
|
listOf(ScriptedAbsdkClient.failure("10002", "expired")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val bridge = bridge(client)
|
||||||
|
bridge.login(profile, credentials)
|
||||||
|
|
||||||
|
val first = bridge.getStatus(profile, DeviceName("socket"))
|
||||||
|
|
||||||
|
assertTrue((first as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||||
|
assertNull(bridge.observeSession(profile).first())
|
||||||
|
val second = bridge.getStatus(profile, DeviceName("socket"))
|
||||||
|
assertTrue((second as DomainResult.Failure).error is DomainError.TokenInvalid)
|
||||||
|
assertEquals(listOf("login:demo", "socket-status:socket"), client.calls)
|
||||||
|
}
|
||||||
|
|
||||||
private fun bridge(client: ScriptedAbsdkClient): AbsdkBridge =
|
private fun bridge(client: ScriptedAbsdkClient): AbsdkBridge =
|
||||||
AbsdkBridge(client, Dispatchers.Unconfined)
|
AbsdkBridge(client, Dispatchers.Unconfined)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,9 +102,11 @@ Fake 只用于开发和测试,不能作为真实 ABox 或真实硬件验收证
|
|||||||
- 不提供设备目录;
|
- 不提供设备目录;
|
||||||
- 不调用未确认的 `setHostInfo`;
|
- 不调用未确认的 `setHostInfo`;
|
||||||
- logout 只清除 Bridge 本地会话,因为 JAR 没有公开 logout 或 Token setter;
|
- logout 只清除 Bridge 本地会话,因为 JAR 没有公开 logout 或 Token setter;
|
||||||
|
- 生产工厂始终返回进程内同一个 Bridge,使互斥锁和本地会话与厂商单例保持一致;
|
||||||
|
- 真实工厂标记为 `@CleartextAbsdkTransport`,调用方必须显式 opt-in 承认厂商 JAR 的硬编码 HTTP 风险;
|
||||||
- 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制。
|
- 当前直接引用归档 Sample 目录中的唯一一份 JAR,不复制厂商二进制。
|
||||||
|
|
||||||
Bridge 本轮尚未加入 `android/app` 依赖,App debug 仍绑定 Fake。提供方选择、安全凭据和真实 Bridge 绑定由后续 `feat/provider-login` 完成。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。
|
Bridge 本轮尚未加入 `android/app` 依赖,App debug 仍绑定 Fake。提供方选择、安全凭据和真实 Bridge 绑定由后续 `feat/provider-login` 完成;在获批的传输控制方案明确前,不得在 App 中启用真实 Bridge。协议证据与限制记录在 `docs/protocol/absdk-0630.md`。
|
||||||
|
|
||||||
## 9. 后续边界
|
## 9. 后续边界
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ commons-logging-1.1.3.jar
|
|||||||
|
|
||||||
JAR 内含宽松的 TLS 信任实现。正式产品不得把这一历史传输行为扩展为 App 其他网络请求的安全策略,也不得记录密码、Token、API Key 或敏感红外载荷。
|
JAR 内含宽松的 TLS 信任实现。正式产品不得把这一历史传输行为扩展为 App 其他网络请求的安全策略,也不得记录密码、Token、API Key 或敏感红外载荷。
|
||||||
|
|
||||||
|
真实 Bridge 工厂使用 `@CleartextAbsdkTransport` 作为编译期 opt-in 门禁,并且当前没有接入 Android App。该标记只防止误接入,不会为链路提供加密。生产集成前必须确定并批准 HTTPS、受控加密代理或其他能够提供机密性与完整性的传输控制;在此之前不得启用真实 ABox 入口。
|
||||||
|
|
||||||
## 6. `setHostInfo` 的已确认行为
|
## 6. `setHostInfo` 的已确认行为
|
||||||
|
|
||||||
`setHostInfo(url, hotelName, roomNo, apikey)` 向以下端点发送初始化请求:
|
`setHostInfo(url, hotelName, roomNo, apikey)` 向以下端点发送初始化请求:
|
||||||
|
|||||||
Reference in New Issue
Block a user