You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SshdSession.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
  3. *
  4. * This program and the accompanying materials are made available under the
  5. * terms of the Eclipse Distribution License v. 1.0 which is available at
  6. * https://www.eclipse.org/org/documents/edl-v10.php.
  7. *
  8. * SPDX-License-Identifier: BSD-3-Clause
  9. */
  10. package org.eclipse.jgit.transport.sshd;
  11. import static java.text.MessageFormat.format;
  12. import java.io.IOException;
  13. import java.io.InputStream;
  14. import java.io.OutputStream;
  15. import java.time.Duration;
  16. import java.util.ArrayList;
  17. import java.util.Collection;
  18. import java.util.EnumSet;
  19. import java.util.List;
  20. import java.util.concurrent.CopyOnWriteArrayList;
  21. import java.util.concurrent.TimeUnit;
  22. import java.util.concurrent.atomic.AtomicReference;
  23. import java.util.function.Supplier;
  24. import org.apache.sshd.client.SshClient;
  25. import org.apache.sshd.client.channel.ChannelExec;
  26. import org.apache.sshd.client.channel.ClientChannelEvent;
  27. import org.apache.sshd.client.session.ClientSession;
  28. import org.apache.sshd.client.subsystem.sftp.SftpClient;
  29. import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
  30. import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
  31. import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
  32. import org.apache.sshd.common.session.Session;
  33. import org.apache.sshd.common.session.SessionListener;
  34. import org.apache.sshd.common.subsystem.sftp.SftpException;
  35. import org.eclipse.jgit.annotations.NonNull;
  36. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  37. import org.eclipse.jgit.transport.FtpChannel;
  38. import org.eclipse.jgit.transport.RemoteSession;
  39. import org.eclipse.jgit.transport.URIish;
  40. import org.slf4j.Logger;
  41. import org.slf4j.LoggerFactory;
  42. /**
  43. * An implementation of {@link RemoteSession} based on Apache MINA sshd.
  44. *
  45. * @since 5.2
  46. */
  47. public class SshdSession implements RemoteSession {
  48. private static final Logger LOG = LoggerFactory
  49. .getLogger(SshdSession.class);
  50. private final CopyOnWriteArrayList<SessionCloseListener> listeners = new CopyOnWriteArrayList<>();
  51. private final URIish uri;
  52. private SshClient client;
  53. private ClientSession session;
  54. SshdSession(URIish uri, Supplier<SshClient> clientFactory) {
  55. this.uri = uri;
  56. this.client = clientFactory.get();
  57. }
  58. void connect(Duration timeout) throws IOException {
  59. if (!client.isStarted()) {
  60. client.start();
  61. }
  62. try {
  63. String username = uri.getUser();
  64. String host = uri.getHost();
  65. int port = uri.getPort();
  66. long t = timeout.toMillis();
  67. if (t <= 0) {
  68. session = client.connect(username, host, port).verify()
  69. .getSession();
  70. } else {
  71. session = client.connect(username, host, port)
  72. .verify(timeout.toMillis()).getSession();
  73. }
  74. session.addSessionListener(new SessionListener() {
  75. @Override
  76. public void sessionClosed(Session s) {
  77. notifyCloseListeners();
  78. }
  79. });
  80. // Authentication timeout is by default 2 minutes.
  81. session.auth().verify(session.getAuthTimeout());
  82. } catch (IOException e) {
  83. disconnect(e);
  84. throw e;
  85. }
  86. }
  87. /**
  88. * Adds a {@link SessionCloseListener} to this session. Has no effect if the
  89. * given {@code listener} is already registered with this session.
  90. *
  91. * @param listener
  92. * to add
  93. */
  94. public void addCloseListener(@NonNull SessionCloseListener listener) {
  95. listeners.addIfAbsent(listener);
  96. }
  97. /**
  98. * Removes the given {@code listener}; has no effect if the listener is not
  99. * currently registered with this session.
  100. *
  101. * @param listener
  102. * to remove
  103. */
  104. public void removeCloseListener(@NonNull SessionCloseListener listener) {
  105. listeners.remove(listener);
  106. }
  107. private void notifyCloseListeners() {
  108. for (SessionCloseListener l : listeners) {
  109. try {
  110. l.sessionClosed(this);
  111. } catch (RuntimeException e) {
  112. LOG.warn(SshdText.get().closeListenerFailed, e);
  113. }
  114. }
  115. }
  116. @Override
  117. public Process exec(String commandName, int timeout) throws IOException {
  118. @SuppressWarnings("resource")
  119. ChannelExec exec = session.createExecChannel(commandName);
  120. if (timeout <= 0) {
  121. try {
  122. exec.open().verify();
  123. } catch (IOException | RuntimeException e) {
  124. exec.close(true);
  125. throw e;
  126. }
  127. } else {
  128. try {
  129. exec.open().verify(TimeUnit.SECONDS.toMillis(timeout));
  130. } catch (IOException | RuntimeException e) {
  131. exec.close(true);
  132. throw new IOException(format(SshdText.get().sshCommandTimeout,
  133. commandName, Integer.valueOf(timeout)), e);
  134. }
  135. }
  136. return new SshdExecProcess(exec, commandName);
  137. }
  138. /**
  139. * Obtain an {@link FtpChannel} to perform SFTP operations in this
  140. * {@link SshdSession}.
  141. */
  142. @Override
  143. @NonNull
  144. public FtpChannel getFtpChannel() {
  145. return new SshdFtpChannel();
  146. }
  147. @Override
  148. public void disconnect() {
  149. disconnect(null);
  150. }
  151. private void disconnect(Throwable reason) {
  152. try {
  153. if (session != null) {
  154. session.close();
  155. session = null;
  156. }
  157. } catch (IOException e) {
  158. if (reason != null) {
  159. reason.addSuppressed(e);
  160. } else {
  161. LOG.error(SshdText.get().sessionCloseFailed, e);
  162. }
  163. } finally {
  164. client.stop();
  165. client = null;
  166. }
  167. }
  168. private static class SshdExecProcess extends Process {
  169. private final ChannelExec channel;
  170. private final String commandName;
  171. public SshdExecProcess(ChannelExec channel, String commandName) {
  172. this.channel = channel;
  173. this.commandName = commandName;
  174. }
  175. @Override
  176. public OutputStream getOutputStream() {
  177. return channel.getInvertedIn();
  178. }
  179. @Override
  180. public InputStream getInputStream() {
  181. return channel.getInvertedOut();
  182. }
  183. @Override
  184. public InputStream getErrorStream() {
  185. return channel.getInvertedErr();
  186. }
  187. @Override
  188. public int waitFor() throws InterruptedException {
  189. if (waitFor(-1L, TimeUnit.MILLISECONDS)) {
  190. return exitValue();
  191. }
  192. return -1;
  193. }
  194. @Override
  195. public boolean waitFor(long timeout, TimeUnit unit)
  196. throws InterruptedException {
  197. long millis = timeout >= 0 ? unit.toMillis(timeout) : -1L;
  198. return channel
  199. .waitFor(EnumSet.of(ClientChannelEvent.CLOSED), millis)
  200. .contains(ClientChannelEvent.CLOSED);
  201. }
  202. @Override
  203. public int exitValue() {
  204. Integer exitCode = channel.getExitStatus();
  205. if (exitCode == null) {
  206. throw new IllegalThreadStateException(
  207. format(SshdText.get().sshProcessStillRunning,
  208. commandName));
  209. }
  210. return exitCode.intValue();
  211. }
  212. @Override
  213. public void destroy() {
  214. if (channel.isOpen()) {
  215. channel.close(true);
  216. }
  217. }
  218. }
  219. /**
  220. * Helper interface like {@link Supplier}, but possibly raising an
  221. * {@link IOException}.
  222. *
  223. * @param <T>
  224. * return type
  225. */
  226. @FunctionalInterface
  227. private interface FtpOperation<T> {
  228. T call() throws IOException;
  229. }
  230. private class SshdFtpChannel implements FtpChannel {
  231. private SftpClient ftp;
  232. /** Current working directory. */
  233. private String cwd = ""; //$NON-NLS-1$
  234. @Override
  235. public void connect(int timeout, TimeUnit unit) throws IOException {
  236. if (timeout <= 0) {
  237. session.getProperties().put(
  238. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  239. Long.valueOf(Long.MAX_VALUE));
  240. } else {
  241. session.getProperties().put(
  242. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  243. Long.valueOf(unit.toMillis(timeout)));
  244. }
  245. ftp = SftpClientFactory.instance().createSftpClient(session);
  246. try {
  247. cd(cwd);
  248. } catch (IOException e) {
  249. ftp.close();
  250. }
  251. }
  252. @Override
  253. public void disconnect() {
  254. try {
  255. ftp.close();
  256. } catch (IOException e) {
  257. LOG.error(SshdText.get().ftpCloseFailed, e);
  258. }
  259. }
  260. @Override
  261. public boolean isConnected() {
  262. return session.isAuthenticated() && ftp.isOpen();
  263. }
  264. private String absolute(String path) {
  265. if (path.isEmpty()) {
  266. return cwd;
  267. }
  268. // Note: there is no path injection vulnerability here. If
  269. // path has too many ".." components, we rely on the server
  270. // catching it and returning an error.
  271. if (path.charAt(0) != '/') {
  272. if (cwd.charAt(cwd.length() - 1) == '/') {
  273. return cwd + path;
  274. }
  275. return cwd + '/' + path;
  276. }
  277. return path;
  278. }
  279. private <T> T map(FtpOperation<T> op) throws IOException {
  280. try {
  281. return op.call();
  282. } catch (IOException e) {
  283. if (e instanceof SftpException) {
  284. throw new FtpChannel.FtpException(e.getLocalizedMessage(),
  285. ((SftpException) e).getStatus(), e);
  286. }
  287. throw e;
  288. }
  289. }
  290. @Override
  291. public void cd(String path) throws IOException {
  292. cwd = map(() -> ftp.canonicalPath(absolute(path)));
  293. if (cwd.isEmpty()) {
  294. cwd += '/';
  295. }
  296. }
  297. @Override
  298. public String pwd() throws IOException {
  299. return cwd;
  300. }
  301. @Override
  302. public Collection<DirEntry> ls(String path) throws IOException {
  303. return map(() -> {
  304. List<DirEntry> result = new ArrayList<>();
  305. try (CloseableHandle handle = ftp.openDir(absolute(path))) {
  306. AtomicReference<Boolean> atEnd = new AtomicReference<>(
  307. Boolean.FALSE);
  308. while (!atEnd.get().booleanValue()) {
  309. List<SftpClient.DirEntry> chunk = ftp.readDir(handle,
  310. atEnd);
  311. if (chunk == null) {
  312. break;
  313. }
  314. for (SftpClient.DirEntry remote : chunk) {
  315. result.add(new DirEntry() {
  316. @Override
  317. public String getFilename() {
  318. return remote.getFilename();
  319. }
  320. @Override
  321. public long getModifiedTime() {
  322. return remote.getAttributes()
  323. .getModifyTime().toMillis();
  324. }
  325. @Override
  326. public boolean isDirectory() {
  327. return remote.getAttributes().isDirectory();
  328. }
  329. });
  330. }
  331. }
  332. }
  333. return result;
  334. });
  335. }
  336. @Override
  337. public void rmdir(String path) throws IOException {
  338. map(() -> {
  339. ftp.rmdir(absolute(path));
  340. return null;
  341. });
  342. }
  343. @Override
  344. public void mkdir(String path) throws IOException {
  345. map(() -> {
  346. ftp.mkdir(absolute(path));
  347. return null;
  348. });
  349. }
  350. @Override
  351. public InputStream get(String path) throws IOException {
  352. return map(() -> ftp.read(absolute(path)));
  353. }
  354. @Override
  355. public OutputStream put(String path) throws IOException {
  356. return map(() -> ftp.write(absolute(path)));
  357. }
  358. @Override
  359. public void rm(String path) throws IOException {
  360. map(() -> {
  361. ftp.remove(absolute(path));
  362. return null;
  363. });
  364. }
  365. @Override
  366. public void rename(String from, String to) throws IOException {
  367. map(() -> {
  368. String src = absolute(from);
  369. String dest = absolute(to);
  370. try {
  371. ftp.rename(src, dest, CopyMode.Atomic, CopyMode.Overwrite);
  372. } catch (UnsupportedOperationException e) {
  373. // Older server cannot do POSIX rename...
  374. if (!src.equals(dest)) {
  375. delete(dest);
  376. ftp.rename(src, dest);
  377. }
  378. }
  379. return null;
  380. });
  381. }
  382. }
  383. }