aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/src/main/java/invalid/lena/rsend/Config.kt16
-rw-r--r--app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt8
-rw-r--r--app/src/main/java/invalid/lena/rsend/RemoteActivity.kt5
-rw-r--r--app/src/test/java/invalid/lena/rsend/ConfigTest.kt57
4 files changed, 85 insertions, 1 deletions
diff --git a/app/src/main/java/invalid/lena/rsend/Config.kt b/app/src/main/java/invalid/lena/rsend/Config.kt
index 25bf338..a38fbde 100644
--- a/app/src/main/java/invalid/lena/rsend/Config.kt
+++ b/app/src/main/java/invalid/lena/rsend/Config.kt
@@ -57,6 +57,9 @@ object RemoteRules {
if (remote.hostKey.length > MAX_HOST_KEY_CHARS) return false
val parts = remote.hostKey.split(' ')
if (parts.size != 3 || parts.any(String::isEmpty)) return false
+ // Must be exactly the address rsh writes, which is knownhosts.Normalize:
+ // the bare host on port 22, including an IPv6 literal, "[host]:port"
+ // otherwise. A different form here silently stops matching the pin.
val address = if (remote.port == 22) remote.host else "[${remote.host}]:${remote.port}"
if (parts[0] != address || parts[1] !in hostKeyTypes) return false
return try {
@@ -187,6 +190,19 @@ data class Config(
}
}
+ // overLimit reports why c cannot be persisted, or null. These are the
+ // only save failures a form cannot rule out on its own, so the editors
+ // ask before adding an entry and report the answer. Every other rule
+ // fromJson enforces is already checked field by field; one firing there
+ // is a bug and must throw.
+ fun overLimit(c: Config): String? = when {
+ c.remotes.size > MAX_REMOTES -> "Too many remotes; the limit is $MAX_REMOTES."
+ c.folders.size > MAX_FOLDERS -> "Too many folders; the limit is $MAX_FOLDERS."
+ c.toJson().toString(2).toByteArray(Charsets.UTF_8).size > MAX_CONFIG_BYTES ->
+ "Configuration is too large; the limit is $MAX_CONFIG_BYTES bytes."
+ else -> null
+ }
+
// legacyShape reports whether o is the pre-0.2 config 0.1.x wrote: one
// "remote" object instead of a "remotes" array.
fun legacyShape(o: JSONObject): Boolean =
diff --git a/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt b/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
index 5565bff..47961c1 100644
--- a/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
+++ b/app/src/main/java/invalid/lena/rsend/FolderEditActivity.kt
@@ -161,7 +161,13 @@ class FolderEditActivity : AppCompatActivity() {
}
val list = cfg.folders.toMutableList()
if (index in list.indices) list[index] = f else list.add(f)
- Config.save(this, cfg.copy(folders = list))
+ val next = cfg.copy(folders = list)
+ val limit = Config.overLimit(next)
+ if (limit != null) {
+ Toast.makeText(this, limit, Toast.LENGTH_LONG).show()
+ return false
+ }
+ Config.save(this, next)
return true
}
diff --git a/app/src/main/java/invalid/lena/rsend/RemoteActivity.kt b/app/src/main/java/invalid/lena/rsend/RemoteActivity.kt
index 994e233..83f499d 100644
--- a/app/src/main/java/invalid/lena/rsend/RemoteActivity.kt
+++ b/app/src/main/java/invalid/lena/rsend/RemoteActivity.kt
@@ -130,6 +130,11 @@ class RemoteActivity : AppCompatActivity() {
"folder \"${b.name.ifEmpty { b.remotePath }}\"."
return -1
}
+ val limit = Config.overLimit(next)
+ if (limit != null) {
+ status.text = limit
+ return -1
+ }
Config.save(this, next)
Scheduler.apply(this)
return at
diff --git a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
index 6b6dfdc..e289afd 100644
--- a/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
+++ b/app/src/test/java/invalid/lena/rsend/ConfigTest.kt
@@ -308,6 +308,30 @@ class ConfigTest {
assertFalse(RemoteRules.hostKeyAllowed(Remote("nas", "home", 22, "backup", "home ssh-ed25519 not-base64")))
}
+ // The address field of a pin must be exactly what rsh writes, which is
+ // knownhosts.Normalize: the bare host on port 22 (an IPv6 literal included),
+ // "[host]:port" otherwise. Bracketing IPv6 on port 22 looks right and would
+ // make every IPv6 pin fail to validate.
+ @Test
+ fun hostPinAddressMatchesTheKnownHostsForm() {
+ val key = "ssh-ed25519 AAAA"
+ val cases = listOf(
+ "home.example.net" to 22,
+ "home.example.net" to 2222,
+ "2001:db8::1" to 22,
+ "2001:db8::1" to 2222,
+ "fe80::1%wlan0" to 22,
+ )
+ for ((host, port) in cases) {
+ val address = if (port == 22) host else "[$host]:$port"
+ assertTrue(
+ "$host:$port",
+ RemoteRules.hostKeyAllowed(Remote("nas", host, port, "backup", "$address $key")),
+ )
+ }
+ assertFalse(RemoteRules.hostKeyAllowed(Remote("nas", "2001:db8::1", 22, "backup", "[2001:db8::1] $key")))
+ }
+
@Test
fun configRejectsCoercedAndUnknownFields() {
val badRemotes = Config().toJson().put("remotes", "not an array")
@@ -370,4 +394,37 @@ class ConfigTest {
} catch (_: IllegalArgumentException) {
}
}
+
+ // The editors ask before adding an entry, because these three limits are
+ // the ones a form cannot see. Reporting them is what keeps save from
+ // throwing at a user who has done nothing wrong.
+ @Test
+ fun limitsAreReportedRatherThanThrown() {
+ val remote = Remote("nas", "nas", 22, "backup", "nas ssh-ed25519 AAAA")
+ val folder = Folder("f", "/a", "nas", "backup")
+ assertEquals(null, Config.overLimit(Config(remotes = listOf(remote), folders = listOf(folder))))
+
+ val remotes = (1..65).map { remote.copy(name = "nas$it", hostKey = "") }
+ assertTrue(Config.overLimit(Config(remotes = remotes))!!.contains("Too many remotes"))
+
+ val folders = (1..257).map { folder.copy(name = "f$it", remotePath = "backup/$it") }
+ assertTrue(
+ Config.overLimit(Config(remotes = listOf(remote), folders = folders))!!
+ .contains("Too many folders")
+ )
+
+ // Field limits alone allow far more than the file limit: 40 folders of
+ // maximum-length excludes already exceed it.
+ val fat = (1..40).map {
+ folder.copy(
+ name = "f$it",
+ remotePath = "backup/$it",
+ excludes = List(FolderRules.MAX_EXCLUDES) { i -> "x".repeat(512 - "$i".length) + i },
+ )
+ }
+ assertTrue(
+ Config.overLimit(Config(remotes = listOf(remote), folders = fat))!!
+ .contains("too large")
+ )
+ }
}