1
2
3
4
5
6
7
8
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.PASSWORD_PROMPTS;
14 import static org.apache.sshd.core.CoreModuleProperties.PREFERRED_AUTHS;
15 import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
16
17 import java.io.IOException;
18 import java.net.InetSocketAddress;
19 import java.net.Proxy;
20 import java.net.SocketAddress;
21 import java.nio.file.Files;
22 import java.nio.file.InvalidPathException;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.security.GeneralSecurityException;
26 import java.security.KeyPair;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.NoSuchElementException;
34 import java.util.Objects;
35 import java.util.stream.Collectors;
36
37 import org.apache.sshd.client.SshClient;
38 import org.apache.sshd.client.config.hosts.HostConfigEntry;
39 import org.apache.sshd.client.future.ConnectFuture;
40 import org.apache.sshd.client.future.DefaultConnectFuture;
41 import org.apache.sshd.client.session.ClientSessionImpl;
42 import org.apache.sshd.client.session.SessionFactory;
43 import org.apache.sshd.common.AttributeRepository;
44 import org.apache.sshd.common.config.keys.FilePasswordProvider;
45 import org.apache.sshd.common.future.SshFutureListener;
46 import org.apache.sshd.common.io.IoConnectFuture;
47 import org.apache.sshd.common.io.IoSession;
48 import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider;
49 import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
50 import org.apache.sshd.common.session.SessionContext;
51 import org.apache.sshd.common.session.helpers.AbstractSession;
52 import org.apache.sshd.common.util.ValidateUtils;
53 import org.apache.sshd.common.util.net.SshdSocketAddress;
54 import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.ChainingAttributes;
55 import org.eclipse.jgit.internal.transport.sshd.JGitClientSession.SessionAttributes;
56 import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector;
57 import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
58 import org.eclipse.jgit.transport.CredentialsProvider;
59 import org.eclipse.jgit.transport.SshConstants;
60 import org.eclipse.jgit.transport.sshd.KeyCache;
61 import org.eclipse.jgit.transport.sshd.ProxyData;
62 import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
63 import org.eclipse.jgit.util.StringUtils;
64
65
66
67
68
69
70 public class JGitSshClient extends SshClient {
71
72
73
74
75
76
77 static final AttributeKey<HostConfigEntry> HOST_CONFIG_ENTRY = new AttributeKey<>();
78
79 static final AttributeKey<InetSocketAddress> ORIGINAL_REMOTE_ADDRESS = new AttributeKey<>();
80
81
82
83
84
85 public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>();
86
87
88
89
90
91
92
93
94
95 public static final AttributeKey<SshdSocketAddress> LOCAL_FORWARD_ADDRESS = new AttributeKey<>();
96
97 private KeyCache keyCache;
98
99 private CredentialsProvider credentialsProvider;
100
101 private ProxyDataFactory proxyDatabase;
102
103 @Override
104 protected SessionFactory createSessionFactory() {
105
106 return new JGitSessionFactory(this);
107 }
108
109 @Override
110 public ConnectFuture connect(HostConfigEntry hostConfig,
111 AttributeRepository context, SocketAddress localAddress)
112 throws IOException {
113 if (connector == null) {
114 throw new IllegalStateException("SshClient not started.");
115 }
116 Objects.requireNonNull(hostConfig, "No host configuration");
117 String originalHost = ValidateUtils.checkNotNullAndNotEmpty(
118 hostConfig.getHostName(), "No target host");
119 int originalPort = hostConfig.getPort();
120 ValidateUtils.checkTrue(originalPort > 0, "Invalid port: %d",
121 originalPort);
122 InetSocketAddress originalAddress = new InetSocketAddress(originalHost,
123 originalPort);
124 InetSocketAddress targetAddress = originalAddress;
125 String userName = hostConfig.getUsername();
126 String id = userName + '@' + originalAddress;
127 AttributeRepository attributes = chain(context, this);
128 SshdSocketAddress localForward = attributes
129 .resolveAttribute(LOCAL_FORWARD_ADDRESS);
130 if (localForward != null) {
131 targetAddress = new InetSocketAddress(localForward.getHostName(),
132 localForward.getPort());
133 id += '/' + targetAddress.toString();
134 }
135 ConnectFuture connectFuture = new DefaultConnectFuture(id, null);
136 SshFutureListener<IoConnectFuture> listener = createConnectCompletionListener(
137 connectFuture, userName, originalAddress, hostConfig);
138 attributes = sessionAttributes(attributes, hostConfig, originalAddress);
139
140 if (localForward == null) {
141 ProxyData proxy = getProxyData(targetAddress);
142 if (proxy != null) {
143 targetAddress = configureProxy(proxy, targetAddress);
144 proxy.clearPassword();
145 }
146 }
147 connector.connect(targetAddress, attributes, localAddress)
148 .addListener(listener);
149 return connectFuture;
150 }
151
152 private AttributeRepository chain(AttributeRepository self,
153 AttributeRepository parent) {
154 if (self == null) {
155 return Objects.requireNonNull(parent);
156 }
157 if (parent == null || parent == self) {
158 return self;
159 }
160 return new ChainingAttributes(self, parent);
161 }
162
163 private AttributeRepository sessionAttributes(AttributeRepository parent,
164 HostConfigEntry hostConfig, InetSocketAddress originalAddress) {
165
166
167
168
169
170 Map<AttributeKey<?>, Object> data = new HashMap<>();
171 data.put(HOST_CONFIG_ENTRY, hostConfig);
172 data.put(ORIGINAL_REMOTE_ADDRESS, originalAddress);
173 data.put(TARGET_SERVER, new SshdSocketAddress(originalAddress));
174 String preferredAuths = hostConfig.getProperty(
175 SshConstants.PREFERRED_AUTHENTICATIONS,
176 resolveAttribute(PREFERRED_AUTHENTICATIONS));
177 if (!StringUtils.isEmptyOrNull(preferredAuths)) {
178 data.put(SessionAttributes.PROPERTIES,
179 Collections.singletonMap(
180 PREFERRED_AUTHS.getName(),
181 preferredAuths));
182 }
183 return new SessionAttributes(
184 AttributeRepository.ofAttributesMap(data),
185 parent, this);
186 }
187
188 private ProxyData getProxyData(InetSocketAddress remoteAddress) {
189 ProxyDataFactory factory = getProxyDatabase();
190 return factory == null ? null : factory.get(remoteAddress);
191 }
192
193 private InetSocketAddress configureProxy(ProxyData proxyData,
194 InetSocketAddress remoteAddress) {
195 Proxy proxy = proxyData.getProxy();
196 if (proxy.type() == Proxy.Type.DIRECT
197 || !(proxy.address() instanceof InetSocketAddress)) {
198 return remoteAddress;
199 }
200 InetSocketAddress address = (InetSocketAddress) proxy.address();
201 if (address.isUnresolved()) {
202 address = new InetSocketAddress(address.getHostName(),
203 address.getPort());
204 }
205 switch (proxy.type()) {
206 case HTTP:
207 setClientProxyConnector(
208 new HttpClientConnector(address, remoteAddress,
209 proxyData.getUser(), proxyData.getPassword()));
210 return address;
211 case SOCKS:
212 setClientProxyConnector(
213 new Socks5ClientConnector(address, remoteAddress,
214 proxyData.getUser(), proxyData.getPassword()));
215 return address;
216 default:
217 log.warn(format(SshdText.get().unknownProxyProtocol,
218 proxy.type().name()));
219 return remoteAddress;
220 }
221 }
222
223 private SshFutureListener<IoConnectFuture> createConnectCompletionListener(
224 ConnectFuture connectFuture, String username,
225 InetSocketAddress address, HostConfigEntry hostConfig) {
226 return new SshFutureListener<IoConnectFuture>() {
227
228 @Override
229 public void operationComplete(IoConnectFuture future) {
230 if (future.isCanceled()) {
231 connectFuture.cancel();
232 return;
233 }
234 Throwable t = future.getException();
235 if (t != null) {
236 connectFuture.setException(t);
237 return;
238 }
239 IoSession ioSession = future.getSession();
240 try {
241 JGitClientSession session = createSession(ioSession,
242 username, address, hostConfig);
243 connectFuture.setSession(session);
244 } catch (RuntimeException e) {
245 connectFuture.setException(e);
246 ioSession.close(true);
247 }
248 }
249
250 @Override
251 public String toString() {
252 return "JGitSshClient$ConnectCompletionListener[" + username
253 + '@' + address + ']';
254 }
255 };
256 }
257
258 private JGitClientSession createSession(IoSession ioSession,
259 String username, InetSocketAddress address,
260 HostConfigEntry hostConfig) {
261 AbstractSession rawSession = AbstractSession.getSession(ioSession);
262 if (!(rawSession instanceof JGitClientSession)) {
263 throw new IllegalStateException("Wrong session type: "
264 + rawSession.getClass().getCanonicalName());
265 }
266 JGitClientSession session = (JGitClientSession) rawSession;
267 session.setUsername(username);
268 session.setConnectAddress(address);
269 session.setHostConfigEntry(hostConfig);
270
271 String pubkeyAlgos = hostConfig
272 .getProperty(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
273 if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
274 List<String> signatures = getSignatureFactoriesNames();
275 signatures = session.modifyAlgorithmList(signatures, pubkeyAlgos,
276 SshConstants.PUBKEY_ACCEPTED_ALGORITHMS);
277 if (!signatures.isEmpty()) {
278 if (log.isDebugEnabled()) {
279 log.debug(SshConstants.PUBKEY_ACCEPTED_ALGORITHMS + ' '
280 + signatures);
281 }
282 session.setSignatureFactoriesNames(signatures);
283 } else {
284 log.warn(format(SshdText.get().configNoKnownAlgorithms,
285 SshConstants.PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
286 }
287 }
288 if (session.getCredentialsProvider() == null) {
289 session.setCredentialsProvider(getCredentialsProvider());
290 }
291 int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
292 PASSWORD_PROMPTS.set(session, Integer.valueOf(numberOfPasswordPrompts));
293 List<Path> identities = hostConfig.getIdentities().stream()
294 .map(s -> {
295 try {
296 return Paths.get(s);
297 } catch (InvalidPathException e) {
298 log.warn(format(SshdText.get().configInvalidPath,
299 SshConstants.IDENTITY_FILE, s), e);
300 return null;
301 }
302 }).filter(p -> p != null && Files.exists(p))
303 .collect(Collectors.toList());
304 CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider(
305 identities, keyCache);
306 FilePasswordProvider passwordProvider = getFilePasswordProvider();
307 ourConfiguredKeysProvider.setPasswordFinder(passwordProvider);
308 if (hostConfig.isIdentitiesOnly()) {
309 session.setKeyIdentityProvider(ourConfiguredKeysProvider);
310 } else {
311 KeyIdentityProvider defaultKeysProvider = getKeyIdentityProvider();
312 if (defaultKeysProvider instanceof AbstractResourceKeyPairProvider<?>) {
313 ((AbstractResourceKeyPairProvider<?>) defaultKeysProvider)
314 .setPasswordFinder(passwordProvider);
315 }
316 KeyIdentityProvider combinedProvider = new CombinedKeyIdentityProvider(
317 ourConfiguredKeysProvider, defaultKeysProvider);
318 session.setKeyIdentityProvider(combinedProvider);
319 }
320 return session;
321 }
322
323 private int getNumberOfPasswordPrompts(HostConfigEntry hostConfig) {
324 String prompts = hostConfig
325 .getProperty(SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
326 if (prompts != null) {
327 prompts = prompts.trim();
328 int value = positive(prompts);
329 if (value > 0) {
330 return value;
331 }
332 log.warn(format(SshdText.get().configInvalidPositive,
333 SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
334 }
335 return PASSWORD_PROMPTS.getRequiredDefault().intValue();
336 }
337
338
339
340
341
342
343
344
345
346 public void setKeyCache(KeyCache cache) {
347 keyCache = cache;
348 }
349
350
351
352
353
354
355
356
357 public void setProxyDatabase(ProxyDataFactory factory) {
358 proxyDatabase = factory;
359 }
360
361
362
363
364
365
366 protected ProxyDataFactory getProxyDatabase() {
367 return proxyDatabase;
368 }
369
370
371
372
373
374
375
376 public void setCredentialsProvider(CredentialsProvider provider) {
377 credentialsProvider = provider;
378 }
379
380
381
382
383
384
385 public CredentialsProvider getCredentialsProvider() {
386 return credentialsProvider;
387 }
388
389
390
391
392
393 private static class JGitSessionFactory extends SessionFactory {
394
395 public JGitSessionFactory(JGitSshClient client) {
396 super(client);
397 }
398
399 @Override
400 protected ClientSessionImpl doCreateSession(IoSession ioSession)
401 throws Exception {
402 return new JGitClientSession(getClient(), ioSession);
403 }
404 }
405
406
407
408
409
410 private static class CombinedKeyIdentityProvider
411 implements KeyIdentityProvider {
412
413 private final List<KeyIdentityProvider> providers;
414
415 public CombinedKeyIdentityProvider(KeyIdentityProvider... providers) {
416 this(Arrays.stream(providers).filter(Objects::nonNull)
417 .collect(Collectors.toList()));
418 }
419
420 public CombinedKeyIdentityProvider(
421 List<KeyIdentityProvider> providers) {
422 this.providers = providers;
423 }
424
425 @Override
426 public Iterable<KeyPair> loadKeys(SessionContext context) {
427 return () -> new Iterator<KeyPair>() {
428
429 private Iterator<KeyIdentityProvider> factories = providers
430 .iterator();
431 private Iterator<KeyPair> current;
432
433 private Boolean hasElement;
434
435 @Override
436 public boolean hasNext() {
437 if (hasElement != null) {
438 return hasElement.booleanValue();
439 }
440 while (current == null || !current.hasNext()) {
441 if (factories.hasNext()) {
442 try {
443 current = factories.next().loadKeys(context)
444 .iterator();
445 } catch (IOException | GeneralSecurityException e) {
446 throw new RuntimeException(e);
447 }
448 } else {
449 current = null;
450 hasElement = Boolean.FALSE;
451 return false;
452 }
453 }
454 hasElement = Boolean.TRUE;
455 return true;
456 }
457
458 @Override
459 public KeyPair next() {
460 if (hasElement == null && !hasNext()
461 || !hasElement.booleanValue()) {
462 throw new NoSuchElementException();
463 }
464 hasElement = null;
465 KeyPair result;
466 try {
467 result = current.next();
468 } catch (NoSuchElementException e) {
469 result = null;
470 }
471 return result;
472 }
473
474 };
475 }
476 }
477 }