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.InputStream;
33 import java.security.Security;
34 import java.text.SimpleDateFormat;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.Date;
38 import java.util.List;
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);
173 InputStream input = channel.getInputStream();
174 byte[] buffer = new byte[BUF_SIZE];
176 while ((count = input.read(buffer)) != -1) {
177 System.out.write(buffer, 0, count);
181 channel.disconnect();
183 session.disconnect();
188 private static class StdErrLogger implements Logger {
189 private final SimpleDateFormat myDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
191 public boolean isEnabled(final int level) {
196 public void log(final int level, final String message) {
197 System.err.print(getTimestamp());
198 System.err.print(" ");
199 System.err.print(getLevel(level));
200 System.err.print(" ");
201 System.err.println(message);
205 private String getTimestamp() {
206 synchronized (myDateFormat) {
207 return myDateFormat.format(new Date());
213 private static class InMemoryLogger implements Logger {
214 private final int myMinLogLevel;
215 private final List<LogEntry> myLogEntries;
216 InMemoryLogger(int minLogLevel) {
217 myMinLogLevel = minLogLevel;
218 myLogEntries = new ArrayList<LogEntry>();
222 public boolean isEnabled(final int level) {
223 return level >= myMinLogLevel;
227 public void log(final int level, final String message) {
228 if (isEnabled(level)) {
229 synchronized (myLogEntries) {
230 myLogEntries.add(new LogEntry(System.currentTimeMillis(), level, message));
236 SimpleDateFormat dateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
237 synchronized (myLogEntries) {
238 for (LogEntry entry : myLogEntries) {
239 System.err.print(dateFormat.format(new Date(entry.myTimestamp)));
240 System.err.print(" ");
241 System.err.print(getLevel(entry.myLogLevel));
242 System.err.print(" ");
243 System.err.println(entry.myMessage);
248 private static class LogEntry {
249 private final long myTimestamp;
250 private final int myLogLevel;
251 private final String myMessage;
252 LogEntry(long timestamp, int logLevel, @NotNull String message) {
253 myTimestamp = timestamp;
254 myLogLevel = logLevel;
262 private static String getLevel(int level) {
280 // Doesn't provide any credentials, used instead the default handler from jdk
281 // which reads credentials them from stdin.
282 public static class EmptySecurityCallbackHandler implements CallbackHandler {
284 public void handle(final Callback[] callbacks) throws UnsupportedCallbackException {
285 if (callbacks.length > 0) {
286 throw new UnsupportedCallbackException(callbacks[0], "Unsupported callback");
290 static void install() {
291 Security.setProperty("auth.login.defaultCallbackHandler", EmptySecurityCallbackHandler.class.getName());