You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

JschConfigSessionFactory.java 17KB

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