You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2023/11/23 03:52:05 UTC
(mina-sshd) branch master updated: GH-428/GH-392 SCP client fails silently when error signalled due to missing file or lacking permissions
This is an automated email from the ASF dual-hosted git repository.
lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new f908189c4 GH-428/GH-392 SCP client fails silently when error signalled due to missing file or lacking permissions
f908189c4 is described below
commit f908189c41a26f2b9b08ea62199a579771d04134
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Nov 10 11:09:07 2023 +0200
GH-428/GH-392 SCP client fails silently when error signalled due to missing file or lacking permissions
---
CHANGES.md | 9 +++
.../org/apache/sshd/cli/client/ScpCommandMain.java | 1 -
.../apache/sshd/scp/client/DefaultScpClient.java | 4 +-
.../java/org/apache/sshd/scp/common/ScpHelper.java | 78 ++++++++++++----------
.../sshd/scp/common/ScpTransferEventListener.java | 18 +++++
.../apache/sshd/scp/common/helpers/ScpAckInfo.java | 10 ++-
.../org/apache/sshd/scp/server/ScpCommand.java | 2 +-
.../java/org/apache/sshd/scp/server/ScpShell.java | 10 +--
.../java/org/apache/sshd/scp/client/ScpTest.java | 23 +++++++
9 files changed, 107 insertions(+), 48 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 180d1de8e..97f72b924 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -28,10 +28,19 @@
## Bug Fixes
+* [GH-428/GH-392](https://github.com/apache/mina-sshd/issues/428) SCP client fails silently when error signalled due to missing file or lacking permissions
+
## New Features
# Behavioral changes and enhancements
+## New `ScpTransferEventListener` callback method
+
+Following [GH-428/GH-392](https://github.com/apache/mina-sshd/issues/428) a new `handleReceiveCommandAckInfo` method has been added to enable users to inspect
+acknowledgements of a `receive` related command. The user is free to inspect the command that was attempted as well as the response code and decide how
+to handle it - including even throwing an exception if OK status (if this makes sense for whatever reason). The default implementation checks for ERROR code and throws
+an exception if so.
+
## Potential compatibility issues
### Server-side SFTP file handle encoding
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
index b089386e2..72102c8cf 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -322,7 +322,6 @@ public class ScpCommandMain extends SshClientCliSupport {
} finally {
session.close();
}
-
}
/* -------------------------------------------------------------------------------- */
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java
index ead682748..7a794f41e 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/client/DefaultScpClient.java
@@ -74,7 +74,7 @@ public class DefaultScpClient extends AbstractScpClient {
OutputStream invIn = channel.getInvertedIn()) {
// NOTE: we use a mock file system since we expect no invocations for it
ScpHelper helper = new ScpHelper(session, invOut, invIn, new MockFileSystem(remote), opener, listener);
- helper.receiveFileStream(local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
+ helper.receiveFileStream(cmd, local, ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE);
handleCommandExitStatus(cmd, channel);
} finally {
channel.close(false);
@@ -89,7 +89,7 @@ public class DefaultScpClient extends AbstractScpClient {
try (InputStream invOut = channel.getInvertedOut();
OutputStream invIn = channel.getInvertedIn()) {
ScpHelper helper = new ScpHelper(session, invOut, invIn, fs, opener, listener);
- helper.receive(local,
+ helper.receive(cmd, local,
options.contains(Option.Recursive),
options.contains(Option.TargetIsDirectory),
options.contains(Option.PreserveAttributes),
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
index 0973df780..f6f37b917 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpHelper.java
@@ -115,8 +115,8 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
return sessionInstance;
}
- public void receiveFileStream(OutputStream local, int bufferSize) throws IOException {
- receive((session, line, isDir, timestamp) -> {
+ public void receiveFileStream(String command, OutputStream local, int bufferSize) throws IOException {
+ receive(command, (session, line, isDir, timestamp) -> {
if (isDir) {
throw new StreamCorruptedException("Cannot download a directory into a file stream: " + line);
}
@@ -163,11 +163,11 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
});
}
- public void receive(Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize)
+ public void receive(String cmd, Path local, boolean recursive, boolean shouldBeDir, boolean preserve, int bufferSize)
throws IOException {
Path localPath = Objects.requireNonNull(local, "No local path").normalize().toAbsolutePath();
Path path = opener.resolveIncomingReceiveLocation(getSession(), localPath, recursive, shouldBeDir, preserve);
- receive((session, line, isDir, time) -> {
+ receive(cmd, (session, line, isDir, time) -> {
if (recursive && isDir) {
receiveDir(line, path, time, preserve, bufferSize);
} else {
@@ -179,60 +179,71 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
/**
* Reads command line(s) and invokes the handler until EOF or and "E" command is received
*
+ * @param cmd The receive command being attempted
* @param handler The {@link ScpReceiveLineHandler} to invoke when a command has been read
* @throws IOException If failed to read/write
*/
- protected void receive(ScpReceiveLineHandler handler) throws IOException {
+ protected void receive(String cmd, ScpReceiveLineHandler handler) throws IOException {
sendOk();
boolean debugEnabled = log.isDebugEnabled();
Session session = getSession();
for (ScpTimestampCommandDetails time = null;; debugEnabled = log.isDebugEnabled()) {
- String line;
- boolean isDir = false;
+ ScpAckInfo ackInfo = receiveNextCmd();
+ if (ackInfo == null) {
+ return;
+ }
- int c = receiveNextCmd();
+ int c = ackInfo.getStatusCode();
+ String line = ackInfo.getLine();
+ // NOTE: we rely on the fact that an SCP command does not start with an ACK code
switch (c) {
- case -1:
- return;
+ case ScpAckInfo.OK:
+ if (debugEnabled) {
+ log.debug("receive({})[{}] ack={}", this, cmd, ackInfo);
+ }
+ listener.handleReceiveCommandAckInfo(session, cmd, ackInfo);
+ continue;
+ case ScpAckInfo.WARNING:
+ log.warn("receive({})[{}] ack={}", this, cmd, ackInfo);
+ listener.handleReceiveCommandAckInfo(session, cmd, ackInfo);
+ continue;
+ case ScpAckInfo.ERROR:
+ log.error("receive({})[{}] bad ack: {}", this, cmd, ackInfo);
+ listener.handleReceiveCommandAckInfo(session, cmd, ackInfo);
+ continue;
case ScpReceiveDirCommandDetails.COMMAND_NAME:
- line = ScpIoUtils.readLine(in, csIn);
- line = Character.toString((char) c) + line;
- isDir = true;
if (debugEnabled) {
log.debug("receive({}) - Received 'D' header: {}", this, line);
}
break;
case ScpReceiveFileCommandDetails.COMMAND_NAME:
- line = ScpIoUtils.readLine(in, csIn);
- line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'C' header: {}", this, line);
}
break;
case ScpTimestampCommandDetails.COMMAND_NAME:
- line = ScpIoUtils.readLine(in, csIn);
- line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'T' header: {}", this, line);
}
time = ScpTimestampCommandDetails.parse(line);
+ cmd = line; // next ACK might be for this command if recursive receive
sendOk();
continue;
case ScpDirEndCommandDetails.COMMAND_NAME:
- line = ScpIoUtils.readLine(in, csIn);
- line = Character.toString((char) c) + line;
if (debugEnabled) {
log.debug("receive({}) - Received 'E' header: {}", this, line);
}
sendOk();
return;
default:
- // a real ack that has been acted upon already
- continue;
+ log.error("receive({}) - Unsupported command: {}", this, line);
+ throw new ScpException("Unsupported command: " + line, ScpAckInfo.ERROR);
}
try {
+ boolean isDir = c == ScpReceiveDirCommandDetails.COMMAND_NAME;
+ cmd = line; // next ACK might be for this command if recursive receive
handler.process(session, line, isDir, time);
} finally {
time = null;
@@ -240,25 +251,20 @@ public class ScpHelper extends AbstractLoggingBean implements SessionHolder<Sess
}
}
- // NOTE: we rely on the fact that an SCP command does not start with an ACK code
- protected int receiveNextCmd() throws IOException {
+ protected ScpAckInfo receiveNextCmd() throws IOException {
int c = in.read();
if (c == -1) {
- return c;
- }
-
- if (c == ScpAckInfo.OK) {
- return c;
- }
-
- if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) {
+ return null;
+ } else if (c == ScpAckInfo.OK) {
+ // OK status has no extra data
+ return ScpAckInfo.OK_ACK_INFO;
+ } else if ((c == ScpAckInfo.WARNING) || (c == ScpAckInfo.ERROR)) {
String line = ScpIoUtils.readLine(in, csIn, true);
- if (log.isDebugEnabled()) {
- log.debug("receiveNextCmd - ACK={}", new ScpAckInfo(c, line));
- }
+ return new ScpAckInfo(c, line);
+ } else {
+ String line = ScpIoUtils.readLine(in, csIn, false);
+ return new ScpAckInfo(c, Character.toString((char) c) + line);
}
-
- return c;
}
public void receiveDir(String header, Path local, ScpTimestampCommandDetails time, boolean preserve, int bufferSize)
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTransferEventListener.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTransferEventListener.java
index 2cf9996ae..25a7fbdb2 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTransferEventListener.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/ScpTransferEventListener.java
@@ -96,6 +96,24 @@ public interface ScpTransferEventListener extends SshdEventListener {
// ignored
}
+ /**
+ * Called after a receive related command has bee acknowledged by the peer
+ *
+ * @param session The client/server {@link Session} through which the transfer is being executed
+ * @param command The <U>raw</U> command that was attempted
+ * @param ackInfo The {@link ScpAckInfo} received after command execution - including if {@link ScpAckInfo#OK
+ * OK}. By default it throws an {@link ScpException} if {@link ScpAckInfo#ERROR ERROR} status
+ * code, but the user is free to override this behavior (see
+ * <A HREF="https://github.com/apache/mina-sshd/issues/428">ScpClient download fails silently
+ * when the remote files does not exist </A>) - including throwing an exception if OK status...
+ * @throws IOException If bad acknowledgment
+ */
+ default void handleReceiveCommandAckInfo(
+ Session session, String command, ScpAckInfo ackInfo)
+ throws IOException {
+ ackInfo.validateCommandStatusCode(command, "receive");
+ }
+
/**
* @param session The client/server {@link Session} through which the transfer is being executed
* @param op The {@link FileOperation}
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java
index 9b0b986a1..2451094ce 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/common/helpers/ScpAckInfo.java
@@ -38,6 +38,8 @@ public class ScpAckInfo {
public static final int WARNING = 1;
public static final int ERROR = 2;
+ public static final ScpAckInfo OK_ACK_INFO = new ScpAckInfo(OK, null);
+
private final int statusCode;
private final String line;
@@ -77,10 +79,12 @@ public class ScpAckInfo {
int code = getStatusCode();
String l = getLine();
// OK code has no line
- if ((code == OK) || GenericUtils.isEmpty(l)) {
+ if (code == OK) {
return Integer.toString(code);
+ } else if ((code == WARNING) || (code == ERROR)) {
+ return GenericUtils.isEmpty(l) ? Integer.toString(code) : code + ": " + l;
} else {
- return code + ": " + l;
+ return l;
}
}
@@ -94,7 +98,7 @@ public class ScpAckInfo {
}
if (statusCode == OK) {
- return new ScpAckInfo(statusCode); // OK status has no extra data
+ return OK_ACK_INFO; // OK status has no extra data
}
String line = ScpIoUtils.readLine(in, cs);
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java
index 41fc6b2d6..b7f8847b6 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpCommand.java
@@ -176,7 +176,7 @@ public class ScpCommand extends AbstractFileSystemCommand implements ServerChann
session, getInputStream(), getOutputStream(), fileSystem, opener, listener);
try {
if (optT) {
- helper.receive(helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
+ helper.receive(command, helper.resolveLocalPath(path), optR, optD, optP, receiveBufferSize);
} else if (optF) {
helper.send(Collections.singletonList(path), optR, optP, sendBufferSize);
} else {
diff --git a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
index 316410d50..98ead95bf 100644
--- a/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
+++ b/sshd-scp/src/main/java/org/apache/sshd/scp/server/ScpShell.java
@@ -272,7 +272,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
ls(argv);
break;
case "scp":
- scp(argv);
+ scp(command, argv);
break;
case "groups":
variables.put(STATUS, 0);
@@ -414,7 +414,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
variables.put(STATUS, (varValue == null) ? 1 : 0);
}
- protected void scp(String[] argv) throws Exception {
+ protected void scp(String command, String[] argv) throws Exception {
boolean optR = false;
boolean optT = false;
boolean optF = false;
@@ -471,11 +471,11 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
return;
}
- doScp(path, optR, optT, optF, optD, optP);
+ doScp(command, path, optR, optT, optF, optD, optP);
}
protected void doScp(
- String path, boolean optR, boolean optT, boolean optF, boolean optD, boolean optP)
+ String command, String path, boolean optR, boolean optT, boolean optF, boolean optD, boolean optP)
throws Exception {
try {
ChannelSession channel = getServerChannelSession();
@@ -484,7 +484,7 @@ public class ScpShell extends AbstractFileSystemCommand implements ServerChannel
fileSystem, opener, listener);
Path localPath = currentDir.resolve(path);
if (optT) {
- helper.receive(localPath, optR, optD, optP, receiveBufferSize);
+ helper.receive(command, localPath, optR, optD, optP, receiveBufferSize);
} else {
helper.send(Collections.singletonList(localPath.toString()), optR, optP, sendBufferSize);
}
diff --git a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java
index 42e274b9a..dce02f74f 100644
--- a/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java
+++ b/sshd-scp/src/test/java/org/apache/sshd/scp/client/ScpTest.java
@@ -823,6 +823,29 @@ public class ScpTest extends AbstractScpTestSupport {
}
}
+ @Test // see GH-428
+ public void testMissingRemoteFile() throws Exception {
+ Path targetPath = detectTargetFolder();
+ Path scpRoot = CommonTestSupportUtils.resolve(targetPath,
+ ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+ Path localDir = assertHierarchyTargetFolderExists(scpRoot.resolve("local"));
+ Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+ Path missingLocal = localDir.resolve("missing.txt");
+ Path missingRemote = remoteDir.resolve(missingLocal.getFileName());
+ Files.deleteIfExists(missingLocal);
+ Files.deleteIfExists(missingRemote);
+ assertFalse("Remote file not deleted", Files.exists(missingRemote));
+
+ String remotePath = CommonTestSupportUtils.resolveRelativeRemotePath(targetPath.getParent(), missingRemote);
+ try (CloseableScpClient scp = createCloseableScpClient()) {
+ scp.download(remotePath, missingLocal.toString());
+ fail("Unexpected success to copy non-existent remote file");
+ } catch (ScpException e) {
+ assertEquals("Mismatched SCP failure code", ScpAckInfo.ERROR, e.getExitStatus().intValue());
+ }
+
+ }
+
@Test
public void testJschScp() throws Exception {
com.jcraft.jsch.Session session = getJschSession();