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.

JGitSshClient.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. /*
  2. * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.internal.transport.sshd;
  11. import static java.text.MessageFormat.format;
  12. import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
  13. import static org.apache.sshd.core.CoreModuleProperties.PREFERRED_AUTHS;
  14. import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
  15. import java.io.IOException;
  16. import java.net.InetSocketAddress;
  17. import java.net.Proxy;
  18. import java.net.SocketAddress;
  19. import java.nio.file.Files;
  20. import java.nio.file.InvalidPathException;
  21. import java.nio.file.Path;
  22. import java.nio.file.Paths;
  23. import java.security.GeneralSecurityException;
  24. import java.security.KeyPair;
  25. import java.util.Arrays;
  26. import java.util.Collections;
  27. import java.util.HashMap;
  28. import java.util.Iterator;
  29. import java.util.List;
  30. import java.util.Map;
  31. import java.util.NoSuchElementException;
  32. import java.util.Objects;
  33. import java.util.stream.Collectors;
  34. import org.apache.sshd.client.SshClient;
  35. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  36. import org.apache.sshd.client.future.ConnectFuture;
  37. import org.apache.sshd.client.future.DefaultConnectFuture;
  38. import org.apache.sshd.client.session.ClientSessionImpl;
  39. import org.apache.sshd.client.session.SessionFactory;
  40. import org.apache.sshd.common.AttributeRepository;
  41. import org.apache.sshd.common.config.keys.FilePasswordProvider;
  42. import org.apache.sshd.common.future.SshFutureListener;
  43. import org.apache.sshd.common.io.IoConnectFuture;
  44. import org.apache.sshd.common.io.IoSession;
  45. import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider;
  46. import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
  47. import org.apache.sshd.common.session.SessionContext;
  48. import org.apache.sshd.common.session.helpers.AbstractSession;
  49. import org.apache.sshd.common.util.ValidateUtils;
  50. import org.apache.sshd.common.util.net.SshdSocketAddress;
  51. import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.ChainingAttributes;
  52. import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.SessionAttributes;
  53. import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector;
  54. import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
  55. import org.eclipse.jgit.transport.CredentialsProvider;
  56. import org.eclipse.jgit.transport.SshConstants;
  57. import org.eclipse.jgit.transport.sshd.KeyCache;
  58. import org.eclipse.jgit.transport.sshd.ProxyData;
  59. import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
  60. import org.eclipse.jgit.util.StringUtils;
  61. /**
  62. * Customized {@link SshClient} for JGit. It creates specialized
  63. * {@link JGitClientSession}s that know about the {@link HostConfigEntry} they
  64. * were created for, and it loads all KeyPair identities lazily.
  65. */
  66. public class JGitSshClient extends SshClient {
  67. /**
  68. * We need access to this during the constructor of the ClientSession,
  69. * before setConnectAddress() can have been called. So we have to remember
  70. * it in an attribute on the SshClient, from where we can then retrieve it.
  71. */
  72. static final AttributeKey<HostConfigEntry> HOST_CONFIG_ENTRY = new AttributeKey<>();
  73. static final AttributeKey<InetSocketAddress> ORIGINAL_REMOTE_ADDRESS = new AttributeKey<>();
  74. /**
  75. * An attribute key for the comma-separated list of default preferred
  76. * authentication mechanisms.
  77. */
  78. public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>();
  79. /**
  80. * An attribute key for storing an alternate local address to connect to if
  81. * a local forward from a ProxyJump ssh config is present. If set,
  82. * {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)}
  83. * will not connect to the address obtained from the {@link HostConfigEntry}
  84. * but to the address stored in this key (which is assumed to forward the
  85. * {@code HostConfigEntry} address).
  86. */
  87. public static final AttributeKey<SshdSocketAddress> LOCAL_FORWARD_ADDRESS = new AttributeKey<>();
  88. private KeyCache keyCache;
  89. private CredentialsProvider credentialsProvider;
  90. private ProxyDataFactory proxyDatabase;
  91. @Override
  92. protected SessionFactory createSessionFactory() {
  93. // Override the parent's default
  94. return new JGitSessionFactory(this);
  95. }
  96. @Override
  97. public ConnectFuture connect(HostConfigEntry hostConfig,
  98. AttributeRepository context, SocketAddress localAddress)
  99. throws IOException {
  100. if (connector == null) {
  101. throw new IllegalStateException("SshClient not started."); //$NON-NLS-1$
  102. }
  103. Objects.requireNonNull(hostConfig, "No host configuration"); //$NON-NLS-1$
  104. String originalHost = ValidateUtils.checkNotNullAndNotEmpty(
  105. hostConfig.getHostName(), "No target host"); //$NON-NLS-1$
  106. int originalPort = hostConfig.getPort();
  107. ValidateUtils.checkTrue(originalPort > 0, "Invalid port: %d", //$NON-NLS-1$
  108. originalPort);
  109. InetSocketAddress originalAddress = new InetSocketAddress(originalHost,
  110. originalPort);
  111. InetSocketAddress targetAddress = originalAddress;
  112. String userName = hostConfig.getUsername();
  113. String id = userName + '@' + originalAddress;
  114. AttributeRepository attributes = chain(context, this);
  115. SshdSocketAddress localForward = attributes
  116. .resolveAttribute(LOCAL_FORWARD_ADDRESS);
  117. if (localForward != null) {
  118. targetAddress = new InetSocketAddress(localForward.getHostName(),
  119. localForward.getPort());
  120. id += '/' + targetAddress.toString();
  121. }
  122. ConnectFuture connectFuture = new DefaultConnectFuture(id, null);
  123. SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener(
  124. connectFuture, userName, originalAddress, hostConfig);
  125. attributes = sessionAttributes(attributes, hostConfig, originalAddress);
  126. // Proxy support
  127. if (localForward == null) {
  128. ProxyData proxy = getProxyData(targetAddress);
  129. if (proxy != null) {
  130. targetAddress = configureProxy(proxy, targetAddress);
  131. proxy.clearPassword();
  132. }
  133. }
  134. connector.connect(targetAddress, attributes, localAddress)
  135. .addListener(listener);
  136. return connectFuture;
  137. }
  138. private AttributeRepository chain(AttributeRepository self,
  139. AttributeRepository parent) {
  140. if (self == null) {
  141. return Objects.requireNonNull(parent);
  142. }
  143. if (parent == null || parent == self) {
  144. return self;
  145. }
  146. return new ChainingAttributes(self, parent);
  147. }
  148. private AttributeRepository sessionAttributes(AttributeRepository parent,
  149. HostConfigEntry hostConfig, InetSocketAddress originalAddress) {
  150. // sshd needs some entries from the host config already in the
  151. // constructor of the session. Put those into a dedicated
  152. // AttributeRepository for the new session where it will find them.
  153. // We can set the host config only once the session object has been
  154. // created.
  155. Map<AttributeKey<?>, Object> data = new HashMap<>();
  156. data.put(HOST_CONFIG_ENTRY, hostConfig);
  157. data.put(ORIGINAL_REMOTE_ADDRESS, originalAddress);
  158. data.put(TARGET_SERVER, new SshdSocketAddress(originalAddress));
  159. String preferredAuths = hostConfig.getProperty(
  160. SshConstants.PREFERRED_AUTHENTICATIONS,
  161. resolveAttribute(PREFERRED_AUTHENTICATIONS));
  162. if (!StringUtils.isEmptyOrNull(preferredAuths)) {
  163. data.put(SessionAttributes.PROPERTIES,
  164. Collections.singletonMap(
  165. PREFERRED_AUTHS.getName(),
  166. preferredAuths));
  167. }
  168. return new SessionAttributes(
  169. AttributeRepository.ofAttributesMap(data),
  170. parent, this);
  171. }
  172. private ProxyData getProxyData(InetSocketAddress remoteAddress) {
  173. ProxyDataFactory factory = getProxyDatabase();
  174. return factory == null ? null : factory.get(remoteAddress);
  175. }
  176. private InetSocketAddress configureProxy(ProxyData proxyData,
  177. InetSocketAddress remoteAddress) {
  178. Proxy proxy = proxyData.getProxy();
  179. if (proxy.type() == Proxy.Type.DIRECT
  180. || !(proxy.address() instanceof InetSocketAddress)) {
  181. return remoteAddress;
  182. }
  183. InetSocketAddress address = (InetSocketAddress) proxy.address();
  184. if (address.isUnresolved()) {
  185. address = new InetSocketAddress(address.getHostName(),
  186. address.getPort());
  187. }
  188. switch (proxy.type()) {
  189. case HTTP:
  190. setClientProxyConnector(
  191. new HttpClientConnector(address, remoteAddress,
  192. proxyData.getUser(), proxyData.getPassword()));
  193. return address;
  194. case SOCKS:
  195. setClientProxyConnector(
  196. new Socks5ClientConnector(address, remoteAddress,
  197. proxyData.getUser(), proxyData.getPassword()));
  198. return address;
  199. default:
  200. log.warn(format(SshdText.get().unknownProxyProtocol,
  201. proxy.type().name()));
  202. return remoteAddress;
  203. }
  204. }
  205. private SshFutureListener<IoConnectFuture> createConnectCompletionListener(
  206. ConnectFuture connectFuture, String username,
  207. InetSocketAddress address, HostConfigEntry hostConfig) {
  208. return new SshFutureListener<IoConnectFuture>() {
  209. @Override
  210. public void operationComplete(IoConnectFuture future) {
  211. if (future.isCanceled()) {
  212. connectFuture.cancel();
  213. return;
  214. }
  215. Throwable t = future.getException();
  216. if (t != null) {
  217. connectFuture.setException(t);
  218. return;
  219. }
  220. IoSession ioSession = future.getSession();
  221. try {
  222. JGitClientSession session = createSession(ioSession,
  223. username, address, hostConfig);
  224. connectFuture.setSession(session);
  225. } catch (RuntimeException e) {
  226. connectFuture.setException(e);
  227. ioSession.close(true);
  228. }
  229. }
  230. @Override
  231. public String toString() {
  232. return "JGitSshClient$ConnectCompletionListener[" + username //$NON-NLS-1$
  233. + '@' + address + ']';
  234. }
  235. };
  236. }
  237. private JGitClientSession createSession(IoSession ioSession,
  238. String username, InetSocketAddress address,
  239. HostConfigEntry hostConfig) {
  240. AbstractSession rawSession = AbstractSession.getSession(ioSession);
  241. if (!(rawSession instanceof JGitClientSession)) {
  242. throw new IllegalStateException("Wrong session type: " //$NON-NLS-1$
  243. + rawSession.getClass().getCanonicalName());
  244. }
  245. JGitClientSession session = (JGitClientSession) rawSession;
  246. session.setUsername(username);
  247. session.setConnectAddress(address);
  248. session.setHostConfigEntry(hostConfig);
  249. // Set signature algorithms for public key authentication
  250. String pubkeyAlgos = hostConfig
  251. .getProperty(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
  252. if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
  253. List<String> signatures = getSignatureFactoriesNames();
  254. signatures = session.modifyAlgorithmList(signatures, pubkeyAlgos,
  255. SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
  256. if (!signatures.isEmpty()) {
  257. if (log.isDebugEnabled()) {
  258. log.debug(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS + ' '
  259. + signatures);
  260. }
  261. session.setSignatureFactoriesNames(signatures);
  262. } else {
  263. log.warn(format(SshdText.get().configNoKnownAlgorithms,
  264. SshConstants.PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
  265. }
  266. }
  267. if (session.getCredentialsProvider() == null) {
  268. session.setCredentialsProvider(getCredentialsProvider());
  269. }
  270. int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
  271. PASSWORD_PROMPTS.set(session, Integer.valueOf(numberOfPasswordPrompts));
  272. List<Path> identities = hostConfig.getIdentities().stream()
  273. .map(s -> {
  274. try {
  275. return Paths.get(s);
  276. } catch (InvalidPathException e) {
  277. log.warn(format(SshdText.get().configInvalidPath,
  278. SshConstants.IDENTITY_FILE, s), e);
  279. return null;
  280. }
  281. }).filter(p -> p != null && Files.exists(p))
  282. .collect(Collectors.toList());
  283. CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider(
  284. identities, keyCache);
  285. FilePasswordProvider passwordProvider = getFilePasswordProvider();
  286. ourConfiguredKeysProvider.setPasswordFinder(passwordProvider);
  287. if (hostConfig.isIdentitiesOnly()) {
  288. session.setKeyIdentityProvider(ourConfiguredKeysProvider);
  289. } else {
  290. KeyIdentityProvider defaultKeysProvider = getKeyIdentityProvider();
  291. if (defaultKeysProvider instanceof AbstractResourceKeyPairProvider<?>) {
  292. ((AbstractResourceKeyPairProvider<?>) defaultKeysProvider)
  293. .setPasswordFinder(passwordProvider);
  294. }
  295. KeyIdentityProvider combinedProvider = new CombinedKeyIdentityProvider(
  296. ourConfiguredKeysProvider, defaultKeysProvider);
  297. session.setKeyIdentityProvider(combinedProvider);
  298. }
  299. return session;
  300. }
  301. private int getNumberOfPasswordPrompts(HostConfigEntry hostConfig) {
  302. String prompts = hostConfig
  303. .getProperty(SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
  304. if (prompts != null) {
  305. prompts = prompts.trim();
  306. int value = positive(prompts);
  307. if (value > 0) {
  308. return value;
  309. }
  310. log.warn(format(SshdText.get().configInvalidPositive,
  311. SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
  312. }
  313. return PASSWORD_PROMPTS.getRequiredDefault().intValue();
  314. }
  315. /**
  316. * Set a cache for loaded keys. Newly discovered keys will be added when
  317. * IdentityFile host entries from the ssh config file are used during
  318. * session authentication.
  319. *
  320. * @param cache
  321. * to use
  322. */
  323. public void setKeyCache(KeyCache cache) {
  324. keyCache = cache;
  325. }
  326. /**
  327. * Sets a {@link ProxyDataFactory} for connecting through proxies.
  328. *
  329. * @param factory
  330. * to use, or {@code null} if proxying is not desired or
  331. * supported
  332. */
  333. public void setProxyDatabase(ProxyDataFactory factory) {
  334. proxyDatabase = factory;
  335. }
  336. /**
  337. * Retrieves the {@link ProxyDataFactory}.
  338. *
  339. * @return the factory, or {@code null} if none is set
  340. */
  341. protected ProxyDataFactory getProxyDatabase() {
  342. return proxyDatabase;
  343. }
  344. /**
  345. * Sets the {@link CredentialsProvider} for this client.
  346. *
  347. * @param provider
  348. * to set
  349. */
  350. public void setCredentialsProvider(CredentialsProvider provider) {
  351. credentialsProvider = provider;
  352. }
  353. /**
  354. * Retrieves the {@link CredentialsProvider} set for this client.
  355. *
  356. * @return the provider, or {@code null} if none is set.
  357. */
  358. public CredentialsProvider getCredentialsProvider() {
  359. return credentialsProvider;
  360. }
  361. /**
  362. * A {@link SessionFactory} to create our own specialized
  363. * {@link JGitClientSession}s.
  364. */
  365. private static class JGitSessionFactory extends SessionFactory {
  366. public JGitSessionFactory(JGitSshClient client) {
  367. super(client);
  368. }
  369. @Override
  370. protected ClientSessionImpl doCreateSession(IoSession ioSession)
  371. throws Exception {
  372. return new JGitClientSession(getClient(), ioSession);
  373. }
  374. }
  375. /**
  376. * A {@link KeyIdentityProvider} that iterates over the {@link Iterable}s
  377. * returned by other {@link KeyIdentityProvider}s.
  378. */
  379. private static class CombinedKeyIdentityProvider
  380. implements KeyIdentityProvider {
  381. private final List<KeyIdentityProvider> providers;
  382. public CombinedKeyIdentityProvider(KeyIdentityProvider... providers) {
  383. this(Arrays.stream(providers).filter(Objects::nonNull)
  384. .collect(Collectors.toList()));
  385. }
  386. public CombinedKeyIdentityProvider(
  387. List<KeyIdentityProvider> providers) {
  388. this.providers = providers;
  389. }
  390. @Override
  391. public Iterable<KeyPair> loadKeys(SessionContext context) {
  392. return () -> new Iterator<KeyPair>() {
  393. private Iterator<KeyIdentityProvider> factories = providers
  394. .iterator();
  395. private Iterator<KeyPair> current;
  396. private Boolean hasElement;
  397. @Override
  398. public boolean hasNext() {
  399. if (hasElement != null) {
  400. return hasElement.booleanValue();
  401. }
  402. while (current == null || !current.hasNext()) {
  403. if (factories.hasNext()) {
  404. try {
  405. current = factories.next().loadKeys(context)
  406. .iterator();
  407. } catch (IOException | GeneralSecurityException e) {
  408. throw new RuntimeException(e);
  409. }
  410. } else {
  411. current = null;
  412. hasElement = Boolean.FALSE;
  413. return false;
  414. }
  415. }
  416. hasElement = Boolean.TRUE;
  417. return true;
  418. }
  419. @Override
  420. public KeyPair next() {
  421. if (hasElement == null && !hasNext()
  422. || !hasElement.booleanValue()) {
  423. throw new NoSuchElementException();
  424. }
  425. hasElement = null;
  426. KeyPair result;
  427. try {
  428. result = current.next();
  429. } catch (NoSuchElementException e) {
  430. result = null;
  431. }
  432. return result;
  433. }
  434. };
  435. }
  436. }
  437. }