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

Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
Client-side protocol V2 support for fetching Make all transports request protocol V2 when fetching. Depending on the transport, set the GIT_PROTOCOL environment variable (file and ssh), pass the Git-Protocol header (http), or set the hidden "\0version=2\0" (git anon). We'll fall back to V0 if the server doesn't reply with a version 2 answer. A user can control which protocol the client requests via the git config protocol.version; if not set, JGit requests protocol V2 for fetching. Pushing always uses protocol V0 still. In the API, there is only a new Transport.openFetch() version that takes a collection of RefSpecs plus additional patterns to construct the Ref prefixes for the "ls-refs" command in protocol V2. If none are given, the server will still advertise all refs, even in protocol V2. BasePackConnection.readAdvertisedRefs() handles falling back to protocol V0. It newly returns true if V0 was used and the advertised refs were read, and false if V2 is used and an explicit "ls-refs" is needed. (This can't be done transparently inside readAdvertisedRefs() because a "stateless RPC" transport like TransportHttp may need to open a new connection for writing.) BasePackFetchConnection implements the changes needed for the protocol V2 "fetch" command (simplified ACK handling, delimiters, section headers). In TransportHttp, change readSmartHeaders() to also recognize the "version 2" packet line as a valid smart server indication. Adapt tests, and run all the HTTP tests not only with both HTTP connection factories (JDK and Apache HttpClient) but also with both protocol V0 and V2. Do the same for the SSH transport tests. Bug: 553083 Change-Id: Ice9866aa78020f5ca8f397cde84dc224bf5d41b4 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch> Signed-off-by: Matthias Sohn <matthias.sohn@sap.com>
3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. /*
  2. * Copyright (C) 2018, 2020 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 static org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE;
  13. import java.io.Closeable;
  14. import java.io.IOException;
  15. import java.io.InputStream;
  16. import java.io.OutputStream;
  17. import java.net.URISyntaxException;
  18. import java.time.Duration;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.EnumSet;
  23. import java.util.LinkedList;
  24. import java.util.List;
  25. import java.util.Map;
  26. import java.util.concurrent.CopyOnWriteArrayList;
  27. import java.util.concurrent.TimeUnit;
  28. import java.util.concurrent.atomic.AtomicReference;
  29. import java.util.function.Supplier;
  30. import java.util.regex.Pattern;
  31. import org.apache.sshd.client.SshClient;
  32. import org.apache.sshd.client.channel.ChannelExec;
  33. import org.apache.sshd.client.channel.ClientChannelEvent;
  34. import org.apache.sshd.client.config.hosts.HostConfigEntry;
  35. import org.apache.sshd.client.future.ConnectFuture;
  36. import org.apache.sshd.client.session.ClientSession;
  37. import org.apache.sshd.client.session.forward.PortForwardingTracker;
  38. import org.apache.sshd.client.subsystem.sftp.SftpClient;
  39. import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
  40. import org.apache.sshd.client.subsystem.sftp.SftpClient.CopyMode;
  41. import org.apache.sshd.client.subsystem.sftp.SftpClientFactory;
  42. import org.apache.sshd.common.AttributeRepository;
  43. import org.apache.sshd.common.SshException;
  44. import org.apache.sshd.common.future.CloseFuture;
  45. import org.apache.sshd.common.future.SshFutureListener;
  46. import org.apache.sshd.common.subsystem.sftp.SftpException;
  47. import org.apache.sshd.common.util.io.IoUtils;
  48. import org.apache.sshd.common.util.net.SshdSocketAddress;
  49. import org.eclipse.jgit.annotations.NonNull;
  50. import org.eclipse.jgit.errors.TransportException;
  51. import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
  52. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  53. import org.eclipse.jgit.transport.FtpChannel;
  54. import org.eclipse.jgit.transport.RemoteSession2;
  55. import org.eclipse.jgit.transport.SshConstants;
  56. import org.eclipse.jgit.transport.URIish;
  57. import org.eclipse.jgit.util.StringUtils;
  58. import org.slf4j.Logger;
  59. import org.slf4j.LoggerFactory;
  60. /**
  61. * An implementation of {@link org.eclipse.jgit.transport.RemoteSession
  62. * RemoteSession} based on Apache MINA sshd.
  63. *
  64. * @since 5.2
  65. */
  66. public class SshdSession implements RemoteSession2 {
  67. private static final Logger LOG = LoggerFactory
  68. .getLogger(SshdSession.class);
  69. private static final Pattern SHORT_SSH_FORMAT = Pattern
  70. .compile("[-\\w.]+(?:@[-\\w.]+)?(?::\\d+)?"); //$NON-NLS-1$
  71. private static final int MAX_DEPTH = 10;
  72. private final CopyOnWriteArrayList<SessionCloseListener> listeners = new CopyOnWriteArrayList<>();
  73. private final URIish uri;
  74. private SshClient client;
  75. private ClientSession session;
  76. SshdSession(URIish uri, Supplier<SshClient> clientFactory) {
  77. this.uri = uri;
  78. this.client = clientFactory.get();
  79. }
  80. void connect(Duration timeout) throws IOException {
  81. if (!client.isStarted()) {
  82. client.start();
  83. }
  84. try {
  85. session = connect(uri, Collections.emptyList(),
  86. future -> notifyCloseListeners(), timeout, MAX_DEPTH);
  87. } catch (IOException e) {
  88. disconnect(e);
  89. throw e;
  90. }
  91. }
  92. private ClientSession connect(URIish target, List<URIish> jumps,
  93. SshFutureListener<CloseFuture> listener, Duration timeout,
  94. int depth) throws IOException {
  95. if (--depth < 0) {
  96. throw new IOException(
  97. format(SshdText.get().proxyJumpAbort, target));
  98. }
  99. HostConfigEntry hostConfig = getHostConfig(target.getUser(),
  100. target.getHost(), target.getPort());
  101. String host = hostConfig.getHostName();
  102. int port = hostConfig.getPort();
  103. List<URIish> hops = determineHops(jumps, hostConfig, target.getHost());
  104. ClientSession resultSession = null;
  105. ClientSession proxySession = null;
  106. PortForwardingTracker portForward = null;
  107. try {
  108. if (!hops.isEmpty()) {
  109. URIish hop = hops.remove(0);
  110. if (LOG.isDebugEnabled()) {
  111. LOG.debug("Connecting to jump host {}", hop); //$NON-NLS-1$
  112. }
  113. proxySession = connect(hop, hops, null, timeout, depth);
  114. }
  115. AttributeRepository context = null;
  116. if (proxySession != null) {
  117. SshdSocketAddress remoteAddress = new SshdSocketAddress(host,
  118. port);
  119. portForward = proxySession.createLocalPortForwardingTracker(
  120. SshdSocketAddress.LOCALHOST_ADDRESS, remoteAddress);
  121. // We must connect to the locally bound address, not the one
  122. // from the host config.
  123. context = AttributeRepository.ofKeyValuePair(
  124. JGitSshClient.LOCAL_FORWARD_ADDRESS,
  125. portForward.getBoundAddress());
  126. }
  127. resultSession = connect(hostConfig, context, timeout);
  128. if (proxySession != null) {
  129. final PortForwardingTracker tracker = portForward;
  130. final ClientSession pSession = proxySession;
  131. resultSession.addCloseFutureListener(future -> {
  132. IoUtils.closeQuietly(tracker);
  133. String sessionName = pSession.toString();
  134. try {
  135. pSession.close();
  136. } catch (IOException e) {
  137. LOG.error(format(
  138. SshdText.get().sshProxySessionCloseFailed,
  139. sessionName), e);
  140. }
  141. });
  142. portForward = null;
  143. proxySession = null;
  144. }
  145. if (listener != null) {
  146. resultSession.addCloseFutureListener(listener);
  147. }
  148. // Authentication timeout is by default 2 minutes.
  149. resultSession.auth().verify(resultSession.getAuthTimeout());
  150. return resultSession;
  151. } catch (IOException e) {
  152. close(portForward, e);
  153. close(proxySession, e);
  154. close(resultSession, e);
  155. if (e instanceof SshException && ((SshException) e)
  156. .getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) {
  157. // Ensure the user gets to know on which URI the authentication
  158. // was denied.
  159. throw new TransportException(target,
  160. format(SshdText.get().loginDenied, host,
  161. Integer.toString(port)),
  162. e);
  163. }
  164. throw e;
  165. }
  166. }
  167. private ClientSession connect(HostConfigEntry config,
  168. AttributeRepository context, Duration timeout)
  169. throws IOException {
  170. ConnectFuture connected = client.connect(config, context, null);
  171. long timeoutMillis = timeout.toMillis();
  172. if (timeoutMillis <= 0) {
  173. connected = connected.verify();
  174. } else {
  175. connected = connected.verify(timeoutMillis);
  176. }
  177. return connected.getSession();
  178. }
  179. private void close(Closeable toClose, Throwable error) {
  180. if (toClose != null) {
  181. try {
  182. toClose.close();
  183. } catch (IOException e) {
  184. error.addSuppressed(e);
  185. }
  186. }
  187. }
  188. private HostConfigEntry getHostConfig(String username, String host,
  189. int port) throws IOException {
  190. HostConfigEntry entry = client.getHostConfigEntryResolver()
  191. .resolveEffectiveHost(host, port, null, username, null);
  192. if (entry == null) {
  193. if (SshdSocketAddress.isIPv6Address(host)) {
  194. return new HostConfigEntry("", host, port, username); //$NON-NLS-1$
  195. }
  196. return new HostConfigEntry(host, host, port, username);
  197. }
  198. return entry;
  199. }
  200. private List<URIish> determineHops(List<URIish> currentHops,
  201. HostConfigEntry hostConfig, String host) throws IOException {
  202. if (currentHops.isEmpty()) {
  203. String jumpHosts = hostConfig.getProperty(SshConstants.PROXY_JUMP);
  204. if (!StringUtils.isEmptyOrNull(jumpHosts)) {
  205. try {
  206. return parseProxyJump(jumpHosts);
  207. } catch (URISyntaxException e) {
  208. throw new IOException(
  209. format(SshdText.get().configInvalidProxyJump, host,
  210. jumpHosts),
  211. e);
  212. }
  213. }
  214. }
  215. return currentHops;
  216. }
  217. private List<URIish> parseProxyJump(String proxyJump)
  218. throws URISyntaxException {
  219. String[] hops = proxyJump.split(","); //$NON-NLS-1$
  220. List<URIish> result = new LinkedList<>();
  221. for (String hop : hops) {
  222. // There shouldn't be any whitespace, but let's be lenient
  223. hop = hop.trim();
  224. if (SHORT_SSH_FORMAT.matcher(hop).matches()) {
  225. // URIish doesn't understand the short SSH format
  226. // user@host:port, only user@host:path
  227. hop = SshConstants.SSH_SCHEME + "://" + hop; //$NON-NLS-1$
  228. }
  229. URIish to = new URIish(hop);
  230. if (!SshConstants.SSH_SCHEME.equalsIgnoreCase(to.getScheme())) {
  231. throw new URISyntaxException(hop,
  232. SshdText.get().configProxyJumpNotSsh);
  233. } else if (!StringUtils.isEmptyOrNull(to.getPath())) {
  234. throw new URISyntaxException(hop,
  235. SshdText.get().configProxyJumpWithPath);
  236. }
  237. result.add(to);
  238. }
  239. return result;
  240. }
  241. /**
  242. * Adds a {@link SessionCloseListener} to this session. Has no effect if the
  243. * given {@code listener} is already registered with this session.
  244. *
  245. * @param listener
  246. * to add
  247. */
  248. public void addCloseListener(@NonNull SessionCloseListener listener) {
  249. listeners.addIfAbsent(listener);
  250. }
  251. /**
  252. * Removes the given {@code listener}; has no effect if the listener is not
  253. * currently registered with this session.
  254. *
  255. * @param listener
  256. * to remove
  257. */
  258. public void removeCloseListener(@NonNull SessionCloseListener listener) {
  259. listeners.remove(listener);
  260. }
  261. private void notifyCloseListeners() {
  262. for (SessionCloseListener l : listeners) {
  263. try {
  264. l.sessionClosed(this);
  265. } catch (RuntimeException e) {
  266. LOG.warn(SshdText.get().closeListenerFailed, e);
  267. }
  268. }
  269. }
  270. @Override
  271. public Process exec(String commandName, int timeout) throws IOException {
  272. return exec(commandName, Collections.emptyMap(), timeout);
  273. }
  274. @Override
  275. public Process exec(String commandName, Map<String, String> environment,
  276. int timeout) throws IOException {
  277. @SuppressWarnings("resource")
  278. ChannelExec exec = session.createExecChannel(commandName, null,
  279. environment);
  280. if (timeout <= 0) {
  281. try {
  282. exec.open().verify();
  283. } catch (IOException | RuntimeException e) {
  284. exec.close(true);
  285. throw e;
  286. }
  287. } else {
  288. try {
  289. exec.open().verify(TimeUnit.SECONDS.toMillis(timeout));
  290. } catch (IOException | RuntimeException e) {
  291. exec.close(true);
  292. throw new IOException(format(SshdText.get().sshCommandTimeout,
  293. commandName, Integer.valueOf(timeout)), e);
  294. }
  295. }
  296. return new SshdExecProcess(exec, commandName);
  297. }
  298. /**
  299. * Obtain an {@link FtpChannel} to perform SFTP operations in this
  300. * {@link SshdSession}.
  301. */
  302. @Override
  303. @NonNull
  304. public FtpChannel getFtpChannel() {
  305. return new SshdFtpChannel();
  306. }
  307. @Override
  308. public void disconnect() {
  309. disconnect(null);
  310. }
  311. private void disconnect(Throwable reason) {
  312. try {
  313. if (session != null) {
  314. session.close();
  315. session = null;
  316. }
  317. } catch (IOException e) {
  318. if (reason != null) {
  319. reason.addSuppressed(e);
  320. } else {
  321. LOG.error(SshdText.get().sessionCloseFailed, e);
  322. }
  323. } finally {
  324. client.stop();
  325. client = null;
  326. }
  327. }
  328. private static class SshdExecProcess extends Process {
  329. private final ChannelExec channel;
  330. private final String commandName;
  331. public SshdExecProcess(ChannelExec channel, String commandName) {
  332. this.channel = channel;
  333. this.commandName = commandName;
  334. }
  335. @Override
  336. public OutputStream getOutputStream() {
  337. return channel.getInvertedIn();
  338. }
  339. @Override
  340. public InputStream getInputStream() {
  341. return channel.getInvertedOut();
  342. }
  343. @Override
  344. public InputStream getErrorStream() {
  345. return channel.getInvertedErr();
  346. }
  347. @Override
  348. public int waitFor() throws InterruptedException {
  349. if (waitFor(-1L, TimeUnit.MILLISECONDS)) {
  350. return exitValue();
  351. }
  352. return -1;
  353. }
  354. @Override
  355. public boolean waitFor(long timeout, TimeUnit unit)
  356. throws InterruptedException {
  357. long millis = timeout >= 0 ? unit.toMillis(timeout) : -1L;
  358. return channel
  359. .waitFor(EnumSet.of(ClientChannelEvent.CLOSED), millis)
  360. .contains(ClientChannelEvent.CLOSED);
  361. }
  362. @Override
  363. public int exitValue() {
  364. Integer exitCode = channel.getExitStatus();
  365. if (exitCode == null) {
  366. throw new IllegalThreadStateException(
  367. format(SshdText.get().sshProcessStillRunning,
  368. commandName));
  369. }
  370. return exitCode.intValue();
  371. }
  372. @Override
  373. public void destroy() {
  374. if (channel.isOpen()) {
  375. channel.close(false);
  376. }
  377. }
  378. }
  379. /**
  380. * Helper interface like {@link Supplier}, but possibly raising an
  381. * {@link IOException}.
  382. *
  383. * @param <T>
  384. * return type
  385. */
  386. @FunctionalInterface
  387. private interface FtpOperation<T> {
  388. T call() throws IOException;
  389. }
  390. private class SshdFtpChannel implements FtpChannel {
  391. private SftpClient ftp;
  392. /** Current working directory. */
  393. private String cwd = ""; //$NON-NLS-1$
  394. @Override
  395. public void connect(int timeout, TimeUnit unit) throws IOException {
  396. if (timeout <= 0) {
  397. session.getProperties().put(
  398. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  399. Long.valueOf(Long.MAX_VALUE));
  400. } else {
  401. session.getProperties().put(
  402. SftpClient.SFTP_CHANNEL_OPEN_TIMEOUT,
  403. Long.valueOf(unit.toMillis(timeout)));
  404. }
  405. ftp = SftpClientFactory.instance().createSftpClient(session);
  406. try {
  407. cd(cwd);
  408. } catch (IOException e) {
  409. ftp.close();
  410. }
  411. }
  412. @Override
  413. public void disconnect() {
  414. try {
  415. ftp.close();
  416. } catch (IOException e) {
  417. LOG.error(SshdText.get().ftpCloseFailed, e);
  418. }
  419. }
  420. @Override
  421. public boolean isConnected() {
  422. return session.isAuthenticated() && ftp.isOpen();
  423. }
  424. private String absolute(String path) {
  425. if (path.isEmpty()) {
  426. return cwd;
  427. }
  428. // Note: there is no path injection vulnerability here. If
  429. // path has too many ".." components, we rely on the server
  430. // catching it and returning an error.
  431. if (path.charAt(0) != '/') {
  432. if (cwd.charAt(cwd.length() - 1) == '/') {
  433. return cwd + path;
  434. }
  435. return cwd + '/' + path;
  436. }
  437. return path;
  438. }
  439. private <T> T map(FtpOperation<T> op) throws IOException {
  440. try {
  441. return op.call();
  442. } catch (IOException e) {
  443. if (e instanceof SftpException) {
  444. throw new FtpChannel.FtpException(e.getLocalizedMessage(),
  445. ((SftpException) e).getStatus(), e);
  446. }
  447. throw e;
  448. }
  449. }
  450. @Override
  451. public void cd(String path) throws IOException {
  452. cwd = map(() -> ftp.canonicalPath(absolute(path)));
  453. if (cwd.isEmpty()) {
  454. cwd += '/';
  455. }
  456. }
  457. @Override
  458. public String pwd() throws IOException {
  459. return cwd;
  460. }
  461. @Override
  462. public Collection<DirEntry> ls(String path) throws IOException {
  463. return map(() -> {
  464. List<DirEntry> result = new ArrayList<>();
  465. try (CloseableHandle handle = ftp.openDir(absolute(path))) {
  466. AtomicReference<Boolean> atEnd = new AtomicReference<>(
  467. Boolean.FALSE);
  468. while (!atEnd.get().booleanValue()) {
  469. List<SftpClient.DirEntry> chunk = ftp.readDir(handle,
  470. atEnd);
  471. if (chunk == null) {
  472. break;
  473. }
  474. for (SftpClient.DirEntry remote : chunk) {
  475. result.add(new DirEntry() {
  476. @Override
  477. public String getFilename() {
  478. return remote.getFilename();
  479. }
  480. @Override
  481. public long getModifiedTime() {
  482. return remote.getAttributes()
  483. .getModifyTime().toMillis();
  484. }
  485. @Override
  486. public boolean isDirectory() {
  487. return remote.getAttributes().isDirectory();
  488. }
  489. });
  490. }
  491. }
  492. }
  493. return result;
  494. });
  495. }
  496. @Override
  497. public void rmdir(String path) throws IOException {
  498. map(() -> {
  499. ftp.rmdir(absolute(path));
  500. return null;
  501. });
  502. }
  503. @Override
  504. public void mkdir(String path) throws IOException {
  505. map(() -> {
  506. ftp.mkdir(absolute(path));
  507. return null;
  508. });
  509. }
  510. @Override
  511. public InputStream get(String path) throws IOException {
  512. return map(() -> ftp.read(absolute(path)));
  513. }
  514. @Override
  515. public OutputStream put(String path) throws IOException {
  516. return map(() -> ftp.write(absolute(path)));
  517. }
  518. @Override
  519. public void rm(String path) throws IOException {
  520. map(() -> {
  521. ftp.remove(absolute(path));
  522. return null;
  523. });
  524. }
  525. @Override
  526. public void rename(String from, String to) throws IOException {
  527. map(() -> {
  528. String src = absolute(from);
  529. String dest = absolute(to);
  530. try {
  531. ftp.rename(src, dest, CopyMode.Atomic, CopyMode.Overwrite);
  532. } catch (UnsupportedOperationException e) {
  533. // Older server cannot do POSIX rename...
  534. if (!src.equals(dest)) {
  535. delete(dest);
  536. ftp.rename(src, dest);
  537. }
  538. }
  539. return null;
  540. });
  541. }
  542. }
  543. }