80 lines
3.1 KiB
Kotlin
80 lines
3.1 KiB
Kotlin
/*
|
|
* SPDX-FileCopyrightText: oxmc / PawletOS
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
package os.pawlet.cache
|
|
|
|
import android.util.Log
|
|
import org.json.JSONObject
|
|
import os.pawlet.cache.discovery.DiscoveredCache
|
|
import os.pawlet.cache.trust.CacheTokenVerifier
|
|
import java.io.File
|
|
|
|
/**
|
|
* Discovery policy, resolved from three tiers, most specific wins:
|
|
*
|
|
* 1. [Constants.POLICY_OVERRIDE_PATH] — runtime override, written by
|
|
* pawletprofiled's "content-cache" MDM payload handler (see
|
|
* pawletprofiled-config-schema/schema/profile.schema.yml). Mutable, can
|
|
* arrive any time after first boot.
|
|
* 2. [Constants.VENDOR_POLICY_PATH] — OEM/vendor-baked default, shipped on
|
|
* the read-only vendor partition at build time (see
|
|
* android_packages_apps_PawletCache/vendor-config/README.md). This is
|
|
* how a device builder ships a standing default (e.g. "our fleet
|
|
* defaults to LAN-only cache discovery") without needing an MDM profile
|
|
* pushed after the fact, and doesn't depend on the fork above existing.
|
|
* 3. Compiled-in default — [Constants.DiscoveryMode.BOTH] — if neither
|
|
* file exists.
|
|
*
|
|
* Both files share the same JSON contract (world-readable by design — it's
|
|
* policy, not a secret):
|
|
* ```
|
|
* {
|
|
* "mode": "lan" | "central" | "both" | "disabled",
|
|
* "pinnedServer": { "host": "...", "port": 8443, "token": "<base64 CacheToken>" }
|
|
* }
|
|
* ```
|
|
* `pinnedServer`, if present, skips discovery entirely — whoever wrote the
|
|
* file already knows which server to use. Its token is still
|
|
* signature-verified either way; a config file can pin a *hostname*, not
|
|
* bypass the trust chain.
|
|
*/
|
|
object PolicyOverride {
|
|
|
|
data class Policy(
|
|
val mode: String,
|
|
val pinnedServer: DiscoveredCache?,
|
|
)
|
|
|
|
fun read(): Policy {
|
|
return readFrom(Constants.POLICY_OVERRIDE_PATH)
|
|
?: readFrom(Constants.VENDOR_POLICY_PATH)
|
|
?: Policy(Constants.DiscoveryMode.BOTH, null)
|
|
}
|
|
|
|
/** @return null if the file doesn't exist — distinct from a present-but-empty file. */
|
|
private fun readFrom(path: String): Policy? {
|
|
val file = File(path)
|
|
if (!file.exists()) return null
|
|
|
|
return try {
|
|
val json = JSONObject(file.readText())
|
|
val mode = json.optString("mode", Constants.DiscoveryMode.BOTH)
|
|
val pinned = json.optJSONObject("pinnedServer")?.let { p ->
|
|
val token = CacheTokenVerifier.verify(p.getString("token")) ?: run {
|
|
Log.w(Constants.TAG, "$path's pinnedServer token failed verification, ignoring pin")
|
|
return@let null
|
|
}
|
|
DiscoveredCache(p.getString("host"), p.getInt("port"), token, source = "policy")
|
|
}
|
|
Policy(mode, pinned)
|
|
} catch (e: Exception) {
|
|
Log.w(Constants.TAG, "Malformed policy override at $path", e)
|
|
// A malformed file at this tier should not silently fall through
|
|
// to a lower-priority tier — it's a real config error the
|
|
// OEM/admin should fix, not a "file doesn't exist" case.
|
|
Policy(Constants.DiscoveryMode.BOTH, null)
|
|
}
|
|
}
|
|
}
|