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