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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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 e) {
  165. exec.close(true);
  166. throw e;
  167. } catch (RuntimeException e) {
  168. exec.close(true);
  169. throw e;
  170. }
  171. if (timeout > 0 && timeoutMillis <= 0) {
  172. // We have used up the whole timeout for opening the channel
  173. exec.close(true);
  174. throw new InterruptedIOException(
  175. format(SshdText.get().sshCommandTimeout, commandName,
  176. Integer.valueOf(timeout)));
  177. }
  178. return new SshdExecProcess(exec, commandName, timeoutMillis);
  179. }
  180. /**
  181. * Obtain an {@link FtpChannel} to perform SFTP operations in this
  182. * {@link SshdSession}.
  183. */
  184. @Override
  185. @NonNull
  186. public FtpChannel getFtpChannel() {
  187. return new SshdFtpChannel();
  188. }
  189. @Override
  190. public void disconnect() {
  191. disconnect(null);
  192. }
  193. private void disconnect(Throwable reason) {
  194. try {
  195. if (session != null) {
  196. session.close();
  197. session = null;
  198. }
  199. } catch (IOException e) {
  200. if (reason != null) {
  201. reason.addSuppressed(e);
  202. } else {
  203. LOG.error(SshdText.get().sessionCloseFailed, e);
  204. }
  205. } finally {
  206. client.stop();
  207. client = null;
  208. }
  209. }
  210. private static class SshdExecProcess extends Process {
  211. private final ChannelExec channel;
  212. private final long timeoutMillis;
  213. private final String commandName;
  214. public SshdExecProcess(ChannelExec channel, String commandName,
  215. long timeoutMillis) {
  216. this.channel = channel;
  217. this.timeoutMillis = timeoutMillis > 0 ? timeoutMillis : -1L;
  218. this.commandName = commandName;
  219. }
  220. @Override
  221. public OutputStream getOutputStream() {
  222. return channel.getInvertedIn();
  223. }
  224. @Override
  225. public InputStream getInputStream() {
  226. return channel.getInvertedOut();
  227. }
  228. @Override
  229. public InputStream getErrorStream() {
  230. return channel.getInvertedErr();
  231. }
  232. @Override
  233. public int waitFor() throws InterruptedException {
  234. if (waitFor(timeoutMillis, TimeUnit.MILLISECONDS)) {
  235. return exitValue();
  236. }
  237. return -1;
  238. }
  239. @Override
  240. public boolean waitFor(long timeout, TimeUnit unit)
  241. throws InterruptedException {
  242. long millis = timeout >= 0 ? unit.toMillis(timeout) : -1L;
  243. return channel
  244. .waitFor(EnumSet.of(ClientChannelEvent.CLOSED), millis)
  245. .contains(ClientChannelEvent.CLOSED);
  246. }
  247. @Override
  248. public int exitValue() {
  249. Integer exitCode = channel.getExitStatus();
  250. if (exitCode == null) {
  251. throw new IllegalThreadStateException(
  252. format(SshdText.get().sshProcessStillRunning,
  253. commandName));
  254. }
  255. return exitCode.intValue();
  256. }
  257. @Override
  258. public void destroy() {
  259. if (channel.isOpen()) {
  260. channel.close(true);
  261. }
  262. }
  263. }
  264. /**
  265. * Helper interface like {@link Supplier}, but possibly raising an
  266. * {@link IOException}.
  267. *
  268. * @param <T>
  269. * return type
  270. */
  271. @FunctionalInterface
  272. private interface FtpOperation<T> {
  273. T call() throws IOException;
  274. }
  275. private class SshdFtpChannel implements FtpChannel {
  276. private SftpClient ftp;
  277. /** Current working directory. */
  278. private String cwd = ""; //$NON-NLS-1$
  279. @Override
  280. public void connect(int timeout, TimeUnit unit) throws IOException {
  281. if (timeout <= 0) {
  282. session.getProperties().put(
  283. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  284. Long.valueOf(Long.MAX_VALUE));
  285. } else {
  286. session.getProperties().put(
  287. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  288. Long.valueOf(unit.toMillis(timeout)));
  289. }
  290. ftp = SftpClientFactory.instance().createSftpClient(session);
  291. try {
  292. cd(cwd);
  293. } catch (IOException e) {
  294. ftp.close();
  295. }
  296. }
  297. @Override
  298. public void disconnect() {
  299. try {
  300. ftp.close();
  301. } catch (IOException e) {
  302. LOG.error(SshdText.get().ftpCloseFailed, e);
  303. }
  304. }
  305. @Override
  306. public boolean isConnected() {
  307. return session.isAuthenticated() && ftp.isOpen();
  308. }
  309. private String absolute(String path) {
  310. if (path.isEmpty()) {
  311. return cwd;
  312. }
  313. // Note: there is no path injection vulnerability here. If
  314. // path has too many ".." components, we rely on the server
  315. // catching it and returning an error.
  316. if (path.charAt(0) != '/') {
  317. if (cwd.charAt(cwd.length() - 1) == '/') {
  318. return cwd + path;
  319. } else {
  320. return cwd + '/' + path;
  321. }
  322. }
  323. return path;
  324. }
  325. private <T> T map(FtpOperation<T> op) throws IOException {
  326. try {
  327. return op.call();
  328. } catch (IOException e) {
  329. if (e instanceof SftpException) {
  330. throw new FtpChannel.FtpException(e.getLocalizedMessage(),
  331. ((SftpException) e).getStatus(), e);
  332. }
  333. throw e;
  334. }
  335. }
  336. @Override
  337. public void cd(String path) throws IOException {
  338. cwd = map(() -> ftp.canonicalPath(absolute(path)));
  339. if (cwd.isEmpty()) {
  340. cwd += '/';
  341. }
  342. }
  343. @Override
  344. public String pwd() throws IOException {
  345. return cwd;
  346. }
  347. @Override
  348. public Collection<DirEntry> ls(String path) throws IOException {
  349. return map(() -> {
  350. List<DirEntry> result = new ArrayList<>();
  351. try (CloseableHandle handle = ftp.openDir(absolute(path))) {
  352. AtomicReference<Boolean> atEnd = new AtomicReference<>(
  353. Boolean.FALSE);
  354. while (!atEnd.get().booleanValue()) {
  355. List<SftpClient.DirEntry> chunk = ftp.readDir(handle,
  356. atEnd);
  357. if (chunk == null) {
  358. break;
  359. }
  360. for (SftpClient.DirEntry remote : chunk) {
  361. result.add(new DirEntry() {
  362. @Override
  363. public String getFilename() {
  364. return remote.getFilename();
  365. }
  366. @Override
  367. public long getModifiedTime() {
  368. return remote.getAttributes()
  369. .getModifyTime().toMillis();
  370. }
  371. @Override
  372. public boolean isDirectory() {
  373. return remote.getAttributes().isDirectory();
  374. }
  375. });
  376. }
  377. }
  378. }
  379. return result;
  380. });
  381. }
  382. @Override
  383. public void rmdir(String path) throws IOException {
  384. map(() -> {
  385. ftp.rmdir(absolute(path));
  386. return null;
  387. });
  388. }
  389. @Override
  390. public void mkdir(String path) throws IOException {
  391. map(() -> {
  392. ftp.mkdir(absolute(path));
  393. return null;
  394. });
  395. }
  396. @Override
  397. public InputStream get(String path) throws IOException {
  398. return map(() -> ftp.read(absolute(path)));
  399. }
  400. @Override
  401. public OutputStream put(String path) throws IOException {
  402. return map(() -> ftp.write(absolute(path)));
  403. }
  404. @Override
  405. public void rm(String path) throws IOException {
  406. map(() -> {
  407. ftp.remove(absolute(path));
  408. return null;
  409. });
  410. }
  411. @Override
  412. public void rename(String from, String to) throws IOException {
  413. map(() -> {
  414. String src = absolute(from);
  415. String dest = absolute(to);
  416. try {
  417. ftp.rename(src, dest, CopyMode.Atomic, CopyMode.Overwrite);
  418. } catch (UnsupportedOperationException e) {
  419. // Older server cannot do POSIX rename...
  420. if (!src.equals(dest)) {
  421. delete(dest);
  422. ftp.rename(src, dest);
  423. }
  424. }
  425. return null;
  426. });
  427. }
  428. }
  429. }