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.FileKeyPairProvider;
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 provider = getFilePasswordProvider();
247 if (provider instanceof RepeatingFilePasswordProvider) {
248 ((RepeatingFilePasswordProvider) provider)
249 .setAttempts(numberOfPasswordPrompts);
250 }
251 FileKeyPairProvider ourConfiguredKeysProvider = null;
252 List<Path> identities = hostConfig.getIdentities().stream()
253 .map(s -> {
254 try {
255 return Paths.get(s);
256 } catch (InvalidPathException e) {
257 log.warn(format(SshdText.get().configInvalidPath,
258 SshConstants.IDENTITY_FILE, s), e);
259 return null;
260 }
261 }).filter(p -> p != null && Files.exists(p))
262 .collect(Collectors.toList());
263 ourConfiguredKeysProvider = new CachingKeyPairProvider(identities,
264 keyCache);
265 ourConfiguredKeysProvider.setPasswordFinder(getFilePasswordProvider());
266 if (hostConfig.isIdentitiesOnly()) {
267 session.setKeyPairProvider(ourConfiguredKeysProvider);
268 } else {
269 KeyPairProvider defaultKeysProvider = getKeyPairProvider();
270 if (defaultKeysProvider instanceof FileKeyPairProvider) {
271 ((FileKeyPairProvider) defaultKeysProvider)
272 .setPasswordFinder(getFilePasswordProvider());
273 }
274 KeyPairProvider combinedProvider = new CombinedKeyPairProvider(
275 ourConfiguredKeysProvider, defaultKeysProvider);
276 session.setKeyPairProvider(combinedProvider);
277 }
278 return session;
279 }
280
281 private int getNumberOfPasswordPrompts(HostConfigEntry hostConfig) {
282 String prompts = hostConfig
283 .getProperty(SshConstants.NUMBER_OF_PASSWORD_PROMPTS);
284 if (prompts != null) {
285 prompts = prompts.trim();
286 int value = positive(prompts);
287 if (value > 0) {
288 return value;
289 }
290 log.warn(format(SshdText.get().configInvalidPositive,
291 SshConstants.NUMBER_OF_PASSWORD_PROMPTS, prompts));
292 }
293
294
295 return 3;
296 }
297
298
299
300
301
302
303
304
305
306 public void setKeyCache(KeyCache cache) {
307 keyCache = cache;
308 }
309
310
311
312
313
314
315
316
317 public void setProxyDatabase(ProxyDataFactory factory) {
318 proxyDatabase = factory;
319 }
320
321
322
323
324
325
326 protected ProxyDataFactory getProxyDatabase() {
327 return proxyDatabase;
328 }
329
330
331
332
333
334
335
336 public void setCredentialsProvider(CredentialsProvider provider) {
337 credentialsProvider = provider;
338 }
339
340
341
342
343
344
345 public CredentialsProvider getCredentialsProvider() {
346 return credentialsProvider;
347 }
348
349
350
351
352
353 private static class JGitSessionFactory extends SessionFactory {
354
355 public JGitSessionFactory(JGitSshClient client) {
356 super(client);
357 }
358
359 @Override
360 protected ClientSessionImpl doCreateSession(IoSession ioSession)
361 throws Exception {
362 return new JGitClientSession(getClient(), ioSession);
363 }
364 }
365
366
367
368
369
370 private static class CombinedKeyPairProvider implements KeyPairProvider {
371
372 private final List<KeyPairProvider> providers;
373
374 public CombinedKeyPairProvider(KeyPairProvider... providers) {
375 this(Arrays.stream(providers).filter(Objects::nonNull)
376 .collect(Collectors.toList()));
377 }
378
379 public CombinedKeyPairProvider(List<KeyPairProvider> providers) {
380 this.providers = providers;
381 }
382
383 @Override
384 public Iterable<String> getKeyTypes() {
385 throw new UnsupportedOperationException(
386 "Should not have been called in a ssh client");
387 }
388
389 @Override
390 public KeyPair loadKey(String type) {
391 throw new UnsupportedOperationException(
392 "Should not have been called in a ssh client");
393 }
394
395 @Override
396 public Iterable<KeyPair> loadKeys() {
397 return () -> new Iterator<KeyPair>() {
398
399 private Iterator<KeyPairProvider> factories = providers.iterator();
400 private Iterator<KeyPair> current;
401
402 private Boolean hasElement;
403
404 @Override
405 public boolean hasNext() {
406 if (hasElement != null) {
407 return hasElement.booleanValue();
408 }
409 while (current == null || !current.hasNext()) {
410 if (factories.hasNext()) {
411 current = factories.next().loadKeys().iterator();
412 } else {
413 current = null;
414 hasElement = Boolean.FALSE;
415 return false;
416 }
417 }
418 hasElement = Boolean.TRUE;
419 return true;
420 }
421
422 @Override
423 public KeyPair next() {
424 if (hasElement == null && !hasNext()
425 || !hasElement.booleanValue()) {
426 throw new NoSuchElementException();
427 }
428 hasElement = null;
429 KeyPair result;
430 try {
431 result = current.next();
432 } catch (NoSuchElementException e) {
433 result = null;
434 }
435 return result;
436 }
437
438 };
439 }
440
441 }
442 }