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;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Date;
39 import java.util.List;
41 public class JSchClient {
43 private final static int BUF_SIZE = 32 * 1024;
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;
51 private JSchClient(@NotNull String host,
52 @Nullable String username,
53 @Nullable Integer port,
54 @NotNull String command,
55 @NotNull Logger logger) {
57 myUsername = username;
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);
68 JSchClient ssh = createClient(logger, args);
70 } catch (Throwable t) {
71 if (logger instanceof InMemoryLogger) {
72 ((InMemoryLogger)logger).printLog();
74 System.err.println(t.getMessage());
75 if (t instanceof NullPointerException || debug)
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));
90 //noinspection HardCodedStringLiteral
91 if ("-p".equals(args[i])) {
93 port = Integer.parseInt(args[i++]);
95 String host = args[i++];
97 int atIndex = host.lastIndexOf('@');
102 user = host.substring(0, atIndex);
103 host = host.substring(atIndex + 1);
105 String command = args[i];
106 return new JSchClient(host, user, port, command, logger);
110 public void run() throws Exception {
111 ChannelExec channel = null;
112 Session session = null;
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));
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");
126 jsch.addIdentity(rsa.getAbsolutePath());
128 File dsa = new File(ssh, "id_dsa");
130 jsch.addIdentity(dsa.getAbsolutePath());
134 session = jsch.getSession(myUsername, myHost, myPort != null ? myPort : 22);
136 String teamCityVersion = System.getenv(GitSSHHandler.TEAMCITY_VERSION);
137 if (teamCityVersion != null) {
138 session.setClientVersion(GitUtils.getSshClientVersion(session.getClientVersion(), teamCityVersion));
141 if (Boolean.parseBoolean(System.getenv(GitSSHHandler.SSH_IGNORE_KNOWN_HOSTS_ENV))) {
142 session.setConfig("StrictHostKeyChecking", "no");
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()) {
151 jsch.setKnownHosts(knownHosts.getAbsolutePath());
152 } catch (Exception e) {
153 myLogger.log(Logger.WARN, "Failed to configure known hosts: '" + e.toString() + "'");
159 String authMethods = System.getenv(GitSSHHandler.TEAMCITY_SSH_PREFERRED_AUTH_METHODS);
160 if (authMethods != null && authMethods.length() > 0)
161 session.setConfig("PreferredAuthentications", authMethods);
163 EmptySecurityCallbackHandler.install();
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();
175 if (!channel.isConnected()) {
176 throw new IOException("Connection failed");
179 byte[] buffer = new byte[BUF_SIZE];
181 while (channel.isConnected() && !channel.isClosed() && (count = input.read(buffer)) != -1) {
182 System.out.write(buffer, 0, count);
186 channel.disconnect();
188 session.disconnect();
193 private static class StdErrLogger implements Logger {
194 private final SimpleDateFormat myDateFormat = new SimpleDateFormat("[HH:mm:ss.SSS]");
196 public boolean isEnabled(final int level) {
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);
210 private String getTimestamp() {
211 synchronized (myDateFormat) {
212 return myDateFormat.format(new Date());
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>();
227 public boolean isEnabled(final int level) {
228 return level >= myMinLogLevel;
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));
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);
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;
267 private static String getLevel(int level) {
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 {
289 public void handle(final Callback[] callbacks) throws UnsupportedCallbackException {
290 if (callbacks.length > 0) {
291 throw new UnsupportedCallbackException(callbacks[0], "Unsupported callback");
295 static void install() {
296 Security.setProperty("auth.login.defaultCallbackHandler", EmptySecurityCallbackHandler.class.getName());