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