1
2
3
4
5
6
7
8
9
10 package org.eclipse.jgit.transport.sshd;
11
12 import static java.text.MessageFormat.format;
13
14 import java.io.IOException;
15 import java.io.InputStream;
16 import java.io.InterruptedIOException;
17 import java.io.OutputStream;
18 import java.time.Duration;
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.EnumSet;
22 import java.util.List;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.atomic.AtomicReference;
26 import java.util.function.Supplier;
27
28 import org.apache.sshd.client.SshClient;
29 import org.apache.sshd.client.channel.ChannelExec;
30 import org.apache.sshd.client.channel.ClientChannelEvent;
31 import org.apache.sshd.client.session.ClientSession;
32 import org.apache.sshd.client.subsystem.sftp.SftpClient;
33 import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
34 import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
35 import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
36 import org.apache.sshd.common.session.Session;
37 import org.apache.sshd.common.session.SessionListener;
38 import org.apache.sshd.common.subsystem.sftp.SftpException;
39 import org.eclipse.jgit.annotations.NonNull;
40 import org.eclipse.jgit.internal.transport.sshd.SshdText;
41 import org.eclipse.jgit.transport.FtpChannel;
42 import org.eclipse.jgit.transport.RemoteSession;
43 import org.eclipse.jgit.transport.URIish;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47
48
49
50
51
52 public class SshdSession implements RemoteSession {
53
54 private static final Logger LOG = LoggerFactory
55 .getLogger(SshdSession.class);
56
57 private final CopyOnWriteArrayList<SessionCloseListener> listeners = new CopyOnWriteArrayList<>();
58
59 private final URIish uri;
60
61 private SshClient client;
62
63 private ClientSession session;
64
65 SshdSession(URIish uri, Supplier<SshClient> clientFactory) {
66 this.uri = uri;
67 this.client = clientFactory.get();
68 }
69
70 void connect(Duration timeout) throws IOException {
71 if (!client.isStarted()) {
72 client.start();
73 }
74 try {
75 String username = uri.getUser();
76 String host = uri.getHost();
77 int port = uri.getPort();
78 long t = timeout.toMillis();
79 if (t <= 0) {
80 session = client.connect(username, host, port).verify()
81 .getSession();
82 } else {
83 session = client.connect(username, host, port)
84 .verify(timeout.toMillis()).getSession();
85 }
86 session.addSessionListener(new SessionListener() {
87
88 @Override
89 public void sessionClosed(Session s) {
90 notifyCloseListeners();
91 }
92 });
93
94 session.auth().verify(session.getAuthTimeout());
95 } catch (IOException e) {
96 disconnect(e);
97 throw e;
98 }
99 }
100
101
102
103
104
105
106
107
108 public void addCloseListener(@NonNull SessionCloseListener listener) {
109 listeners.addIfAbsent(listener);
110 }
111
112
113
114
115
116
117
118
119 public void removeCloseListener(@NonNull SessionCloseListener listener) {
120 listeners.remove(listener);
121 }
122
123 private void notifyCloseListeners() {
124 for (SessionCloseListener l : listeners) {
125 try {
126 l.sessionClosed(this);
127 } catch (RuntimeException e) {
128 LOG.warn(SshdText.get().closeListenerFailed, e);
129 }
130 }
131 }
132
133 @Override
134 public Process exec(String commandName, int timeout) throws IOException {
135 @SuppressWarnings("resource")
136 ChannelExec exec = session.createExecChannel(commandName);
137 long timeoutMillis = TimeUnit.SECONDS.toMillis(timeout);
138 try {
139 if (timeout <= 0) {
140 exec.open().verify();
141 } else {
142 long start = System.nanoTime();
143 exec.open().verify(timeoutMillis);
144 timeoutMillis -= TimeUnit.NANOSECONDS
145 .toMillis(System.nanoTime() - start);
146 }
147 } catch (IOException | RuntimeException e) {
148 exec.close(true);
149 throw e;
150 }
151 if (timeout > 0 && timeoutMillis <= 0) {
152
153 exec.close(true);
154 throw new InterruptedIOException(
155 format(SshdText.get().sshCommandTimeout, commandName,
156 Integer.valueOf(timeout)));
157 }
158 return new SshdExecProcess(exec, commandName, timeoutMillis);
159 }
160
161
162
163
164
165 @Override
166 @NonNull
167 public FtpChannel getFtpChannel() {
168 return new SshdFtpChannel();
169 }
170
171 @Override
172 public void disconnect() {
173 disconnect(null);
174 }
175
176 private void disconnect(Throwable reason) {
177 try {
178 if (session != null) {
179 session.close();
180 session = null;
181 }
182 } catch (IOException e) {
183 if (reason != null) {
184 reason.addSuppressed(e);
185 } else {
186 LOG.error(SshdText.get().sessionCloseFailed, e);
187 }
188 } finally {
189 client.stop();
190 client = null;
191 }
192 }
193
194 private static class SshdExecProcess extends Process {
195
196 private final ChannelExec channel;
197
198 private final long timeoutMillis;
199
200 private final String commandName;
201
202 public SshdExecProcess(ChannelExec channel, String commandName,
203 long timeoutMillis) {
204 this.channel = channel;
205 this.timeoutMillis = timeoutMillis > 0 ? timeoutMillis : -1L;
206 this.commandName = commandName;
207 }
208
209 @Override
210 public OutputStream getOutputStream() {
211 return channel.getInvertedIn();
212 }
213
214 @Override
215 public InputStream getInputStream() {
216 return channel.getInvertedOut();
217 }
218
219 @Override
220 public InputStream getErrorStream() {
221 return channel.getInvertedErr();
222 }
223
224 @Override
225 public int waitFor() throws InterruptedException {
226 if (waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) {
227 return exitValue();
228 }
229 return -1;
230 }
231
232 @Override
233 public boolean waitFor(long timeout, TimeUnit unit)
234 throws InterruptedException {
235 long millis = timeout >= 0 ? unit.toMillis(timeout) : -1L;
236 return channel
237 .waitFor(EnumSet.of(ClientChannelEvent.CLOSED), millis)
238 .contains(ClientChannelEvent.CLOSED);
239 }
240
241 @Override
242 public int exitValue() {
243 Integer exitCode = channel.getExitStatus();
244 if (exitCode == null) {
245 throw new IllegalThreadStateException(
246 format(SshdText.get().sshProcessStillRunning,
247 commandName));
248 }
249 return exitCode.intValue();
250 }
251
252 @Override
253 public void destroy() {
254 if (channel.isOpen()) {
255 channel.close(true);
256 }
257 }
258 }
259
260
261
262
263
264
265
266
267 @FunctionalInterface
268 private interface FtpOperation<T> {
269
270 T call() throws IOException;
271
272 }
273
274 private class SshdFtpChannel implements FtpChannel {
275
276 private SftpClient ftp;
277
278
279 private String cwd = "";
280
281 @Override
282 public void connect(int timeout, TimeUnit unit) throws IOException {
283 if (timeout <= 0) {
284 session.getProperties().put(
285 SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
286 Long.valueOf(Long.MAX_VALUE));
287 } else {
288 session.getProperties().put(
289 SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
290 Long.valueOf(unit.toMillis(timeout)));
291 }
292 ftp = SftpClientFactory.instance().createSftpClient(session);
293 try {
294 cd(cwd);
295 } catch (IOException e) {
296 ftp.close();
297 }
298 }
299
300 @Override
301 public void disconnect() {
302 try {
303 ftp.close();
304 } catch (IOException e) {
305 LOG.error(SshdText.get().ftpCloseFailed, e);
306 }
307 }
308
309 @Override
310 public boolean isConnected() {
311 return session.isAuthenticated() && ftp.isOpen();
312 }
313
314 private String absolute(String path) {
315 if (path.isEmpty()) {
316 return cwd;
317 }
318
319
320
321 if (path.charAt(0) != '/') {
322 if (cwd.charAt(cwd.length() - 1) == '/') {
323 return cwd + path;
324 }
325 return cwd + '/' + path;
326 }
327 return path;
328 }
329
330 private <T> T map(FtpOperation<T> op) throws IOException {
331 try {
332 return op.call();
333 } catch (IOException e) {
334 if (e instanceof SftpException) {
335 throw new FtpChannel.FtpException(e.getLocalizedMessage(),
336 ((SftpException) e).getStatus(), e);
337 }
338 throw e;
339 }
340 }
341
342 @Override
343 public void cd(String path) throws IOException {
344 cwd = map(() -> ftp.canonicalPath(absolute(path)));
345 if (cwd.isEmpty()) {
346 cwd += '/';
347 }
348 }
349
350 @Override
351 public String pwd() throws IOException {
352 return cwd;
353 }
354
355 @Override
356 public Collection<DirEntry> ls(String path) throws IOException {
357 return map(() -> {
358 List<DirEntry> result = new ArrayList<>();
359 try (CloseableHandle handle = ftp.openDir(absolute(path))) {
360 AtomicReference<Boolean> atEnd = new AtomicReference<>(
361 Boolean.FALSE);
362 while (!atEnd.get().booleanValue()) {
363 List<SftpClient.DirEntry> chunk = ftp.readDir(handle,
364 atEnd);
365 if (chunk == null) {
366 break;
367 }
368 for (SftpClient.DirEntry remote : chunk) {
369 result.add(new DirEntry() {
370
371 @Override
372 public String getFilename() {
373 return remote.getFilename();
374 }
375
376 @Override
377 public long getModifiedTime() {
378 return remote.getAttributes()
379 .getModifyTime().toMillis();
380 }
381
382 @Override
383 public boolean isDirectory() {
384 return remote.getAttributes().isDirectory();
385 }
386
387 });
388 }
389 }
390 }
391 return result;
392 });
393 }
394
395 @Override
396 public void rmdir(String path) throws IOException {
397 map(() -> {
398 ftp.rmdir(absolute(path));
399 return null;
400 });
401
402 }
403
404 @Override
405 public void mkdir(String path) throws IOException {
406 map(() -> {
407 ftp.mkdir(absolute(path));
408 return null;
409 });
410 }
411
412 @Override
413 public InputStream get(String path) throws IOException {
414 return map(() -> ftp.read(absolute(path)));
415 }
416
417 @Override
418 public OutputStream put(String path) throws IOException {
419 return map(() -> ftp.write(absolute(path)));
420 }
421
422 @Override
423 public void rm(String path) throws IOException {
424 map(() -> {
425 ftp.remove(absolute(path));
426 return null;
427 });
428 }
429
430 @Override
431 public void rename(String from, String to) throws IOException {
432 map(() -> {
433 String src = absolute(from);
434 String dest = absolute(to);
435 try {
436 ftp.rename(src, dest, CopyMode.Atomic, CopyMode.Overwrite);
437 } catch (UnsupportedOperationException e) {
438
439 if (!src.equals(dest)) {
440 delete(dest);
441 ftp.rename(src, dest);
442 }
443 }
444 return null;
445 });
446 }
447 }
448 }