Skip to content

Commit 6d2c9ff

Browse files
committed
fix: the URL_PATTERN regex had a nested quantifier (/[-a-z\d%_.~+]*)* that caused catastrophic backtracking in the ICU regex engine on Android, running on every keystroke on the main thread
1 parent f99e57a commit 6d2c9ff

2 files changed

Lines changed: 68 additions & 10 deletions

File tree

app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerViewModel.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import to.bitkit.data.SettingsStore
1616
import to.bitkit.di.BgDispatcher
1717
import to.bitkit.env.Env
1818
import to.bitkit.repositories.LightningRepo
19+
import java.net.URI
1920
import javax.inject.Inject
2021

2122
@HiltViewModel
@@ -26,13 +27,11 @@ class RgsServerViewModel @Inject constructor(
2627
) : ViewModel() {
2728

2829
companion object {
29-
private val URL_PATTERN = Regex(
30-
"^(https?://)?" + // protocol
31-
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name
32-
"((\\d{1,3}\\.){3}\\d{1,3}))" + // IP (v4) address
33-
"(:\\d+)?(/[-a-z\\d%_.~+]*)*", // port and path
34-
RegexOption.IGNORE_CASE
30+
private val HOSTNAME_PATTERN = Regex(
31+
"^([a-z\\d]([a-z\\d-]*[a-z\\d])*\\.)+[a-z]{2,}|(\\d{1,3}\\.){3}\\d{1,3}$",
32+
RegexOption.IGNORE_CASE,
3533
)
34+
private val PATH_PATTERN = Regex("^(/[a-zA-Z\\d_.~%+-]*)*$")
3635
}
3736

3837
private val _uiState = MutableStateFlow(RgsServerUiState())
@@ -121,12 +120,25 @@ class RgsServerViewModel @Inject constructor(
121120
}
122121

123122
private fun isValidURL(data: String): Boolean {
124-
// Allow localhost in development mode
125-
if (Env.isDebug && data.contains("localhost")) {
126-
return true
123+
val normalized = if (!data.startsWith("http://") && !data.startsWith("https://")) {
124+
"https://$data"
125+
} else {
126+
data
127127
}
128128

129-
return URL_PATTERN.matches(data)
129+
return try {
130+
val uri = URI(normalized)
131+
val hostname = uri.host ?: return false
132+
133+
if (Env.isDebug && hostname == "localhost") return true
134+
135+
if (!HOSTNAME_PATTERN.matches(hostname)) return false
136+
137+
val path = uri.path.orEmpty()
138+
path.isEmpty() || PATH_PATTERN.matches(path)
139+
} catch (_: Throwable) {
140+
false
141+
}
130142
}
131143
}
132144

app/src/test/java/to/bitkit/ui/settings/advanced/RgsServerViewModelTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ import to.bitkit.data.SettingsData
1414
import to.bitkit.data.SettingsStore
1515
import to.bitkit.repositories.LightningRepo
1616
import to.bitkit.test.BaseUnitTest
17+
import kotlinx.coroutines.withTimeout
1718
import kotlin.test.assertEquals
1819
import kotlin.test.assertFalse
1920
import kotlin.test.assertNotNull
2021
import kotlin.test.assertNull
2122
import kotlin.test.assertTrue
23+
import kotlin.time.Duration.Companion.seconds
2224

2325
@OptIn(ExperimentalCoroutinesApi::class)
2426
class RgsServerViewModelTest : BaseUnitTest() {
@@ -246,4 +248,48 @@ class RgsServerViewModelTest : BaseUnitTest() {
246248
cancelAndIgnoreRemainingEvents()
247249
}
248250
}
251+
252+
@Test
253+
fun `setRgsUrl does not hang on long urls with special characters`() = test {
254+
sut = createSut()
255+
advanceUntilIdle()
256+
257+
withTimeout(2.seconds) {
258+
sut.setRgsUrl("https://rapidsync.lightningdevkit/snapshot/" + "a".repeat(100) + "!")
259+
}
260+
261+
assertFalse(sut.uiState.value.canConnect)
262+
}
263+
264+
@Test
265+
fun `setRgsUrl does not hang on url that caused ANR`() = test {
266+
sut = createSut()
267+
advanceUntilIdle()
268+
269+
withTimeout(2.seconds) {
270+
sut.setRgsUrl("https://rapidsync.lightningdevkit/snapshot")
271+
}
272+
273+
assertTrue(sut.uiState.value.canConnect)
274+
}
275+
276+
@Test
277+
fun `setRgsUrl accepts valid rgs url with path`() = test {
278+
sut = createSut()
279+
advanceUntilIdle()
280+
281+
sut.setRgsUrl("https://rgs.blocktank.to/snapshot")
282+
283+
assertFalse(sut.uiState.value.canConnect)
284+
}
285+
286+
@Test
287+
fun `setRgsUrl accepts ip address url`() = test {
288+
sut = createSut()
289+
advanceUntilIdle()
290+
291+
sut.setRgsUrl("https://192.168.1.1:8080/snapshot")
292+
293+
assertTrue(sut.uiState.value.canConnect)
294+
}
249295
}

0 commit comments

Comments
 (0)