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