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

commit 3e382d75e8b629afe5a6297b6e43818a341bd78d
Author: Lyor Goldstein <lgoldst...@apache.org>
AuthorDate: Fri Nov 19 08:57:41 2021 +0200

    [SSHD-1226] Added SFTP server-side capability to provide file/folder custom 
extension attributes
---
 CHANGES.md                                         |   1 +
 docs/sftp.md                                       |  57 +++++++-
 .../sftp/server/AbstractSftpSubsystemHelper.java   |  20 ++-
 .../sshd/sftp/server/SftpFileSystemAccessor.java   |  34 +++++
 .../sftp/client/extensions/SftpExtensionsTest.java | 147 +++++++++++++++++++++
 .../extensions/UnsupportedExtensionTest.java       |  62 ---------
 6 files changed, 250 insertions(+), 71 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 7cdd68d..1abc7d5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -44,3 +44,4 @@
 * [SSHD-1219](https://issues.apache.org/jira/browse/SSHD-1219) Obtaining 
rsa-sha2-256 or rsa-sha2-512 signatures from an SSH agent
 * [SSHD-1220](https://issues.apache.org/jira/browse/SSHD-1220) Reduce number 
of L(STAT) calls made by the SftpFileSystem
 * [SSHD-1221](https://issues.apache.org/jira/browse/SSHD-1221) Support key 
constraints when adding a key to an SSH agent
+* [SSHD-1226](https://issues.apache.org/jira/browse/SSHD-1226) Added SFTP 
server-side capability to provide file/folder custom extension attributes
diff --git a/docs/sftp.md b/docs/sftp.md
index b7c6362..e76ddf3 100644
--- a/docs/sftp.md
+++ b/docs/sftp.md
@@ -601,7 +601,11 @@ Collection<ScanDirEntry> matches = ds.scan(client);
 
 ```
 
-## Extensions
+## Extensions & custom file/folder attributes
+
+Extending the SFTP protocol and/or the reported file/folder attributes
+
+### SFTP protocol extensions
 
 Both client and server support several of the SFTP extensions specified in 
various drafts:
 
@@ -685,7 +689,7 @@ try (ClientSession session = client.connect(username, host, 
port).verify(timeout
 
 ```
 
-### Contributing support for a new extension
+#### Contributing support for a new SFTP extension
 
 * Add the code to handle the new extension in 
`AbstractSftpSubsystemHelper#executeExtendedCommand`
 
@@ -698,6 +702,55 @@ for sending and receiving the newly added extension.
 
 See how other extensions are implemented and follow their example
 
+### Providing/processing file/folder custom attributes
+
+According to [SFTP - File 
Attributes](https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-5)
 it is possible to provide
+custom attributes for a referenced file/folder. The client-side code supports 
this via the `Attributes#getExtensions` call. On the server-side
+one needs to provide a custom `SftpFileSystemAccessor` that overrides the 
`resolveReportedFileAttributes` method (which by default
+simply returns the original attrbiutes as-is. A similar hook method has been 
provided in case a client attempts to apply custom attributes - simply
+need to provide a implementation that obverrides 
`applyExtensionFileAttributes` (which by default ignores the attributes).
+
+```java
+class MyCustomSftpFileSystemAccessor implements SftpFileSystemAccessor {
+    @Override
+    public NavigableMap<String, Object> resolveReportedFileAttributes(
+            SftpSubsystemProxy subsystem, Path file, int flags, 
NavigableMap<String, Object> attrs, LinkOption... options)
+                throws IOException {
+        Map<String, Object> extra = (Map<String, Object>) 
attrs.get(IoUtils.EXTENDED_VIEW_ATTR);
+        if (extra == null) {
+            extra = new HashMap<>();
+            attrs.put(IoUtils.EXTENDED_VIEW_ATTR, extra)
+        }
+        extra.put("custom1", ...some string...);
+        extra.put("custom", ...some byte[]...)
+    }
+    
+    @Override
+    public void applyExtensionFileAttributes(
+            SftpSubsystemProxy subsystem, Path file, Map<String, byte[]> 
extensions, LinkOption... options)
+                throws IOException {
+        if (MapEntryUtils.isNotEmpty(extensions)) {
+            ...process the extensions...
+        }
+    }
+}
+
+SftpSubsystemFactory factory = new SftpSubsystemFactory.Builder()
+    .withFileSystemAccessor(new MyCustomSftpFileSystemAccessor())
+    .build();
+
+SshdServer sshd = ...setup...
+sshd.setSubsystemFactories(Collections.singletonList(factory));
+```
+
+
+
+**Note:**
+
+* The code assumes that the extension name is a **string** - the draft 
specification actually allows an array of bytes as well, but we chose 
simplicity.
+
+* The value can be either a string or an array of bytes. If the value is 
neither (e.g., an integer) then the value's *toString()* will be used.
+
 ## References
 
 * [SFTP drafts for the various 
versions](https://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/)
diff --git 
a/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java
 
b/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java
index 6687c12..812f24b 100644
--- 
a/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java
+++ 
b/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/AbstractSftpSubsystemHelper.java
@@ -2418,7 +2418,10 @@ public abstract class AbstractSftpSubsystemHelper
      */
     protected NavigableMap<String, Object> getAttributes(Path path, int flags, 
LinkOption... options)
             throws IOException {
-        return SftpPathImpl.withAttributeCache(path, file -> 
resolveReportedFileAttributes(file, flags, options));
+        NavigableMap<String, Object> attrs
+                = SftpPathImpl.withAttributeCache(path, file -> 
resolveReportedFileAttributes(file, flags, options));
+        SftpFileSystemAccessor accessor = getFileSystemAccessor();
+        return accessor.resolveReportedFileAttributes(this, path, flags, 
attrs, options);
     }
 
     protected NavigableMap<String, Object> resolveReportedFileAttributes(Path 
file, int flags, LinkOption... options)
@@ -2780,10 +2783,6 @@ public abstract class AbstractSftpSubsystemHelper
     protected void setFileExtensions(
             Path file, Map<String, byte[]> extensions, LinkOption... options)
             throws IOException {
-        if (MapEntryUtils.isEmpty(extensions)) {
-            return;
-        }
-
         /*
          * According to v3,4,5:
          *
@@ -2794,10 +2793,17 @@ public abstract class AbstractSftpSubsystemHelper
          */
         int version = getVersion();
         if (version < SftpConstants.SFTP_V6) {
-            if (log.isDebugEnabled()) {
-                log.debug("setFileExtensions({})[{}]: {}", getServerSession(), 
file, extensions);
+            if (MapEntryUtils.isNotEmpty(extensions) && log.isDebugEnabled()) {
+                log.debug("setFileExtensions({})[{}]: {}", getServerSession(), 
file, extensions.keySet());
             }
+
+            SftpFileSystemAccessor accessor = getFileSystemAccessor();
+            accessor.applyExtensionFileAttributes(this, file, extensions, 
options);
         } else {
+            if (MapEntryUtils.isEmpty(extensions)) {
+                return;
+            }
+
             throw new UnsupportedOperationException("File extensions not 
supported");
         }
     }
diff --git 
a/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java
 
b/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java
index 8360312..6d7c18f 100644
--- 
a/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java
+++ 
b/sshd-sftp/src/main/java/org/apache/sshd/sftp/server/SftpFileSystemAccessor.java
@@ -134,6 +134,40 @@ public interface SftpFileSystemAccessor {
     }
 
     /**
+     * Invoked in order to allow intervention to the reported file attributes 
- e.g., add custom/extended properties
+     *
+     * @param  subsystem   The SFTP subsystem instance that manages the session
+     * @param  file        The referenced file
+     * @param  flags       A mask of the original required attributes
+     * @param  attrs       The default resolved attributes map
+     * @param  options     The {@link LinkOption}-s that were used to access 
the file's attributes
+     * @return             The updated attributes map
+     * @throws IOException If failed to resolve the attributes
+     * @see                <A 
HREF="https://issues.apache.org/jira/browse/SSHD-1226";>SSHD-1226</A>
+     */
+    default NavigableMap<String, Object> resolveReportedFileAttributes(
+            SftpSubsystemProxy subsystem, Path file, int flags,
+            NavigableMap<String, Object> attrs, LinkOption... options)
+            throws IOException {
+        return attrs;
+    }
+
+    /**
+     * Invoked in order to allow processing of custom file attributes
+     *
+     * @param  subsystem   The SFTP subsystem instance that manages the session
+     * @param  file        The referenced file
+     * @param  extensions  The received extensions - may be {@code null}/empty
+     * @param  options     The {@link LinkOption}-s that were used to access 
the file's standard attributes
+     * @throws IOException If failed to apply the attributes
+     */
+    default void applyExtensionFileAttributes(
+            SftpSubsystemProxy subsystem, Path file, Map<String, byte[]> 
extensions, LinkOption... options)
+            throws IOException {
+        // ignored
+    }
+
+    /**
      * Invoked in order to encode the outgoing referenced file name/path
      *
      * @param  subsystem   The SFTP subsystem instance that manages the session
diff --git 
a/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/SftpExtensionsTest.java
 
b/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/SftpExtensionsTest.java
new file mode 100644
index 0000000..dbdcdf8
--- /dev/null
+++ 
b/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/SftpExtensionsTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.sftp.client.extensions;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.MapEntryUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.server.subsystem.SubsystemFactory;
+import org.apache.sshd.sftp.client.AbstractSftpClientTestSupport;
+import org.apache.sshd.sftp.client.RawSftpClient;
+import org.apache.sshd.sftp.client.SftpClient;
+import org.apache.sshd.sftp.client.SftpClient.Attributes;
+import org.apache.sshd.sftp.common.SftpConstants;
+import org.apache.sshd.sftp.server.SftpFileSystemAccessor;
+import org.apache.sshd.sftp.server.SftpSubsystemFactory;
+import org.apache.sshd.sftp.server.SftpSubsystemProxy;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.testcontainers.shaded.com.google.common.base.Objects;
+
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SftpExtensionsTest extends AbstractSftpClientTestSupport {
+    public SftpExtensionsTest() throws IOException {
+        super();
+    }
+
+    @Test // see SSHD-890
+    public void testUnsupportedExtension() throws IOException {
+        try (SftpClient sftpClient = createSingleSessionClient()) {
+            RawSftpClient sftp = assertObjectInstanceOf("Not a raw SFTP 
client", RawSftpClient.class, sftpClient);
+
+            String opcode = getCurrentTestName();
+            Buffer buffer = new ByteArrayBuffer(Integer.BYTES + 
GenericUtils.length(opcode) + Byte.SIZE, false);
+            buffer.putString(opcode);
+
+            int cmd = sftp.send(SftpConstants.SSH_FXP_EXTENDED, buffer);
+            Buffer responseBuffer = sftp.receive(cmd);
+
+            responseBuffer.getInt(); // Ignoring length
+            int type = responseBuffer.getUByte();
+            responseBuffer.getInt(); // Ignoring message ID
+            int substatus = responseBuffer.getInt();
+
+            assertEquals("Type is not STATUS", SftpConstants.SSH_FXP_STATUS, 
type);
+            assertEquals("Sub-Type is not UNSUPPORTED", 
SftpConstants.SSH_FX_OP_UNSUPPORTED, substatus);
+        }
+    }
+
+    @Test   // see SSHD-1266
+    public void testCustomFileExtensionAttributes() throws IOException {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path localFile = CommonTestSupportUtils.resolve(
+                targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, 
getClass().getSimpleName(), getCurrentTestName());
+        Files.createDirectories(localFile.getParent());
+        Files.writeString(localFile, getClass().getName() + "#" + 
getCurrentTestName() + "@" + new Date(),
+                StandardCharsets.UTF_8);
+
+        List<? extends SubsystemFactory> factories = 
sshd.getSubsystemFactories();
+        assertEquals("Mismatched subsystem factories count", 1, 
GenericUtils.size(factories));
+
+        SubsystemFactory f = factories.get(0);
+        assertObjectInstanceOf("Not an SFTP subsystem factory", 
SftpSubsystemFactory.class, f);
+
+        Map<String, String> expected = Collections.unmodifiableMap(
+                MapEntryUtils.MapBuilder.<String, String> builder()
+                        .put("test", getCurrentTestName())
+                        .put("class", getClass().getSimpleName())
+                        .put("package", getClass().getPackage().getName())
+                        .build());
+
+        SftpSubsystemFactory factory = (SftpSubsystemFactory) f;
+        SftpFileSystemAccessor accessor = factory.getFileSystemAccessor();
+        Attributes attrs;
+        try {
+            factory.setFileSystemAccessor(new SftpFileSystemAccessor() {
+                @Override
+                public NavigableMap<String, Object> 
resolveReportedFileAttributes(
+                        SftpSubsystemProxy subsystem,
+                        Path file, int flags, NavigableMap<String, Object> 
attrs, LinkOption... options)
+                        throws IOException {
+                    if (Objects.equal(file, localFile)) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> extra = (Map<String, Object>) 
attrs.get(IoUtils.EXTENDED_VIEW_ATTR);
+                        if (MapEntryUtils.isEmpty(extra)) {
+                            attrs.put(IoUtils.EXTENDED_VIEW_ATTR, expected);
+                        } else {
+                            extra.putAll(expected);
+                        }
+                    }
+                    return attrs;
+                }
+            });
+
+            try (SftpClient sftp = createSingleSessionClient()) {
+                attrs = 
sftp.stat(CommonTestSupportUtils.resolveRelativeRemotePath(parentPath, 
localFile));
+            }
+
+        } finally {
+            factory.setFileSystemAccessor(accessor);    // restore original 
value
+        }
+
+        Map<String, byte[]> extsMap = attrs.getExtensions();
+        assertTrue("No extended attributes provided", 
MapEntryUtils.isNotEmpty(extsMap));
+
+        Map<String, String> actual = extsMap.entrySet()
+                .stream()
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey, e -> new String(e.getValue(), 
StandardCharsets.UTF_8),
+                        MapEntryUtils.throwingMerger(), () -> new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER)));
+        assertMapEquals(IoUtils.EXTENDED_VIEW_ATTR, expected, actual);
+    }
+}
diff --git 
a/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/UnsupportedExtensionTest.java
 
b/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/UnsupportedExtensionTest.java
deleted file mode 100644
index cea742a..0000000
--- 
a/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/extensions/UnsupportedExtensionTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.sshd.sftp.client.extensions;
-
-import java.io.IOException;
-
-import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.buffer.Buffer;
-import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.sftp.client.AbstractSftpClientTestSupport;
-import org.apache.sshd.sftp.client.RawSftpClient;
-import org.apache.sshd.sftp.client.SftpClient;
-import org.apache.sshd.sftp.common.SftpConstants;
-import org.junit.FixMethodOrder;
-import org.junit.Test;
-import org.junit.runners.MethodSorters;
-
-@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-public class UnsupportedExtensionTest extends AbstractSftpClientTestSupport {
-    public UnsupportedExtensionTest() throws IOException {
-        super();
-    }
-
-    @Test // see SSHD-890
-    public void testUnsupportedExtension() throws IOException {
-        try (SftpClient sftpClient = createSingleSessionClient()) {
-            RawSftpClient sftp = assertObjectInstanceOf("Not a raw SFTP 
client", RawSftpClient.class, sftpClient);
-
-            String opcode = getCurrentTestName();
-            Buffer buffer = new ByteArrayBuffer(Integer.BYTES + 
GenericUtils.length(opcode) + Byte.SIZE, false);
-            buffer.putString(opcode);
-
-            int cmd = sftp.send(SftpConstants.SSH_FXP_EXTENDED, buffer);
-            Buffer responseBuffer = sftp.receive(cmd);
-
-            responseBuffer.getInt(); // Ignoring length
-            int type = responseBuffer.getUByte();
-            responseBuffer.getInt(); // Ignoring message ID
-            int substatus = responseBuffer.getInt();
-
-            assertEquals("Type is not STATUS", SftpConstants.SSH_FXP_STATUS, 
type);
-            assertEquals("Sub-Type is not UNSUPPORTED", 
SftpConstants.SSH_FX_OP_UNSUPPORTED, substatus);
-        }
-    }
-}

Reply via email to