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 &quot;E&quot; 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();