Fix content cache and clone-host safety
This commit is contained in:
@@ -90,7 +90,7 @@ class ApkBuildCache(private val context: Context) {
|
||||
}
|
||||
|
||||
fun shellTemplateId(templateApk: File): String {
|
||||
return "name=${templateApk.name}|size=${templateApk.length()}"
|
||||
return "sha256=${fileSha256(templateApk)}"
|
||||
}
|
||||
|
||||
fun plan(
|
||||
@@ -388,27 +388,44 @@ class ApkBuildCache(private val context: Context) {
|
||||
private fun fileFingerprint(path: String?): String {
|
||||
if (path.isNullOrBlank()) return "none"
|
||||
val file = File(path)
|
||||
if (!file.isFile) return "missing:$path"
|
||||
return "file:${file.absolutePath}|${file.length()}|${file.lastModified()}"
|
||||
if (!file.isFile) return "missing"
|
||||
return "sha256=${fileSha256(file)}"
|
||||
}
|
||||
|
||||
private fun treeFingerprint(dir: File): String {
|
||||
if (!dir.isDirectory) return "missing:${dir.absolutePath}"
|
||||
if (!dir.isDirectory) return "missing"
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
dir.walkTopDown()
|
||||
.filter { it.isFile }
|
||||
.sortedBy { it.relativeTo(dir).path }
|
||||
.sortedBy { it.relativeTo(dir).invariantSeparatorsPath }
|
||||
.forEach { file ->
|
||||
val rel = file.relativeTo(dir).path
|
||||
digest.update(rel.toByteArray())
|
||||
digest.update(file.length().toString().toByteArray())
|
||||
digest.update(file.lastModified().toString().toByteArray())
|
||||
val relativePath = file.relativeTo(dir).invariantSeparatorsPath
|
||||
digest.update(relativePath.toByteArray(Charsets.UTF_8))
|
||||
digest.update(0)
|
||||
digest.update(fileSha256(file).toByteArray(Charsets.UTF_8))
|
||||
digest.update(0)
|
||||
}
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||
return digest.digest().toHexString()
|
||||
}
|
||||
|
||||
private fun fileSha256(file: File): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
file.inputStream().buffered().use { input ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
while (true) {
|
||||
val read = input.read(buffer)
|
||||
if (read < 0) break
|
||||
digest.update(buffer, 0, read)
|
||||
}
|
||||
}
|
||||
return digest.digest().toHexString()
|
||||
}
|
||||
|
||||
private fun sha256(text: String): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
return digest.digest(text.toByteArray()).joinToString("") { "%02x".format(it) }
|
||||
return digest.digest(text.toByteArray(Charsets.UTF_8)).toHexString()
|
||||
}
|
||||
|
||||
private fun ByteArray.toHexString(): String =
|
||||
joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
@@ -259,6 +259,14 @@ class AppCloner(private val context: Context) {
|
||||
|
||||
safeProgress(20, "修改包名和应用名...")
|
||||
|
||||
val cloneHostDex = loadCloneHostDex()
|
||||
if (hasModifications && cloneHostDex == null) {
|
||||
AppLogger.w("AppCloner", "Clone-host enhancements requested but clone_host.dex is unavailable")
|
||||
return@withContext AppModifyResult.Error(
|
||||
"启动页、激活和公告增强当前不可用;请移除这些增强设置后重试"
|
||||
)
|
||||
}
|
||||
|
||||
var iconBitmap: Bitmap? = null
|
||||
try {
|
||||
iconBitmap = config.newIconPath?.let { loadBitmapFromPath(it) }
|
||||
@@ -267,11 +275,6 @@ class AppCloner(private val context: Context) {
|
||||
AppLogger.e("AppCloner", "Failed to load icon: ${e.message}")
|
||||
}
|
||||
|
||||
val cloneHostDex = loadCloneHostDex()
|
||||
if (hasModifications && cloneHostDex == null) {
|
||||
AppLogger.w("AppCloner", "Modifications requested but clone_host.dex not found; proceeding without injection")
|
||||
}
|
||||
|
||||
val splashMediaPath = if (CloneConfigBuilder.needsSplashMedia(config)) {
|
||||
CloneConfigBuilder.getSplashMediaPath(config)
|
||||
} else null
|
||||
|
||||
@@ -73,17 +73,17 @@ class ApkBuildCacheTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shell template id changes with size but not mtime`() {
|
||||
fun `shell template id tracks content rather than mtime or size`() {
|
||||
val cache = ApkBuildCache(RuntimeEnvironment.getApplication())
|
||||
val file = File(RuntimeEnvironment.getApplication().cacheDir, "t.apk")
|
||||
file.writeBytes(ByteArray(10))
|
||||
val a = cache.shellTemplateId(file)
|
||||
file.writeBytes(byteArrayOf(1, 2, 3, 4))
|
||||
val initial = cache.shellTemplateId(file)
|
||||
|
||||
file.setLastModified(file.lastModified() + 60_000L)
|
||||
val same = cache.shellTemplateId(file)
|
||||
assertThat(same).isEqualTo(a)
|
||||
file.writeBytes(ByteArray(20))
|
||||
val b = cache.shellTemplateId(file)
|
||||
assertThat(a).isNotEqualTo(b)
|
||||
assertThat(cache.shellTemplateId(file)).isEqualTo(initial)
|
||||
|
||||
file.writeBytes(byteArrayOf(4, 3, 2, 1))
|
||||
assertThat(cache.shellTemplateId(file)).isNotEqualTo(initial)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -119,7 +119,7 @@ class CloneLauncherActivity : Activity() {
|
||||
|
||||
if (cfg.remoteActivationEnabled) {
|
||||
Thread {
|
||||
val activated = isActivated(appId) && checkRemoteActivation(cfg)
|
||||
val activated = isActivated(appId) && checkRemoteActivation(cfg, appId)
|
||||
handler.post {
|
||||
if (activated) {
|
||||
proceedAfterActivation(cfg)
|
||||
@@ -181,7 +181,7 @@ class CloneLauncherActivity : Activity() {
|
||||
return@setOnClickListener
|
||||
}
|
||||
Thread {
|
||||
val result = verifyCode(cfg, code)
|
||||
val result = verifyCode(cfg, code, appId)
|
||||
handler.post {
|
||||
if (result) {
|
||||
saveActivation(appId)
|
||||
@@ -195,9 +195,9 @@ class CloneLauncherActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyCode(cfg: CloneConfig, inputCode: String): Boolean {
|
||||
private fun verifyCode(cfg: CloneConfig, inputCode: String, appId: Long): Boolean {
|
||||
if (cfg.remoteActivationEnabled) {
|
||||
return verifyRemoteCode(cfg, inputCode)
|
||||
return verifyRemoteCode(cfg, inputCode, appId)
|
||||
}
|
||||
val normalized = normalizeCode(inputCode)
|
||||
return cfg.activationCodes.any { validCode ->
|
||||
@@ -207,55 +207,64 @@ class CloneLauncherActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyRemoteCode(cfg: CloneConfig, inputCode: String): Boolean {
|
||||
private fun verifyRemoteCode(cfg: CloneConfig, inputCode: String, appId: Long): Boolean {
|
||||
return try {
|
||||
val deviceId = generateDeviceId()
|
||||
val normalized = normalizeCode(inputCode)
|
||||
val conn = (URL(cfg.remoteVerifyUrl).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
connectTimeout = 10000
|
||||
readTimeout = 10000
|
||||
}
|
||||
val requestBody = """{"code":"$normalized","deviceId":"$deviceId","packageName":"$packageName"}"""
|
||||
conn.outputStream.use { it.write(requestBody.toByteArray()) }
|
||||
if (conn.responseCode == 200) {
|
||||
val conn = openRemoteConnection(cfg.remoteVerifyUrl, 10_000) ?: return offlineAllowed(cfg, appId)
|
||||
val requestBody = JSONObject()
|
||||
.put("code", normalizeCode(inputCode))
|
||||
.put("deviceId", generateDeviceId())
|
||||
.put("packageName", packageName)
|
||||
.toString()
|
||||
conn.outputStream.use { it.write(requestBody.toByteArray(Charsets.UTF_8)) }
|
||||
if (conn.responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val response = conn.inputStream.bufferedReader().use { it.readText() }
|
||||
val result = JSONObject(response)
|
||||
result.optBoolean("success", false)
|
||||
JSONObject(response).optBoolean("success", false)
|
||||
} else {
|
||||
cfg.remoteOfflinePolicy == "ALLOW"
|
||||
offlineAllowed(cfg, appId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cfg.remoteOfflinePolicy == "ALLOW" || cfg.remoteOfflinePolicy == "ALLOW_CACHED"
|
||||
} catch (_: Exception) {
|
||||
offlineAllowed(cfg, appId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRemoteActivation(cfg: CloneConfig): Boolean {
|
||||
private fun checkRemoteActivation(cfg: CloneConfig, appId: Long): Boolean {
|
||||
return try {
|
||||
val deviceId = generateDeviceId()
|
||||
val conn = (URL(cfg.remoteVerifyUrl).openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
connectTimeout = 5000
|
||||
readTimeout = 5000
|
||||
}
|
||||
val requestBody = """{"code":"","deviceId":"$deviceId","packageName":"$packageName","checkOnly":true}"""
|
||||
conn.outputStream.use { it.write(requestBody.toByteArray()) }
|
||||
if (conn.responseCode == 200) {
|
||||
val conn = openRemoteConnection(cfg.remoteVerifyUrl, 5_000) ?: return offlineAllowed(cfg, appId)
|
||||
val requestBody = JSONObject()
|
||||
.put("code", "")
|
||||
.put("deviceId", generateDeviceId())
|
||||
.put("packageName", packageName)
|
||||
.put("checkOnly", true)
|
||||
.toString()
|
||||
conn.outputStream.use { it.write(requestBody.toByteArray(Charsets.UTF_8)) }
|
||||
if (conn.responseCode == HttpURLConnection.HTTP_OK) {
|
||||
val response = conn.inputStream.bufferedReader().use { it.readText() }
|
||||
val result = JSONObject(response)
|
||||
result.optBoolean("success", false)
|
||||
JSONObject(response).optBoolean("success", false)
|
||||
} else {
|
||||
cfg.remoteOfflinePolicy == "ALLOW_CACHED"
|
||||
offlineAllowed(cfg, appId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
cfg.remoteOfflinePolicy == "ALLOW_CACHED" || cfg.remoteOfflinePolicy == "ALLOW"
|
||||
} catch (_: Exception) {
|
||||
offlineAllowed(cfg, appId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openRemoteConnection(url: String, timeoutMs: Int): HttpURLConnection? {
|
||||
if (!url.startsWith("https://", ignoreCase = true)) return null
|
||||
return (URL(url).openConnection() as? HttpURLConnection)?.apply {
|
||||
requestMethod = "POST"
|
||||
doOutput = true
|
||||
setRequestProperty("Content-Type", "application/json")
|
||||
connectTimeout = timeoutMs
|
||||
readTimeout = timeoutMs
|
||||
}
|
||||
}
|
||||
|
||||
private fun offlineAllowed(cfg: CloneConfig, appId: Long): Boolean = when (cfg.remoteOfflinePolicy) {
|
||||
"ALLOW" -> true
|
||||
"ALLOW_CACHED" -> isActivated(appId)
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun proceedAfterActivation(cfg: CloneConfig) {
|
||||
if (cfg.announcementEnabled && cfg.announcementTitle.isNotBlank()) {
|
||||
showAnnouncement(cfg)
|
||||
|
||||
Reference in New Issue
Block a user