Skip to content

Commit 5e11642

Browse files
committed
Break out link handler
This is to share as much as possible with the Toolbox branch. Part of this added a URI query param parser, since in Toolbox we get the raw URI and there does not seem to be anything in Kotlin to parse query parameters.
1 parent 8adf608 commit 5e11642

File tree

11 files changed

+699
-646
lines changed

11 files changed

+699
-646
lines changed

src/main/kotlin/com/coder/gateway/CoderGatewayConnectionProvider.kt

Lines changed: 5 additions & 323 deletions
Original file line numberDiff line numberDiff line change
@@ -2,94 +2,14 @@
22

33
package com.coder.gateway
44

5-
import com.coder.gateway.cli.CoderCLIManager
6-
import com.coder.gateway.cli.ensureCLI
7-
import com.coder.gateway.models.AGENT_ID
8-
import com.coder.gateway.models.AGENT_NAME
9-
import com.coder.gateway.models.TOKEN
10-
import com.coder.gateway.models.URL
11-
import com.coder.gateway.models.WORKSPACE
12-
import com.coder.gateway.models.WorkspaceAndAgentStatus
13-
import com.coder.gateway.models.WorkspaceProjectIDE
14-
import com.coder.gateway.models.agentID
15-
import com.coder.gateway.models.agentName
16-
import com.coder.gateway.models.folder
17-
import com.coder.gateway.models.ideBuildNumber
18-
import com.coder.gateway.models.ideDownloadLink
19-
import com.coder.gateway.models.idePathOnHost
20-
import com.coder.gateway.models.ideProductCode
21-
import com.coder.gateway.models.isCoder
22-
import com.coder.gateway.models.token
23-
import com.coder.gateway.models.url
24-
import com.coder.gateway.models.workspace
25-
import com.coder.gateway.sdk.CoderRestClient
26-
import com.coder.gateway.sdk.ex.APIResponseException
27-
import com.coder.gateway.sdk.v2.models.Workspace
28-
import com.coder.gateway.sdk.v2.models.WorkspaceAgent
29-
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
30-
import com.coder.gateway.services.CoderRestClientService
315
import com.coder.gateway.services.CoderSettingsService
32-
import com.coder.gateway.settings.Source
33-
import com.coder.gateway.util.toURL
34-
import com.coder.gateway.views.steps.CoderWorkspaceProjectIDEStepView
35-
import com.coder.gateway.views.steps.CoderWorkspacesStepSelection
36-
import com.intellij.openapi.application.ApplicationManager
6+
import com.coder.gateway.util.handleLink
7+
import com.coder.gateway.util.isCoder
378
import com.intellij.openapi.components.service
389
import com.intellij.openapi.diagnostic.Logger
39-
import com.intellij.openapi.ui.DialogWrapper
40-
import com.intellij.ui.dsl.builder.panel
41-
import com.intellij.util.ui.JBUI
4210
import com.jetbrains.gateway.api.ConnectionRequestor
4311
import com.jetbrains.gateway.api.GatewayConnectionHandle
4412
import com.jetbrains.gateway.api.GatewayConnectionProvider
45-
import javax.swing.JComponent
46-
import javax.swing.border.Border
47-
48-
/**
49-
* A dialog wrapper around CoderWorkspaceStepView.
50-
*/
51-
class CoderWorkspaceStepDialog(
52-
name: String,
53-
private val state: CoderWorkspacesStepSelection,
54-
) : DialogWrapper(true) {
55-
private val view = CoderWorkspaceProjectIDEStepView(showTitle = false)
56-
57-
init {
58-
init()
59-
title = CoderGatewayBundle.message("gateway.connector.view.coder.remoteproject.choose.text", name)
60-
}
61-
62-
override fun show() {
63-
view.init(state)
64-
view.onPrevious = { close(1) }
65-
view.onNext = { close(0) }
66-
super.show()
67-
view.dispose()
68-
}
69-
70-
fun showAndGetData(): WorkspaceProjectIDE? {
71-
if (showAndGet()) {
72-
return view.data()
73-
}
74-
return null
75-
}
76-
77-
override fun createContentPaneBorder(): Border {
78-
return JBUI.Borders.empty()
79-
}
80-
81-
override fun createCenterPanel(): JComponent {
82-
return view
83-
}
84-
85-
override fun createSouthPanel(): JComponent {
86-
// The plugin provides its own buttons.
87-
// TODO: Is it more idiomatic to handle buttons out here?
88-
return panel {}.apply {
89-
border = JBUI.Borders.empty()
90-
}
91-
}
92-
}
9313

9414
// CoderGatewayConnectionProvider handles connecting via a Gateway link such as
9515
// jetbrains-gateway://connect#type=coder.
@@ -101,204 +21,14 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
10121
requestor: ConnectionRequestor,
10222
): GatewayConnectionHandle? {
10323
CoderRemoteConnectionHandle().connect { indicator ->
104-
logger.debug("Launched Coder connection provider", parameters)
105-
106-
val deploymentURL =
107-
parameters.url()
108-
?: CoderRemoteConnectionHandle.ask("Enter the full URL of your Coder deployment")
109-
if (deploymentURL.isNullOrBlank()) {
110-
throw IllegalArgumentException("Query parameter \"$URL\" is missing")
111-
}
112-
113-
val client = authenticate(deploymentURL, parameters.token())
114-
115-
// TODO: If the workspace is missing we could launch the wizard.
116-
val workspaceName = parameters.workspace() ?: throw IllegalArgumentException("Query parameter \"$WORKSPACE\" is missing")
117-
118-
val workspaces = client.workspaces()
119-
val workspace =
120-
workspaces.firstOrNull {
121-
it.name == workspaceName
122-
} ?: throw IllegalArgumentException("The workspace $workspaceName does not exist")
123-
124-
when (workspace.latestBuild.status) {
125-
WorkspaceStatus.PENDING, WorkspaceStatus.STARTING ->
126-
// TODO: Wait for the workspace to turn on.
127-
throw IllegalArgumentException(
128-
"The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; please wait then try again",
129-
)
130-
WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED,
131-
WorkspaceStatus.CANCELING, WorkspaceStatus.CANCELED,
132-
->
133-
// TODO: Turn on the workspace.
134-
throw IllegalArgumentException(
135-
"The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; please start the workspace and try again",
136-
)
137-
WorkspaceStatus.FAILED, WorkspaceStatus.DELETING, WorkspaceStatus.DELETED ->
138-
throw IllegalArgumentException(
139-
"The workspace \"$workspaceName\" is ${workspace.latestBuild.status.toString().lowercase()}; unable to connect",
140-
)
141-
WorkspaceStatus.RUNNING -> Unit // All is well
142-
}
143-
144-
// TODO: Show a dropdown and ask for an agent if missing.
145-
val agent = getMatchingAgent(parameters, workspace)
146-
val status = WorkspaceAndAgentStatus.from(workspace, agent)
147-
148-
if (status.pending()) {
149-
// TODO: Wait for the agent to be ready.
150-
throw IllegalArgumentException(
151-
"The agent \"${agent.name}\" is ${status.toString().lowercase()}; please wait then try again",
152-
)
153-
} else if (!status.ready()) {
154-
throw IllegalArgumentException("The agent \"${agent.name}\" is ${status.toString().lowercase()}; unable to connect")
155-
}
156-
157-
val cli =
158-
ensureCLI(
159-
deploymentURL.toURL(),
160-
client.buildInfo().version,
161-
settings,
162-
indicator,
163-
)
164-
165-
// We only need to log in if we are using token-based auth.
166-
if (client.token !== null) {
167-
indicator.text = "Authenticating Coder CLI..."
168-
cli.login(client.token)
169-
}
170-
171-
indicator.text = "Configuring Coder CLI..."
172-
cli.configSsh(client.agentNames(workspaces))
173-
174-
val name = "${workspace.name}.${agent.name}"
175-
val openDialog =
176-
parameters.ideProductCode().isNullOrBlank() ||
177-
parameters.ideBuildNumber().isNullOrBlank() ||
178-
(parameters.idePathOnHost().isNullOrBlank() && parameters.ideDownloadLink().isNullOrBlank()) ||
179-
parameters.folder().isNullOrBlank()
180-
181-
if (openDialog) {
182-
var data: WorkspaceProjectIDE? = null
183-
ApplicationManager.getApplication().invokeAndWait {
184-
val dialog =
185-
CoderWorkspaceStepDialog(
186-
name,
187-
CoderWorkspacesStepSelection(agent, workspace, cli, client, workspaces),
188-
)
189-
data = dialog.showAndGetData()
190-
}
191-
data ?: throw Exception("IDE selection aborted; unable to connect")
192-
} else {
193-
// Check that both the domain and the redirected domain are
194-
// allowlisted. If not, check with the user whether to proceed.
195-
verifyDownloadLink(parameters)
196-
WorkspaceProjectIDE.fromInputs(
197-
name = name,
198-
hostname = CoderCLIManager.getHostName(deploymentURL.toURL(), name),
199-
projectPath = parameters.folder(),
200-
ideProductCode = parameters.ideProductCode(),
201-
ideBuildNumber = parameters.ideBuildNumber(),
202-
idePathOnHost = parameters.idePathOnHost(),
203-
downloadSource = parameters.ideDownloadLink(),
204-
deploymentURL = deploymentURL,
205-
lastOpened = null, // Have not opened yet.
206-
)
24+
logger.debug("Launched Coder link handler", parameters)
25+
handleLink(parameters, settings) {
26+
indicator.text = it
20727
}
20828
}
20929
return null
21030
}
21131

212-
/**
213-
* Return an authenticated Coder CLI, asking for the token as long as it
214-
* continues to result in an authentication failure and token authentication
215-
* is required.
216-
*/
217-
private fun authenticate(
218-
deploymentURL: String,
219-
queryToken: String?,
220-
lastToken: Pair<String, Source>? = null,
221-
): CoderRestClient {
222-
val token =
223-
if (settings.requireTokenAuth) {
224-
// Use the token from the query, unless we already tried that.
225-
val isRetry = lastToken != null
226-
if (!queryToken.isNullOrBlank() && !isRetry) {
227-
Pair(queryToken, Source.QUERY)
228-
} else {
229-
CoderRemoteConnectionHandle.askToken(
230-
deploymentURL.toURL(),
231-
lastToken,
232-
isRetry,
233-
useExisting = true,
234-
settings,
235-
)
236-
}
237-
} else {
238-
null
239-
}
240-
if (settings.requireTokenAuth && token == null) { // User aborted.
241-
throw IllegalArgumentException("Unable to connect to $deploymentURL, query parameter \"$TOKEN\" is missing")
242-
}
243-
val client = CoderRestClientService(deploymentURL.toURL(), token?.first)
244-
return try {
245-
client.authenticate()
246-
client
247-
} catch (ex: APIResponseException) {
248-
// If doing token auth we can ask and try again.
249-
if (settings.requireTokenAuth && ex.isUnauthorized) {
250-
authenticate(deploymentURL, queryToken, token)
251-
} else {
252-
throw ex
253-
}
254-
}
255-
}
256-
257-
/**
258-
* Check that the link is allowlisted. If not, confirm with the user.
259-
*/
260-
private fun verifyDownloadLink(parameters: Map<String, String>) {
261-
val link = parameters.ideDownloadLink()
262-
if (link.isNullOrBlank()) {
263-
return // Nothing to verify
264-
}
265-
266-
val url =
267-
try {
268-
link.toURL()
269-
} catch (ex: Exception) {
270-
throw IllegalArgumentException("$link is not a valid URL")
271-
}
272-
273-
val (allowlisted, https, linkWithRedirect) =
274-
try {
275-
CoderRemoteConnectionHandle.isAllowlisted(url)
276-
} catch (e: Exception) {
277-
throw IllegalArgumentException("Unable to verify $url: $e")
278-
}
279-
if (allowlisted && https) {
280-
return
281-
}
282-
283-
val comment =
284-
if (allowlisted) {
285-
"The download link is from a non-allowlisted URL"
286-
} else if (https) {
287-
"The download link is not using HTTPS"
288-
} else {
289-
"The download link is from a non-allowlisted URL and is not using HTTPS"
290-
}
291-
292-
if (!CoderRemoteConnectionHandle.confirm(
293-
"Confirm download URL",
294-
"$comment. Would you like to proceed?",
295-
linkWithRedirect,
296-
)
297-
) {
298-
throw IllegalArgumentException("$linkWithRedirect is not allowlisted")
299-
}
300-
}
301-
30232
override fun isApplicable(parameters: Map<String, String>): Boolean {
30333
return parameters.isCoder()
30434
}
@@ -307,51 +37,3 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
30737
val logger = Logger.getInstance(CoderGatewayConnectionProvider::class.java.simpleName)
30838
}
30939
}
310-
311-
/**
312-
* Return the agent matching the provided agent ID or name in the parameters.
313-
* The name is ignored if the ID is set. If neither was supplied and the
314-
* workspace has only one agent, return that. Otherwise throw an error.
315-
*
316-
* @throws [MissingArgumentException, IllegalArgumentException]
317-
*/
318-
fun getMatchingAgent(
319-
parameters: Map<String, String?>,
320-
workspace: Workspace,
321-
): WorkspaceAgent {
322-
val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
323-
if (agents.isEmpty()) {
324-
throw IllegalArgumentException("The workspace \"${workspace.name}\" has no agents")
325-
}
326-
327-
// If the agent is missing and the workspace has only one, use that.
328-
// Prefer the ID over the name if both are set.
329-
val agent =
330-
if (!parameters.agentID().isNullOrBlank()) {
331-
agents.firstOrNull { it.id.toString() == parameters.agentID() }
332-
} else if (!parameters.agentName().isNullOrBlank()) {
333-
agents.firstOrNull { it.name == parameters.agentName() }
334-
} else if (agents.size == 1) {
335-
agents.first()
336-
} else {
337-
null
338-
}
339-
340-
if (agent == null) {
341-
if (!parameters.agentID().isNullOrBlank()) {
342-
throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"")
343-
} else if (!parameters.agentName().isNullOrBlank()) {
344-
throw IllegalArgumentException(
345-
"The workspace \"${workspace.name}\"does not have an agent named \"${parameters.agentName()}\"",
346-
)
347-
} else {
348-
throw MissingArgumentException(
349-
"Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent",
350-
)
351-
}
352-
}
353-
354-
return agent
355-
}
356-
357-
class MissingArgumentException(message: String) : IllegalArgumentException(message)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy