-
Notifications
You must be signed in to change notification settings - Fork 16
Handle Gateway links #289
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle Gateway links #289
Changes from 5 commits
bc0ce3f
8d60046
77787a8
e20d503
78a8df4
5569d46
809f5b2
153a48e
b654ace
aa453f4
2d27c46
64fe78f
64cf76d
e0b8293
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,9 +35,11 @@ import kotlinx.coroutines.launch | |
import net.schmizz.sshj.common.SSHException | ||
import net.schmizz.sshj.connection.ConnectionException | ||
import java.awt.Dimension | ||
import java.net.HttpURLConnection | ||
import java.net.URL | ||
import java.time.Duration | ||
import java.util.concurrent.TimeoutException | ||
import javax.net.ssl.SSLHandshakeException | ||
|
||
// CoderRemoteConnection uses the provided workspace SSH parameters to launch an | ||
// IDE against the workspace. If successful the connection is added to recent | ||
|
@@ -105,6 +107,33 @@ class CoderRemoteConnectionHandle { | |
companion object { | ||
val logger = Logger.getInstance(CoderRemoteConnectionHandle::class.java.simpleName) | ||
|
||
/** | ||
* Generic function to ask for consent. | ||
*/ | ||
fun confirm(title: String, comment: String, details: String): Boolean { | ||
var inputFromUser = false | ||
ApplicationManager.getApplication().invokeAndWait({ | ||
val panel = panel { | ||
row { | ||
label(comment) | ||
} | ||
row { | ||
label(details) | ||
} | ||
} | ||
AppIcon.getInstance().requestAttention(null, true) | ||
if (!dialog( | ||
title = title, | ||
panel = panel, | ||
).showAndGet() | ||
) { | ||
return@invokeAndWait | ||
} | ||
inputFromUser = true | ||
}, ModalityState.defaultModalityState()) | ||
return inputFromUser | ||
} | ||
|
||
/** | ||
* Generic function to ask for input. | ||
*/ | ||
|
@@ -166,16 +195,25 @@ class CoderRemoteConnectionHandle { | |
): Pair<String, TokenSource>? { | ||
var (existingToken, tokenSource) = token ?: Pair("", TokenSource.USER) | ||
val getTokenUrl = url.withPath("/login?redirect=%2Fcli-auth") | ||
if (!isRetry && !useExisting) { | ||
BrowserUtil.browse(getTokenUrl) | ||
} else if (!isRetry && useExisting) { | ||
val (u, t) = CoderCLIManager.readConfig() | ||
if (url == u?.toURL() && !t.isNullOrBlank() && t != existingToken) { | ||
logger.info("Injecting token for $url from CLI config") | ||
tokenSource = TokenSource.CONFIG | ||
existingToken = t | ||
|
||
// On the first run either open a browser to generate a new token | ||
// or, if using an existing token, use the token on disk if it | ||
// exists otherwise assume the user already copied an existing | ||
// token and they will paste in. | ||
if (!isRetry) { | ||
if (!useExisting) { | ||
BrowserUtil.browse(getTokenUrl) | ||
} else { | ||
val (u, t) = CoderCLIManager.readConfig() | ||
if (url == u?.toURL() && !t.isNullOrBlank() && t != existingToken) { | ||
logger.info("Injecting token for $url from CLI config") | ||
return Pair(t, TokenSource.CONFIG) | ||
} | ||
} | ||
} | ||
|
||
// On subsequent tries or if not using an existing token, ask the user | ||
// for the token. | ||
val tokenFromUser = ask( | ||
CoderGatewayBundle.message( | ||
if (isRetry) "gateway.connector.view.workspaces.token.rejected" | ||
|
@@ -200,5 +238,66 @@ class CoderRemoteConnectionHandle { | |
} | ||
return Pair(tokenFromUser, tokenSource) | ||
} | ||
|
||
/** | ||
* Return if the URL is whitelisted, https, and the URL and its final | ||
* destination, if it is a different host. | ||
*/ | ||
@JvmStatic | ||
fun isWhitelisted(url: URL, deploymentURL: URL): Triple<Boolean, Boolean, String> { | ||
code-asher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// TODO: Setting for the whitelist, and remember previously allowed | ||
// domains. | ||
val domainWhitelist = listOf("intellij.net", "jetbrains.com", deploymentURL.host) | ||
code-asher marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Resolve any redirects. | ||
val finalUrl = try { | ||
resolveRedirects(url) | ||
} catch (e: Exception) { | ||
when (e) { | ||
is SSLHandshakeException -> | ||
throw Exception(CoderGatewayBundle.message( | ||
"gateway.connector.view.workspaces.connect.ssl-error", | ||
url.host, | ||
e.message ?: CoderGatewayBundle.message("gateway.connector.view.workspaces.connect.no-reason") | ||
)) | ||
else -> throw e | ||
Comment on lines
+257
to
+263
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may cause some headaches if the required certs are not available in the JRE keystore. I think it should be fine if it's trusted by the system though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From my testing (although I might have missed something), adding to the system trust store was not enough, it had to be in the JRE key store that was bundled with Gateway. The help text has a link to the docs that explains how to add the cert, but I do think it would be nice as a future enhancement to give the option to view and accept the cert. Not sure how difficult that would be. |
||
} | ||
} | ||
|
||
var linkWithRedirect = url.toString() | ||
if (finalUrl.host != url.host) { | ||
linkWithRedirect = "$linkWithRedirect (redirects to to $finalUrl)" | ||
} | ||
|
||
val whitelisted = domainWhitelist.any { url.host == it || url.host.endsWith(".$it") } | ||
&& domainWhitelist.any { finalUrl.host == it || finalUrl.host.endsWith(".$it") } | ||
val https = url.protocol == "https" && finalUrl.protocol == "https" | ||
return Triple(whitelisted, https, linkWithRedirect) | ||
} | ||
|
||
/** | ||
* Follow a URL's redirects to its final destination. | ||
*/ | ||
@JvmStatic | ||
fun resolveRedirects(url: URL): URL { | ||
var location = url | ||
val maxRedirects = 10 | ||
for (i in 1..maxRedirects) { | ||
val conn = location.openConnection() as HttpURLConnection | ||
conn.instanceFollowRedirects = false | ||
conn.connect() | ||
val code = conn.responseCode | ||
val nextLocation = conn.getHeaderField("Location"); | ||
conn.disconnect() | ||
// Redirects are triggered by any code starting with 3 plus a | ||
// location header. | ||
if (code < 300 || code >= 400 || nextLocation.isNullOrBlank()) { | ||
return location | ||
} | ||
// Location headers might be relative. | ||
location = URL(location, nextLocation) | ||
} | ||
throw Exception("Too many redirects") | ||
} | ||
} | ||
} |
code-asher marked this conversation as resolved.
Show resolved
Hide resolved
|
Uh oh!
There was an error while loading. Please reload this page.