View Javadoc
1   /*
2    * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
3    * Copyright (C) 2008-2009, Google Inc.
4    * Copyright (C) 2009, Google, Inc.
5    * Copyright (C) 2009, JetBrains s.r.o.
6    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
7    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
8    *
9    * This program and the accompanying materials are made available under the
10   * terms of the Eclipse Distribution License v. 1.0 which is available at
11   * https://www.eclipse.org/org/documents/edl-v10.php.
12   *
13   * SPDX-License-Identifier: BSD-3-Clause
14   */
15  
16  package org.eclipse.jgit.transport.ssh.jsch;
17  
18  import java.io.BufferedOutputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.List;
26  import java.util.Map;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.TimeUnit;
29  
30  import org.eclipse.jgit.errors.TransportException;
31  import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText;
32  import org.eclipse.jgit.transport.FtpChannel;
33  import org.eclipse.jgit.transport.RemoteSession2;
34  import org.eclipse.jgit.transport.URIish;
35  import org.eclipse.jgit.util.io.IsolatedOutputStream;
36  
37  import com.jcraft.jsch.Channel;
38  import com.jcraft.jsch.ChannelExec;
39  import com.jcraft.jsch.ChannelSftp;
40  import com.jcraft.jsch.JSchException;
41  import com.jcraft.jsch.Session;
42  import com.jcraft.jsch.SftpException;
43  
44  /**
45   * Run remote commands using Jsch.
46   * <p>
47   * This class is the default session implementation using Jsch. Note that
48   * {@link org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory} is used to
49   * create the actual session passed to the constructor.
50   *
51   * @since 6.0
52   */
53  public class JschSession implements RemoteSession2 {
54  	final Session sock;
55  	final URIish uri;
56  
57  	/**
58  	 * Create a new session object by passing the real Jsch session and the URI
59  	 * information.
60  	 *
61  	 * @param session
62  	 *            the real Jsch session created elsewhere.
63  	 * @param uri
64  	 *            the URI information for the remote connection
65  	 */
66  	public JschSession(Session session, URIish uri) {
67  		sock = session;
68  		this.uri = uri;
69  	}
70  
71  	/** {@inheritDoc} */
72  	@Override
73  	public Process exec(String command, int timeout) throws IOException {
74  		return exec(command, Collections.emptyMap(), timeout);
75  	}
76  
77  	/** {@inheritDoc} */
78  	@Override
79  	public Process exec(String command, Map<String, String> environment,
80  			int timeout) throws IOException {
81  		return new JschProcess(command, environment, timeout);
82  	}
83  
84  	/** {@inheritDoc} */
85  	@Override
86  	public void disconnect() {
87  		if (sock.isConnected())
88  			sock.disconnect();
89  	}
90  
91  	/**
92  	 * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get
93  	 * an Sftp channel from Jsch. Ideally, this method would be generic, which
94  	 * would require implementing generic Sftp channel operations in the
95  	 * RemoteSession class.
96  	 *
97  	 * @return a channel suitable for Sftp operations.
98  	 * @throws com.jcraft.jsch.JSchException
99  	 *             on problems getting the channel.
100 	 * @deprecated since 5.2; use {@link #getFtpChannel()} instead
101 	 */
102 	@Deprecated
103 	public Channel getSftpChannel() throws JSchException {
104 		return sock.openChannel("sftp"); //$NON-NLS-1$
105 	}
106 
107 	/**
108 	 * {@inheritDoc}
109 	 *
110 	 * @since 5.2
111 	 */
112 	@Override
113 	public FtpChannel getFtpChannel() {
114 		return new JschFtpChannel();
115 	}
116 
117 	/**
118 	 * Implementation of Process for running a single command using Jsch.
119 	 * <p>
120 	 * Uses the Jsch session to do actual command execution and manage the
121 	 * execution.
122 	 */
123 	private class JschProcess extends Process {
124 		private ChannelExec channel;
125 
126 		final int timeout;
127 
128 		private InputStream inputStream;
129 
130 		private OutputStream outputStream;
131 
132 		private InputStream errStream;
133 
134 		/**
135 		 * Opens a channel on the session ("sock") for executing the given
136 		 * command, opens streams, and starts command execution.
137 		 *
138 		 * @param commandName
139 		 *            the command to execute
140 		 * @param environment
141 		 *            environment variables to pass on
142 		 * @param tms
143 		 *            the timeout value, in seconds, for the command.
144 		 * @throws TransportException
145 		 *             on problems opening a channel or connecting to the remote
146 		 *             host
147 		 * @throws IOException
148 		 *             on problems opening streams
149 		 */
150 		JschProcess(String commandName, Map<String, String> environment,
151 				int tms) throws TransportException, IOException {
152 			timeout = tms;
153 			try {
154 				channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$
155 				if (environment != null) {
156 					for (Map.Entry<String, String> envVar : environment
157 							.entrySet()) {
158 						channel.setEnv(envVar.getKey(), envVar.getValue());
159 					}
160 				}
161 				channel.setCommand(commandName);
162 				setupStreams();
163 				channel.connect(timeout > 0 ? timeout * 1000 : 0);
164 				if (!channel.isConnected()) {
165 					closeOutputStream();
166 					throw new TransportException(uri,
167 							JSchText.get().connectionFailed);
168 				}
169 			} catch (JSchException e) {
170 				closeOutputStream();
171 				throw new TransportException(uri, e.getMessage(), e);
172 			}
173 		}
174 
175 		private void closeOutputStream() {
176 			if (outputStream != null) {
177 				try {
178 					outputStream.close();
179 				} catch (IOException ioe) {
180 					// ignore
181 				}
182 			}
183 		}
184 
185 		private void setupStreams() throws IOException {
186 			inputStream = channel.getInputStream();
187 
188 			// JSch won't let us interrupt writes when we use our InterruptTimer
189 			// to break out of a long-running write operation. To work around
190 			// that we spawn a background thread to shuttle data through a pipe,
191 			// as we can issue an interrupted write out of that. Its slower, so
192 			// we only use this route if there is a timeout.
193 			OutputStream out = channel.getOutputStream();
194 			if (timeout <= 0) {
195 				outputStream = out;
196 			} else {
197 				IsolatedOutputStream i = new IsolatedOutputStream(out);
198 				outputStream = new BufferedOutputStream(i, 16 * 1024);
199 			}
200 
201 			errStream = channel.getErrStream();
202 		}
203 
204 		@Override
205 		public InputStream getInputStream() {
206 			return inputStream;
207 		}
208 
209 		@Override
210 		public OutputStream getOutputStream() {
211 			return outputStream;
212 		}
213 
214 		@Override
215 		public InputStream getErrorStream() {
216 			return errStream;
217 		}
218 
219 		@Override
220 		public int exitValue() {
221 			if (isRunning())
222 				throw new IllegalThreadStateException();
223 			return channel.getExitStatus();
224 		}
225 
226 		private boolean isRunning() {
227 			return channel.getExitStatus() < 0 && channel.isConnected();
228 		}
229 
230 		@Override
231 		public void destroy() {
232 			if (channel.isConnected())
233 				channel.disconnect();
234 			closeOutputStream();
235 		}
236 
237 		@Override
238 		public int waitFor() throws InterruptedException {
239 			while (isRunning())
240 				Thread.sleep(100);
241 			return exitValue();
242 		}
243 	}
244 
245 	private class JschFtpChannel implements FtpChannel {
246 
247 		private ChannelSftp ftp;
248 
249 		@Override
250 		public void connect(int timeout, TimeUnit unit) throws IOException {
251 			try {
252 				ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$
253 				ftp.connect((int) unit.toMillis(timeout));
254 			} catch (JSchException e) {
255 				ftp = null;
256 				throw new IOException(e.getLocalizedMessage(), e);
257 			}
258 		}
259 
260 		@Override
261 		public void disconnect() {
262 			ftp.disconnect();
263 			ftp = null;
264 		}
265 
266 		private <T> T map(Callable<T> op) throws IOException {
267 			try {
268 				return op.call();
269 			} catch (Exception e) {
270 				if (e instanceof SftpException) {
271 					throw new FtpChannel.FtpException(e.getLocalizedMessage(),
272 							((SftpException) e).id, e);
273 				}
274 				throw new IOException(e.getLocalizedMessage(), e);
275 			}
276 		}
277 
278 		@Override
279 		public boolean isConnected() {
280 			return ftp != null && sock.isConnected();
281 		}
282 
283 		@Override
284 		public void cd(String path) throws IOException {
285 			map(() -> {
286 				ftp.cd(path);
287 				return null;
288 			});
289 		}
290 
291 		@Override
292 		public String pwd() throws IOException {
293 			return map(() -> ftp.pwd());
294 		}
295 
296 		@Override
297 		public Collection<DirEntry> ls(String path) throws IOException {
298 			return map(() -> {
299 				List<DirEntry> result = new ArrayList<>();
300 				for (Object e : ftp.ls(path)) {
301 					ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e;
302 					result.add(new DirEntry() {
303 
304 						@Override
305 						public String getFilename() {
306 							return entry.getFilename();
307 						}
308 
309 						@Override
310 						public long getModifiedTime() {
311 							return entry.getAttrs().getMTime();
312 						}
313 
314 						@Override
315 						public boolean isDirectory() {
316 							return entry.getAttrs().isDir();
317 						}
318 					});
319 				}
320 				return result;
321 			});
322 		}
323 
324 		@Override
325 		public void rmdir(String path) throws IOException {
326 			map(() -> {
327 				ftp.rm(path);
328 				return null;
329 			});
330 		}
331 
332 		@Override
333 		public void mkdir(String path) throws IOException {
334 			map(() -> {
335 				ftp.mkdir(path);
336 				return null;
337 			});
338 		}
339 
340 		@Override
341 		public InputStream get(String path) throws IOException {
342 			return map(() -> ftp.get(path));
343 		}
344 
345 		@Override
346 		public OutputStream put(String path) throws IOException {
347 			return map(() -> ftp.put(path));
348 		}
349 
350 		@Override
351 		public void rm(String path) throws IOException {
352 			map(() -> {
353 				ftp.rm(path);
354 				return null;
355 			});
356 		}
357 
358 		@Override
359 		public void rename(String from, String to) throws IOException {
360 			map(() -> {
361 				// Plain FTP rename will fail if "to" exists. Jsch knows about
362 				// the FTP extension "posix-rename@openssh.com", which will
363 				// remove "to" first if it exists.
364 				if (hasPosixRename()) {
365 					ftp.rename(from, to);
366 				} else if (!to.equals(from)) {
367 					// Try to remove "to" first. With git, we typically get this
368 					// when a lock file is moved over the file locked. Note that
369 					// the check for to being equal to from may still fail in
370 					// the general case, but for use with JGit's TransportSftp
371 					// it should be good enough.
372 					delete(to);
373 					ftp.rename(from, to);
374 				}
375 				return null;
376 			});
377 		}
378 
379 		/**
380 		 * Determine whether the server has the posix-rename extension.
381 		 *
382 		 * @return {@code true} if it is supported, {@code false} otherwise
383 		 * @see <a href=
384 		 *      "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD">OpenSSH
385 		 *      deviations and extensions to the published SSH protocol</a>
386 		 * @see <a href=
387 		 *      "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h:
388 		 *      rename()</a>
389 		 */
390 		private boolean hasPosixRename() {
391 			return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$
392 		}
393 	}
394 }