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