1
1
/*
2
2
* The MIT License
3
3
*
4
- * Copyright (c) 2014, Eccam s.r.o., Milan Kriz
4
+ * Copyright (c) 2014, Eccam s.r.o., Milan Kriz, CloudBees Inc.
5
5
*
6
6
* Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
* of this software and associated documentation files (the "Software"), to deal
25
25
package com .cloudbees .jenkins .plugins .sshagent .exec ;
26
26
27
27
import com .cloudbees .jenkins .plugins .sshagent .RemoteAgent ;
28
-
28
+ import hudson .AbortException ;
29
+ import hudson .FilePath ;
30
+ import hudson .Launcher ;
29
31
import hudson .model .TaskListener ;
30
-
31
- import java .lang .Process ;
32
- import java .lang .ProcessBuilder ;
33
-
34
- import java .util .Map ;
35
- import java .util .HashMap ;
36
- import java .util .List ;
37
- import java .util .Arrays ;
38
-
32
+ import java .io .ByteArrayOutputStream ;
39
33
import java .io .IOException ;
40
- import java .io .File ;
41
- import java .io .FileOutputStream ;
42
- import java .io .OutputStream ;
43
- import java .io .OutputStreamWriter ;
44
- import java .io .Writer ;
45
- import java .nio .charset .Charset ;
46
- import java .nio .charset .StandardCharsets ;
34
+ import java .util .HashMap ;
35
+ import java .util .Map ;
36
+ import java .util .concurrent .TimeUnit ;
47
37
48
- import org .apache .commons .io .IOUtils ;
49
38
50
39
/**
51
40
* An implementation that uses native SSH agent installed on a system.
@@ -54,49 +43,44 @@ public class ExecRemoteAgent implements RemoteAgent {
54
43
private static final String AuthSocketVar = "SSH_AUTH_SOCK" ;
55
44
private static final String AgentPidVar = "SSH_AGENT_PID" ;
56
45
57
- /** Process builder keeping environment for all ExecRemoteAgent related processes. */
58
- private final ProcessBuilder processBuilder ;
46
+ private final Launcher launcher ;
59
47
60
48
/**
61
49
* The listener in case we need to report exceptions
62
50
*/
63
51
private final TaskListener listener ;
64
-
65
- /**
66
- * Process in which the ssh-agent is running.
67
- */
68
- private final Process agent ;
52
+
53
+ private final FilePath temp ;
69
54
70
55
/**
71
56
* The socket bound by the agent.
72
57
*/
73
58
private final String socket ;
74
59
75
- /**
76
- * Constructor.
77
- *
78
- * @param listener the listener.
79
- * @throws Exception if the agent could not start.
80
- */
81
- public ExecRemoteAgent (TaskListener listener ) throws Exception {
82
- this .processBuilder = new ProcessBuilder ();
60
+ /** Agent environment used for {@code ssh-add} and {@code ssh-agent -k}. */
61
+ private final Map <String , String > agentEnv ;
62
+
63
+ ExecRemoteAgent (Launcher launcher , TaskListener listener , FilePath temp ) throws Exception {
64
+ this .launcher = launcher ;
83
65
this .listener = listener ;
84
-
85
- this .agent = execProcess ("ssh-agent" );
86
- Map <String , String > agentEnv = parseAgentEnv (this .agent );
66
+ this .temp = temp ;
67
+
68
+ ByteArrayOutputStream baos = new ByteArrayOutputStream ();
69
+ if (launcher .launch ().cmds ("ssh-agent" ).stdout (baos ).start ().joinWithTimeout (1 , TimeUnit .MINUTES , listener ) != 0 ) {
70
+ throw new AbortException ("Failed to run ssh-agent" );
71
+ }
72
+ agentEnv = parseAgentEnv (baos .toString ()); // TODO could include local filenames, better to look up remote charset
87
73
88
74
if (agentEnv .containsKey (AuthSocketVar ))
89
75
socket = agentEnv .get (AuthSocketVar );
90
76
else
91
77
socket = "" ; // socket is not set
92
-
93
- // set agent environment to the process builder to provide it to ssh-add which will be executed later
94
- processBuilder .environment ().putAll (agentEnv );
95
78
}
96
79
97
80
/**
98
81
* {@inheritDoc}
99
82
*/
83
+ @ Override
100
84
public String getSocket () {
101
85
return socket ;
102
86
}
@@ -105,80 +89,46 @@ public String getSocket() {
105
89
* {@inheritDoc}
106
90
*/
107
91
@ Override
108
- public void addIdentity (String privateKey , final String passphrase , String comment ) throws IOException {
109
- File keyFile = File .createTempFile ("private_key_" , ".key" );
110
- try (FileOutputStream os = new FileOutputStream (keyFile ); Writer keyWriter = new OutputStreamWriter (os , StandardCharsets .US_ASCII )) {
111
- keyWriter .write (privateKey );
112
- }
113
- setReadOnlyForOwner (keyFile );
114
-
115
- File askpass = createAskpassScript ();
116
-
117
- processBuilder .environment ().put ("SSH_PASSPHRASE" , passphrase );
118
- processBuilder .environment ().put ("DISPLAY" , ":0" ); // just to force using SSH_ASKPASS
119
- processBuilder .environment ().put ("SSH_ASKPASS" , askpass .getPath ());
120
-
121
- final Process sshAdd = execProcess ("ssh-add " + keyFile .getPath ());
122
-
123
- String errorString = IOUtils .toString (sshAdd .getErrorStream ());
124
- if (!errorString .isEmpty ()) {
125
- errorString += "Check the passphrase for the private key." ;
126
- listener .getLogger ().println (errorString );
127
- }
128
- IOUtils .copy (sshAdd .getInputStream (), listener .getLogger ()); // default encoding appropriate here: local process output
129
-
92
+ public void addIdentity (String privateKey , final String passphrase , String comment ) throws IOException , InterruptedException {
93
+ FilePath keyFile = temp .createTextTempFile ("private_key_" , ".key" , privateKey );
130
94
try {
131
- sshAdd .waitFor ();
132
- }
133
- catch (InterruptedException e ) {
134
- // waiting or process somehow interrupted
135
- }
136
-
137
- processBuilder .environment ().remove ("SSH_ASKPASS" );
138
- processBuilder .environment ().remove ("DISPLAY" );
139
- processBuilder .environment ().remove ("SSH_PASSPHRASE" );
140
-
141
- if (askpass .isFile () && !askpass .delete ()) { // the ASKPASS script is self-deleting, anyway rather try to delete it in case of some error
142
- listener .getLogger ().println ("ExecRemoteAgent::addIdentity - failed to delete " + askpass );
143
- }
144
-
145
- if (!keyFile .delete ()) {
146
- listener .getLogger ().println ("ExecRemoteAgent::addIdentity - could NOT delete a temp file with a private key!" );
95
+ keyFile .chmod (0600 );
96
+
97
+ FilePath askpass = createAskpassScript ();
98
+ try {
99
+
100
+ Map <String ,String > env = new HashMap <>(agentEnv );
101
+ env .put ("SSH_PASSPHRASE" , passphrase );
102
+ env .put ("DISPLAY" , ":0" ); // just to force using SSH_ASKPASS
103
+ env .put ("SSH_ASKPASS" , askpass .getRemote ());
104
+ if (launcher .launch ().cmds ("ssh-add" , keyFile .getRemote ()).envs (env ).stdout (listener ).start ().joinWithTimeout (1 , TimeUnit .MINUTES , listener ) != 0 ) {
105
+ throw new AbortException ("Failed to run ssh-add" );
106
+ }
107
+ } finally {
108
+ if (askpass .exists ()) { // the ASKPASS script is self-deleting, anyway rather try to delete it in case of some error
109
+ askpass .delete ();
110
+ }
111
+ }
112
+ } finally {
113
+ keyFile .delete ();
147
114
}
148
115
}
149
116
150
117
/**
151
118
* {@inheritDoc}
152
119
*/
153
- public void stop () {
154
- try {
155
- execProcess ("ssh-agent -k" );
156
- }
157
- catch (IOException e ) {
158
- listener .error ("ExecRemoteAgent::stop - " + e .getCause ());
120
+ @ Override
121
+ public void stop () throws IOException , InterruptedException {
122
+ if (launcher .launch ().cmds ("ssh-agent" , "-k" ).envs (agentEnv ).stdout (listener ).start ().joinWithTimeout (1 , TimeUnit .MINUTES , listener ) != 0 ) {
123
+ throw new AbortException ("Failed to run ssh-agent -k" );
159
124
}
160
- agent .destroy ();
161
- }
162
-
163
- /**
164
- * Executes a new process using ProcessBuilder with custom environment variables.
165
- */
166
- private Process execProcess (String command ) throws IOException {
167
- listener .getLogger ().println ("ExecRemoteAgent::execProcess - " + command );
168
- List <String > command_list = Arrays .asList (command .split (" " ));
169
- processBuilder .command (command_list );
170
- Process process = processBuilder .start ();
171
- processBuilder .command ("" );
172
- return process ;
173
125
}
174
126
175
127
/**
176
128
* Parses ssh-agent output.
177
129
*/
178
- private Map <String ,String > parseAgentEnv (Process agent ) throws Exception {
179
- Map <String , String > env = new HashMap <String , String >();
180
-
181
- String agentOutput = IOUtils .toString (agent .getInputStream ()); // default encoding appropriate here: local filenames
130
+ private Map <String ,String > parseAgentEnv (String agentOutput ) throws Exception {
131
+ Map <String , String > env = new HashMap <>();
182
132
183
133
// get SSH_AUTH_SOCK
184
134
env .put (AuthSocketVar , getAgentValue (agentOutput , AuthSocketVar ));
@@ -196,46 +146,23 @@ private Map<String,String> parseAgentEnv(Process agent) throws Exception{
196
146
*/
197
147
private String getAgentValue (String agentOutput , String envVar ) {
198
148
int pos = agentOutput .indexOf (envVar ) + envVar .length () + 1 ; // +1 for '='
199
- int end = agentOutput .indexOf (";" , pos );
149
+ int end = agentOutput .indexOf (';' , pos );
200
150
return agentOutput .substring (pos , end );
201
151
}
202
152
203
- /**
204
- * Sets file's permissions to readable only for an owner.
205
- */
206
- private boolean setReadOnlyForOwner (File file ) {
207
- boolean ok = file .setExecutable (false , false );
208
- ok &= file .setWritable (false , false );
209
- ok &= file .setReadable (false , false );
210
- ok &= file .setReadable (true , true );
211
- return ok ;
212
- }
213
-
214
153
/**
215
154
* Creates a self-deleting script for SSH_ASKPASS. Self-deleting to be able to detect a wrong passphrase.
216
155
*/
217
- private File createAskpassScript () throws IOException {
156
+ private FilePath createAskpassScript () throws IOException , InterruptedException {
218
157
// TODO: assuming that ssh-add runs the script in shell even on Windows, not cmd
219
158
// for cmd following could work
220
159
// suffix = ".bat";
221
160
// script = "@ECHO %SSH_PASSPHRASE%\nDEL \"" + askpass.getAbsolutePath() + "\"\n";
222
161
223
- final String suffix ;
224
- final String script ;
225
-
226
- suffix = ".sh" ;
227
-
228
- File askpass = File .createTempFile ("askpass_" , suffix );
162
+ FilePath askpass = temp .createTextTempFile ("askpass_" , ".sh" , "#!/bin/sh\n echo $SSH_PASSPHRASE\n rm $0\n " );
229
163
230
- script = "#!/bin/sh\n echo $SSH_PASSPHRASE\n rm " + askpass .getAbsolutePath ().replace ("\\ " , "\\ \\ " ) + "\n " ; // TODO try using `rm $0` instead
231
-
232
- try (OutputStream os = new FileOutputStream (askpass ); Writer askpassWriter = new OutputStreamWriter (os , /* due to presence of a local filename */ Charset .defaultCharset ())) {
233
- askpassWriter .write (script );
234
- }
235
-
236
164
// executable only for a current user
237
- askpass .setExecutable (false , false );
238
- askpass .setExecutable (true , true );
165
+ askpass .chmod (0700 );
239
166
return askpass ;
240
167
}
241
168
}
0 commit comments