Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Changed

- URL validation is stricter in the connection screen and URI protocol handler

## 0.6.0 - 2025-07-25

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.coder.toolbox.sdk.CoderRestClient
import com.coder.toolbox.sdk.v2.models.Workspace
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import kotlinx.coroutines.Job
import kotlinx.coroutines.TimeoutCancellationException
Expand Down Expand Up @@ -107,6 +108,11 @@ open class CoderProtocolHandler(
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$URL\" is missing from URI")
return null
}
val validationResult = deploymentURL.validateStrictWebUrl()
if (validationResult is Invalid) {
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "\"$URL\" is invalid: ${validationResult.reason}")
return null
}
return deploymentURL
}

Expand Down
25 changes: 25 additions & 0 deletions src/main/kotlin/com/coder/toolbox/util/URLExtensions.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
package com.coder.toolbox.util

import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.coder.toolbox.util.WebUrlValidationResult.Valid
import java.net.IDN
import java.net.URI
import java.net.URL

fun String.toURL(): URL = URI.create(this).toURL()

fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
val uri = URI(this)

when {
uri.isOpaque -> Invalid("$this is opaque, instead of hierarchical")
!uri.isAbsolute -> Invalid("$this is relative, it must be absolute")
uri.scheme?.lowercase() !in setOf("http", "https") ->
Invalid("Scheme for $this must be either http or https")

uri.authority.isNullOrBlank() ->
Invalid("$this does not have a hostname")

else -> Valid
}
} catch (e: Exception) {
Invalid(e.message ?: "$this could not be parsed as a URI reference")
}

fun URL.withPath(path: String): URL = URL(
this.protocol,
this.host,
Expand All @@ -30,3 +50,8 @@ fun URI.toQueryParameters(): Map<String, String> = (this.query ?: "")
parts[0] to ""
}
}

sealed class WebUrlValidationResult {
object Valid : WebUrlValidationResult()
data class Invalid(val reason: String) : WebUrlValidationResult()
}
13 changes: 7 additions & 6 deletions src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.coder.toolbox.settings.SignatureFallbackStrategy
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
import com.coder.toolbox.util.toURL
import com.coder.toolbox.util.validateStrictWebUrl
import com.coder.toolbox.views.state.CoderCliSetupContext
import com.coder.toolbox.views.state.CoderCliSetupWizardState
import com.jetbrains.toolbox.api.ui.components.CheckboxField
Expand Down Expand Up @@ -69,16 +71,11 @@ class DeploymentUrlStep(

override fun onNext(): Boolean {
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
var url = urlField.textState.value
val url = urlField.textState.value
if (url.isBlank()) {
errorField.textState.update { context.i18n.ptrl("URL is required") }
return false
}
url = if (!url.startsWith("http://") && !url.startsWith("https://")) {
"https://$url"
} else {
url
}
try {
CoderCliSetupContext.url = validateRawUrl(url)
} catch (e: MalformedURLException) {
Expand All @@ -98,6 +95,10 @@ class DeploymentUrlStep(
*/
private fun validateRawUrl(url: String): URL {
try {
val result = url.validateStrictWebUrl()
if (result is Invalid) {
throw MalformedURLException(result.reason)
}
return url.toURL()
} catch (e: Exception) {
throw MalformedURLException(e.message)
Expand Down
92 changes: 92 additions & 0 deletions src/test/kotlin/com/coder/toolbox/util/URLExtensionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,96 @@ internal class URLExtensionsTest {
)
}
}

@Test
fun `valid http URL should return Valid`() {
val result = "http://coder.com".validateStrictWebUrl()
assertEquals(WebUrlValidationResult.Valid, result)
}

@Test
fun `valid https URL with path and query should return Valid`() {
val result = "https://coder.com/bin/coder-linux-amd64?query=1".validateStrictWebUrl()
assertEquals(WebUrlValidationResult.Valid, result)
}

@Test
fun `relative URL should return Invalid with appropriate message`() {
val url = "/bin/coder-linux-amd64"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("$url is relative, it must be absolute"),
result
)
}

@Test
fun `opaque URI like mailto should return Invalid`() {
val url = "mailto:[email protected]"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("$url is opaque, instead of hierarchical"),
result
)
}

@Test
fun `unsupported scheme like ftp should return Invalid`() {
val url = "ftp://coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("Scheme for $url must be either http or https"),
result
)
}

@Test
fun `http URL with missing authority should return Invalid`() {
val url = "http:///bin/coder-linux-amd64"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("$url does not have a hostname"),
result
)
}

@Test
fun `malformed URI should return Invalid with parsing error message`() {
val url = "http://[invalid-uri]"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("Malformed IPv6 address at index 8: $url"),
result
)
}

@Test
fun `URI without colon should return Invalid as URI is not absolute`() {
val url = "http//coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("http//coder.com is relative, it must be absolute"),
result
)
}

@Test
fun `URI without double forward slashes should return Invalid because the URI is not hierarchical`() {
val url = "http:coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("http:coder.com is opaque, instead of hierarchical"),
result
)
}

@Test
fun `URI without a single forward slash should return Invalid because the URI does not have a hostname`() {
val url = "https:/coder.com"
val result = url.validateStrictWebUrl()
assertEquals(
WebUrlValidationResult.Invalid("https:/coder.com does not have a hostname"),
result
)
}
}
Loading