@@ -15,13 +15,7 @@ import { Authn } from "../../types/identity";
15
15
import { SshProvider , SshRequest , SupportedSshProvider } from "../../types/ssh" ;
16
16
import { delay } from "../../util" ;
17
17
import { AwsCredentials } from "../aws/types" ;
18
- import {
19
- ChildProcessByStdio ,
20
- StdioNull ,
21
- StdioPipe ,
22
- spawn ,
23
- } from "node:child_process" ;
24
- import { Readable } from "node:stream" ;
18
+ import { StdioNull , StdioPipe , spawn } from "node:child_process" ;
25
19
26
20
/** Matches the error message that AWS SSM print1 when access is not propagated */
27
21
// Note that the resource will randomly be either the SSM document or the EC2 instance
@@ -51,7 +45,9 @@ const SUDO_MESSAGE = /Sorry, user .+ may not run sudo on .+/; // The output of `
51
45
* in the process's stderr
52
46
*/
53
47
const DEFAULT_VALIDATION_WINDOW_MS = 5e3 ;
54
-
48
+ /** How long we wait for output on stderr before we consider the channel to be successfully established */
49
+ const STDERR_TIMEOUT_PERIOD = 10000 ;
50
+ /** Retry delay between SSH command spawn attempts */
55
51
const RETRY_DELAY_MS = 5000 ;
56
52
57
53
/**
@@ -92,60 +88,6 @@ const UNPROVISIONED_ACCESS_MESSAGES = [
92
88
{ pattern : DESTINATION_READ_ERROR } ,
93
89
] ;
94
90
95
- /** Checks if access has propagated through AWS to the SSM agent
96
- *
97
- * AWS takes about 8 minutes, GCP takes under 1 minute
98
- * to fully resolve access after it is granted.
99
- * During this time, calls to `aws ssm start-session` / `gcloud compute start-iap-tunnel`
100
- * will fail randomly with an various error messages.
101
- *
102
- * This function checks the subprocess output to see if any of the error messages
103
- * are printed to the error output within the first 5 seconds of startup.
104
- * If they are, the returned `isAccessPropagated()` function will return false.
105
- * When this occurs, the consumer of this function should retry the `aws` / `gcloud` command.
106
- *
107
- * Note that this function requires interception of the subprocess stderr stream.
108
- * This works because AWS SSM wraps the session in a single-stream pty, so we
109
- * do not capture stderr emitted from the wrapped shell session.
110
- */
111
- const accessPropagationGuard = (
112
- child : ChildProcessByStdio < null , null , Readable > ,
113
- debug ?: boolean
114
- ) => {
115
- let isEphemeralAccessDeniedException = false ;
116
- let isGoogleLoginException = false ;
117
- const beforeStart = Date . now ( ) ;
118
-
119
- child . stderr . on ( "data" , ( chunk ) => {
120
- const chunkString : string = chunk . toString ( "utf-8" ) ;
121
-
122
- if ( debug ) print2 ( chunkString ) ;
123
-
124
- const match = UNPROVISIONED_ACCESS_MESSAGES . find ( ( message ) =>
125
- chunkString . match ( message . pattern )
126
- ) ;
127
-
128
- if (
129
- match &&
130
- Date . now ( ) <=
131
- beforeStart + ( match . validationWindowMs || DEFAULT_VALIDATION_WINDOW_MS )
132
- ) {
133
- isEphemeralAccessDeniedException = true ;
134
- }
135
-
136
- const googleLoginMatch = chunkString . match ( GOOGLE_LOGIN_MESSAGE ) ;
137
- isGoogleLoginException = isGoogleLoginException || ! ! googleLoginMatch ; // once true, always true
138
- if ( isGoogleLoginException ) {
139
- isEphemeralAccessDeniedException = false ; // always overwrite to false so we don't retry the access
140
- }
141
- } ) ;
142
-
143
- return {
144
- isAccessPropagated : ( ) => ! isEphemeralAccessDeniedException ,
145
- isGoogleLoginException : ( ) => isGoogleLoginException ,
146
- } ;
147
- } ;
148
-
149
91
const spawnChildProcess = (
150
92
credential : AwsCredentials | undefined ,
151
93
command : string ,
@@ -203,11 +145,60 @@ async function spawnSshNode(
203
145
) ;
204
146
205
147
// TODO ENG-2284 support login with Google Cloud: currently return a boolean to indicate if the exception was a Google login error.
206
- const { isAccessPropagated, isGoogleLoginException } =
207
- accessPropagationGuard ( child , options . debug ) ;
148
+ let isEphemeralAccessDeniedException = false ;
149
+ let isGoogleLoginException = false ;
150
+
151
+ let timeout : NodeJS . Timeout ;
152
+
153
+ const remoteCommandOption = hasRemoteCommandOption ( options . args ) ;
154
+
155
+ const resetTimeout = ( ) => {
156
+ if ( ! remoteCommandOption ) {
157
+ return ;
158
+ }
159
+ if ( timeout ) {
160
+ clearTimeout ( timeout ) ;
161
+ }
162
+ timeout = setTimeout ( ( ) => {
163
+ console . log ( "SSH connection established." ) ;
164
+ } , STDERR_TIMEOUT_PERIOD ) ;
165
+ } ;
166
+
167
+ const beforeStart = Date . now ( ) ;
168
+ child . stderr . on ( "data" , ( chunk ) => {
169
+ resetTimeout ( ) ;
170
+
171
+ const chunkString : string = chunk . toString ( "utf-8" ) ;
172
+
173
+ if ( options . debug ) print2 ( chunkString ) ;
174
+
175
+ const match = UNPROVISIONED_ACCESS_MESSAGES . find ( ( message ) =>
176
+ chunkString . match ( message . pattern )
177
+ ) ;
178
+
179
+ if (
180
+ match &&
181
+ Date . now ( ) <=
182
+ beforeStart +
183
+ ( match . validationWindowMs || DEFAULT_VALIDATION_WINDOW_MS )
184
+ ) {
185
+ isEphemeralAccessDeniedException = true ;
186
+ }
187
+
188
+ const googleLoginMatch = chunkString . match ( GOOGLE_LOGIN_MESSAGE ) ;
189
+ isGoogleLoginException = isGoogleLoginException || ! ! googleLoginMatch ; // once true, always true
190
+ if ( isGoogleLoginException ) {
191
+ // always overwrite to false so we don't retry the access
192
+ isEphemeralAccessDeniedException = false ;
193
+ }
194
+ } ) ;
195
+
196
+ const isAccessPropagated = ( ) => ! isEphemeralAccessDeniedException ;
208
197
209
198
const exitListener = child . on ( "exit" , ( code ) => {
199
+ clearTimeout ( timeout ) ;
210
200
exitListener . unref ( ) ;
201
+
211
202
// In the case of ephemeral AccessDenied exceptions due to unpropagated
212
203
// permissions, continually retry access until success
213
204
if ( ! isAccessPropagated ( ) ) {
@@ -229,7 +220,7 @@ async function spawnSshNode(
229
220
. catch ( reject ) ;
230
221
231
222
return ;
232
- } else if ( isGoogleLoginException ( ) ) {
223
+ } else if ( isGoogleLoginException ) {
233
224
reject ( `Please login to Google Cloud CLI with 'gcloud auth login'` ) ;
234
225
return ;
235
226
}
@@ -241,11 +232,16 @@ async function spawnSshNode(
241
232
} ) ;
242
233
}
243
234
235
+ const hasRemoteCommandOption = ( options : string [ ] ) : boolean => {
236
+ return options . some ( ( opt ) => opt . startsWith ( "-N" ) ) ;
237
+ } ;
238
+
244
239
const createCommand = (
245
240
data : SshRequest ,
246
241
args : CommandArgs ,
247
242
proxyCommand : string [ ]
248
243
) => {
244
+ // TODO: Unpack no-command options
249
245
addCommonArgs ( args , proxyCommand ) ;
250
246
251
247
if ( "source" in args ) {
0 commit comments