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