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 17KB

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