[SSHD-878] Provide AttributeStore capabilities for file/directory handles used by...
authorLyor Goldstein <lgoldstein@apache.org>
Sun, 30 Dec 2018 06:50:03 +0000 (08:50 +0200)
committerLyor Goldstein <lgoldstein@apache.org>
Sun, 30 Dec 2018 18:52:37 +0000 (20:52 +0200)
13 files changed:
CHANGES.md
README.md
sshd-common/src/main/java/org/apache/sshd/common/AttributeRepository.java
sshd-common/src/main/java/org/apache/sshd/common/AttributeStore.java
sshd-core/src/main/java/org/apache/sshd/common/channel/AbstractChannel.java
sshd-core/src/main/java/org/apache/sshd/common/helpers/AbstractFactoryManager.java
sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
sshd-sftp/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpRemotePathChannel.java
sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/DirectoryHandle.java
sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/Handle.java
sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpFileSystemAccessor.java
sshd-sftp/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java

index 7ccb862..eae9bf8 100644 (file)
@@ -152,7 +152,7 @@ for exposing the cipher's block size. **Note:** for the time being we declare a
 (e.g., RC4) in order to facilitate the automatic re-keying mechanism described in [RFC 4253 - section 9](https://tools.ietf.org/html/rfc4253#section-9)
  and [RFC 4344 - section 3.2](https://tools.ietf.org/html/rfc4344#section-3.2).
 
-* [SSHD-876](https://issues.apache.org/jira/browse/SSHD-873) - Looking through the resolvable class-loaders "hierarchy"
+* [SSHD-876](https://issues.apache.org/jira/browse/SSHD-876) - Looking through the resolvable class-loaders "hierarchy"
 (thread-context => anchor => system) for `sshd-version.properties` file instead of just in the thread context class loader.
 
     * In this context, the default reported client/server SSH version string has been set to `APACHE-SSHD-...version...`.
@@ -160,6 +160,10 @@ for exposing the cipher's block size. **Note:** for the time being we declare a
     (see `AbstractSession#resolveIdentificationString`, `ClientFactoryManager#CLIENT_IDENTIFICATION`, and
     `ServerFactoryManager#SERVER_IDENTIFICATION`).
 
+* [SSHD-878](https://issues.apache.org/jira/browse/SSHD-876) - The `File/DirectoryHandle`(s) used by the SFTP subsystem
+implement `AttributeStore` interface - which means that `SftpEventListener`(s) can now attach user-defined attributes
+to the generated handle(s).
+
 * `SftpCommandMain` shows by default `get/put` command progress using the hash sign (`#`) marker. The marker
 can be enabled/disabled via the `progress` command:
 
index c8eee62..ee5045f 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1108,7 +1108,7 @@ the (default) password-based one:
 
 ```
 
-#### Tracking accessed location via `SftpFileSystemAccessor`
+#### Tracking accessed locations via `SftpFileSystemAccessor`
 
 One can override the default `SftpFileSystemAccessor` and thus be able to track all opened files and folders
 throughout the SFTP server subsystem code. The accessor is registered/overwritten in via the `SftpSubSystemFactory`:
@@ -1518,10 +1518,30 @@ in [RFC 4254 - section 6.7](https://tools.ietf.org/html/rfc4254#section-6.7)
 
 ### `SftpEventListener`
 
-Provides information about major SFTP protocol events. The listener is registered at the `SftpSubsystemFactory`:
+Provides information about major SFTP protocol events. The provided `File/DirectoryHandle` to the various callbacks an also be used to
+store user-defined attributes via its `AttributeStore` implementation. The listener is registered at the `SftpSubsystemFactory`:
 
 
 ```java
+    public class MySfpEventListener implements SftpEventListener {
+        private static final AttributeKey<SomeType> MY_SPECIAL_KEY = new Attribute<SomeType>();
+
+        ...
+        @Override
+        public void opening(ServerSession session, String remoteHandle, Handle localHandle) throws IOException {
+            localHandle.setAttribute(MY_SPECIAL_KEY, instanceOfSomeType);
+        }
+
+        @Override
+        public void writing(
+                ServerSession session, String remoteHandle, FileHandle localHandle,
+                long offset, byte[] data, int dataOffset, int dataLen)
+                    throws IOException {
+            SomeType myData = localHandle.getAttribute(MY_SPECIAL_KEY);
+            ...do something based on my data...
+        }
+    }
+
 
     SftpSubsystemFactory factory = new SftpSubsystemFactory();
     factory.addSftpEventListener(new MySftpEventListener());
@@ -1529,6 +1549,9 @@ Provides information about major SFTP protocol events. The listener is registere
 
 ```
 
+**Note:** the attached attributed are automatically removed once handle has been closed - regardless of
+whether the close attempt was successful or not. In other words, after `SftpEventListener#closed` has been
+called, all attributes associated with the handle are cleared.
 
 ### `PortForwardingEventListener`
 
index cce88ab..18caaa4 100644 (file)
@@ -63,6 +63,11 @@ public interface AttributeRepository {
     // CHECKSTYLE:ON
 
     /**
+     * @return Current number of user-defined attributes stored in the repository
+     */
+    int getAttributesCount();
+
+    /**
      * Returns the value of the user-defined attribute.
      *
      * @param <T> The generic attribute type
@@ -99,6 +104,11 @@ public interface AttributeRepository {
     static AttributeRepository ofAttributesMap(Map<AttributeKey<?>, ?> attributes) {
         return new AttributeRepository() {
             @Override
+            public int getAttributesCount() {
+                return attributes.size();
+            }
+
+            @Override
             @SuppressWarnings("unchecked")
             public <T> T getAttribute(AttributeKey<T> key) {
                 Objects.requireNonNull(key, "No key provided");
index 466cafe..0df4ad5 100644 (file)
@@ -77,5 +77,10 @@ public interface AttributeStore extends AttributeRepository {
      * @return The removed value; {@code null} if no previous value
      */
     <T> T removeAttribute(AttributeKey<T> key);
+
+    /**
+     * Removes all currently stored user-defined attributes
+     */
+    void clearAttributes();
 }
 
index d8918a0..267d115 100644 (file)
@@ -950,6 +950,11 @@ public abstract class AbstractChannel
     }
 
     @Override
+    public int getAttributesCount() {
+        return attributes.size();
+    }
+
+    @Override
     @SuppressWarnings("unchecked")
     public <T> T getAttribute(AttributeRepository.AttributeKey<T> key) {
         return (T) attributes.get(Objects.requireNonNull(key, "No key"));
@@ -981,6 +986,11 @@ public abstract class AbstractChannel
         return (T) attributes.remove(Objects.requireNonNull(key, "No key"));
     }
 
+    @Override
+    public void clearAttributes() {
+        attributes.clear();
+    }
+
     protected void configureWindow() {
         localWindow.init(this);
     }
index 5ff8614..16c563f 100644 (file)
@@ -148,6 +148,11 @@ public abstract class AbstractFactoryManager extends AbstractKexFactoryManager i
     }
 
     @Override
+    public int getAttributesCount() {
+        return attributes.size();
+    }
+
+    @Override
     @SuppressWarnings("unchecked")
     public <T> T getAttribute(AttributeRepository.AttributeKey<T> key) {
         return (T) attributes.get(Objects.requireNonNull(key, "No key"));
@@ -180,6 +185,11 @@ public abstract class AbstractFactoryManager extends AbstractKexFactoryManager i
     }
 
     @Override
+    public void clearAttributes() {
+        attributes.clear();
+    }
+
+    @Override
     public PropertyResolver getParentPropertyResolver() {
         return parentResolver;
     }
index e91ab82..b2f16dd 100644 (file)
@@ -149,6 +149,11 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
     }
 
     @Override
+    public int getAttributesCount() {
+        return attributes.size();
+    }
+
+    @Override
     @SuppressWarnings("unchecked")
     public <T> T getAttribute(AttributeRepository.AttributeKey<T> key) {
         return (T) attributes.get(Objects.requireNonNull(key, "No key"));
@@ -181,6 +186,11 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
     }
 
     @Override
+    public void clearAttributes() {
+        attributes.clear();
+    }
+
+    @Override
     public String getUsername() {
         return username;
     }
index 5ea348d..fc81e17 100644 (file)
@@ -360,7 +360,7 @@ public class SftpRemotePathChannel extends FileChannel {
     @Override
     protected void implCloseChannel() throws IOException {
         try {
-            final Thread thread = blockingThreadHolder.get();
+            Thread thread = blockingThreadHolder.get();
             if (thread != null) {
                 thread.interrupt();
             }
index 0ae60cf..a69d397 100644 (file)
@@ -29,7 +29,6 @@ import org.apache.sshd.server.session.ServerSession;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class DirectoryHandle extends Handle implements Iterator<Path> {
-
     private boolean done;
     private boolean sendDotDot = true;
     private boolean sendDot = true;
@@ -39,6 +38,7 @@ public class DirectoryHandle extends Handle implements Iterator<Path> {
 
     public DirectoryHandle(SftpSubsystem subsystem, Path dir, String handle) throws IOException {
         super(dir, handle);
+
         signalHandleOpening(subsystem);
 
         SftpFileSystemAccessor accessor = subsystem.getFileSystemAccessor();
index b499524..bb27d28 100644 (file)
@@ -55,6 +55,7 @@ public class FileHandle extends Handle {
 
     public FileHandle(SftpSubsystem subsystem, Path file, String handle, int flags, int access, Map<String, Object> attrs) throws IOException {
         super(file, handle);
+
         this.subsystem = Objects.requireNonNull(subsystem, "No subsystem instance provided");
         this.access = access;
         this.openOptions = Collections.unmodifiableSet(getOpenOptions(flags, access));
@@ -62,8 +63,9 @@ public class FileHandle extends Handle {
         signalHandleOpening(subsystem);
 
         FileAttribute<?>[] fileAttrs = GenericUtils.isEmpty(fileAttributes)
-                ? IoUtils.EMPTY_FILE_ATTRIBUTES
-                : fileAttributes.toArray(new FileAttribute<?>[fileAttributes.size()]);
+            ? IoUtils.EMPTY_FILE_ATTRIBUTES
+            : fileAttributes.toArray(new FileAttribute<?>[fileAttributes.size()]);
+
         SftpFileSystemAccessor accessor = subsystem.getFileSystemAccessor();
         ServerSession session = subsystem.getServerSession();
         SeekableByteChannel channel;
@@ -91,7 +93,7 @@ public class FileHandle extends Handle {
         return fileAttributes;
     }
 
-    public final SeekableByteChannel getFileChannel() {
+    public SeekableByteChannel getFileChannel() {
         return fileChannel;
     }
 
index a860eec..f300b16 100644 (file)
@@ -20,19 +20,28 @@ package org.apache.sshd.server.subsystem.sftp;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
 
+import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.AttributeStore;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.server.session.ServerSession;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
-public abstract class Handle implements java.nio.channels.Channel {
+public abstract class Handle implements java.nio.channels.Channel, AttributeStore {
     private final AtomicBoolean closed = new AtomicBoolean(false);
     private final Path file;
     private final String handle;
+    private final Map<AttributeRepository.AttributeKey<?>, Object> attributes = new ConcurrentHashMap<>();
 
     protected Handle(Path file, String handle) {
         this.file = Objects.requireNonNull(file, "No local file path");
@@ -60,6 +69,48 @@ public abstract class Handle implements java.nio.channels.Channel {
     }
 
     @Override
+    public int getAttributesCount() {
+        return attributes.size();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T getAttribute(AttributeRepository.AttributeKey<T> key) {
+        return (T) attributes.get(Objects.requireNonNull(key, "No key"));
+    }
+
+    @Override
+    public Collection<AttributeKey<?>> attributeKeys() {
+        return attributes.isEmpty() ? Collections.emptySet() : new HashSet<>(attributes.keySet());
+    }
+
+    @Override
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public <T> T computeAttributeIfAbsent(
+            AttributeRepository.AttributeKey<T> key, Function<? super AttributeRepository.AttributeKey<T>, ? extends T> resolver) {
+        return (T) attributes.computeIfAbsent(Objects.requireNonNull(key, "No key"), (Function) resolver);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T setAttribute(AttributeRepository.AttributeKey<T> key, T value) {
+        return (T) attributes.put(
+                Objects.requireNonNull(key, "No key"),
+                Objects.requireNonNull(value, "No value"));
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> T removeAttribute(AttributeRepository.AttributeKey<T> key) {
+        return (T) attributes.remove(Objects.requireNonNull(key, "No key"));
+    }
+
+    @Override
+    public void clearAttributes() {
+        attributes.clear();
+    }
+
+    @Override
     public boolean isOpen() {
         return !closed.get();
     }
index a001532..f84b562 100644 (file)
@@ -84,7 +84,7 @@ public interface SftpFileSystemAccessor {
     default SeekableByteChannel openFile(
             ServerSession session, SftpEventListenerManager subsystem,
             Path file, String handle, Set<? extends OpenOption> options, FileAttribute<?>... attrs)
-                    throws IOException {
+                throws IOException {
         return FileChannel.open(file, options, attrs);
     }
 
@@ -107,7 +107,7 @@ public interface SftpFileSystemAccessor {
      */
     default FileLock tryLock(ServerSession session, SftpEventListenerManager subsystem,
             Path file, String handle, Channel channel, long position, long size, boolean shared)
-                    throws IOException {
+                throws IOException {
         if (!(channel instanceof FileChannel)) {
             throw new StreamCorruptedException("Non file channel to lock: " + channel);
         }
@@ -127,8 +127,8 @@ public interface SftpFileSystemAccessor {
      * @see FileChannel#force(boolean)
      * @see <A HREF="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL">OpenSSH -  section 10</A>
      */
-    default void syncFileData(ServerSession session, SftpEventListenerManager subsystem,
-            Path file, String handle, Channel channel)
+    default void syncFileData(
+            ServerSession session, SftpEventListenerManager subsystem, Path file, String handle, Channel channel)
                 throws IOException {
         if (!(channel instanceof FileChannel)) {
             throw new StreamCorruptedException("Non file channel to sync: " + channel);
@@ -149,7 +149,7 @@ public interface SftpFileSystemAccessor {
      */
     default DirectoryStream<Path> openDirectory(
             ServerSession session, SftpEventListenerManager subsystem, Path dir, String handle)
-                    throws IOException {
+                throws IOException {
         return Files.newDirectoryStream(dir);
     }
 }
index 361fd11..66a0ced 100644 (file)
@@ -86,7 +86,7 @@ import org.apache.sshd.server.session.ServerSession;
 public class SftpSubsystem
         extends AbstractSftpSubsystemHelper
         implements Command, Runnable, SessionAware, FileSystemAware, ExecutorServiceCarrier,
-                    AsyncCommand, ChannelSessionAware, ChannelDataReceiver {
+        AsyncCommand, ChannelSessionAware, ChannelDataReceiver {
 
     /**
      * Properties key for the maximum of available open handles per session.
@@ -162,8 +162,9 @@ public class SftpSubsystem
      * use when generating failed commands error messages
      * @see ThreadUtils#newSingleThreadExecutor(String)
      */
-    public SftpSubsystem(CloseableExecutorService executorService, UnsupportedAttributePolicy policy,
-                 SftpFileSystemAccessor accessor, SftpErrorStatusDataHandler errorStatusDataHandler) {
+    public SftpSubsystem(
+            CloseableExecutorService executorService, UnsupportedAttributePolicy policy,
+            SftpFileSystemAccessor accessor, SftpErrorStatusDataHandler errorStatusDataHandler) {
         super(policy, accessor, errorStatusDataHandler);
 
         if (executorService == null) {
@@ -850,6 +851,8 @@ public class SftpSubsystem
             listener.closed(session, handle, nodeHandle, null);
         } catch (IOException | RuntimeException e) {
             listener.closed(session, handle, nodeHandle, e);
+        } finally {
+            nodeHandle.clearAttributes();
         }
     }
 
@@ -1024,6 +1027,8 @@ public class SftpSubsystem
             } catch (IOException e) {
                 log.error("closeAllHandles({}) failed ({}) to close handle={}[{}]: {}",
                     session, e.getClass().getSimpleName(), id, handle, e.getMessage());
+            } finally {
+                handle.clearAttributes();
             }
         });
         handles.clear();