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 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /*
  2. * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
  3. * and other copyright owners as documented in the project's IP log.
  4. *
  5. * This program and the accompanying materials are made available
  6. * under the terms of the Eclipse Distribution License v1.0 which
  7. * accompanies this distribution, is reproduced below, and is
  8. * available at http://www.eclipse.org/org/documents/edl-v10.php
  9. *
  10. * All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or
  13. * without modification, are permitted provided that the following
  14. * conditions are met:
  15. *
  16. * - Redistributions of source code must retain the above copyright
  17. * notice, this list of conditions and the following disclaimer.
  18. *
  19. * - Redistributions in binary form must reproduce the above
  20. * copyright notice, this list of conditions and the following
  21. * disclaimer in the documentation and/or other materials provided
  22. * with the distribution.
  23. *
  24. * - Neither the name of the Eclipse Foundation, Inc. nor the
  25. * names of its contributors may be used to endorse or promote
  26. * products derived from this software without specific prior
  27. * written permission.
  28. *
  29. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
  30. * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  31. * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  32. * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  33. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
  34. * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  35. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  36. * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  37. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  38. * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
  39. * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
  41. * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  42. */
  43. package org.eclipse.jgit.transport.sshd;
  44. import static java.text.MessageFormat.format;
  45. import java.io.IOException;
  46. import java.io.InputStream;
  47. import java.io.InterruptedIOException;
  48. import java.io.OutputStream;
  49. import java.time.Duration;
  50. import java.util.ArrayList;
  51. import java.util.Collection;
  52. import java.util.EnumSet;
  53. import java.util.List;
  54. import java.util.concurrent.CopyOnWriteArrayList;
  55. import java.util.concurrent.TimeUnit;
  56. import java.util.concurrent.atomic.AtomicReference;
  57. import java.util.function.Supplier;
  58. import org.apache.sshd.client.SshClient;
  59. import org.apache.sshd.client.channel.ChannelExec;
  60. import org.apache.sshd.client.channel.ClientChannelEvent;
  61. import org.apache.sshd.client.session.ClientSession;
  62. import org.apache.sshd.client.subsystem.sftp.SftpClient;
  63. import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
  64. import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
  65. import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
  66. import org.apache.sshd.common.session.Session;
  67. import org.apache.sshd.common.session.SessionListener;
  68. import org.apache.sshd.common.subsystem.sftp.SftpException;
  69. import org.eclipse.jgit.annotations.NonNull;
  70. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  71. import org.eclipse.jgit.transport.FtpChannel;
  72. import org.eclipse.jgit.transport.RemoteSession;
  73. import org.eclipse.jgit.transport.URIish;
  74. import org.slf4j.Logger;
  75. import org.slf4j.LoggerFactory;
  76. /**
  77. * An implementation of {@link RemoteSession} based on Apache MINA sshd.
  78. *
  79. * @since 5.2
  80. */
  81. public class SshdSession implements RemoteSession {
  82. private static final Logger LOG = LoggerFactory
  83. .getLogger(SshdSession.class);
  84. private final CopyOnWriteArrayList<SessionCloseListener> listeners = new CopyOnWriteArrayList<>();
  85. private final URIish uri;
  86. private SshClient client;
  87. private ClientSession session;
  88. SshdSession(URIish uri, Supplier<SshClient> clientFactory) {
  89. this.uri = uri;
  90. this.client = clientFactory.get();
  91. }
  92. void connect(Duration timeout) throws IOException {
  93. if (!client.isStarted()) {
  94. client.start();
  95. }
  96. try {
  97. String username = uri.getUser();
  98. String host = uri.getHost();
  99. int port = uri.getPort();
  100. long t = timeout.toMillis();
  101. if (t <= 0) {
  102. session = client.connect(username, host, port).verify()
  103. .getSession();
  104. } else {
  105. session = client.connect(username, host, port)
  106. .verify(timeout.toMillis()).getSession();
  107. }
  108. session.addSessionListener(new SessionListener() {
  109. @Override
  110. public void sessionClosed(Session s) {
  111. notifyCloseListeners();
  112. }
  113. });
  114. // Authentication timeout is by default 2 minutes.
  115. session.auth().verify(session.getAuthTimeout());
  116. } catch (IOException e) {
  117. disconnect(e);
  118. throw e;
  119. }
  120. }
  121. /**
  122. * Adds a {@link SessionCloseListener} to this session. Has no effect if the
  123. * given {@code listener} is already registered with this session.
  124. *
  125. * @param listener
  126. * to add
  127. */
  128. public void addCloseListener(@NonNull SessionCloseListener listener) {
  129. listeners.addIfAbsent(listener);
  130. }
  131. /**
  132. * Removes the given {@code listener}; has no effect if the listener is not
  133. * currently registered with this session.
  134. *
  135. * @param listener
  136. * to remove
  137. */
  138. public void removeCloseListener(@NonNull SessionCloseListener listener) {
  139. listeners.remove(listener);
  140. }
  141. private void notifyCloseListeners() {
  142. for (SessionCloseListener l : listeners) {
  143. try {
  144. l.sessionClosed(this);
  145. } catch (RuntimeException e) {
  146. LOG.warn(SshdText.get().closeListenerFailed, e);
  147. }
  148. }
  149. }
  150. @Override
  151. public Process exec(String commandName, int timeout) throws IOException {
  152. @SuppressWarnings("resource")
  153. ChannelExec exec = session.createExecChannel(commandName);
  154. long timeoutMillis = TimeUnit.SECONDS.toMillis(timeout);
  155. try {
  156. if (timeout <= 0) {
  157. exec.open().verify();
  158. } else {
  159. long start = System.nanoTime();
  160. exec.open().verify(timeoutMillis);
  161. timeoutMillis -= TimeUnit.NANOSECONDS
  162. .toMillis(System.nanoTime() - start);
  163. }
  164. } catch (IOException | RuntimeException e) {
  165. exec.close(true);
  166. throw e;
  167. }
  168. if (timeout > 0 && timeoutMillis <= 0) {
  169. // We have used up the whole timeout for opening the channel
  170. exec.close(true);
  171. throw new InterruptedIOException(
  172. format(SshdText.get().sshCommandTimeout, commandName,
  173. Integer.valueOf(timeout)));
  174. }
  175. return new SshdExecProcess(exec, commandName, timeoutMillis);
  176. }
  177. /**
  178. * Obtain an {@link FtpChannel} to perform SFTP operations in this
  179. * {@link SshdSession}.
  180. */
  181. @Override
  182. @NonNull
  183. public FtpChannel getFtpChannel() {
  184. return new SshdFtpChannel();
  185. }
  186. @Override
  187. public void disconnect() {
  188. disconnect(null);
  189. }
  190. private void disconnect(Throwable reason) {
  191. try {
  192. if (session != null) {
  193. session.close();
  194. session = null;
  195. }
  196. } catch (IOException e) {
  197. if (reason != null) {
  198. reason.addSuppressed(e);
  199. } else {
  200. LOG.error(SshdText.get().sessionCloseFailed, e);
  201. }
  202. } finally {
  203. client.stop();
  204. client = null;
  205. }
  206. }
  207. private static class SshdExecProcess extends Process {
  208. private final ChannelExec channel;
  209. private final long timeoutMillis;
  210. private final String commandName;
  211. public SshdExecProcess(ChannelExec channel, String commandName,
  212. long timeoutMillis) {
  213. this.channel = channel;
  214. this.timeoutMillis = timeoutMillis > 0 ? timeoutMillis : -1L;
  215. this.commandName = commandName;
  216. }
  217. @Override
  218. public OutputStream getOutputStream() {
  219. return channel.getInvertedIn();
  220. }
  221. @Override
  222. public InputStream getInputStream() {
  223. return channel.getInvertedOut();
  224. }
  225. @Override
  226. public InputStream getErrorStream() {
  227. return channel.getInvertedErr();
  228. }
  229. @Override
  230. public int waitFor() throws InterruptedException {
  231. if (waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) {
  232. return exitValue();
  233. }
  234. return -1;
  235. }
  236. @Override
  237. public boolean waitFor(long timeout, TimeUnit unit)
  238. throws InterruptedException {
  239. long millis = timeout >= 0 ? unit.toMillis(timeout) : -1L;
  240. return channel
  241. .waitFor(EnumSet.of(ClientChannelEvent.CLOSED), millis)
  242. .contains(ClientChannelEvent.CLOSED);
  243. }
  244. @Override
  245. public int exitValue() {
  246. Integer exitCode = channel.getExitStatus();
  247. if (exitCode == null) {
  248. throw new IllegalThreadStateException(
  249. format(SshdText.get().sshProcessStillRunning,
  250. commandName));
  251. }
  252. return exitCode.intValue();
  253. }
  254. @Override
  255. public void destroy() {
  256. if (channel.isOpen()) {
  257. channel.close(true);
  258. }
  259. }
  260. }
  261. /**
  262. * Helper interface like {@link Supplier}, but possibly raising an
  263. * {@link IOException}.
  264. *
  265. * @param <T>
  266. * return type
  267. */
  268. @FunctionalInterface
  269. private interface FtpOperation<T> {
  270. T call() throws IOException;
  271. }
  272. private class SshdFtpChannel implements FtpChannel {
  273. private SftpClient ftp;
  274. /** Current working directory. */
  275. private String cwd = ""; //$NON-NLS-1$
  276. @Override
  277. public void connect(int timeout, TimeUnit unit) throws IOException {
  278. if (timeout <= 0) {
  279. session.getProperties().put(
  280. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  281. Long.valueOf(Long.MAX_VALUE));
  282. } else {
  283. session.getProperties().put(
  284. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  285. Long.valueOf(unit.toMillis(timeout)));
  286. }
  287. ftp = SftpClientFactory.instance().createSftpClient(session);
  288. try {
  289. cd(cwd);
  290. } catch (IOException e) {
  291. ftp.close();
  292. }
  293. }
  294. @Override
  295. public void disconnect() {
  296. try {
  297. ftp.close();
  298. } catch (IOException e) {
  299. LOG.error(SshdText.get().ftpCloseFailed, e);
  300. }
  301. }
  302. @Override
  303. public boolean isConnected() {
  304. return session.isAuthenticated() && ftp.isOpen();
  305. }
  306. private String absolute(String path) {
  307. if (path.isEmpty()) {
  308. return cwd;
  309. }
  310. // Note: there is no path injection vulnerability here. If
  311. // path has too many ".." components, we rely on the server
  312. // catching it and returning an error.
  313. if (path.charAt(0) != '/') {
  314. if (cwd.charAt(cwd.length() - 1) == '/') {
  315. return cwd + path;
  316. }
  317. return cwd + '/' + path;
  318. }
  319. return path;
  320. }
  321. private <T> T map(FtpOperation<T> op) throws IOException {
  322. try {
  323. return op.call();
  324. } catch (IOException e) {
  325. if (e instanceof SftpException) {
  326. throw new FtpChannel.FtpException(e.getLocalizedMessage(),
  327. ((SftpException) e).getStatus(), e);
  328. }
  329. throw e;
  330. }
  331. }
  332. @Override
  333. public void cd(String path) throws IOException {
  334. cwd = map(() -> ftp.canonicalPath(absolute(path)));
  335. if (cwd.isEmpty()) {
  336. cwd += '/';
  337. }
  338. }
  339. @Override
  340. public String pwd() throws IOException {
  341. return cwd;
  342. }
  343. @Override
  344. public Collection<DirEntry> ls(String path) throws IOException {
  345. return map(() -> {
  346. List<DirEntry> result = new ArrayList<>();
  347. try (CloseableHandle handle = ftp.openDir(absolute(path))) {
  348. AtomicReference<Boolean> atEnd = new AtomicReference<>(
  349. Boolean.FALSE);
  350. while (!atEnd.get().booleanValue()) {
  351. List<SftpClient.DirEntry> chunk = ftp.readDir(handle,
  352. atEnd);
  353. if (chunk == null) {
  354. break;
  355. }
  356. for (SftpClient.DirEntry remote : chunk) {
  357. result.add(new DirEntry() {
  358. @Override
  359. public String getFilename() {
  360. return remote.getFilename();
  361. }
  362. @Override
  363. public long getModifiedTime() {
  364. return remote.getAttributes()
  365. .getModifyTime().toMillis();
  366. }
  367. @Override
  368. public boolean isDirectory() {
  369. return remote.getAttributes().isDirectory();
  370. }
  371. });
  372. }
  373. }
  374. }
  375. return result;
  376. });
  377. }
  378. @Override
  379. public void rmdir(String path) throws IOException {
  380. map(() -> {
  381. ftp.rmdir(absolute(path));
  382. return null;
  383. });
  384. }
  385. @Override
  386. public void mkdir(String path) throws IOException {
  387. map(() -> {
  388. ftp.mkdir(absolute(path));
  389. return null;
  390. });
  391. }
  392. @Override
  393. public InputStream get(String path) throws IOException {
  394. return map(() -> ftp.read(absolute(path)));
  395. }
  396. @Override
  397. public OutputStream put(String path) throws IOException {
  398. return map(() -> ftp.write(absolute(path)));
  399. }
  400. @Override
  401. public void rm(String path) throws IOException {
  402. map(() -> {
  403. ftp.remove(absolute(path));
  404. return null;
  405. });
  406. }
  407. @Override
  408. public void rename(String from, String to) throws IOException {
  409. map(() -> {
  410. String src = absolute(from);
  411. String dest = absolute(to);
  412. try {
  413. ftp.rename(src, dest, CopyMode.Atomic, CopyMode.Overwrite);
  414. } catch (UnsupportedOperationException e) {
  415. // Older server cannot do POSIX rename...
  416. if (!src.equals(dest)) {
  417. delete(dest);
  418. ftp.rename(src, dest);
  419. }
  420. }
  421. return null;
  422. });
  423. }
  424. }
  425. }