Skip to content

Commit ad1566f

Browse files
authored
[PM-14846] Improve IP Address and Port Handling in StringExtensions (#5118)
1 parent 32d0ca7 commit ad1566f

File tree

2 files changed

+101
-2
lines changed

2 files changed

+101
-2
lines changed

app/src/main/java/com/x8bit/bitwarden/data/platform/util/StringExtensions.kt

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,55 @@ import java.net.URISyntaxException
1010
*/
1111
private const val ANDROID_APP_PROTOCOL: String = "androidapp://"
1212

13+
/**
14+
* Regex to match an IP address (IPv4) with an optional port within the valid range (0-65535).
15+
*
16+
* This regex matches the following:
17+
* - An optional "http://" or "https://" prefix.
18+
* - A standard IPv4 address (four groups of digits separated by dots).
19+
* - An optional colon (:) followed by a port number (digits).
20+
* - The port number, if present, must be within the range of 0-65535.
21+
*
22+
* Example valid strings:
23+
* - 192.168.1.1
24+
* - 10.0.0.5:8080
25+
* - 255.255.255.0:9000
26+
* - 0.0.0.0
27+
* - 1.1.1.1:0
28+
* - 1.1.1.1:65535
29+
* - https://192.168.1.1
30+
* - https://10.0.0.5:8080
31+
* - http://255.255.255.0:9000
32+
* - http://10.0.0.5:8080
33+
*
34+
* Example invalid strings:
35+
* - 256.1.1.1 (invalid IP component)
36+
* - 10.1.1 (missing IP components)
37+
* - 10.1.1.1:abc (non-numeric port)
38+
* - 10.1.1.1:-1 (invalid port)
39+
* - 10.1.1.1: (missing port)
40+
* - 1.1.1.1:65536 (invalid port)
41+
* - 1.1.1.1:99999 (invalid port)
42+
* - ://192.168.1.1 (invalid scheme)
43+
* - androidapp://192.168.1.1 (invalid scheme)
44+
* - file://usr/docs/file.txt (invalid scheme)
45+
*/
46+
@Suppress("MaxLineLength")
47+
private val IP_ADDRESS_WITH_OPTIONAL_PORT =
48+
Regex("""^(https?://)?((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?$""")
49+
1350
/**
1451
* Try creating a [URI] out of this [String]. If it fails, return null.
1552
*/
1653
fun String.toUriOrNull(): URI? =
1754
try {
18-
URI(this)
19-
} catch (e: URISyntaxException) {
55+
// URI cannot parse IP addresses without a scheme, so add one if necessary.
56+
if (this.isIpAddress() && !this.hasHttpProtocol()) {
57+
URI("https://$this")
58+
} else {
59+
URI(this)
60+
}
61+
} catch (_: URISyntaxException) {
2062
null
2163
}
2264

@@ -32,6 +74,12 @@ fun String.isAndroidApp(): Boolean =
3274
fun String.hasHttpProtocol(): Boolean =
3375
this.startsWith(prefix = "http://") || this.startsWith(prefix = "https://")
3476

77+
/**
78+
* Whether this [String] represents an IP address with an optional port.
79+
*/
80+
fun String.isIpAddress(): Boolean =
81+
this.matches(IP_ADDRESS_WITH_OPTIONAL_PORT)
82+
3583
/**
3684
* Try and extract the web host from this [String] if it represents an Android app.
3785
*/

app/src/test/java/com/x8bit/bitwarden/data/util/StringExtensionsTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
99
import com.x8bit.bitwarden.data.platform.util.hasHttpProtocol
1010
import com.x8bit.bitwarden.data.platform.util.hasPort
1111
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
12+
import com.x8bit.bitwarden.data.platform.util.isIpAddress
1213
import com.x8bit.bitwarden.data.platform.util.parseDomainOrNull
1314
import com.x8bit.bitwarden.data.platform.util.toUriOrNull
1415
import io.mockk.every
@@ -200,6 +201,12 @@ class StringExtensionsTest {
200201
assertTrue(uriString.hasPort())
201202
}
202203

204+
@Test
205+
fun `hasPort returns true when URI is IP address and port`() {
206+
val uriString = "192.168.1.1:8080"
207+
assertTrue(uriString.hasPort())
208+
}
209+
203210
@Test
204211
fun `getHostWithPortOrNull should return host with port when present`() {
205212
val uriString = "www.google.com:8080"
@@ -223,4 +230,48 @@ class StringExtensionsTest {
223230
val uriString = "androidapp://www.google.com:8080"
224231
assertEquals("www.google.com:8080", uriString.getHostWithPortOrNull())
225232
}
233+
234+
@Suppress("MaxLineLength")
235+
@Test
236+
fun `getHostWithPortOrNull should return host with port when URI is IP address with port and scheme`() {
237+
val uriString = "https://192.168.1.1:8080"
238+
assertEquals("192.168.1.1:8080", uriString.getHostWithPortOrNull())
239+
}
240+
241+
@Test
242+
fun `isIpAddress should return correct value for various IP addresses`() {
243+
testIpAddresses.forEach { (ipAddress, expected) ->
244+
assertEquals(expected, ipAddress.isIpAddress()) { "Failed for $ipAddress" }
245+
}
246+
}
247+
248+
private val testIpAddresses = listOf(
249+
"192.168.1.1" to true,
250+
"10.0.0.5:8080" to true,
251+
"255.255.255.0:9000" to true,
252+
"0.0.0.0" to true,
253+
"256.1.1.1" to false,
254+
"10.1.1" to false,
255+
"10.1.1.1:abc" to false,
256+
"10.1.1.1:-1" to false,
257+
"10.1.1.1:" to false,
258+
"10.1.1.1:0" to true,
259+
"1.1.1.1:123" to true,
260+
"1.1.1.1:65535" to true,
261+
"1.1.1.1:65536" to false,
262+
"1.1.1.1:99999" to false,
263+
":1234" to false,
264+
"255.255.255.255:65535" to true,
265+
"255.255.255.255:0" to true,
266+
"255.255.255.255:" to false,
267+
"255.255.255.255:-1" to false,
268+
"http://192.168.1.1" to true,
269+
"https://10.0.0.5:8080" to true,
270+
"http://255.255.255.0:9000" to true,
271+
"https://0.0.0.0" to true,
272+
"http://256.1.1.1" to false,
273+
"https://10.1.1" to false,
274+
"http://10.1.1.1:abc" to false,
275+
"https://10.1.1.1:-1" to false,
276+
)
226277
}

0 commit comments

Comments
 (0)