188082ca5524a2b7652858410b61fb276976ebd0
[teamcity/git-plugin.git] / git-agent / src / jetbrains / buildServer / buildTriggers / vcs / git / agent / JSchClient.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
18
19 import com.jcraft.jsch.ChannelExec;
20 import com.jcraft.jsch.JSch;
21 import com.jcraft.jsch.Logger;
22 import com.jcraft.jsch.Session;
23 import jetbrains.buildServer.buildTriggers.vcs.git.GitUtils;
24 import org.jetbrains.annotations.NotNull;
25 import org.jetbrains.annotations.Nullable;
26 import org.jetbrains.git4idea.ssh.GitSSHHandler;
27
28 import javax.security.auth.callback.Callback;
29 import javax.security.auth.callback.CallbackHandler;
30 import javax.security.auth.callback.UnsupportedCallbackException;
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.security.Security;
35 import java.text.SimpleDateFormat;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.List;
40
41 public class JSchClient {
42
43   private final static int BUF_SIZE = 32 * 1024;
44
45   private final String myHost;
46   private final String myUsername;
47   private final Integer myPort;
48   private final String myCommand;
49   private final Logger myLogger;
50
51   private JSchClient(@NotNull String host,
52                      @Nullable String username,
53                      @Nullable Integer port,
54                      @NotNull String command,
55                      @NotNull Logger logger) {
56     myHost = host;
57     myUsername = username;
58     myPort = port;
59     myCommand = command;
60     myLogger = logger;
61   }
62
63
64   public static void main(String... args) {
65     boolean debug = Boolean.parseBoolean(System.getenv(GitSSHHandler.TEAMCITY_DEBUG_SSH));
66     Logger logger = debug ? new StdErrLogger() : new InMemoryLogger(Logger.INFO);
67     try {
68       JSchClient ssh = createClient(logger, args);
69       ssh.run();
70     } catch (Throwable t) {
71       if (logger instanceof InMemoryLogger) {
72         ((InMemoryLogger)logger).printLog();
73       }
74       System.err.println(t.getMessage());
75       if (t instanceof NullPointerException || debug)
76         t.printStackTrace();
77       System.exit(1);
78     }
79   }
80
81
82   private static JSchClient createClient(@NotNull Logger logger, String[] args) {
83     if (args.length != 2 && args.length != 4) {
84       System.err.println("Invalid arguments " + Arrays.asList(args));
85       System.exit(1);
86     }
87
88     int i = 0;
89     Integer port = null;
90     //noinspection HardCodedStringLiteral
91     if ("-p".equals(args[i])) {
92       i++;
93       port = Integer.parseInt(args[i++]);
94     }
95     String host = args[i++];
96     String user;
97     int atIndex = host.lastIndexOf('@');
98     if (atIndex == -1) {
99       user = null;
100     }
101     else {
102       user = host.substring(0, atIndex);
103       host = host.substring(atIndex + 1);
104     }
105     String command = args[i];
106     return new JSchClient(host, user, port, command, logger);
107   }
108
109
110   public void run() throws Exception {
111     ChannelExec channel = null;
112     Session session = null;
113     try {
114       JSch.setLogger(myLogger);
115       JSch jsch = new JSch();
116       String privateKeyPath = System.getenv(GitSSHHandler.TEAMCITY_PRIVATE_KEY_PATH);
117       if (privateKeyPath != null) {
118         jsch.addIdentity(privateKeyPath, System.getenv(GitSSHHandler.TEAMCITY_PASSPHRASE));
119       } else {
120         String userHome = System.getProperty("user.home");
121         if (userHome != null) {
122           File homeDir = new File(userHome);
123           File ssh = new File(homeDir, ".ssh");
124           File rsa = new File(ssh, "id_rsa");
125           if (rsa.isFile()) {
126             jsch.addIdentity(rsa.getAbsolutePath());
127           }
128           File dsa = new File(ssh, "id_dsa");
129           if (dsa.isFile()) {
130             jsch.addIdentity(dsa.getAbsolutePath());
131           }
132         }
133       }
134       session = jsch.getSession(myUsername, myHost, myPort != null ? myPort : 22);
135
136       String teamCityVersion = System.getenv(GitSSHHandler.TEAMCITY_VERSION);
137       if (teamCityVersion != null) {
138         session.setClientVersion(GitUtils.getSshClientVersion(session.getClientVersion(), teamCityVersion));
139       }
140
141       if (Boolean.parseBoolean(System.getenv(GitSSHHandler.SSH_IGNORE_KNOWN_HOSTS_ENV))) {
142         session.setConfig("StrictHostKeyChecking", "no");
143       } else {
144         String userHome = System.getProperty("user.home");
145         if (userHome != null) {
146           File homeDir = new File(userHome);
147           File ssh = new File(homeDir, ".ssh");
148           File knownHosts = new File(ssh, "known_hosts");
149           if (knownHosts.isFile()) {
150             try {
151               jsch.setKnownHosts(knownHosts.getAbsolutePath());
152             } catch (Exception e) {
153               myLogger.log(Logger.WARN, "Failed to configure known hosts: '" + e.toString() + "'");
154             }
155           }
156         }
157       }
158
159       String authMethods = System.getenv(GitSSHHandler.TEAMCITY_SSH_PREFERRED_AUTH_METHODS);
160       if (authMethods != null && authMethods.length() > 0)
161         session.setConfig("PreferredAuthentications", authMethods);
162
163       EmptySecurityCallbackHandler.install();
164
165       session.connect();
166
167       channel = (ChannelExec) session.openChannel("exec");
168       channel.setPty(false);
169       channel.setCommand(myCommand);
170       channel.setInputStream(System.in);
171       channel.setErrStream(System.err);
172       InputStream input = channel.getInputStream();
173       channel.connect();
174
175       if (!channel.isConnected()) {
176         throw new IOException("Connection failed");
177       }
178
179       byte[] buffer = new byte[BUF_SIZE];
180       int count;
181       while ((count = input.read(buffer)) != -1) {
182         System.out.write(buffer, 0, count);
183       }
184     } finally {
185       if (channel != null)
186         channel.disconnect();
187       if (session != null)
188         session.disconnect();
189     }
190   }
191
192
193   private static class StdErrLogger implements Logger {
194     private final SimpleDateFormat myDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
195     @Override
196     public boolean isEnabled(final int level) {
197       return true;
198     }
199
200     @Override
201     public void log(final int level, final String message) {
202       System.err.print(getTimestamp());
203       System.err.print(" ");
204       System.err.print(getLevel(level));
205       System.err.print(" ");
206       System.err.println(message);
207     }
208
209     @NotNull
210     private String getTimestamp() {
211       synchronized (myDateFormat) {
212         return myDateFormat.format(new Date());
213       }
214     }
215   }
216
217
218   private static class InMemoryLogger implements Logger {
219     private final int myMinLogLevel;
220     private final List<LogEntry> myLogEntries;
221     InMemoryLogger(int minLogLevel) {
222       myMinLogLevel = minLogLevel;
223       myLogEntries = new ArrayList<LogEntry>();
224     }
225
226     @Override
227     public boolean isEnabled(final int level) {
228       return level >= myMinLogLevel;
229     }
230
231     @Override
232     public void log(final int level, final String message) {
233       if (isEnabled(level)) {
234         synchronized (myLogEntries) {
235           myLogEntries.add(new LogEntry(System.currentTimeMillis(), level, message));
236         }
237       }
238     }
239
240     void printLog() {
241       SimpleDateFormat dateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
242       synchronized (myLogEntries) {
243         for (LogEntry entry : myLogEntries) {
244           System.err.print(dateFormat.format(new Date(entry.myTimestamp)));
245           System.err.print(" ");
246           System.err.print(getLevel(entry.myLogLevel));
247           System.err.print(" ");
248           System.err.println(entry.myMessage);
249         }
250       }
251     }
252
253     private static class LogEntry {
254       private final long myTimestamp;
255       private final int myLogLevel;
256       private final String myMessage;
257       LogEntry(long timestamp, int logLevel, @NotNull String message) {
258         myTimestamp = timestamp;
259         myLogLevel = logLevel;
260         myMessage = message;
261       }
262     }
263   }
264
265
266   @NotNull
267   private static String getLevel(int level) {
268     switch (level) {
269       case Logger.DEBUG:
270         return "DEBUG";
271       case Logger.INFO:
272         return "INFO";
273       case Logger.WARN:
274         return "WARN";
275       case Logger.ERROR:
276         return "ERROR";
277       case Logger.FATAL:
278         return "FATAL";
279       default:
280         return "UNKNOWN";
281     }
282   }
283
284
285   // Doesn't provide any credentials, used instead the default handler from jdk
286   // which reads credentials them from stdin.
287   public static class EmptySecurityCallbackHandler implements CallbackHandler {
288     @Override
289     public void handle(final Callback[] callbacks) throws UnsupportedCallbackException {
290       if (callbacks.length > 0) {
291         throw new UnsupportedCallbackException(callbacks[0], "Unsupported callback");
292       }
293     }
294
295     static void install() {
296       Security.setProperty("auth.login.defaultCallbackHandler", EmptySecurityCallbackHandler.class.getName());
297     }
298   }
299 }