Skip to content

Commit a6efd99

Browse files
authored
Add header command setting (#303)
* Add header command setting * Inject header command into proxy command * Add headers to API requests * Update changelog with header command * Add --info to CI tests It seems the default is to say a test fails but give no explanation as to why. * Matche Coder CLI behavior for escaping to SSH config
1 parent 8223fc9 commit a6efd99

27 files changed

+257
-43
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
- uses: gradle/wrapper-validation-action@v1.1.0
3333

3434
# Run tests
35-
- run: ./gradlew test
35+
- run: ./gradlew test --info
3636

3737
# Collect Tests Result of failed tests
3838
- if: ${{ failure() }}

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
## Unreleased
66

7+
### Added
8+
- Add a setting for a command to run to get headers that will be set on all
9+
requests to the Coder deployment.
10+
711
## 2.6.0 - 2023-09-06
812

913
### Added

gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
pluginGroup=com.coder.gateway
44
pluginName=coder-gateway
55
# SemVer format -> https://semver.org
6-
pluginVersion=2.6.0
6+
pluginVersion=2.7.0
77
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
88
# for insight into build numbers and IntelliJ Platform versions.
99
pluginSinceBuild=223.7571.70

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
100100
cli.login(client.token)
101101

102102
indicator.text = "Configuring Coder CLI..."
103-
cli.configSsh(workspaces.flatMap { it.toAgentModels() })
103+
cli.configSsh(workspaces.flatMap { it.toAgentModels() }, settings.headerCommand)
104104

105105
// TODO: Ask for these if missing. Maybe we can reuse the second
106106
// step of the wizard? Could also be nice if we automatically used
@@ -150,7 +150,7 @@ class CoderGatewayConnectionProvider : GatewayConnectionProvider {
150150
if (token == null) { // User aborted.
151151
throw IllegalArgumentException("Unable to connect to $deploymentURL, $TOKEN is missing")
152152
}
153-
val client = CoderRestClient(deploymentURL, token.first)
153+
val client = CoderRestClient(deploymentURL, token.first, settings.headerCommand)
154154
return try {
155155
Pair(client, client.me().username)
156156
} catch (ex: AuthenticationResponseException) {

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

+7
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
6666
CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.comment")
6767
)
6868
}.layout(RowLayout.PARENT_GRID)
69+
row(CoderGatewayBundle.message("gateway.connector.settings.header-command.title")) {
70+
textField().resizableColumn().align(AlignX.FILL)
71+
.bindText(state::headerCommand)
72+
.comment(
73+
CoderGatewayBundle.message("gateway.connector.settings.header-command.comment")
74+
)
75+
}.layout(RowLayout.PARENT_GRID)
6976
}
7077
}
7178

src/main/kotlin/com/coder/gateway/sdk/CoderCLIManager.kt

+33-4
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,9 @@ class CoderCLIManager @JvmOverloads constructor(
179179
/**
180180
* Configure SSH to use this binary.
181181
*/
182-
fun configSsh(workspaces: List<WorkspaceAgentModel>) {
183-
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces))
182+
@JvmOverloads
183+
fun configSsh(workspaces: List<WorkspaceAgentModel>, headerCommand: String? = null) {
184+
writeSSHConfig(modifySSHConfig(readSSHConfig(), workspaces, headerCommand))
184185
}
185186

186187
/**
@@ -199,11 +200,21 @@ class CoderCLIManager @JvmOverloads constructor(
199200
* this deployment and return the modified config or null if it does not
200201
* need to be modified.
201202
*/
202-
private fun modifySSHConfig(contents: String?, workspaces: List<WorkspaceAgentModel>): String? {
203+
private fun modifySSHConfig(
204+
contents: String?,
205+
workspaces: List<WorkspaceAgentModel>,
206+
headerCommand: String?,
207+
): String? {
203208
val host = getSafeHost(deploymentURL)
204209
val startBlock = "# --- START CODER JETBRAINS $host"
205210
val endBlock = "# --- END CODER JETBRAINS $host"
206211
val isRemoving = workspaces.isEmpty()
212+
val proxyArgs = listOfNotNull(
213+
escape(localBinaryPath.toString()),
214+
"--global-config", escape(coderConfigPath.toString()),
215+
if (!headerCommand.isNullOrBlank()) "--header-command" else null,
216+
if (!headerCommand.isNullOrBlank()) escape(headerCommand) else null,
217+
"ssh", "--stdio")
207218
val blockContent = workspaces.joinToString(
208219
System.lineSeparator(),
209220
startBlock + System.lineSeparator(),
@@ -212,7 +223,7 @@ class CoderCLIManager @JvmOverloads constructor(
212223
"""
213224
Host ${getHostName(deploymentURL, it)}
214225
HostName coder.${it.name}
215-
ProxyCommand "$localBinaryPath" --global-config "$coderConfigPath" ssh --stdio ${it.name}
226+
ProxyCommand ${proxyArgs.joinToString(" ")} ${it.name}
216227
ConnectTimeout 0
217228
StrictHostKeyChecking no
218229
UserKnownHostsFile /dev/null
@@ -495,6 +506,24 @@ class CoderCLIManager @JvmOverloads constructor(
495506
// working binary and the binary directory does not.
496507
return if (cliMatches == null && dataCLIMatches != null) dataCLI else cli
497508
}
509+
510+
/**
511+
* Escape a command argument to be used in the ProxyCommand of an SSH
512+
* config. Surround with double quotes if the argument contains
513+
* whitespace and escape any existing double quotes.
514+
*
515+
* Throws if the argument is invalid.
516+
*/
517+
@JvmStatic
518+
fun escape(s: String): String {
519+
if (s.contains("\n")) {
520+
throw Exception("argument cannot contain newlines")
521+
}
522+
if (s.contains(" ") || s.contains("\t")) {
523+
return "\"" + s.replace("\"", "\\\"") + "\""
524+
}
525+
return s.replace("\"", "\\\"")
526+
}
498527
}
499528
}
500529

src/main/kotlin/com/coder/gateway/sdk/CoderRestClientService.kt

+55-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.intellij.openapi.extensions.PluginId
2020
import com.intellij.openapi.util.SystemInfo
2121
import okhttp3.OkHttpClient
2222
import okhttp3.logging.HttpLoggingInterceptor
23+
import org.zeroturnaround.exec.ProcessExecutor
2324
import retrofit2.Retrofit
2425
import retrofit2.converter.gson.GsonConverterFactory
2526
import java.net.HttpURLConnection.HTTP_CREATED
@@ -41,16 +42,16 @@ class CoderRestClientService {
4142
*
4243
* @throws [AuthenticationResponseException] if authentication failed.
4344
*/
44-
fun initClientSession(url: URL, token: String): User {
45-
client = CoderRestClient(url, token)
45+
fun initClientSession(url: URL, token: String, headerCommand: String?): User {
46+
client = CoderRestClient(url, token, headerCommand)
4647
me = client.me()
4748
buildVersion = client.buildInfo().version
4849
isReady = true
4950
return me
5051
}
5152
}
5253

53-
class CoderRestClient(var url: URL, var token: String) {
54+
class CoderRestClient(var url: URL, var token: String, var headerCommand: String?) {
5455
private var httpClient: OkHttpClient
5556
private var retroRestClient: CoderV2RestFacade
5657

@@ -61,6 +62,16 @@ class CoderRestClient(var url: URL, var token: String) {
6162
httpClient = OkHttpClient.Builder()
6263
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("Coder-Session-Token", token).build()) }
6364
.addInterceptor { it.proceed(it.request().newBuilder().addHeader("User-Agent", "Coder Gateway/${pluginVersion.version} (${SystemInfo.getOsNameAndVersion()}; ${SystemInfo.OS_ARCH})").build()) }
65+
.addInterceptor {
66+
var request = it.request()
67+
val headers = getHeaders(url, headerCommand)
68+
if (headers.size > 0) {
69+
val builder = request.newBuilder()
70+
headers.forEach { h -> builder.addHeader(h.key, h.value) }
71+
request = builder.build()
72+
}
73+
it.proceed(request)
74+
}
6475
// this should always be last if we want to see previous interceptors logged
6576
.addInterceptor(HttpLoggingInterceptor().apply { setLevel(HttpLoggingInterceptor.Level.BASIC) })
6677
.build()
@@ -141,4 +152,45 @@ class CoderRestClient(var url: URL, var token: String) {
141152

142153
return buildResponse.body()!!
143154
}
155+
156+
companion object {
157+
private val newlineRegex = "\r?\n".toRegex()
158+
private val endingNewlineRegex = "\r?\n$".toRegex()
159+
160+
// TODO: This really only needs to be a private function, but
161+
// unfortunately it is not possible to test the client because it fails
162+
// on the plugin manager core call and I do not know how to fix it. So,
163+
// for now make this static and test it directly instead.
164+
@JvmStatic
165+
fun getHeaders(url: URL, headerCommand: String?): Map<String, String> {
166+
if (headerCommand.isNullOrBlank()) {
167+
return emptyMap()
168+
}
169+
val (shell, caller) = when (getOS()) {
170+
OS.WINDOWS -> Pair("cmd.exe", "/c")
171+
else -> Pair("sh", "-c")
172+
}
173+
return ProcessExecutor()
174+
.command(shell, caller, headerCommand)
175+
.environment("CODER_URL", url.toString())
176+
.exitValues(0)
177+
.readOutput(true)
178+
.execute()
179+
.outputUTF8()
180+
.replaceFirst(endingNewlineRegex, "")
181+
.split(newlineRegex)
182+
.associate {
183+
// Header names cannot be blank or contain whitespace and
184+
// the Coder CLI requires that there be an equals sign (the
185+
// value can be blank though). The second case is taken
186+
// care of by the destructure here, as it will throw if
187+
// there are not enough parts.
188+
val (name, value) = it.split("=", limit=2)
189+
if (name.contains(" ") || name == "") {
190+
throw Exception("\"$name\" is not a valid header name")
191+
}
192+
name to value
193+
}
194+
}
195+
}
144196
}

src/main/kotlin/com/coder/gateway/services/CoderSettingsState.kt

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class CoderSettingsState : PersistentStateComponent<CoderSettingsState> {
1818
var dataDirectory: String = ""
1919
var enableDownloads: Boolean = true
2020
var enableBinaryDirectoryFallback: Boolean = false
21+
var headerCommand: String = ""
2122
override fun getState(): CoderSettingsState {
2223
return this
2324
}

src/main/kotlin/com/coder/gateway/views/CoderGatewayRecentWorkspaceConnectionsView.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.coder.gateway.sdk.toURL
1313
import com.coder.gateway.sdk.v2.models.WorkspaceStatus
1414
import com.coder.gateway.sdk.v2.models.toAgentModels
1515
import com.coder.gateway.services.CoderRecentWorkspaceConnectionsService
16+
import com.coder.gateway.services.CoderSettingsState
1617
import com.coder.gateway.toWorkspaceParams
1718
import com.intellij.icons.AllIcons
1819
import com.intellij.ide.BrowserUtil
@@ -72,6 +73,7 @@ data class DeploymentInfo(
7273
)
7374

7475
class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback: (Component) -> Unit) : GatewayRecentConnections, Disposable {
76+
private val settings: CoderSettingsState = service()
7577
private val recentConnectionsService = service<CoderRecentWorkspaceConnectionsService>()
7678
private val cs = CoroutineScope(Dispatchers.Main)
7779

@@ -259,7 +261,7 @@ class CoderGatewayRecentWorkspaceConnectionsView(private val setContentCallback:
259261
deployments[dir] ?: try {
260262
val url = Path.of(dir).resolve("url").readText()
261263
val token = Path.of(dir).resolve("session").readText()
262-
DeploymentInfo(CoderRestClient(url.toURL(), token))
264+
DeploymentInfo(CoderRestClient(url.toURL(), token, settings.headerCommand))
263265
} catch (e: Exception) {
264266
logger.error("Unable to create client from $dir", e)
265267
DeploymentInfo(error = "Error trying to read $dir: ${e.message}")

src/main/kotlin/com/coder/gateway/views/steps/CoderWorkspacesStepView.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
527527
*/
528528
private fun authenticate(url: URL, token: String) {
529529
logger.info("Authenticating to $url...")
530-
clientService.initClientSession(url, token)
530+
clientService.initClientSession(url, token, settings.headerCommand)
531531

532532
try {
533533
logger.info("Checking compatibility with Coder version ${clientService.buildVersion}...")
@@ -614,7 +614,7 @@ class CoderWorkspacesStepView(val setNextButtonEnabled: (Boolean) -> Unit) : Cod
614614
poller?.cancel()
615615

616616
logger.info("Configuring Coder CLI...")
617-
cli.configSsh(tableOfWorkspaces.items)
617+
cli.configSsh(tableOfWorkspaces.items, settings.headerCommand)
618618

619619
// The config directory can be used to pull the URL and token in
620620
// order to query this workspace's status in other flows, for

src/main/resources/messages/CoderGatewayBundle.properties

+5
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,8 @@ gateway.connector.settings.enable-binary-directory-fallback.title=Fall back to d
8787
gateway.connector.settings.enable-binary-directory-fallback.comment=Checking this \
8888
box will allow the plugin to fall back to the data directory when the CLI \
8989
directory is not writable.
90+
gateway.connector.settings.header-command.title=Header command:
91+
gateway.connector.settings.header-command.comment=An external command that \
92+
outputs additional HTTP headers added to all requests. The command must \
93+
output each header as `key=value` on its own line. The following \
94+
environment variables will be available to the process: CODER_URL.

src/test/fixtures/outputs/append-blank-newlines.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# --- START CODER JETBRAINS test.coder.invalid
66
Host coder-jetbrains--foo-bar--test.coder.invalid
77
HostName coder.foo-bar
8-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
8+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
99
ConnectTimeout 0
1010
StrictHostKeyChecking no
1111
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/append-blank.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# --- START CODER JETBRAINS test.coder.invalid
22
Host coder-jetbrains--foo-bar--test.coder.invalid
33
HostName coder.foo-bar
4-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
4+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
55
ConnectTimeout 0
66
StrictHostKeyChecking no
77
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/append-no-blocks.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Host test2
66
# --- START CODER JETBRAINS test.coder.invalid
77
Host coder-jetbrains--foo-bar--test.coder.invalid
88
HostName coder.foo-bar
9-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
9+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
1010
ConnectTimeout 0
1111
StrictHostKeyChecking no
1212
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/append-no-newline.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Host test2
55
# --- START CODER JETBRAINS test.coder.invalid
66
Host coder-jetbrains--foo-bar--test.coder.invalid
77
HostName coder.foo-bar
8-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
8+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
99
ConnectTimeout 0
1010
StrictHostKeyChecking no
1111
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/append-no-related-blocks.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ some jetbrains config
1212
# --- START CODER JETBRAINS test.coder.invalid
1313
Host coder-jetbrains--foo-bar--test.coder.invalid
1414
HostName coder.foo-bar
15-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
15+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
1616
ConnectTimeout 0
1717
StrictHostKeyChecking no
1818
UserKnownHostsFile /dev/null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# --- START CODER JETBRAINS test.coder.invalid
2+
Host coder-jetbrains--header--test.coder.invalid
3+
HostName coder.header
4+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config --header-command "C:\Program Files\My Header Command\\"also has quotes\"\HeaderCommand.exe" ssh --stdio header
5+
ConnectTimeout 0
6+
StrictHostKeyChecking no
7+
UserKnownHostsFile /dev/null
8+
LogLevel ERROR
9+
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
10+
# --- END CODER JETBRAINS test.coder.invalid
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# --- START CODER JETBRAINS test.coder.invalid
2+
Host coder-jetbrains--header--test.coder.invalid
3+
HostName coder.header
4+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config --header-command "my-header-command \"test\"" ssh --stdio header
5+
ConnectTimeout 0
6+
StrictHostKeyChecking no
7+
UserKnownHostsFile /dev/null
8+
LogLevel ERROR
9+
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
10+
# --- END CODER JETBRAINS test.coder.invalid

src/test/fixtures/outputs/multiple-workspaces.conf

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
# --- START CODER JETBRAINS test.coder.invalid
22
Host coder-jetbrains--foo--test.coder.invalid
33
HostName coder.foo
4-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo
4+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo
55
ConnectTimeout 0
66
StrictHostKeyChecking no
77
UserKnownHostsFile /dev/null
88
LogLevel ERROR
99
SetEnv CODER_SSH_SESSION_TYPE=JetBrains
1010
Host coder-jetbrains--bar--test.coder.invalid
1111
HostName coder.bar
12-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio bar
12+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio bar
1313
ConnectTimeout 0
1414
StrictHostKeyChecking no
1515
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/replace-end-no-newline.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Host test2
44
Port 443 # --- START CODER JETBRAINS test.coder.invalid
55
Host coder-jetbrains--foo-bar--test.coder.invalid
66
HostName coder.foo-bar
7-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
7+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
88
ConnectTimeout 0
99
StrictHostKeyChecking no
1010
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/replace-end.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Host test2
55
# --- START CODER JETBRAINS test.coder.invalid
66
Host coder-jetbrains--foo-bar--test.coder.invalid
77
HostName coder.foo-bar
8-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
8+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
99
ConnectTimeout 0
1010
StrictHostKeyChecking no
1111
UserKnownHostsFile /dev/null

src/test/fixtures/outputs/replace-middle-ignore-unrelated.conf

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ some coder config
66
# --- START CODER JETBRAINS test.coder.invalid
77
Host coder-jetbrains--foo-bar--test.coder.invalid
88
HostName coder.foo-bar
9-
ProxyCommand "/tmp/coder-gateway/test.coder.invalid/coder-linux-amd64" --global-config "/tmp/coder-gateway/test.coder.invalid/config" ssh --stdio foo-bar
9+
ProxyCommand /tmp/coder-gateway/test.coder.invalid/coder-linux-amd64 --global-config /tmp/coder-gateway/test.coder.invalid/config ssh --stdio foo-bar
1010
ConnectTimeout 0
1111
StrictHostKeyChecking no
1212
UserKnownHostsFile /dev/null

0 commit comments

Comments
 (0)