Files

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)
}
}
}