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); - } - } -}