2 * Copyright 2000-2018 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package jetbrains.buildServer.buildTriggers.vcs.git.agent;
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;
28 import javax.security.auth.callback.Callback;
29 import javax.security.auth.callback.CallbackHandler;
30 import javax.security.auth.callback.UnsupportedCallbackException;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.security.Security;
35 import java.text.SimpleDateFormat;
37 import java.util.concurrent.TimeUnit;
38 import java.util.concurrent.atomic.AtomicLong;
40 public class JSchClient {
42 private final static int BUF_SIZE = 32 * 1024;
44 private final String myHost;
45 private final String myUsername;
46 private final Integer myPort;
47 private final String myCommand;
48 private final Logger myLogger;
50 private JSchClient(@NotNull String host,
51 @Nullable String username,
52 @Nullable Integer port,
53 @NotNull String command,
54 @NotNull Logger logger) {
56 myUsername = username;
63 public static void main(String... args) {
64 boolean debug = Boolean.parseBoolean(System.getenv(GitSSHHandler.TEAMCITY_DEBUG_SSH));
65 Logger logger = debug ? new StdErrLogger() : new InMemoryLogger(Logger.INFO);
67 JSchClient ssh = createClient(logger, args);
69 } catch (Throwable t) {
70 if (logger instanceof InMemoryLogger) {
71 ((InMemoryLogger)logger).printLog();
73 System.err.println(t.getMessage());
74 if (t instanceof NullPointerException || debug)
81 private static JSchClient createClient(@NotNull Logger logger, String[] args) {
82 if (args.length != 2 && args.length != 4) {
83 System.err.println("Invalid arguments " + Arrays.asList(args));
89 //noinspection HardCodedStringLiteral
90 if ("-p".equals(args[i])) {
92 port = Integer.parseInt(args[i++]);
94 String host = args[i++];
96 int atIndex = host.lastIndexOf('@');
101 user = host.substring(0, atIndex);
102 host = host.substring(atIndex + 1);
104 String command = args[i];
105 return new JSchClient(host, user, port, command, logger);
109 public void run() throws Exception {
110 ChannelExec channel = null;
111 Session session = null;
113 JSch.setLogger(myLogger);
114 JSch jsch = new JSch();
115 String privateKeyPath = System.getenv(GitSSHHandler.TEAMCITY_PRIVATE_KEY_PATH);
116 if (privateKeyPath != null) {
117 jsch.addIdentity(privateKeyPath, System.getenv(GitSSHHandler.TEAMCITY_PASSPHRASE));
119 String userHome = System.getProperty("user.home");
120 if (userHome != null) {
121 File homeDir = new File(userHome);
122 File ssh = new File(homeDir, ".ssh");
123 File rsa = new File(ssh, "id_rsa");
125 jsch.addIdentity(rsa.getAbsolutePath());
127 File dsa = new File(ssh, "id_dsa");
129 jsch.addIdentity(dsa.getAbsolutePath());
133 session = jsch.getSession(myUsername, myHost, myPort != null ? myPort : 22);
135 String teamCityVersion = System.getenv(GitSSHHandler.TEAMCITY_VERSION);
136 if (teamCityVersion != null) {
137 session.setClientVersion(GitUtils.getSshClientVersion(session.getClientVersion(), teamCityVersion));
140 if (Boolean.parseBoolean(System.getenv(GitSSHHandler.SSH_IGNORE_KNOWN_HOSTS_ENV))) {
141 session.setConfig("StrictHostKeyChecking", "no");
143 String userHome = System.getProperty("user.home");
144 if (userHome != null) {
145 File homeDir = new File(userHome);
146 File ssh = new File(homeDir, ".ssh");
147 File knownHosts = new File(ssh, "known_hosts");
148 if (knownHosts.isFile()) {
150 jsch.setKnownHosts(knownHosts.getAbsolutePath());
151 } catch (Exception e) {
152 myLogger.log(Logger.WARN, "Failed to configure known hosts: '" + e.toString() + "'");
158 String authMethods = System.getenv(GitSSHHandler.TEAMCITY_SSH_PREFERRED_AUTH_METHODS);
159 if (authMethods != null && authMethods.length() > 0)
160 session.setConfig("PreferredAuthentications", authMethods);
162 EmptySecurityCallbackHandler.install();
166 channel = (ChannelExec) session.openChannel("exec");
167 channel.setPty(false);
168 channel.setCommand(myCommand);
169 channel.setInputStream(System.in);
170 channel.setErrStream(System.err);
171 InputStream input = channel.getInputStream();
172 Integer timeoutSeconds = getTimeoutSeconds();
173 if (timeoutSeconds != null) {
174 channel.connect(timeoutSeconds * 1000);
180 if (!channel.isConnected()) {
181 throw new IOException("Connection failed");
184 Copy copyThread = new Copy(input);
185 if (timeoutSeconds != null) {
186 new Timer(copyThread, timeoutSeconds * 1000).start();
190 copyThread.rethrowError();
193 channel.disconnect();
195 session.disconnect();
201 private Integer getTimeoutSeconds() {
202 String timeout = System.getenv(GitSSHHandler.TEAMCITY_SSH_IDLE_TIMEOUT_SECONDS);
206 return Integer.parseInt(timeout);
207 } catch (NumberFormatException e) {
208 myLogger.log(Logger.WARN, "Failed to parse idle timeout: '" + timeout + "'");
214 private class Timer extends Thread {
215 private final long myThresholdNanos;
216 private volatile Copy myCopyThread;
217 Timer(@NotNull Copy copyThread, long timeoutSeconds) {
218 myCopyThread = copyThread;
219 myThresholdNanos = TimeUnit.SECONDS.toNanos(timeoutSeconds);
226 boolean logged = false;
227 long sleepInterval = Math.min(TimeUnit.SECONDS.toMillis(10), TimeUnit.NANOSECONDS.toMillis(myThresholdNanos));
228 //noinspection InfiniteLoopStatement: it is a daemon thread and doesn't prevent process from termination
230 if (System.nanoTime() - myCopyThread.getTimestamp() > myThresholdNanos) {
232 myLogger.log(Logger.ERROR, String.format("Timeout error: no activity for %s seconds", TimeUnit.NANOSECONDS.toSeconds(myThresholdNanos)));
235 myCopyThread.interrupt();
238 Thread.sleep(sleepInterval);
239 } catch (Exception e) {
248 private class Copy extends Thread {
249 private final InputStream myInput;
250 private final AtomicLong myTimestamp = new AtomicLong(System.nanoTime());
251 private volatile Exception myError;
252 Copy(@NotNull InputStream input) {
259 byte[] buffer = new byte[BUF_SIZE];
262 while ((count = myInput.read(buffer)) != -1) {
263 System.out.write(buffer, 0, count);
264 myTimestamp.set(System.nanoTime());
265 if (System.out.checkError()) {
266 myLogger.log(Logger.ERROR, "Error while writing to stdout");
267 throw new IOException("Error while writing to stdout");
270 } catch (Exception e) {
275 long getTimestamp() {
276 return myTimestamp.get();
279 void rethrowError() throws Exception {
286 private static class StdErrLogger implements Logger {
287 private final SimpleDateFormat myDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
289 public boolean isEnabled(final int level) {
294 public void log(final int level, final String message) {
295 System.err.print(getTimestamp());
296 System.err.print(" ");
297 System.err.print(getLevel(level));
298 System.err.print(" ");
299 System.err.println(message);
303 private String getTimestamp() {
304 synchronized (myDateFormat) {
305 return myDateFormat.format(new Date());
311 private static class InMemoryLogger implements Logger {
312 private final int myMinLogLevel;
313 private final List<LogEntry> myLogEntries;
314 InMemoryLogger(int minLogLevel) {
315 myMinLogLevel = minLogLevel;
316 myLogEntries = new ArrayList<LogEntry>();
320 public boolean isEnabled(final int level) {
321 return level >= myMinLogLevel;
325 public void log(final int level, final String message) {
326 if (isEnabled(level)) {
327 synchronized (myLogEntries) {
328 myLogEntries.add(new LogEntry(System.currentTimeMillis(), level, message));
334 SimpleDateFormat dateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
335 synchronized (myLogEntries) {
336 for (LogEntry entry : myLogEntries) {
337 System.err.print(dateFormat.format(new Date(entry.myTimestamp)));
338 System.err.print(" ");
339 System.err.print(getLevel(entry.myLogLevel));
340 System.err.print(" ");
341 System.err.println(entry.myMessage);
346 private static class LogEntry {
347 private final long myTimestamp;
348 private final int myLogLevel;
349 private final String myMessage;
350 LogEntry(long timestamp, int logLevel, @NotNull String message) {
351 myTimestamp = timestamp;
352 myLogLevel = logLevel;
360 private static String getLevel(int level) {
378 // Doesn't provide any credentials, used instead the default handler from jdk
379 // which reads credentials them from stdin.
380 public static class EmptySecurityCallbackHandler implements CallbackHandler {
382 public void handle(final Callback[] callbacks) throws UnsupportedCallbackException {
383 if (callbacks.length > 0) {
384 throw new UnsupportedCallbackException(callbacks[0], "Unsupported callback");
388 static void install() {
389 Security.setProperty("auth.login.defaultCallbackHandler", EmptySecurityCallbackHandler.class.getName());