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.

JGitClientSession.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. /*
  2. * Copyright (C) 2018, 2019 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.MAX_IDENTIFICATION_SIZE;
  13. import java.io.IOException;
  14. import java.io.StreamCorruptedException;
  15. import java.net.SocketAddress;
  16. import java.nio.charset.StandardCharsets;
  17. import java.security.GeneralSecurityException;
  18. import java.security.PublicKey;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.HashSet;
  23. import java.util.Iterator;
  24. import java.util.LinkedHashSet;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Objects;
  28. import java.util.Set;
  29. import org.apache.sshd.client.ClientFactoryManager;
  30. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  31. import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
  32. import org.apache.sshd.client.session.ClientSessionImpl;
  33. import org.apache.sshd.common.AttributeRepository;
  34. import org.apache.sshd.common.FactoryManager;
  35. import org.apache.sshd.common.PropertyResolver;
  36. import org.apache.sshd.common.config.keys.KeyUtils;
  37. import org.apache.sshd.common.io.IoSession;
  38. import org.apache.sshd.common.io.IoWriteFuture;
  39. import org.apache.sshd.common.kex.KexProposalOption;
  40. import org.apache.sshd.common.util.Readable;
  41. import org.apache.sshd.common.util.buffer.Buffer;
  42. import org.eclipse.jgit.errors.InvalidPatternException;
  43. import org.eclipse.jgit.fnmatch.FileNameMatcher;
  44. import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
  45. import org.eclipse.jgit.transport.CredentialsProvider;
  46. import org.eclipse.jgit.transport.SshConstants;
  47. import org.eclipse.jgit.util.StringUtils;
  48. /**
  49. * A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can
  50. * be associated with the {@link HostConfigEntry} the session was created for.
  51. * The {@link JGitSshClient} creates such sessions and sets this association.
  52. * <p>
  53. * Also provides for associating a JGit {@link CredentialsProvider} with a
  54. * session.
  55. * </p>
  56. */
  57. public class JGitClientSession extends ClientSessionImpl {
  58. /**
  59. * Default setting for the maximum number of bytes to read in the initial
  60. * protocol version exchange. 64kb is what OpenSSH < 8.0 read; OpenSSH 8.0
  61. * changed it to 8Mb, but that seems excessive for the purpose stated in RFC
  62. * 4253. The Apache MINA sshd default in
  63. * {@link org.apache.sshd.core.CoreModuleProperties#MAX_IDENTIFICATION_SIZE}
  64. * is 16kb.
  65. */
  66. private static final int DEFAULT_MAX_IDENTIFICATION_SIZE = 64 * 1024;
  67. private HostConfigEntry hostConfig;
  68. private CredentialsProvider credentialsProvider;
  69. private volatile StatefulProxyConnector proxyHandler;
  70. /**
  71. * @param manager
  72. * @param session
  73. * @throws Exception
  74. */
  75. public JGitClientSession(ClientFactoryManager manager, IoSession session)
  76. throws Exception {
  77. super(manager, session);
  78. }
  79. /**
  80. * Retrieves the {@link HostConfigEntry} this session was created for.
  81. *
  82. * @return the {@link HostConfigEntry}, or {@code null} if none set
  83. */
  84. public HostConfigEntry getHostConfigEntry() {
  85. return hostConfig;
  86. }
  87. /**
  88. * Sets the {@link HostConfigEntry} this session was created for.
  89. *
  90. * @param hostConfig
  91. * the {@link HostConfigEntry}
  92. */
  93. public void setHostConfigEntry(HostConfigEntry hostConfig) {
  94. this.hostConfig = hostConfig;
  95. }
  96. /**
  97. * Sets the {@link CredentialsProvider} for this session.
  98. *
  99. * @param provider
  100. * to set
  101. */
  102. public void setCredentialsProvider(CredentialsProvider provider) {
  103. credentialsProvider = provider;
  104. }
  105. /**
  106. * Retrieves the {@link CredentialsProvider} set for this session.
  107. *
  108. * @return the provider, or {@code null} if none is set.
  109. */
  110. public CredentialsProvider getCredentialsProvider() {
  111. return credentialsProvider;
  112. }
  113. /**
  114. * Sets a {@link StatefulProxyConnector} to handle proxy connection
  115. * protocols.
  116. *
  117. * @param handler
  118. * to set
  119. */
  120. public void setProxyHandler(StatefulProxyConnector handler) {
  121. proxyHandler = handler;
  122. }
  123. @Override
  124. protected IoWriteFuture sendIdentification(String ident,
  125. List<String> extraLines) throws Exception {
  126. StatefulProxyConnector proxy = proxyHandler;
  127. if (proxy != null) {
  128. try {
  129. // We must not block here; the framework starts reading messages
  130. // from the peer only once the initial sendKexInit() following
  131. // this call to sendIdentification() has returned!
  132. proxy.runWhenDone(() -> {
  133. JGitClientSession.super.sendIdentification(ident,
  134. extraLines);
  135. return null;
  136. });
  137. // Called only from the ClientSessionImpl constructor, where the
  138. // return value is ignored.
  139. return null;
  140. } catch (IOException e) {
  141. throw e;
  142. } catch (Exception other) {
  143. throw new IOException(other.getLocalizedMessage(), other);
  144. }
  145. }
  146. return super.sendIdentification(ident, extraLines);
  147. }
  148. @Override
  149. protected byte[] sendKexInit() throws Exception {
  150. StatefulProxyConnector proxy = proxyHandler;
  151. if (proxy != null) {
  152. try {
  153. // We must not block here; the framework starts reading messages
  154. // from the peer only once the initial sendKexInit() has
  155. // returned!
  156. proxy.runWhenDone(() -> {
  157. JGitClientSession.super.sendKexInit();
  158. return null;
  159. });
  160. // This is called only from the ClientSessionImpl
  161. // constructor, where the return value is ignored.
  162. return null;
  163. } catch (IOException | GeneralSecurityException e) {
  164. throw e;
  165. } catch (Exception other) {
  166. throw new IOException(other.getLocalizedMessage(), other);
  167. }
  168. }
  169. return super.sendKexInit();
  170. }
  171. /**
  172. * {@inheritDoc}
  173. *
  174. * As long as we're still setting up the proxy connection, diverts messages
  175. * to the {@link StatefulProxyConnector}.
  176. */
  177. @Override
  178. public void messageReceived(Readable buffer) throws Exception {
  179. StatefulProxyConnector proxy = proxyHandler;
  180. if (proxy != null) {
  181. proxy.messageReceived(getIoSession(), buffer);
  182. } else {
  183. super.messageReceived(buffer);
  184. }
  185. }
  186. @Override
  187. protected Map<KexProposalOption, String> setNegotiationResult(
  188. Map<KexProposalOption, String> guess) {
  189. Map<KexProposalOption, String> result = super.setNegotiationResult(
  190. guess);
  191. // This should be doable with a SessionListener, too, but I don't see
  192. // how to add a listener in time to catch the negotiation end for sure
  193. // given that the super-constructor already starts KEX.
  194. //
  195. // TODO: This override can be removed once we use sshd 2.8.0.
  196. if (log.isDebugEnabled()) {
  197. result.forEach((option, value) -> log.debug(
  198. "setNegotiationResult({}) Kex: {} = {}", this, //$NON-NLS-1$
  199. option.getDescription(), value));
  200. }
  201. return result;
  202. }
  203. @Override
  204. protected String resolveAvailableSignaturesProposal(
  205. FactoryManager manager) {
  206. List<String> defaultSignatures = getSignatureFactoriesNames();
  207. HostConfigEntry config = resolveAttribute(
  208. JGitSshClient.HOST_CONFIG_ENTRY);
  209. String algorithms = config
  210. .getProperty(SshConstants.HOST_KEY_ALGORITHMS);
  211. if (!StringUtils.isEmptyOrNull(algorithms)) {
  212. List<String> result = modifyAlgorithmList(defaultSignatures,
  213. algorithms, SshConstants.HOST_KEY_ALGORITHMS);
  214. if (!result.isEmpty()) {
  215. if (log.isDebugEnabled()) {
  216. log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + result);
  217. }
  218. return String.join(",", result); //$NON-NLS-1$
  219. }
  220. log.warn(format(SshdText.get().configNoKnownAlgorithms,
  221. SshConstants.HOST_KEY_ALGORITHMS,
  222. algorithms));
  223. }
  224. // No HostKeyAlgorithms; using default -- change order to put existing
  225. // keys first.
  226. ServerKeyVerifier verifier = getServerKeyVerifier();
  227. if (verifier instanceof ServerKeyLookup) {
  228. SocketAddress remoteAddress = resolvePeerAddress(
  229. resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS));
  230. List<PublicKey> allKnownKeys = ((ServerKeyLookup) verifier)
  231. .lookup(this, remoteAddress);
  232. Set<String> reordered = new LinkedHashSet<>();
  233. for (PublicKey key : allKnownKeys) {
  234. if (key != null) {
  235. String keyType = KeyUtils.getKeyType(key);
  236. if (keyType != null) {
  237. reordered.add(keyType);
  238. }
  239. }
  240. }
  241. reordered.addAll(defaultSignatures);
  242. if (log.isDebugEnabled()) {
  243. log.debug(SshConstants.HOST_KEY_ALGORITHMS + ' ' + reordered);
  244. }
  245. return String.join(",", reordered); //$NON-NLS-1$
  246. }
  247. if (log.isDebugEnabled()) {
  248. log.debug(
  249. SshConstants.HOST_KEY_ALGORITHMS + ' ' + defaultSignatures);
  250. }
  251. return String.join(",", defaultSignatures); //$NON-NLS-1$
  252. }
  253. /**
  254. * Modifies a given algorithm list according to a list from the ssh config,
  255. * including remove ('-') and reordering ('^') operators. Addition ('+') is
  256. * not handled since we have no way of adding dynamically implementations,
  257. * and the defaultList is supposed to contain all known implementations
  258. * already.
  259. *
  260. * @param defaultList
  261. * to modify
  262. * @param fromConfig
  263. * telling how to modify the {@code defaultList}, must not be
  264. * {@code null} or empty
  265. * @param overrideKey
  266. * ssh config key; used for logging
  267. * @return the modified list or {@code null} if {@code overrideKey} is not
  268. * set
  269. */
  270. public List<String> modifyAlgorithmList(List<String> defaultList,
  271. String fromConfig, String overrideKey) {
  272. Set<String> defaults = new LinkedHashSet<>();
  273. defaults.addAll(defaultList);
  274. switch (fromConfig.charAt(0)) {
  275. case '+':
  276. // Additions make not much sense -- it's either in
  277. // defaultList already, or we have no implementation for
  278. // it. No point in proposing it.
  279. return defaultList;
  280. case '-':
  281. // This takes wildcard patterns!
  282. removeFromList(defaults, overrideKey, fromConfig.substring(1));
  283. return new ArrayList<>(defaults);
  284. case '^':
  285. // Specified entries go to the front of the default list
  286. List<String> allSignatures = filteredList(defaults,
  287. fromConfig.substring(1));
  288. Set<String> atFront = new HashSet<>(allSignatures);
  289. for (String sig : defaults) {
  290. if (!atFront.contains(sig)) {
  291. allSignatures.add(sig);
  292. }
  293. }
  294. return allSignatures;
  295. default:
  296. // Default is overridden -- only accept the ones for which we do
  297. // have an implementation.
  298. return filteredList(defaults, fromConfig);
  299. }
  300. }
  301. private void removeFromList(Set<String> current, String key,
  302. String patterns) {
  303. for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$
  304. if (toRemove.indexOf('*') < 0 && toRemove.indexOf('?') < 0) {
  305. current.remove(toRemove);
  306. continue;
  307. }
  308. try {
  309. FileNameMatcher matcher = new FileNameMatcher(toRemove, null);
  310. for (Iterator<String> i = current.iterator(); i.hasNext();) {
  311. matcher.reset();
  312. matcher.append(i.next());
  313. if (matcher.isMatch()) {
  314. i.remove();
  315. }
  316. }
  317. } catch (InvalidPatternException e) {
  318. log.warn(format(SshdText.get().configInvalidPattern, key,
  319. toRemove));
  320. }
  321. }
  322. }
  323. private List<String> filteredList(Set<String> known, String values) {
  324. List<String> newNames = new ArrayList<>();
  325. for (String newValue : values.split("\\s*,\\s*")) { //$NON-NLS-1$
  326. if (known.contains(newValue)) {
  327. newNames.add(newValue);
  328. }
  329. }
  330. return newNames;
  331. }
  332. /**
  333. * Reads the RFC 4253, section 4.2 protocol version identification. The
  334. * Apache MINA sshd default implementation checks for NUL bytes also in any
  335. * preceding lines, whereas RFC 4253 requires such a check only for the
  336. * actual identification string starting with "SSH-". Likewise, the 255
  337. * character limit exists only for the identification string, not for the
  338. * preceding lines. CR-LF handling is also relaxed.
  339. *
  340. * @param buffer
  341. * to read from
  342. * @param server
  343. * whether we're an SSH server (should always be {@code false})
  344. * @return the lines read, with the server identification line last, or
  345. * {@code null} if no identification line was found and more bytes
  346. * are needed
  347. * @throws StreamCorruptedException
  348. * if the identification is malformed
  349. * @see <a href="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253,
  350. * section 4.2</a>
  351. */
  352. @Override
  353. protected List<String> doReadIdentification(Buffer buffer, boolean server)
  354. throws StreamCorruptedException {
  355. if (server) {
  356. // Should never happen. No translation; internal bug.
  357. throw new IllegalStateException(
  358. "doReadIdentification of client called with server=true"); //$NON-NLS-1$
  359. }
  360. Integer maxIdentLength = MAX_IDENTIFICATION_SIZE.get(this).orElse(null);
  361. int maxIdentSize;
  362. if (maxIdentLength == null || maxIdentLength
  363. .intValue() < DEFAULT_MAX_IDENTIFICATION_SIZE) {
  364. maxIdentSize = DEFAULT_MAX_IDENTIFICATION_SIZE;
  365. MAX_IDENTIFICATION_SIZE.set(this, Integer.valueOf(maxIdentSize));
  366. } else {
  367. maxIdentSize = maxIdentLength.intValue();
  368. }
  369. int current = buffer.rpos();
  370. int end = current + buffer.available();
  371. if (current >= end) {
  372. return null;
  373. }
  374. byte[] raw = buffer.array();
  375. List<String> ident = new ArrayList<>();
  376. int start = current;
  377. boolean hasNul = false;
  378. for (int i = current; i < end; i++) {
  379. switch (raw[i]) {
  380. case 0:
  381. hasNul = true;
  382. break;
  383. case '\n':
  384. int eol = 1;
  385. if (i > start && raw[i - 1] == '\r') {
  386. eol++;
  387. }
  388. String line = new String(raw, start, i + 1 - eol - start,
  389. StandardCharsets.UTF_8);
  390. start = i + 1;
  391. if (log.isDebugEnabled()) {
  392. log.debug(format("doReadIdentification({0}) line: ", this) + //$NON-NLS-1$
  393. escapeControls(line));
  394. }
  395. ident.add(line);
  396. if (line.startsWith("SSH-")) { //$NON-NLS-1$
  397. if (hasNul) {
  398. throw new StreamCorruptedException(
  399. format(SshdText.get().serverIdWithNul,
  400. escapeControls(line)));
  401. }
  402. if (line.length() + eol > 255) {
  403. throw new StreamCorruptedException(
  404. format(SshdText.get().serverIdTooLong,
  405. escapeControls(line)));
  406. }
  407. buffer.rpos(start);
  408. return ident;
  409. }
  410. // If this were a server, we could throw an exception here: a
  411. // client is not supposed to send any extra lines before its
  412. // identification string.
  413. hasNul = false;
  414. break;
  415. default:
  416. break;
  417. }
  418. if (i - current + 1 >= maxIdentSize) {
  419. String msg = format(SshdText.get().serverIdNotReceived,
  420. Integer.toString(maxIdentSize));
  421. if (log.isDebugEnabled()) {
  422. log.debug(msg);
  423. log.debug(buffer.toHex());
  424. }
  425. throw new StreamCorruptedException(msg);
  426. }
  427. }
  428. // Need more data
  429. return null;
  430. }
  431. private static String escapeControls(String s) {
  432. StringBuilder b = new StringBuilder();
  433. int l = s.length();
  434. for (int i = 0; i < l; i++) {
  435. char ch = s.charAt(i);
  436. if (Character.isISOControl(ch)) {
  437. b.append(ch <= 0xF ? "\\u000" : "\\u00") //$NON-NLS-1$ //$NON-NLS-2$
  438. .append(Integer.toHexString(ch));
  439. } else {
  440. b.append(ch);
  441. }
  442. }
  443. return b.toString();
  444. }
  445. @Override
  446. public <T> T getAttribute(AttributeKey<T> key) {
  447. T value = super.getAttribute(key);
  448. if (value == null) {
  449. IoSession ioSession = getIoSession();
  450. if (ioSession != null) {
  451. Object obj = ioSession.getAttribute(AttributeRepository.class);
  452. if (obj instanceof AttributeRepository) {
  453. AttributeRepository sessionAttributes = (AttributeRepository) obj;
  454. value = sessionAttributes.resolveAttribute(key);
  455. }
  456. }
  457. }
  458. return value;
  459. }
  460. @Override
  461. public PropertyResolver getParentPropertyResolver() {
  462. IoSession ioSession = getIoSession();
  463. if (ioSession != null) {
  464. Object obj = ioSession.getAttribute(AttributeRepository.class);
  465. if (obj instanceof PropertyResolver) {
  466. return (PropertyResolver) obj;
  467. }
  468. }
  469. return super.getParentPropertyResolver();
  470. }
  471. /**
  472. * An {@link AttributeRepository} that chains together two other attribute
  473. * sources in a hierarchy.
  474. */
  475. public static class ChainingAttributes implements AttributeRepository {
  476. private final AttributeRepository delegate;
  477. private final AttributeRepository parent;
  478. /**
  479. * Create a new {@link ChainingAttributes} attribute source.
  480. *
  481. * @param self
  482. * to search for attributes first
  483. * @param parent
  484. * to search for attributes if not found in {@code self}
  485. */
  486. public ChainingAttributes(AttributeRepository self,
  487. AttributeRepository parent) {
  488. this.delegate = self;
  489. this.parent = parent;
  490. }
  491. @Override
  492. public int getAttributesCount() {
  493. return delegate.getAttributesCount();
  494. }
  495. @Override
  496. public <T> T getAttribute(AttributeKey<T> key) {
  497. return delegate.getAttribute(Objects.requireNonNull(key));
  498. }
  499. @Override
  500. public Collection<AttributeKey<?>> attributeKeys() {
  501. return delegate.attributeKeys();
  502. }
  503. @Override
  504. public <T> T resolveAttribute(AttributeKey<T> key) {
  505. T value = getAttribute(Objects.requireNonNull(key));
  506. if (value == null) {
  507. return parent.getAttribute(key);
  508. }
  509. return value;
  510. }
  511. }
  512. /**
  513. * A {@link ChainingAttributes} repository that doubles as a
  514. * {@link PropertyResolver}. The property map can be set via the attribute
  515. * key {@link SessionAttributes#PROPERTIES}.
  516. */
  517. public static class SessionAttributes extends ChainingAttributes
  518. implements PropertyResolver {
  519. /** Key for storing a map of properties in the attributes. */
  520. public static final AttributeKey<Map<String, Object>> PROPERTIES = new AttributeKey<>();
  521. private final PropertyResolver parentProperties;
  522. /**
  523. * Creates a new {@link SessionAttributes} attribute and property
  524. * source.
  525. *
  526. * @param self
  527. * to search for attributes first
  528. * @param parent
  529. * to search for attributes if not found in {@code self}
  530. * @param parentProperties
  531. * to search for properties if not found in {@code self}
  532. */
  533. public SessionAttributes(AttributeRepository self,
  534. AttributeRepository parent, PropertyResolver parentProperties) {
  535. super(self, parent);
  536. this.parentProperties = parentProperties;
  537. }
  538. @Override
  539. public PropertyResolver getParentPropertyResolver() {
  540. return parentProperties;
  541. }
  542. @Override
  543. public Map<String, Object> getProperties() {
  544. Map<String, Object> props = getAttribute(PROPERTIES);
  545. return props == null ? Collections.emptyMap() : props;
  546. }
  547. }
  548. }