Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions app/src/main/java/net/megaproxy487/data/ConfigStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ class ConfigStore(context: Context) {
val missing = existing.filter { it.id !in importedById }
requireUi(merged.isNotEmpty()) { UiException(R.string.error_config_no_usable) }
val editor = prefs.edit().putString(PROFILES, encodeProfiles(mergeResolvedProfiles(existing, merged, added)))
.putInt(DIAGNOSTIC_LOG_LIMIT_MB, configuration.diagnosticLogLimitMb)
if (configuration.diagnosticLogLimitPresent) editor.putInt(DIAGNOSTIC_LOG_LIMIT_MB, configuration.diagnosticLogLimitMb)
if (existing.isEmpty()) {
val first = merged.first().id
editor.putString(ACTIVE_PROFILE_ID, first)
Expand All @@ -316,10 +316,11 @@ class ConfigStore(context: Context) {
}
configuration.activeProfileId?.takeIf(importedById::containsKey)?.let { editor.putString(ACTIVE_PROFILE_ID, it) }
configuration.alwaysOnProfileId?.takeIf(importedById::containsKey)?.let { editor.putString(ALWAYS_ON_PROFILE_ID, it) }
configuration.globalConnectionSettings?.let { importedSettings ->
configuration.globalConnectionSettings?.let {
val importedSettings = configuration.mergeGlobalSettings(globalConnectionSettings())
editor.putBoolean(IPV6_PROFILE_MIGRATED, true)
.putString(GLOBAL_CONNECTION_SETTINGS, encodeGlobalConnectionSettings(importedSettings.copy(
failoverProfileIds = importedSettings.failoverProfileIds.filter(importedById::containsKey),
failoverProfileIds = importedSettings.failoverProfileIds.filter { it in importedById || it in existingById },
)))
}
if (configuration.subscriptionPresent) {
Expand Down Expand Up @@ -385,7 +386,7 @@ class ConfigStore(context: Context) {
requireUi(ids.size == merged.size) { UiException(R.string.error_config_duplicate_ids) }
val oldGlobal = globalConnectionSettings()
val oldConnection = oldGlobal.applyTo(connectionProfile().config)
val nextGlobal = (configuration.globalConnectionSettings ?: oldGlobal).let {
val nextGlobal = configuration.mergeGlobalSettings(oldGlobal).let {
it.copy(failoverProfileIds = it.failoverProfileIds.filter(ids::contains))
}
val preferred = configuration.activeProfileId?.takeIf { id -> incoming.any { it.id == id } } ?: incoming.first().id
Expand All @@ -399,7 +400,7 @@ class ConfigStore(context: Context) {
.putString(CONNECTION_PROFILE_ID, nextConnection)
.putString(GLOBAL_CONNECTION_SETTINGS, encodeGlobalConnectionSettings(nextGlobal))
.putBoolean(IPV6_PROFILE_MIGRATED, true)
if (configuration.globalConnectionSettings != null) editor.putInt(DIAGNOSTIC_LOG_LIMIT_MB, configuration.diagnosticLogLimitMb)
if (configuration.diagnosticLogLimitPresent) editor.putInt(DIAGNOSTIC_LOG_LIMIT_MB, configuration.diagnosticLogLimitMb)
if ((connectionProfileId() !in ids || alwaysOnProfileId() !in ids) && isFailoverActive()) {
editor.putBoolean(FAILOVER_ACTIVE, false).remove(FAILOVER_NOTICE)
}
Expand Down
31 changes: 29 additions & 2 deletions app/src/main/java/net/megaproxy487/data/ConfigTransfer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,29 @@ data class PortableConfiguration(
val notice: ConfigImportNotice = ConfigImportNotice(),
val subscriptionPresent: Boolean = false,
val subscription: ConfigSubscription? = null,
)
val globalSettingFields: Set<String>? = null,
val diagnosticLogLimitPresent: Boolean = true,
) {
internal fun mergeGlobalSettings(current: GlobalConnectionSettings): GlobalConnectionSettings {
val incoming = globalConnectionSettings ?: return current
val fields = globalSettingFields ?: return incoming
return current.copy(
tlsProfile = if ("tls.fingerprint" in fields) incoming.tlsProfile else current.tlsProfile,
customJa3 = if ("tls.customJa3" in fields) incoming.customJa3 else current.customJa3,
sshProfile = if ("ssh.fingerprint" in fields) incoming.sshProfile else current.sshProfile,
sshAuthMode = if ("ssh.authMode" in fields) incoming.sshAuthMode else current.sshAuthMode,
sshKeepaliveSeconds = if ("ssh.keepaliveSeconds" in fields) incoming.sshKeepaliveSeconds else current.sshKeepaliveSeconds,
sshMaxChannels = if ("ssh.maxChannels" in fields) incoming.sshMaxChannels else current.sshMaxChannels,
sshRotationMinutes = if ("ssh.rotationMinutes" in fields) incoming.sshRotationMinutes else current.sshRotationMinutes,
sshRotationMb = if ("ssh.rotationMb" in fields) incoming.sshRotationMb else current.sshRotationMb,
failoverMode = if ("failover.mode" in fields) incoming.failoverMode else current.failoverMode,
failoverProfileIds = if ("failover.profileIds" in fields) incoming.failoverProfileIds else current.failoverProfileIds,
selectedPackages = if ("routing.selectedPackages" in fields) incoming.selectedPackages else current.selectedPackages,
routeAllApps = if ("routing.routeAllApps" in fields) incoming.routeAllApps else current.routeAllApps,
bypassLocalNetworks = if ("routing.bypassLocalNetworks" in fields) incoming.bypassLocalNetworks else current.bypassLocalNetworks,
)
}
}

data class ProfileSecretPresence(
val password: Boolean = false,
Expand Down Expand Up @@ -137,7 +159,12 @@ object ConfigTransfer {
val profiles = if (version in 2..5) decodedProfiles.map {
it.copy(config = it.config.copy(allowIpv6 = legacyIpv6))
} else decodedProfiles
val globalFields = listOf("tls", "ssh", "failover", "routing").flatMap { group ->
root.optJSONObject(group)?.keys()?.asSequence()?.map { "$group.$it" }?.toList().orEmpty()
}.toSet()
return PortableConfiguration(
globalSettingFields = globalFields + if (version in 2..3) setOf("ssh.fingerprint") else emptySet(),
diagnosticLogLimitPresent = root.has("diagnosticLogLimitMb"),
notice = ConfigImportNotices.inspect(root),
subscriptionPresent = acceptSubscription && root.has("subscription"),
subscription = if (!acceptSubscription || !root.has("subscription") || root.isNull("subscription")) null
Expand All @@ -147,7 +174,7 @@ object ConfigTransfer {
activeProfileId = root.optString("activeProfileId").ifBlank { null },
alwaysOnProfileId = root.optString("alwaysOnProfileId").ifBlank { null },
diagnosticLogLimitMb = root.optInt("diagnosticLogLimitMb", 3).coerceIn(1, 100),
globalConnectionSettings = if (version >= 2) {
globalConnectionSettings = if (version >= 2 && (globalFields.isNotEmpty() || version < 4)) {
decodeGlobalSettings(root).let { global ->
if (version < 4) global.copy(sshProfile = profiles.first().config.sshProfile) else global
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,52 @@ class ConfigSubscriptionIntegrationTest {
private fun apply(value: PortableConfiguration, running: Boolean = false) = store.applySubscriptionSnapshot(
store.subscriptionState()!!.generation, value, 0, 1000, running)

@Test fun omittedAndPartialGlobalsPreserveLocalSettingsOnImportAndRefresh() {
bootstrap(profile("one"))
store.setConnectionProfile("one")
val local = GlobalConnectionSettings(
tlsProfile = TlsProfile.FIREFOX_ANDROID, customJa3 = "local-ja3",
sshProfile = SshProfile.OPENSSH_TERMUX, sshAuthMode = SshAuthMode.KEY_ONLY,
sshKeepaliveSeconds = 90, sshMaxChannels = 16, sshRotationMinutes = 10, sshRotationMb = 128,
failoverMode = FailoverMode.SELECTED, failoverProfileIds = listOf("one"),
routeAllApps = false, selectedPackages = setOf("org.example.app"), bypassLocalNetworks = false,
)
store.saveGlobalConnectionSettings(local)
store.setDiagnosticLogLimitMb(20)
val json = document(profile("one"))
val minimal = parseSubscriptionSnapshot(json.toString(), store.profiles())
assertNull(minimal.globalConnectionSettings)
assertTrue(apply(minimal, running = true))
assertEquals(local, ConfigStore(context).globalConnectionSettings())
assertEquals(20, store.diagnosticLogLimitMb())
assertFalse(store.hasPendingReconnect())
store.importConfiguration(ConfigTransfer.importJson(json.toString()))
assertEquals(local, store.globalConnectionSettings())
assertEquals(20, store.diagnosticLogLimitMb())

json.put("tls", JSONObject().put("fingerprint", "CHROME_ANDROID"))
json.put("routing", JSONObject().put("bypassLocalNetworks", true))
val partial = parseSubscriptionSnapshot(json.toString(), store.profiles())
// Resolve against storage at commit time, including edits made after parsing.
val newer = local.copy(selectedPackages = setOf("org.example.new"))
store.saveGlobalConnectionSettings(newer)
assertTrue(apply(partial, running = true))
val expected = newer.copy(tlsProfile = TlsProfile.CHROME_ANDROID, bypassLocalNetworks = true)
assertEquals(expected, ConfigStore(context).globalConnectionSettings())
assertEquals(20, store.diagnosticLogLimitMb())
assertTrue(store.hasPendingReconnect())
store.saveGlobalConnectionSettings(newer)
store.importConfiguration(ConfigTransfer.importJson(json.toString()))
assertEquals(expected, store.globalConnectionSettings())

json.put("routing", JSONObject().put("routeAllApps", true).put("selectedPackages", JSONArray()))
json.put("diagnosticLogLimitMb", 8)
assertTrue(apply(parseSubscriptionSnapshot(json.toString(), store.profiles())))
assertTrue(store.globalConnectionSettings().routeAllApps)
assertTrue(store.globalConnectionSettings().selectedPackages.isEmpty())
assertEquals(8, store.diagnosticLogLimitMb())
}

@Test fun updateDuringStartupRequiresReconnectAndNotificationAfterCommit() {
bootstrap(profile("one"))
store.setConnectionProfile("one")
Expand Down
Loading