1 /* 2 * Copyright (C) 2018, Sasa Zivkov <sasa.zivkov@sap.com> 3 * Copyright (C) 2016, Mark Ingram <markdingram@gmail.com> 4 * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> 5 * Copyright (C) 2008-2009, Google Inc. 6 * Copyright (C) 2009, Google, Inc. 7 * Copyright (C) 2009, JetBrains s.r.o. 8 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> 9 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others 10 * 11 * This program and the accompanying materials are made available under the 12 * terms of the Eclipse Distribution License v. 1.0 which is available at 13 * https://www.eclipse.org/org/documents/edl-v10.php. 14 * 15 * SPDX-License-Identifier: BSD-3-Clause 16 */ 17 18 //TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 19 package org.eclipse.jgit.transport; 20 21 import static java.util.stream.Collectors.joining; 22 import static java.util.stream.Collectors.toList; 23 24 import java.io.File; 25 import java.io.FileInputStream; 26 import java.io.FileNotFoundException; 27 import java.io.IOException; 28 import java.lang.reflect.InvocationTargetException; 29 import java.lang.reflect.Method; 30 import java.net.ConnectException; 31 import java.net.UnknownHostException; 32 import java.text.MessageFormat; 33 import java.util.HashMap; 34 import java.util.List; 35 import java.util.Locale; 36 import java.util.Map; 37 import java.util.concurrent.TimeUnit; 38 import java.util.stream.Stream; 39 40 import org.eclipse.jgit.errors.TransportException; 41 import org.eclipse.jgit.internal.transport.jsch.JSchText; 42 import org.eclipse.jgit.util.FS; 43 import org.slf4j.Logger; 44 import org.slf4j.LoggerFactory; 45 46 import com.jcraft.jsch.ConfigRepository; 47 import com.jcraft.jsch.ConfigRepository.Config; 48 import com.jcraft.jsch.HostKey; 49 import com.jcraft.jsch.HostKeyRepository; 50 import com.jcraft.jsch.JSch; 51 import com.jcraft.jsch.JSchException; 52 import com.jcraft.jsch.Session; 53 54 /** 55 * The base session factory that loads known hosts and private keys from 56 * <code>$HOME/.ssh</code>. 57 * <p> 58 * This is the default implementation used by JGit and provides most of the 59 * compatibility necessary to match OpenSSH, a popular implementation of SSH 60 * used by C Git. 61 * <p> 62 * The factory does not provide UI behavior. Override the method 63 * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} to 64 * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session. 65 */ 66 public class JschConfigSessionFactory extends SshSessionFactory { 67 68 private static final String JSCH = "jsch"; //$NON-NLS-1$ 69 70 private static final Logger LOG = LoggerFactory 71 .getLogger(JschConfigSessionFactory.class); 72 73 /** 74 * We use different Jsch instances for hosts that have an IdentityFile 75 * configured in ~/.ssh/config. Jsch by default would cache decrypted keys 76 * only per session, which results in repeated password prompts. Using 77 * different Jsch instances, we can cache the keys on these instances so 78 * that they will be re-used for successive sessions, and thus the user is 79 * prompted for a key password only once while Eclipse runs. 80 */ 81 private final Map<String, JSch> byIdentityFile = new HashMap<>(); 82 83 private JSch defaultJSch; 84 85 private OpenSshConfig config; 86 87 /** {@inheritDoc} */ 88 @Override getSession(URIish uri, CredentialsProvider credentialsProvider, FS fs, int tms)89 public synchronized RemoteSession getSession(URIish uri, 90 CredentialsProvider credentialsProvider, FS fs, int tms) 91 throws TransportException { 92 93 String user = uri.getUser(); 94 final String pass = uri.getPass(); 95 String host = uri.getHost(); 96 int port = uri.getPort(); 97 98 try { 99 if (config == null) 100 config = OpenSshConfig.get(fs); 101 102 final OpenSshConfig.Host hc = config.lookup(host); 103 if (port <= 0) 104 port = hc.getPort(); 105 if (user == null) 106 user = hc.getUser(); 107 108 Session session = createSession(credentialsProvider, fs, user, 109 pass, host, port, hc); 110 111 int retries = 0; 112 while (!session.isConnected()) { 113 try { 114 retries++; 115 session.connect(tms); 116 } catch (JSchException e) { 117 session.disconnect(); 118 session = null; 119 // Make sure our known_hosts is not outdated 120 knownHosts(getJSch(hc, fs), fs); 121 122 if (isAuthenticationCanceled(e)) { 123 throw e; 124 } else if (isAuthenticationFailed(e) 125 && credentialsProvider != null) { 126 // if authentication failed maybe credentials changed at 127 // the remote end therefore reset credentials and retry 128 if (retries < 3) { 129 credentialsProvider.reset(uri); 130 session = createSession(credentialsProvider, fs, 131 user, pass, host, port, hc); 132 } else 133 throw e; 134 } else if (retries >= hc.getConnectionAttempts()) { 135 throw e; 136 } else { 137 try { 138 Thread.sleep(1000); 139 session = createSession(credentialsProvider, fs, 140 user, pass, host, port, hc); 141 } catch (InterruptedException e1) { 142 throw new TransportException( 143 JSchText.get().transportSSHRetryInterrupt, 144 e1); 145 } 146 } 147 } 148 } 149 150 return new JschSession(session, uri); 151 152 } catch (JSchException je) { 153 final Throwable c = je.getCause(); 154 if (c instanceof UnknownHostException) { 155 throw new TransportException(uri, 156 JSchText.get().unknownHost, 157 je); 158 } 159 if (c instanceof ConnectException) { 160 throw new TransportException(uri, c.getMessage(), je); 161 } 162 throw new TransportException(uri, je.getMessage(), je); 163 } 164 165 } 166 167 @Override getType()168 public String getType() { 169 return JSCH; 170 } 171 isAuthenticationFailed(JSchException e)172 private static boolean isAuthenticationFailed(JSchException e) { 173 return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$ 174 } 175 isAuthenticationCanceled(JSchException e)176 private static boolean isAuthenticationCanceled(JSchException e) { 177 return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$ 178 } 179 180 // Package visibility for tests createSession(CredentialsProvider credentialsProvider, FS fs, String user, final String pass, String host, int port, final OpenSshConfig.Host hc)181 Session createSession(CredentialsProvider credentialsProvider, 182 FS fs, String user, final String pass, String host, int port, 183 final OpenSshConfig.Host hc) throws JSchException { 184 final Session session = createSession(hc, user, host, port, fs); 185 // Jsch will have overridden the explicit user by the one from the SSH 186 // config file... 187 setUserName(session, user); 188 // Jsch will also have overridden the port. 189 if (port > 0 && port != session.getPort()) { 190 session.setPort(port); 191 } 192 // We retry already in getSession() method. JSch must not retry 193 // on its own. 194 session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$ 195 if (pass != null) 196 session.setPassword(pass); 197 final String strictHostKeyCheckingPolicy = hc 198 .getStrictHostKeyChecking(); 199 if (strictHostKeyCheckingPolicy != null) 200 session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$ 201 strictHostKeyCheckingPolicy); 202 final String pauth = hc.getPreferredAuthentications(); 203 if (pauth != null) 204 session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$ 205 if (credentialsProvider != null 206 && (!hc.isBatchMode() || !credentialsProvider.isInteractive())) { 207 session.setUserInfo(new CredentialsProviderUserInfo(session, 208 credentialsProvider)); 209 } 210 safeConfig(session, hc.getConfig()); 211 if (hc.getConfig().getValue("HostKeyAlgorithms") == null) { //$NON-NLS-1$ 212 setPreferredKeyTypesOrder(session); 213 } 214 configure(hc, session); 215 return session; 216 } 217 safeConfig(Session session, Config cfg)218 private void safeConfig(Session session, Config cfg) { 219 // Ensure that Jsch checks all configured algorithms, not just its 220 // built-in ones. Otherwise it may propose an algorithm for which it 221 // doesn't have an implementation, and then run into an NPE if that 222 // algorithm ends up being chosen. 223 copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$ 224 copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$ 225 copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$ 226 "CheckSignatures"); //$NON-NLS-1$ 227 } 228 setPreferredKeyTypesOrder(Session session)229 private static void setPreferredKeyTypesOrder(Session session) { 230 HostKeyRepository hkr = session.getHostKeyRepository(); 231 HostKey[] hostKeys = hkr.getHostKey(hostName(session), null); 232 233 if (hostKeys == null) { 234 return; 235 } 236 237 List<String> known = Stream.of(hostKeys) 238 .map(HostKey::getType) 239 .collect(toList()); 240 241 if (!known.isEmpty()) { 242 String serverHostKey = "server_host_key"; //$NON-NLS-1$ 243 String current = session.getConfig(serverHostKey); 244 if (current == null) { 245 session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$ 246 return; 247 } 248 249 String knownFirst = Stream.concat( 250 known.stream(), 251 Stream.of(current.split(",")) //$NON-NLS-1$ 252 .filter(s -> !known.contains(s))) 253 .collect(joining(",")); //$NON-NLS-1$ 254 session.setConfig(serverHostKey, knownFirst); 255 } 256 } 257 hostName(Session s)258 private static String hostName(Session s) { 259 if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) { 260 return s.getHost(); 261 } 262 return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$ 263 Integer.valueOf(s.getPort())); 264 } 265 copyConfigValueToSession(Session session, Config cfg, String from, String to)266 private void copyConfigValueToSession(Session session, Config cfg, 267 String from, String to) { 268 String value = cfg.getValue(from); 269 if (value != null) { 270 session.setConfig(to, value); 271 } 272 } 273 setUserName(Session session, String userName)274 private void setUserName(Session session, String userName) { 275 // Jsch 0.1.54 picks up the user name from the ssh config, even if an 276 // explicit user name was given! We must correct that if ~/.ssh/config 277 // has a different user name. 278 if (userName == null || userName.isEmpty() 279 || userName.equals(session.getUserName())) { 280 return; 281 } 282 try { 283 Class<?>[] parameterTypes = { String.class }; 284 Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$ 285 parameterTypes); 286 method.setAccessible(true); 287 method.invoke(session, userName); 288 } catch (NullPointerException | IllegalAccessException 289 | IllegalArgumentException | InvocationTargetException 290 | NoSuchMethodException | SecurityException e) { 291 LOG.error(MessageFormat.format(JSchText.get().sshUserNameError, 292 userName, session.getUserName()), e); 293 } 294 } 295 296 /** 297 * Create a new remote session for the requested address. 298 * 299 * @param hc 300 * host configuration 301 * @param user 302 * login to authenticate as. 303 * @param host 304 * server name to connect to. 305 * @param port 306 * port number of the SSH daemon (typically 22). 307 * @param fs 308 * the file system abstraction which will be necessary to 309 * perform certain file system operations. 310 * @return new session instance, but otherwise unconfigured. 311 * @throws com.jcraft.jsch.JSchException 312 * the session could not be created. 313 */ createSession(final OpenSshConfig.Host hc, final String user, final String host, final int port, FS fs)314 protected Session createSession(final OpenSshConfig.Host hc, 315 final String user, final String host, final int port, FS fs) 316 throws JSchException { 317 return getJSch(hc, fs).getSession(user, host, port); 318 } 319 320 /** 321 * Provide additional configuration for the JSch instance. This method could 322 * be overridden to supply a preferred 323 * {@link com.jcraft.jsch.IdentityRepository}. 324 * 325 * @param jsch 326 * jsch instance 327 * @since 4.5 328 */ configureJSch(JSch jsch)329 protected void configureJSch(JSch jsch) { 330 // No additional configuration required. 331 } 332 333 /** 334 * Provide additional configuration for the session based on the host 335 * information. This method could be used to supply 336 * {@link com.jcraft.jsch.UserInfo}. 337 * 338 * @param hc 339 * host configuration 340 * @param session 341 * session to configure 342 */ configure(OpenSshConfig.Host hc, Session session)343 protected void configure(OpenSshConfig.Host hc, Session session) { 344 // No additional configuration required. 345 } 346 347 /** 348 * Obtain the JSch used to create new sessions. 349 * 350 * @param hc 351 * host configuration 352 * @param fs 353 * the file system abstraction which will be necessary to 354 * perform certain file system operations. 355 * @return the JSch instance to use. 356 * @throws com.jcraft.jsch.JSchException 357 * the user configuration could not be created. 358 */ getJSch(OpenSshConfig.Host hc, FS fs)359 protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException { 360 if (defaultJSch == null) { 361 defaultJSch = createDefaultJSch(fs); 362 if (defaultJSch.getConfigRepository() == null) { 363 defaultJSch.setConfigRepository( 364 new JschBugFixingConfigRepository(config)); 365 } 366 for (Object name : defaultJSch.getIdentityNames()) 367 byIdentityFile.put((String) name, defaultJSch); 368 } 369 370 final File identityFile = hc.getIdentityFile(); 371 if (identityFile == null) 372 return defaultJSch; 373 374 final String identityKey = identityFile.getAbsolutePath(); 375 JSch jsch = byIdentityFile.get(identityKey); 376 if (jsch == null) { 377 jsch = new JSch(); 378 configureJSch(jsch); 379 if (jsch.getConfigRepository() == null) { 380 jsch.setConfigRepository(defaultJSch.getConfigRepository()); 381 } 382 jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository()); 383 jsch.addIdentity(identityKey); 384 byIdentityFile.put(identityKey, jsch); 385 } 386 return jsch; 387 } 388 389 /** 390 * Create default instance of jsch 391 * 392 * @param fs 393 * the file system abstraction which will be necessary to perform 394 * certain file system operations. 395 * @return the new default JSch implementation. 396 * @throws com.jcraft.jsch.JSchException 397 * known host keys cannot be loaded. 398 */ createDefaultJSch(FS fs)399 protected JSch createDefaultJSch(FS fs) throws JSchException { 400 final JSch jsch = new JSch(); 401 JSch.setConfig("ssh-rsa", JSch.getConfig("signature.rsa")); //$NON-NLS-1$ //$NON-NLS-2$ 402 JSch.setConfig("ssh-dss", JSch.getConfig("signature.dss")); //$NON-NLS-1$ //$NON-NLS-2$ 403 configureJSch(jsch); 404 knownHosts(jsch, fs); 405 identities(jsch, fs); 406 return jsch; 407 } 408 knownHosts(JSch sch, FS fs)409 private static void knownHosts(JSch sch, FS fs) throws JSchException { 410 final File home = fs.userHome(); 411 if (home == null) 412 return; 413 final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$ 414 try (FileInputStream in = new FileInputStream(known_hosts)) { 415 sch.setKnownHosts(in); 416 } catch (FileNotFoundException none) { 417 // Oh well. They don't have a known hosts in home. 418 } catch (IOException err) { 419 // Oh well. They don't have a known hosts in home. 420 } 421 } 422 identities(JSch sch, FS fs)423 private static void identities(JSch sch, FS fs) { 424 final File home = fs.userHome(); 425 if (home == null) 426 return; 427 final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$ 428 if (sshdir.isDirectory()) { 429 loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$ 430 loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$ 431 loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$ 432 } 433 } 434 loadIdentity(JSch sch, File priv)435 private static void loadIdentity(JSch sch, File priv) { 436 if (priv.isFile()) { 437 try { 438 sch.addIdentity(priv.getAbsolutePath()); 439 } catch (JSchException e) { 440 // Instead, pretend the key doesn't exist. 441 } 442 } 443 } 444 445 private static class JschBugFixingConfigRepository 446 implements ConfigRepository { 447 448 private final ConfigRepository base; 449 JschBugFixingConfigRepository(ConfigRepository base)450 public JschBugFixingConfigRepository(ConfigRepository base) { 451 this.base = base; 452 } 453 454 @Override getConfig(String host)455 public Config getConfig(String host) { 456 return new JschBugFixingConfig(base.getConfig(host)); 457 } 458 459 /** 460 * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms 461 * some values from the config file into the format Jsch 0.1.54 expects. 462 * This is a work-around for bugs in Jsch. 463 * <p> 464 * Additionally, this config hides the IdentityFile config entries from 465 * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords 466 * (or rather, decrypted keys) only for a single session, resulting in 467 * multiple password prompts for user operations that use several Jsch 468 * sessions. 469 */ 470 private static class JschBugFixingConfig implements Config { 471 472 private static final String[] NO_IDENTITIES = {}; 473 474 private final Config real; 475 JschBugFixingConfig(Config delegate)476 public JschBugFixingConfig(Config delegate) { 477 real = delegate; 478 } 479 480 @Override getHostname()481 public String getHostname() { 482 return real.getHostname(); 483 } 484 485 @Override getUser()486 public String getUser() { 487 return real.getUser(); 488 } 489 490 @Override getPort()491 public int getPort() { 492 return real.getPort(); 493 } 494 495 @Override getValue(String key)496 public String getValue(String key) { 497 String k = key.toUpperCase(Locale.ROOT); 498 if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ 499 return null; 500 } 501 String result = real.getValue(key); 502 if (result != null) { 503 if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$ 504 || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$ 505 // These values are in seconds. Jsch 0.1.54 passes them 506 // on as is to java.net.Socket.setSoTimeout(), which 507 // expects milliseconds. So convert here to 508 // milliseconds. 509 try { 510 int timeout = Integer.parseInt(result); 511 result = Long.toString( 512 TimeUnit.SECONDS.toMillis(timeout)); 513 } catch (NumberFormatException e) { 514 // Ignore 515 } 516 } 517 } 518 return result; 519 } 520 521 @Override getValues(String key)522 public String[] getValues(String key) { 523 String k = key.toUpperCase(Locale.ROOT); 524 if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ 525 return NO_IDENTITIES; 526 } 527 return real.getValues(key); 528 } 529 } 530 } 531 532 /** 533 * Set the {@link OpenSshConfig} to use. Intended for use in tests. 534 * 535 * @param config 536 * to use 537 */ setConfig(OpenSshConfig config)538 synchronized void setConfig(OpenSshConfig config) { 539 this.config = config; 540 } 541 } 542