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