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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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 java.io.IOException;
  13. import java.io.StreamCorruptedException;
  14. import java.net.SocketAddress;
  15. import java.nio.charset.StandardCharsets;
  16. import java.security.GeneralSecurityException;
  17. import java.security.PublicKey;
  18. import java.util.ArrayList;
  19. import java.util.Iterator;
  20. import java.util.LinkedHashSet;
  21. import java.util.List;
  22. import java.util.Set;
  23. import org.apache.sshd.client.ClientFactoryManager;
  24. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  25. import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
  26. import org.apache.sshd.client.session.ClientSessionImpl;
  27. import org.apache.sshd.common.FactoryManager;
  28. import org.apache.sshd.common.PropertyResolverUtils;
  29. import org.apache.sshd.common.SshException;
  30. import org.apache.sshd.common.config.keys.KeyUtils;
  31. import org.apache.sshd.common.io.IoSession;
  32. import org.apache.sshd.common.io.IoWriteFuture;
  33. import org.apache.sshd.common.util.Readable;
  34. import org.apache.sshd.common.util.buffer.Buffer;
  35. import org.eclipse.jgit.errors.InvalidPatternException;
  36. import org.eclipse.jgit.fnmatch.FileNameMatcher;
  37. import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
  38. import org.eclipse.jgit.transport.CredentialsProvider;
  39. import org.eclipse.jgit.transport.SshConstants;
  40. /**
  41. * A {@link org.apache.sshd.client.session.ClientSession ClientSession} that can
  42. * be associated with the {@link HostConfigEntry} the session was created for.
  43. * The {@link JGitSshClient} creates such sessions and sets this association.
  44. * <p>
  45. * Also provides for associating a JGit {@link CredentialsProvider} with a
  46. * session.
  47. * </p>
  48. */
  49. public class JGitClientSession extends ClientSessionImpl {
  50. /**
  51. * Default setting for the maximum number of bytes to read in the initial
  52. * protocol version exchange. 64kb is what OpenSSH < 8.0 read; OpenSSH 8.0
  53. * changed it to 8Mb, but that seems excessive for the purpose stated in RFC
  54. * 4253. The Apache MINA sshd default in
  55. * {@link FactoryManager#DEFAULT_MAX_IDENTIFICATION_SIZE} is 16kb.
  56. */
  57. private static final int DEFAULT_MAX_IDENTIFICATION_SIZE = 64 * 1024;
  58. private HostConfigEntry hostConfig;
  59. private CredentialsProvider credentialsProvider;
  60. private volatile StatefulProxyConnector proxyHandler;
  61. /**
  62. * @param manager
  63. * @param session
  64. * @throws Exception
  65. */
  66. public JGitClientSession(ClientFactoryManager manager, IoSession session)
  67. throws Exception {
  68. super(manager, session);
  69. }
  70. /**
  71. * Retrieves the {@link HostConfigEntry} this session was created for.
  72. *
  73. * @return the {@link HostConfigEntry}, or {@code null} if none set
  74. */
  75. public HostConfigEntry getHostConfigEntry() {
  76. return hostConfig;
  77. }
  78. /**
  79. * Sets the {@link HostConfigEntry} this session was created for.
  80. *
  81. * @param hostConfig
  82. * the {@link HostConfigEntry}
  83. */
  84. public void setHostConfigEntry(HostConfigEntry hostConfig) {
  85. this.hostConfig = hostConfig;
  86. }
  87. /**
  88. * Sets the {@link CredentialsProvider} for this session.
  89. *
  90. * @param provider
  91. * to set
  92. */
  93. public void setCredentialsProvider(CredentialsProvider provider) {
  94. credentialsProvider = provider;
  95. }
  96. /**
  97. * Retrieves the {@link CredentialsProvider} set for this session.
  98. *
  99. * @return the provider, or {@code null} if none is set.
  100. */
  101. public CredentialsProvider getCredentialsProvider() {
  102. return credentialsProvider;
  103. }
  104. /**
  105. * Sets a {@link StatefulProxyConnector} to handle proxy connection
  106. * protocols.
  107. *
  108. * @param handler
  109. * to set
  110. */
  111. public void setProxyHandler(StatefulProxyConnector handler) {
  112. proxyHandler = handler;
  113. }
  114. @Override
  115. protected IoWriteFuture sendIdentification(String ident)
  116. throws IOException {
  117. StatefulProxyConnector proxy = proxyHandler;
  118. if (proxy != null) {
  119. try {
  120. // We must not block here; the framework starts reading messages
  121. // from the peer only once the initial sendKexInit() following
  122. // this call to sendIdentification() has returned!
  123. proxy.runWhenDone(() -> {
  124. JGitClientSession.super.sendIdentification(ident);
  125. return null;
  126. });
  127. // Called only from the ClientSessionImpl constructor, where the
  128. // return value is ignored.
  129. return null;
  130. } catch (IOException e) {
  131. throw e;
  132. } catch (Exception other) {
  133. throw new IOException(other.getLocalizedMessage(), other);
  134. }
  135. }
  136. return super.sendIdentification(ident);
  137. }
  138. @Override
  139. protected byte[] sendKexInit()
  140. throws IOException, GeneralSecurityException {
  141. StatefulProxyConnector proxy = proxyHandler;
  142. if (proxy != null) {
  143. try {
  144. // We must not block here; the framework starts reading messages
  145. // from the peer only once the initial sendKexInit() has
  146. // returned!
  147. proxy.runWhenDone(() -> {
  148. JGitClientSession.super.sendKexInit();
  149. return null;
  150. });
  151. // This is called only from the ClientSessionImpl
  152. // constructor, where the return value is ignored.
  153. return null;
  154. } catch (IOException | GeneralSecurityException e) {
  155. throw e;
  156. } catch (Exception other) {
  157. throw new IOException(other.getLocalizedMessage(), other);
  158. }
  159. }
  160. return super.sendKexInit();
  161. }
  162. /**
  163. * {@inheritDoc}
  164. *
  165. * As long as we're still setting up the proxy connection, diverts messages
  166. * to the {@link StatefulProxyConnector}.
  167. */
  168. @Override
  169. public void messageReceived(Readable buffer) throws Exception {
  170. StatefulProxyConnector proxy = proxyHandler;
  171. if (proxy != null) {
  172. proxy.messageReceived(getIoSession(), buffer);
  173. } else {
  174. super.messageReceived(buffer);
  175. }
  176. }
  177. @Override
  178. protected void checkKeys() throws SshException {
  179. ServerKeyVerifier serverKeyVerifier = getServerKeyVerifier();
  180. // The super implementation always uses
  181. // getIoSession().getRemoteAddress(). In case of a proxy connection,
  182. // that would be the address of the proxy!
  183. SocketAddress remoteAddress = getConnectAddress();
  184. PublicKey serverKey = getKex().getServerKey();
  185. if (!serverKeyVerifier.verifyServerKey(this, remoteAddress,
  186. serverKey)) {
  187. throw new SshException(
  188. org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE,
  189. SshdText.get().kexServerKeyInvalid);
  190. }
  191. }
  192. @Override
  193. protected String resolveAvailableSignaturesProposal(
  194. FactoryManager manager) {
  195. Set<String> defaultSignatures = new LinkedHashSet<>();
  196. defaultSignatures.addAll(getSignatureFactoriesNames());
  197. HostConfigEntry config = resolveAttribute(
  198. JGitSshClient.HOST_CONFIG_ENTRY);
  199. String hostKeyAlgorithms = config
  200. .getProperty(SshConstants.HOST_KEY_ALGORITHMS);
  201. if (hostKeyAlgorithms != null && !hostKeyAlgorithms.isEmpty()) {
  202. char first = hostKeyAlgorithms.charAt(0);
  203. switch (first) {
  204. case '+':
  205. // Additions make not much sense -- it's either in
  206. // defaultSignatures already, or we have no implementation for
  207. // it. No point in proposing it.
  208. return String.join(",", defaultSignatures); //$NON-NLS-1$
  209. case '-':
  210. // This takes wildcard patterns!
  211. removeFromList(defaultSignatures,
  212. SshConstants.HOST_KEY_ALGORITHMS,
  213. hostKeyAlgorithms.substring(1));
  214. if (defaultSignatures.isEmpty()) {
  215. // Too bad: user config error. Warn here, and then fail
  216. // later.
  217. log.warn(format(
  218. SshdText.get().configNoRemainingHostKeyAlgorithms,
  219. hostKeyAlgorithms));
  220. }
  221. return String.join(",", defaultSignatures); //$NON-NLS-1$
  222. default:
  223. // Default is overridden -- only accept the ones for which we do
  224. // have an implementation.
  225. List<String> newNames = filteredList(defaultSignatures,
  226. hostKeyAlgorithms);
  227. if (newNames.isEmpty()) {
  228. log.warn(format(
  229. SshdText.get().configNoKnownHostKeyAlgorithms,
  230. hostKeyAlgorithms));
  231. // Use the default instead.
  232. } else {
  233. return String.join(",", newNames); //$NON-NLS-1$
  234. }
  235. break;
  236. }
  237. }
  238. // No HostKeyAlgorithms; using default -- change order to put existing
  239. // keys first.
  240. ServerKeyVerifier verifier = getServerKeyVerifier();
  241. if (verifier instanceof ServerKeyLookup) {
  242. SocketAddress remoteAddress = resolvePeerAddress(
  243. resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS));
  244. List<PublicKey> allKnownKeys = ((ServerKeyLookup) verifier)
  245. .lookup(this, remoteAddress);
  246. Set<String> reordered = new LinkedHashSet<>();
  247. for (PublicKey key : allKnownKeys) {
  248. if (key != null) {
  249. String keyType = KeyUtils.getKeyType(key);
  250. if (keyType != null) {
  251. reordered.add(keyType);
  252. }
  253. }
  254. }
  255. reordered.addAll(defaultSignatures);
  256. return String.join(",", reordered); //$NON-NLS-1$
  257. }
  258. return String.join(",", defaultSignatures); //$NON-NLS-1$
  259. }
  260. private void removeFromList(Set<String> current, String key,
  261. String patterns) {
  262. for (String toRemove : patterns.split("\\s*,\\s*")) { //$NON-NLS-1$
  263. if (toRemove.indexOf('*') < 0 && toRemove.indexOf('?') < 0) {
  264. current.remove(toRemove);
  265. continue;
  266. }
  267. try {
  268. FileNameMatcher matcher = new FileNameMatcher(toRemove, null);
  269. for (Iterator<String> i = current.iterator(); i.hasNext();) {
  270. matcher.reset();
  271. matcher.append(i.next());
  272. if (matcher.isMatch()) {
  273. i.remove();
  274. }
  275. }
  276. } catch (InvalidPatternException e) {
  277. log.warn(format(SshdText.get().configInvalidPattern, key,
  278. toRemove));
  279. }
  280. }
  281. }
  282. private List<String> filteredList(Set<String> known, String values) {
  283. List<String> newNames = new ArrayList<>();
  284. for (String newValue : values.split("\\s*,\\s*")) { //$NON-NLS-1$
  285. if (known.contains(newValue)) {
  286. newNames.add(newValue);
  287. }
  288. }
  289. return newNames;
  290. }
  291. /**
  292. * Reads the RFC 4253, section 4.2 protocol version identification. The
  293. * Apache MINA sshd default implementation checks for NUL bytes also in any
  294. * preceding lines, whereas RFC 4253 requires such a check only for the
  295. * actual identification string starting with "SSH-". Likewise, the 255
  296. * character limit exists only for the identification string, not for the
  297. * preceding lines. CR-LF handling is also relaxed.
  298. *
  299. * @param buffer
  300. * to read from
  301. * @param server
  302. * whether we're an SSH server (should always be {@code false})
  303. * @return the lines read, with the server identification line last, or
  304. * {@code null} if no identification line was found and more bytes
  305. * are needed
  306. * @throws StreamCorruptedException
  307. * if the identification is malformed
  308. * @see <a href="https://tools.ietf.org/html/rfc4253#section-4.2">RFC 4253,
  309. * section 4.2</a>
  310. */
  311. @Override
  312. protected List<String> doReadIdentification(Buffer buffer, boolean server)
  313. throws StreamCorruptedException {
  314. if (server) {
  315. // Should never happen. No translation; internal bug.
  316. throw new IllegalStateException(
  317. "doReadIdentification of client called with server=true"); //$NON-NLS-1$
  318. }
  319. int maxIdentSize = PropertyResolverUtils.getIntProperty(this,
  320. FactoryManager.MAX_IDENTIFICATION_SIZE,
  321. DEFAULT_MAX_IDENTIFICATION_SIZE);
  322. int current = buffer.rpos();
  323. int end = current + buffer.available();
  324. if (current >= end) {
  325. return null;
  326. }
  327. byte[] raw = buffer.array();
  328. List<String> ident = new ArrayList<>();
  329. int start = current;
  330. boolean hasNul = false;
  331. for (int i = current; i < end; i++) {
  332. switch (raw[i]) {
  333. case 0:
  334. hasNul = true;
  335. break;
  336. case '\n':
  337. int eol = 1;
  338. if (i > start && raw[i - 1] == '\r') {
  339. eol++;
  340. }
  341. String line = new String(raw, start, i + 1 - eol - start,
  342. StandardCharsets.UTF_8);
  343. start = i + 1;
  344. if (log.isDebugEnabled()) {
  345. log.debug(format("doReadIdentification({0}) line: ", this) + //$NON-NLS-1$
  346. escapeControls(line));
  347. }
  348. ident.add(line);
  349. if (line.startsWith("SSH-")) { //$NON-NLS-1$
  350. if (hasNul) {
  351. throw new StreamCorruptedException(
  352. format(SshdText.get().serverIdWithNul,
  353. escapeControls(line)));
  354. }
  355. if (line.length() + eol > 255) {
  356. throw new StreamCorruptedException(
  357. format(SshdText.get().serverIdTooLong,
  358. escapeControls(line)));
  359. }
  360. buffer.rpos(start);
  361. return ident;
  362. }
  363. // If this were a server, we could throw an exception here: a
  364. // client is not supposed to send any extra lines before its
  365. // identification string.
  366. hasNul = false;
  367. break;
  368. default:
  369. break;
  370. }
  371. if (i - current + 1 >= maxIdentSize) {
  372. String msg = format(SshdText.get().serverIdNotReceived,
  373. Integer.toString(maxIdentSize));
  374. if (log.isDebugEnabled()) {
  375. log.debug(msg);
  376. log.debug(buffer.toHex());
  377. }
  378. throw new StreamCorruptedException(msg);
  379. }
  380. }
  381. // Need more data
  382. return null;
  383. }
  384. private static String escapeControls(String s) {
  385. StringBuilder b = new StringBuilder();
  386. int l = s.length();
  387. for (int i = 0; i < l; i++) {
  388. char ch = s.charAt(i);
  389. if (Character.isISOControl(ch)) {
  390. b.append(ch <= 0xF ? "\\u000" : "\\u00") //$NON-NLS-1$ //$NON-NLS-2$
  391. .append(Integer.toHexString(ch));
  392. } else {
  393. b.append(ch);
  394. }
  395. }
  396. return b.toString();
  397. }
  398. }