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 2017/10/07 15:58:41 UTC

[2/5] mina-sshd git commit: [SSHD-775] SftpSubSystem::sendStatus leaks Exception information

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/2529a4c3/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
index cc66d02..5d87d8e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpSubsystem.java
@@ -19,106 +19,52 @@
 package org.apache.sshd.server.subsystem.sftp;
 
 import java.io.EOFException;
-import java.io.File;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.StreamCorruptedException;
-import java.nio.ByteBuffer;
-import java.nio.channels.SeekableByteChannel;
-import java.nio.charset.StandardCharsets;
+import java.net.UnknownServiceException;
 import java.nio.file.AccessDeniedException;
-import java.nio.file.CopyOption;
-import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.FileStore;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystemLoopException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
 import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.NotDirectoryException;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.StandardOpenOption;
-import java.nio.file.attribute.AclEntry;
-import java.nio.file.attribute.AclFileAttributeView;
-import java.nio.file.attribute.FileOwnerAttributeView;
-import java.nio.file.attribute.FileTime;
-import java.nio.file.attribute.GroupPrincipal;
-import java.nio.file.attribute.PosixFileAttributeView;
-import java.nio.file.attribute.PosixFilePermission;
-import java.nio.file.attribute.UserPrincipal;
-import java.nio.file.attribute.UserPrincipalLookupService;
-import java.nio.file.attribute.UserPrincipalNotFoundException;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
-import java.util.EnumSet;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.IntUnaryOperator;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 
 import org.apache.sshd.common.Factory;
 import org.apache.sshd.common.FactoryManager;
-import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.NamedResource;
-import org.apache.sshd.common.OptionalFeature;
-import org.apache.sshd.common.PropertyResolver;
-import org.apache.sshd.common.PropertyResolverUtils;
-import org.apache.sshd.common.config.VersionProperties;
 import org.apache.sshd.common.digest.BuiltinDigests;
-import org.apache.sshd.common.digest.Digest;
 import org.apache.sshd.common.digest.DigestFactory;
 import org.apache.sshd.common.file.FileSystemAware;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
-import org.apache.sshd.common.subsystem.sftp.SftpException;
 import org.apache.sshd.common.subsystem.sftp.SftpHelper;
-import org.apache.sshd.common.subsystem.sftp.extensions.AclSupportedParser;
-import org.apache.sshd.common.subsystem.sftp.extensions.SpaceAvailableExtensionInfo;
-import org.apache.sshd.common.subsystem.sftp.extensions.openssh.AbstractOpenSSHExtensionParser.OpenSSHExtension;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.FsyncExtensionParser;
 import org.apache.sshd.common.subsystem.sftp.extensions.openssh.HardLinkExtensionParser;
-import org.apache.sshd.common.util.EventListenerUtils;
 import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.NumberUtils;
-import org.apache.sshd.common.util.OsUtils;
-import org.apache.sshd.common.util.Pair;
-import org.apache.sshd.common.util.SelectorUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.buffer.Buffer;
 import org.apache.sshd.common.util.buffer.BufferUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
-import org.apache.sshd.common.util.io.FileInfoExtractor;
 import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.logging.AbstractLoggingBean;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.server.Command;
 import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.ExitCallback;
 import org.apache.sshd.server.SessionAware;
 import org.apache.sshd.server.session.ServerSession;
-import org.apache.sshd.server.session.ServerSessionHolder;
 
 /**
  * SFTP subsystem
@@ -126,8 +72,8 @@ import org.apache.sshd.server.session.ServerSessionHolder;
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public class SftpSubsystem
-        extends AbstractLoggingBean
-        implements Command, Runnable, SessionAware, FileSystemAware, ServerSessionHolder, SftpEventListenerManager {
+        extends AbstractSftpSubsystemHelper
+        implements Command, Runnable, SessionAware, FileSystemAware {
 
     /**
      * Properties key for the maximum of available open handles per session.
@@ -158,26 +104,6 @@ public class SftpSubsystem
     public static final int MAX_FILE_HANDLE_ROUNDS = MAX_FILE_HANDLE_SIZE;
 
     /**
-     * Force the use of a given sftp version
-     */
-    public static final String SFTP_VERSION = "sftp-version";
-
-    public static final int LOWER_SFTP_IMPL = SftpConstants.SFTP_V3; // Working implementation from v3
-    public static final int HIGHER_SFTP_IMPL = SftpConstants.SFTP_V6; //  .. up to and including
-    public static final String ALL_SFTP_IMPL = IntStream.rangeClosed(LOWER_SFTP_IMPL, HIGHER_SFTP_IMPL)
-                            .mapToObj(Integer::toString)
-                            .collect(Collectors.joining(","));
-
-    /**
-     * Force the use of a max. packet length for {@link #doRead(Buffer, int)} protection
-     * against malicious packets
-     *
-     * @see #DEFAULT_MAX_READDATA_PACKET_LENGTH
-     */
-    public static final String MAX_READDATA_PACKET_LENGTH_PROP = "sftp-max-readdata-packet-length";
-    public static final int DEFAULT_MAX_READDATA_PACKET_LENGTH = 63 * 1024;
-
-    /**
      * Maximum amount of data allocated for listing the contents of a directory
      * in any single invocation of {@link #doReadDir(Buffer, int)}
      *
@@ -186,99 +112,6 @@ public class SftpSubsystem
     public static final String MAX_READDIR_DATA_SIZE_PROP = "sftp-max-readdir-data-size";
     public static final int DEFAULT_MAX_READDIR_DATA_SIZE = 16 * 1024;
 
-    /**
-     * Allows controlling reports of which client extensions are supported
-     * (and reported via &quot;support&quot; and &quot;support2&quot; server
-     * extensions) as a comma-separate list of names. <B>Note:</B> requires
-     * overriding the {@link #executeExtendedCommand(Buffer, int, String)}
-     * command accordingly. If empty string is set then no server extensions
-     * are reported
-     *
-     * @see #DEFAULT_SUPPORTED_CLIENT_EXTENSIONS
-     */
-    public static final String CLIENT_EXTENSIONS_PROP = "sftp-client-extensions";
-
-    /**
-     * The default reported supported client extensions
-     */
-    public static final Map<String, OptionalFeature> DEFAULT_SUPPORTED_CLIENT_EXTENSIONS =
-            // TODO text-seek - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt
-            // TODO home-directory - see http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
-            GenericUtils.<String, OptionalFeature>mapBuilder()
-                .put(SftpConstants.EXT_VERSION_SELECT, OptionalFeature.TRUE)
-                .put(SftpConstants.EXT_COPY_FILE, OptionalFeature.TRUE)
-                .put(SftpConstants.EXT_MD5_HASH, BuiltinDigests.md5)
-                .put(SftpConstants.EXT_MD5_HASH_HANDLE, BuiltinDigests.md5)
-                .put(SftpConstants.EXT_CHECK_FILE_HANDLE, OptionalFeature.any(BuiltinDigests.VALUES))
-                .put(SftpConstants.EXT_CHECK_FILE_NAME, OptionalFeature.any(BuiltinDigests.VALUES))
-                .put(SftpConstants.EXT_COPY_DATA, OptionalFeature.TRUE)
-                .put(SftpConstants.EXT_SPACE_AVAILABLE, OptionalFeature.TRUE)
-                .immutable();
-
-    /**
-     * Comma-separated list of which {@code OpenSSH} extensions are reported and
-     * what version is reported for each - format: {@code name=version}. If empty
-     * value set, then no such extensions are reported. Otherwise, the
-     * {@link #DEFAULT_OPEN_SSH_EXTENSIONS} are used
-     */
-    public static final String OPENSSH_EXTENSIONS_PROP = "sftp-openssh-extensions";
-    public static final List<OpenSSHExtension> DEFAULT_OPEN_SSH_EXTENSIONS =
-            Collections.unmodifiableList(
-                    Arrays.asList(
-                            new OpenSSHExtension(FsyncExtensionParser.NAME, "1"),
-                            new OpenSSHExtension(HardLinkExtensionParser.NAME, "1")
-                    ));
-
-    public static final List<String> DEFAULT_OPEN_SSH_EXTENSIONS_NAMES =
-            Collections.unmodifiableList(NamedResource.getNameList(DEFAULT_OPEN_SSH_EXTENSIONS));
-
-    public static final List<String> DEFAULT_UNIX_VIEW = Collections.singletonList("unix:*");
-
-    /**
-     * Comma separate list of {@code SSH_ACL_CAP_xxx} names - where name can be without
-     * the prefix. If not defined then {@link #DEFAULT_ACL_SUPPORTED_MASK} is used
-     */
-    public static final String ACL_SUPPORTED_MASK_PROP = "sftp-acl-supported-mask";
-    public static final Set<Integer> DEFAULT_ACL_SUPPORTED_MASK =
-            Collections.unmodifiableSet(
-                    new HashSet<>(Arrays.asList(
-                            SftpConstants.SSH_ACL_CAP_ALLOW,
-                            SftpConstants.SSH_ACL_CAP_DENY,
-                            SftpConstants.SSH_ACL_CAP_AUDIT,
-                            SftpConstants.SSH_ACL_CAP_ALARM)));
-
-    /**
-     * Property that can be used to set the reported NL value.
-     * If not set, then {@link IoUtils#EOL} is used
-     */
-    public static final String NEWLINE_VALUE = "sftp-newline";
-
-    /**
-     * A {@link Map} of {@link FileInfoExtractor}s to be used to complete
-     * attributes that are deemed important enough to warrant an extra
-     * effort if not accessible via the file system attributes views
-     */
-    public static final Map<String, FileInfoExtractor<?>> FILEATTRS_RESOLVERS =
-            GenericUtils.<String, FileInfoExtractor<?>>mapBuilder(String.CASE_INSENSITIVE_ORDER)
-                .put("isRegularFile", FileInfoExtractor.ISREG)
-                .put("isDirectory", FileInfoExtractor.ISDIR)
-                .put("isSymbolicLink", FileInfoExtractor.ISSYMLINK)
-                .put("permissions", FileInfoExtractor.PERMISSIONS)
-                .put("size", FileInfoExtractor.SIZE)
-                .put("lastModifiedTime", FileInfoExtractor.LASTMODIFIED)
-                .immutable();
-
-    /**
-     * Whether to automatically follow symbolic links when resolving paths
-     * @see #DEFAULT_AUTO_FOLLOW_LINKS
-     */
-    public static final String AUTO_FOLLOW_LINKS = "sftp-auto-follow-links";
-
-    /**
-     * Default value of {@value #AUTO_FOLLOW_LINKS}
-     */
-    public static final boolean DEFAULT_AUTO_FOLLOW_LINKS = true;
-
     protected ExitCallback callback;
     protected InputStream in;
     protected OutputStream out;
@@ -297,13 +130,9 @@ public class SftpSubsystem
     protected int version;
     protected final Map<String, byte[]> extensions = new TreeMap<>(Comparator.naturalOrder());
     protected final Map<String, Handle> handles = new HashMap<>();
-    protected final UnsupportedAttributePolicy unsupportedAttributePolicy;
 
     private ServerSession serverSession;
     private final AtomicBoolean closed = new AtomicBoolean(false);
-    private final Collection<SftpEventListener> sftpEventListeners = new CopyOnWriteArraySet<>();
-    private final SftpEventListener sftpEventListenerProxy;
-    private final SftpFileSystemAccessor fileSystemAccessor;
 
     /**
      * @param executorService The {@link ExecutorService} to be used by
@@ -315,9 +144,14 @@ public class SftpSubsystem
      * @param policy          The {@link UnsupportedAttributePolicy} to use if failed to access
      *                        some local file attributes
      * @param accessor        The {@link SftpFileSystemAccessor} to use for opening files and directories
+     * @param errorStatusDataHandler The (never {@code null}) {@link SftpErrorStatusDataHandler} to
+     * use when generating failed commands error messages
      * @see ThreadUtils#newSingleThreadExecutor(String)
      */
-    public SftpSubsystem(ExecutorService executorService, boolean shutdownOnExit, UnsupportedAttributePolicy policy, SftpFileSystemAccessor accessor) {
+    public SftpSubsystem(ExecutorService executorService, boolean shutdownOnExit, UnsupportedAttributePolicy policy,
+            SftpFileSystemAccessor accessor, SftpErrorStatusDataHandler errorStatusDataHandler) {
+        super(policy, accessor, errorStatusDataHandler);
+
         if (executorService == null) {
             executors = ThreadUtils.newSingleThreadExecutor(getClass().getSimpleName());
             shutdownExecutor = true;    // we always close the ad-hoc executor service
@@ -325,41 +159,16 @@ public class SftpSubsystem
             executors = executorService;
             shutdownExecutor = shutdownOnExit;
         }
-
-        unsupportedAttributePolicy = Objects.requireNonNull(policy, "No policy provided");
-        fileSystemAccessor = Objects.requireNonNull(accessor, "No accessor");
-        sftpEventListenerProxy = EventListenerUtils.proxyWrapper(SftpEventListener.class, getClass().getClassLoader(), sftpEventListeners);
     }
 
+    @Override
     public int getVersion() {
         return version;
     }
 
-    public final UnsupportedAttributePolicy getUnsupportedAttributePolicy() {
-        return unsupportedAttributePolicy;
-    }
-
-    public final SftpFileSystemAccessor getFileSystemAccessor() {
-        return fileSystemAccessor;
-    }
-
-    @Override
-    public SftpEventListener getSftpEventListenerProxy() {
-        return sftpEventListenerProxy;
-    }
-
-    @Override
-    public boolean addSftpEventListener(SftpEventListener listener) {
-        return sftpEventListeners.add(SftpEventListener.validateListener(listener));
-    }
-
     @Override
-    public boolean removeSftpEventListener(SftpEventListener listener) {
-        if (listener == null) {
-            return false;
-        }
-
-        return sftpEventListeners.remove(SftpEventListener.validateListener(listener));
+    public Path getDefaultDirectory() {
+        return defaultDir;
     }
 
     @Override
@@ -477,6 +286,7 @@ public class SftpSubsystem
         }
     }
 
+    @Override
     protected void process(Buffer buffer) throws IOException {
         int length = buffer.getInt();
         int type = buffer.getUByte();
@@ -570,16 +380,7 @@ public class SftpSubsystem
         }
     }
 
-    protected void doExtended(Buffer buffer, int id) throws IOException {
-        executeExtendedCommand(buffer, id, buffer.getString());
-    }
-
-    /**
-     * @param buffer    The command {@link Buffer}
-     * @param id        The request id
-     * @param extension The extension name
-     * @throws IOException If failed to execute the extension
-     */
+    @Override
     protected void executeExtendedCommand(Buffer buffer, int id, String extension) throws IOException {
         switch (extension) {
             case SftpConstants.EXT_TEXT_SEEK:
@@ -620,76 +421,32 @@ public class SftpSubsystem
         }
     }
 
-    // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 10
-    protected void doOpenSSHHardLink(Buffer buffer, int id) throws IOException {
-        String srcFile = buffer.getString();
-        String dstFile = buffer.getString();
-
-        try {
-            doOpenSSHHardLink(id, srcFile, dstFile);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doOpenSSHHardLink(int id, String srcFile, String dstFile) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("doOpenSSHHardLink({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={})",
-                      getServerSession(), id, HardLinkExtensionParser.NAME, srcFile, dstFile);
-        }
-
-        createLink(id, srcFile, dstFile, false);
-    }
-
-    protected void doSpaceAvailable(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        SpaceAvailableExtensionInfo info;
-        try {
-            info = doSpaceAvailable(id, path);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        buffer.clear();
-        buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY);
-        buffer.putInt(id);
-        SpaceAvailableExtensionInfo.encode(buffer, info);
-        send(buffer);
-    }
-
-    protected SpaceAvailableExtensionInfo doSpaceAvailable(int id, String path) throws IOException {
-        Path nrm = resolveNormalizedLocation(path);
+    @Override
+    protected void createLink(int id, String existingPath, String linkPath, boolean symLink) throws IOException {
+        Path link = resolveFile(linkPath);
+        Path existing = fileSystem.getPath(existingPath);
         if (log.isDebugEnabled()) {
-            log.debug("doSpaceAvailable({})[id={}] path={}[{}]", getServerSession(), id, path, nrm);
-        }
-
-        FileStore store = Files.getFileStore(nrm);
-        if (log.isTraceEnabled()) {
-            log.trace("doSpaceAvailable({})[id={}] path={}[{}] - {}[{}]",
-                      getServerSession(), id, path, nrm, store.name(), store.type());
+            log.debug("createLink({})[id={}], existing={}[{}], link={}[{}], symlink={})",
+                      getServerSession(), id, linkPath, link, existingPath, existing, symLink);
         }
 
-        return new SpaceAvailableExtensionInfo(store);
-    }
-
-    protected void doTextSeek(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long line = buffer.getLong();
+        SftpEventListener listener = getSftpEventListenerProxy();
+        ServerSession session = getServerSession();
+        listener.linking(session, link, existing, symLink);
         try {
-            // TODO : implement text-seek - see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-03#section-6.3
-            doTextSeek(id, handle, line);
+            if (symLink) {
+                Files.createSymbolicLink(link, existing);
+            } else {
+                Files.createLink(link, existing);
+            }
         } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
+            listener.linked(session, link, existing, symLink, e);
+            throw e;
         }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
+        listener.linked(session, link, existing, symLink, null);
     }
 
+    @Override
     protected void doTextSeek(int id, String handle, long line) throws IOException {
         Handle h = handles.get(handle);
         if (log.isDebugEnabled()) {
@@ -698,22 +455,10 @@ public class SftpSubsystem
         }
 
         FileHandle fileHandle = validateHandle(handle, h, FileHandle.class);
-        throw new UnsupportedOperationException("doTextSeek(" + fileHandle + ")");
-    }
-
-    // see https://github.com/openssh/openssh-portable/blob/master/PROTOCOL section 10
-    protected void doOpenSSHFsync(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        try {
-            doOpenSSHFsync(id, handle);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
+        throw new UnknownServiceException("doTextSeek(" + fileHandle + ")");
     }
 
+    @Override
     protected void doOpenSSHFsync(int id, String handle) throws IOException {
         Handle h = handles.get(handle);
         if (log.isDebugEnabled()) {
@@ -726,30 +471,11 @@ public class SftpSubsystem
         accessor.syncFileData(session, this, fileHandle.getFile(), fileHandle.getFileHandle(), fileHandle.getFileChannel());
     }
 
-    protected void doCheckFileHash(Buffer buffer, int id, String targetType) throws IOException {
-        String target = buffer.getString();
-        String algList = buffer.getString();
-        String[] algos = GenericUtils.split(algList, ',');
-        long startOffset = buffer.getLong();
-        long length = buffer.getLong();
-        int blockSize = buffer.getInt();
-        try {
-            buffer.clear();
-            buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY);
-            buffer.putInt(id);
-            buffer.putString(SftpConstants.EXT_CHECK_FILE);
-            doCheckFileHash(id, targetType, target, Arrays.asList(algos), startOffset, length, blockSize, buffer);
-        } catch (Exception e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        send(buffer);
-    }
-
-    protected void doCheckFileHash(int id, String targetType, String target, Collection<String> algos,
-                                   long startOffset, long length, int blockSize, Buffer buffer)
-            throws Exception {
+    @Override
+    protected void doCheckFileHash(
+            int id, String targetType, String target, Collection<String> algos,
+            long startOffset, long length, int blockSize, Buffer buffer)
+                    throws Exception {
         Path path;
         if (SftpConstants.EXT_CHECK_FILE_HANDLE.equalsIgnoreCase(targetType)) {
             Handle h = handles.get(target);
@@ -764,7 +490,7 @@ public class SftpSubsystem
              */
             int access = fileHandle.getAccessMask();
             if ((access & SftpConstants.ACE4_READ_DATA) == 0) {
-                throw new AccessDeniedException("File not opened for read: " + path);
+                throw new AccessDeniedException(path.toString(), path.toString(), "File not opened for read");
             }
         } else {
             path = resolveFile(target);
@@ -780,7 +506,7 @@ public class SftpSubsystem
             }
 
             if (Files.isSymbolicLink(path)) {
-                throw new FileSystemLoopException(target + " yields a circular or too long chain of symlinks");
+                throw new FileSystemLoopException(target);
             }
 
             if (Files.isDirectory(path, IoUtils.getLinkOptions(false))) {
@@ -802,121 +528,7 @@ public class SftpSubsystem
         doCheckFileHash(id, path, factory, startOffset, length, blockSize, buffer);
     }
 
-    protected void doCheckFileHash(int id, Path file, NamedFactory<? extends Digest> factory,
-            long startOffset, long length, int blockSize, Buffer buffer)
-                    throws Exception {
-        ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset);
-        ValidateUtils.checkTrue(length >= 0L, "Invalid length: %d", length);
-        ValidateUtils.checkTrue((blockSize == 0) || (blockSize >= SftpConstants.MIN_CHKFILE_BLOCKSIZE), "Invalid block size: %d", blockSize);
-        Objects.requireNonNull(factory, "No digest factory provided");
-        buffer.putString(factory.getName());
-
-        long effectiveLength = length;
-        long totalLength = Files.size(file);
-        if (effectiveLength == 0L) {
-            effectiveLength = totalLength - startOffset;
-        } else {
-            long maxRead = startOffset + length;
-            if (maxRead > totalLength) {
-                effectiveLength = totalLength - startOffset;
-            }
-        }
-        ValidateUtils.checkTrue(effectiveLength > 0L, "Non-positive effective hash data length: %d", effectiveLength);
-
-        byte[] digestBuf = (blockSize == 0)
-                ? new byte[Math.min((int) effectiveLength, IoUtils.DEFAULT_COPY_SIZE)]
-                : new byte[Math.min((int) effectiveLength, blockSize)];
-        ByteBuffer wb = ByteBuffer.wrap(digestBuf);
-        SftpFileSystemAccessor accessor = getFileSystemAccessor();
-        try (SeekableByteChannel channel = accessor.openFile(getServerSession(), this, file, "", Collections.emptySet())) {
-            channel.position(startOffset);
-
-            Digest digest = factory.create();
-            digest.init();
-
-            if (blockSize == 0) {
-                while (effectiveLength > 0L) {
-                    int remainLen = Math.min(digestBuf.length, (int) effectiveLength);
-                    ByteBuffer bb = wb;
-                    if (remainLen < digestBuf.length) {
-                        bb = ByteBuffer.wrap(digestBuf, 0, remainLen);
-                    }
-                    bb.clear(); // prepare for next read
-
-                    int readLen = channel.read(bb);
-                    if (readLen < 0) {
-                        break;
-                    }
-
-                    effectiveLength -= readLen;
-                    digest.update(digestBuf, 0, readLen);
-                }
-
-                byte[] hashValue = digest.digest();
-                if (log.isTraceEnabled()) {
-                    log.trace("doCheckFileHash({})[{}] offset={}, length={} - algo={}, hash={}",
-                              getServerSession(), file, startOffset, length,
-                              digest.getAlgorithm(), BufferUtils.toHex(':', hashValue));
-                }
-                buffer.putBytes(hashValue);
-            } else {
-                for (int count = 0; effectiveLength > 0L; count++) {
-                    int remainLen = Math.min(digestBuf.length, (int) effectiveLength);
-                    ByteBuffer bb = wb;
-                    if (remainLen < digestBuf.length) {
-                        bb = ByteBuffer.wrap(digestBuf, 0, remainLen);
-                    }
-                    bb.clear(); // prepare for next read
-
-                    int readLen = channel.read(bb);
-                    if (readLen < 0) {
-                        break;
-                    }
-
-                    effectiveLength -= readLen;
-                    digest.update(digestBuf, 0, readLen);
-
-                    byte[] hashValue = digest.digest(); // NOTE: this also resets the hash for the next read
-                    if (log.isTraceEnabled()) {
-                        log.trace("doCheckFileHash({})({})[{}] offset={}, length={} - algo={}, hash={}",
-                                  getServerSession(), file, count, startOffset, length,
-                                  digest.getAlgorithm(), BufferUtils.toHex(':', hashValue));
-                    }
-                    buffer.putBytes(hashValue);
-                }
-            }
-        }
-    }
-
-    protected void doMD5Hash(Buffer buffer, int id, String targetType) throws IOException {
-        String target = buffer.getString();
-        long startOffset = buffer.getLong();
-        long length = buffer.getLong();
-        byte[] quickCheckHash = buffer.getBytes();
-        byte[] hashValue;
-
-        try {
-            hashValue = doMD5Hash(id, targetType, target, startOffset, length, quickCheckHash);
-            if (log.isTraceEnabled()) {
-                log.trace("doMD5Hash({})({})[{}] offset={}, length={}, quick-hash={} - hash={}",
-                          getServerSession(), targetType, target, startOffset, length,
-                          BufferUtils.toHex(':', quickCheckHash),
-                          BufferUtils.toHex(':', hashValue));
-            }
-
-        } catch (Exception e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        buffer.clear();
-        buffer.putByte((byte) SftpConstants.SSH_FXP_EXTENDED_REPLY);
-        buffer.putInt(id);
-        buffer.putString(targetType);
-        buffer.putBytes(hashValue);
-        send(buffer);
-    }
-
+    @Override
     protected byte[] doMD5Hash(
             int id, String targetType, String target, long startOffset, long length, byte[] quickCheckHash)
                     throws Exception {
@@ -941,7 +553,7 @@ public class SftpSubsystem
              */
             int access = fileHandle.getAccessMask();
             if ((access & SftpConstants.ACE4_READ_DATA) == 0) {
-                throw new AccessDeniedException("File not opened for read: " + path);
+                throw new AccessDeniedException(path.toString(), path.toString(), "File not opened for read");
             }
         } else {
             path = resolveFile(target);
@@ -969,105 +581,6 @@ public class SftpSubsystem
         return doMD5Hash(id, path, startOffset, effectiveLength, quickCheckHash);
     }
 
-    protected byte[] doMD5Hash(int id, Path path, long startOffset, long length, byte[] quickCheckHash) throws Exception {
-        ValidateUtils.checkTrue(startOffset >= 0L, "Invalid start offset: %d", startOffset);
-        ValidateUtils.checkTrue(length > 0L, "Invalid length: %d", length);
-        if (!BuiltinDigests.md5.isSupported()) {
-            throw new UnsupportedOperationException(BuiltinDigests.md5.getAlgorithm() + " hash not supported");
-        }
-
-        Digest digest = BuiltinDigests.md5.create();
-        digest.init();
-
-        long effectiveLength = length;
-        byte[] digestBuf = new byte[(int) Math.min(effectiveLength, SftpConstants.MD5_QUICK_HASH_SIZE)];
-        ByteBuffer wb = ByteBuffer.wrap(digestBuf);
-        boolean hashMatches = false;
-        byte[] hashValue = null;
-        SftpFileSystemAccessor accessor = getFileSystemAccessor();
-        try (SeekableByteChannel channel = accessor.openFile(getServerSession(), this, path, null, EnumSet.of(StandardOpenOption.READ))) {
-            channel.position(startOffset);
-
-            /*
-             * To quote http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt section 9.1.1:
-             *
-             *      If this is a zero length string, the client does not have the
-             *      data, and is requesting the hash for reasons other than comparing
-             *      with a local file.  The server MAY return SSH_FX_OP_UNSUPPORTED in
-             *      this case.
-             */
-            if (NumberUtils.length(quickCheckHash) <= 0) {
-                // TODO consider limiting it - e.g., if the requested effective length is <= than some (configurable) threshold
-                hashMatches = true;
-            } else {
-                int readLen = channel.read(wb);
-                if (readLen < 0) {
-                    throw new EOFException("EOF while read initial buffer from " + path);
-                }
-                effectiveLength -= readLen;
-                digest.update(digestBuf, 0, readLen);
-
-                hashValue = digest.digest();
-                hashMatches = Arrays.equals(quickCheckHash, hashValue);
-                if (hashMatches) {
-                    /*
-                     * Need to re-initialize the digester due to the Javadoc:
-                     *
-                     *      "The digest method can be called once for a given number
-                     *       of updates. After digest has been called, the MessageDigest
-                     *       object is reset to its initialized state."
-                     */
-                    if (effectiveLength > 0L) {
-                        digest = BuiltinDigests.md5.create();
-                        digest.init();
-                        digest.update(digestBuf, 0, readLen);
-                        hashValue = null;   // start again
-                    }
-                } else {
-                    if (log.isTraceEnabled()) {
-                        log.trace("doMD5Hash({})({}) offset={}, length={} - quick-hash mismatched expected={}, actual={}",
-                                  getServerSession(), path, startOffset, length,
-                                  BufferUtils.toHex(':', quickCheckHash),
-                                  BufferUtils.toHex(':', hashValue));
-                    }
-                }
-            }
-
-            if (hashMatches) {
-                while (effectiveLength > 0L) {
-                    int remainLen = Math.min(digestBuf.length, (int) effectiveLength);
-                    ByteBuffer bb = wb;
-                    if (remainLen < digestBuf.length) {
-                        bb = ByteBuffer.wrap(digestBuf, 0, remainLen);
-                    }
-                    bb.clear(); // prepare for next read
-
-                    int readLen = channel.read(bb);
-                    if (readLen < 0) {
-                        break;  // user may have specified more than we have available
-                    }
-                    effectiveLength -= readLen;
-                    digest.update(digestBuf, 0, readLen);
-                }
-
-                if (hashValue == null) {    // check if did any more iterations after the quick hash
-                    hashValue = digest.digest();
-                }
-            } else {
-                hashValue = GenericUtils.EMPTY_BYTE_ARRAY;
-            }
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("doMD5Hash({})({}) offset={}, length={} - matches={}, quick={} hash={}",
-                      getServerSession(), path, startOffset, length, hashMatches,
-                      BufferUtils.toHex(':', quickCheckHash),
-                      BufferUtils.toHex(':', hashValue));
-        }
-
-        return hashValue;
-    }
-
     protected void doVersionSelect(Buffer buffer, int id) throws IOException {
         String proposed = buffer.getString();
         ServerSession session = getServerSession();
@@ -1101,100 +614,7 @@ public class SftpSubsystem
         }
     }
 
-    /**
-     * @param buffer   The {@link Buffer} holding the request
-     * @param id       The request id
-     * @param proposed The proposed value
-     * @return A {@link Boolean} indicating whether to accept/reject the proposal.
-     * If {@code null} then rejection response has been sent, otherwise and
-     * appropriate response is generated
-     * @throws IOException If failed send an independent rejection response
-     */
-    protected Boolean validateProposedVersion(Buffer buffer, int id, String proposed) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("validateProposedVersion({})[id={}] SSH_FXP_EXTENDED(version-select) (version={})",
-                      getServerSession(), id, proposed);
-        }
-
-        if (GenericUtils.length(proposed) != 1) {
-            return Boolean.FALSE;
-        }
-
-        char digit = proposed.charAt(0);
-        if ((digit < '0') || (digit > '9')) {
-            return Boolean.FALSE;
-        }
-
-        int value = digit - '0';
-        String all = checkVersionCompatibility(buffer, id, value, SftpConstants.SSH_FX_FAILURE);
-        if (GenericUtils.isEmpty(all)) {    // validation failed
-            return null;
-        } else {
-            return Boolean.TRUE;
-        }
-    }
-
-    /**
-     * Checks if a proposed version is within supported range. <B>Note:</B>
-     * if the user forced a specific value via the {@link #SFTP_VERSION}
-     * property, then it is used to validate the proposed value
-     *
-     * @param buffer        The {@link Buffer} containing the request
-     * @param id            The SSH message ID to be used to send the failure message
-     *                      if required
-     * @param proposed      The proposed version value
-     * @param failureOpcode The failure opcode to send if validation fails
-     * @return A {@link String} of comma separated values representing all
-     * the supported version - {@code null} if validation failed and an
-     * appropriate status message was sent
-     * @throws IOException If failed to send the failure status message
-     */
-    protected String checkVersionCompatibility(Buffer buffer, int id, int proposed, int failureOpcode) throws IOException {
-        int low = LOWER_SFTP_IMPL;
-        int hig = HIGHER_SFTP_IMPL;
-        String available = ALL_SFTP_IMPL;
-        // check if user wants to use a specific version
-        ServerSession session = getServerSession();
-        Integer sftpVersion = session.getInteger(SFTP_VERSION);
-        if (sftpVersion != null) {
-            int forcedValue = sftpVersion;
-            if ((forcedValue < LOWER_SFTP_IMPL) || (forcedValue > HIGHER_SFTP_IMPL)) {
-                throw new IllegalStateException("Forced SFTP version (" + sftpVersion + ") not within supported values: " + available);
-            }
-            hig = sftpVersion;
-            low = hig;
-            available = sftpVersion.toString();
-        }
-
-        if (log.isTraceEnabled()) {
-            log.trace("checkVersionCompatibility({})[id={}] - proposed={}, available={}",
-                      getServerSession(), id, proposed, available);
-        }
-
-        if ((proposed < low) || (proposed > hig)) {
-            sendStatus(BufferUtils.clear(buffer), id, failureOpcode, "Proposed version (" + proposed + ") not in supported range: " + available);
-            return null;
-        }
-
-        return available;
-    }
-
-    protected void doBlock(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        long length = buffer.getLong();
-        int mask = buffer.getInt();
-
-        try {
-            doBlock(id, handle, offset, length, mask);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
+    @Override
     protected void doBlock(int id, String handle, long offset, long length, int mask) throws IOException {
         Handle p = handles.get(handle);
         if (log.isDebugEnabled()) {
@@ -1215,20 +635,7 @@ public class SftpSubsystem
         listener.blocked(session, handle, fileHandle, offset, length, mask, null);
     }
 
-    protected void doUnblock(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        long length = buffer.getLong();
-        try {
-            doUnblock(id, handle, offset, length);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
+    @Override
     protected void doUnblock(int id, String handle, long offset, long length) throws IOException {
         Handle p = handles.get(handle);
         if (log.isDebugEnabled()) {
@@ -1249,172 +656,7 @@ public class SftpSubsystem
         listener.unblocked(session, handle, fileHandle, offset, length, null);
     }
 
-    protected void doLink(Buffer buffer, int id) throws IOException {
-        String targetPath = buffer.getString();
-        String linkPath = buffer.getString();
-        boolean symLink = buffer.getBoolean();
-
-        try {
-            if (log.isDebugEnabled()) {
-                log.debug("doLink({})[id={}] SSH_FXP_LINK linkpath={}, targetpath={}, symlink={}",
-                          getServerSession(), id, linkPath, targetPath, symLink);
-            }
-
-            doLink(id, targetPath, linkPath, symLink);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doLink(int id, String targetPath, String linkPath, boolean symLink) throws IOException {
-        createLink(id, targetPath, linkPath, symLink);
-    }
-
-    protected void doSymLink(Buffer buffer, int id) throws IOException {
-        String targetPath = buffer.getString();
-        String linkPath = buffer.getString();
-        try {
-            if (log.isDebugEnabled()) {
-                log.debug("doSymLink({})[id={}] SSH_FXP_SYMLINK linkpath={}, targetpath={}",
-                          getServerSession(), id, targetPath, linkPath);
-            }
-            doSymLink(id, targetPath, linkPath);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doSymLink(int id, String targetPath, String linkPath) throws IOException {
-        createLink(id, targetPath, linkPath, true);
-    }
-
-    protected void createLink(int id, String existingPath, String linkPath, boolean symLink) throws IOException {
-        Path link = resolveFile(linkPath);
-        Path existing = fileSystem.getPath(existingPath);
-        if (log.isDebugEnabled()) {
-            log.debug("createLink({})[id={}], existing={}[{}], link={}[{}], symlink={})",
-                      getServerSession(), id, linkPath, link, existingPath, existing, symLink);
-        }
-
-        SftpEventListener listener = getSftpEventListenerProxy();
-        ServerSession session = getServerSession();
-        listener.linking(session, link, existing, symLink);
-        try {
-            if (symLink) {
-                Files.createSymbolicLink(link, existing);
-            } else {
-                Files.createLink(link, existing);
-            }
-        } catch (IOException | RuntimeException e) {
-            listener.linked(session, link, existing, symLink, e);
-            throw e;
-        }
-        listener.linked(session, link, existing, symLink, null);
-    }
-
-    protected void doReadLink(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        String l;
-        try {
-            if (log.isDebugEnabled()) {
-                log.debug("doReadLink({})[id={}] SSH_FXP_READLINK path={}",
-                          getServerSession(), id, path);
-            }
-            l = doReadLink(id, path);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendLink(BufferUtils.clear(buffer), id, l);
-    }
-
-    protected String doReadLink(int id, String path) throws IOException {
-        Path f = resolveFile(path);
-        Path t = Files.readSymbolicLink(f);
-        if (log.isDebugEnabled()) {
-            log.debug("doReadLink({})[id={}] path={}[{}]: {}",
-                      getServerSession(), id, path, f, t);
-        }
-        return t.toString();
-    }
-
-    protected void doRename(Buffer buffer, int id) throws IOException {
-        String oldPath = buffer.getString();
-        String newPath = buffer.getString();
-        int flags = 0;
-        if (version >= SftpConstants.SFTP_V5) {
-            flags = buffer.getInt();
-        }
-        try {
-            doRename(id, oldPath, newPath, flags);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doRename(int id, String oldPath, String newPath, int flags) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("doRename({})[id={}] SSH_FXP_RENAME (oldPath={}, newPath={}, flags=0x{})",
-                      getServerSession(), id, oldPath, newPath, Integer.toHexString(flags));
-        }
-
-        Collection<CopyOption> opts = Collections.emptyList();
-        if (flags != 0) {
-            opts = new ArrayList<>();
-            if ((flags & SftpConstants.SSH_FXP_RENAME_ATOMIC) == SftpConstants.SSH_FXP_RENAME_ATOMIC) {
-                opts.add(StandardCopyOption.ATOMIC_MOVE);
-            }
-            if ((flags & SftpConstants.SSH_FXP_RENAME_OVERWRITE) == SftpConstants.SSH_FXP_RENAME_OVERWRITE) {
-                opts.add(StandardCopyOption.REPLACE_EXISTING);
-            }
-        }
-
-        doRename(id, oldPath, newPath, opts);
-    }
-
-    protected void doRename(int id, String oldPath, String newPath, Collection<CopyOption> opts) throws IOException {
-        Path o = resolveFile(oldPath);
-        Path n = resolveFile(newPath);
-        SftpEventListener listener = getSftpEventListenerProxy();
-        ServerSession session = getServerSession();
-
-        listener.moving(session, o, n, opts);
-        try {
-            Files.move(o, n, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : opts.toArray(new CopyOption[opts.size()]));
-        } catch (IOException | RuntimeException e) {
-            listener.moved(session, o, n, opts, e);
-            throw e;
-        }
-        listener.moved(session, o, n, opts, null);
-    }
-
-    // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-7
-    protected void doCopyData(Buffer buffer, int id) throws IOException {
-        String readHandle = buffer.getString();
-        long readOffset = buffer.getLong();
-        long readLength = buffer.getLong();
-        String writeHandle = buffer.getString();
-        long writeOffset = buffer.getLong();
-        try {
-            doCopyData(id, readHandle, readOffset, readLength, writeHandle, writeOffset);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
+    @Override
     @SuppressWarnings("resource")
     protected void doCopyData(int id, String readHandle, long readOffset, long readLength, String writeHandle, long writeOffset) throws IOException {
         boolean inPlaceCopy = readHandle.equals(writeHandle);
@@ -1431,7 +673,7 @@ public class SftpSubsystem
         Path srcPath = srcHandle.getFile();
         int srcAccess = srcHandle.getAccessMask();
         if ((srcAccess & SftpConstants.ACE4_READ_DATA) != SftpConstants.ACE4_READ_DATA) {
-            throw new AccessDeniedException("File not opened for read: " + srcPath);
+            throw new AccessDeniedException(srcPath.toString(), srcPath.toString(), "Source file not opened for read");
         }
 
         ValidateUtils.checkTrue(readLength >= 0L, "Invalid read length: %d", readLength);
@@ -1452,7 +694,7 @@ public class SftpSubsystem
         FileHandle dstHandle = inPlaceCopy ? srcHandle : validateHandle(writeHandle, wh, FileHandle.class);
         int dstAccess = dstHandle.getAccessMask();
         if ((dstAccess & SftpConstants.ACE4_WRITE_DATA) != SftpConstants.ACE4_WRITE_DATA) {
-            throw new AccessDeniedException("File not opened for write: " + srcHandle);
+            throw new AccessDeniedException(srcHandle.toString(), srcHandle.toString(), "Source handle not opened for write");
         }
 
         ValidateUtils.checkTrue(writeOffset >= 0L, "Invalid write offset: %d", writeOffset);
@@ -1488,344 +730,7 @@ public class SftpSubsystem
         }
     }
 
-    // see https://tools.ietf.org/html/draft-ietf-secsh-filexfer-extensions-00#section-6
-    protected void doCopyFile(Buffer buffer, int id) throws IOException {
-        String srcFile = buffer.getString();
-        String dstFile = buffer.getString();
-        boolean overwriteDestination = buffer.getBoolean();
-
-        try {
-            doCopyFile(id, srcFile, dstFile, overwriteDestination);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doCopyFile(int id, String srcFile, String dstFile, boolean overwriteDestination) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("doCopyFile({})[id={}] SSH_FXP_EXTENDED[{}] (src={}, dst={}, overwrite=0x{})",
-                      getServerSession(), id, SftpConstants.EXT_COPY_FILE,
-                      srcFile, dstFile, overwriteDestination);
-        }
-
-        doCopyFile(id, srcFile, dstFile,
-                overwriteDestination
-                        ? Collections.singletonList(StandardCopyOption.REPLACE_EXISTING)
-                        : Collections.emptyList());
-    }
-
-    protected void doCopyFile(int id, String srcFile, String dstFile, Collection<CopyOption> opts) throws IOException {
-        Path src = resolveFile(srcFile);
-        Path dst = resolveFile(dstFile);
-        Files.copy(src, dst, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : opts.toArray(new CopyOption[opts.size()]));
-    }
-
-    protected void doStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL;
-        if (version >= SftpConstants.SFTP_V4) {
-            flags = buffer.getInt();
-        }
-
-        Map<String, Object> attrs;
-        try {
-            attrs = doStat(id, path, flags);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendAttrs(BufferUtils.clear(buffer), id, attrs);
-    }
-
-    protected Map<String, Object> doStat(int id, String path, int flags) throws IOException {
-        if (log.isDebugEnabled()) {
-            log.debug("doStat({})[id={}] SSH_FXP_STAT (path={}, flags=0x{})",
-                      getServerSession(), id, path, Integer.toHexString(flags));
-        }
-
-        /*
-         * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT
-         * follows symbolic links on the server, whereas SSH_FXP_LSTAT does not.
-         */
-        Path p = resolveFile(path);
-        return resolveFileAttributes(p, flags, IoUtils.getLinkOptions(true));
-    }
-
-    protected void doRealPath(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        if (log.isDebugEnabled()) {
-            log.debug("doRealPath({})[id={}] SSH_FXP_REALPATH (path={})", getServerSession(), id, path);
-        }
-        path = GenericUtils.trimToEmpty(path);
-        if (GenericUtils.isEmpty(path)) {
-            path = ".";
-        }
-
-        Map<String, ?> attrs = Collections.emptyMap();
-        Pair<Path, Boolean> result;
-        try {
-            if (version < SftpConstants.SFTP_V6) {
-                /*
-                 * See http://www.openssh.com/txt/draft-ietf-secsh-filexfer-02.txt:
-                 *
-                 *      The SSH_FXP_REALPATH request can be used to have the server
-                 *      canonicalize any given path name to an absolute path.
-                 *
-                 * See also SSHD-294
-                 */
-                Path p = resolveFile(path);
-                LinkOption[] options =
-                    getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
-                result = doRealPathV345(id, path, p, options);
-            } else {
-                /*
-                 * See https://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9
-                 *
-                 *      This field is optional, and if it is not present in the packet, it
-                 *      is assumed to be SSH_FXP_REALPATH_NO_CHECK.
-                 */
-                int control = SftpConstants.SSH_FXP_REALPATH_NO_CHECK;
-                if (buffer.available() > 0) {
-                    control = buffer.getUByte();
-                    if (log.isDebugEnabled()) {
-                        log.debug("doRealPath({}) - control=0x{} for path={}",
-                                  getServerSession(), Integer.toHexString(control), path);
-                    }
-                }
-
-                Collection<String> extraPaths = new LinkedList<>();
-                while (buffer.available() > 0) {
-                    extraPaths.add(buffer.getString());
-                }
-
-                Path p = resolveFile(path);
-                LinkOption[] options =
-                    getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
-                result = doRealPathV6(id, path, extraPaths, p, options);
-
-                p = result.getFirst();
-                options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
-                Boolean status = result.getSecond();
-                switch (control) {
-                    case SftpConstants.SSH_FXP_REALPATH_STAT_IF:
-                        if (status == null) {
-                            attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
-                        } else if (status) {
-                            try {
-                                attrs = getAttributes(p, options);
-                            } catch (IOException e) {
-                                if (log.isDebugEnabled()) {
-                                    log.debug("doRealPath({}) - failed ({}) to retrieve attributes of {}: {}",
-                                              getServerSession(), e.getClass().getSimpleName(), p, e.getMessage());
-                                }
-                                if (log.isTraceEnabled()) {
-                                    log.trace("doRealPath(" + getServerSession() + ")[" + p + "] attributes retrieval failure details", e);
-                                }
-                            }
-                        } else {
-                            if (log.isDebugEnabled()) {
-                                log.debug("doRealPath({}) - dummy attributes for non-existing file: {}", getServerSession(), p);
-                            }
-                        }
-                        break;
-                    case SftpConstants.SSH_FXP_REALPATH_STAT_ALWAYS:
-                        if (status == null) {
-                            attrs = handleUnknownStatusFileAttributes(p, SftpConstants.SSH_FILEXFER_ATTR_ALL, options);
-                        } else if (status) {
-                            attrs = getAttributes(p, options);
-                        } else {
-                            throw new FileNotFoundException(p.toString());
-                        }
-                        break;
-                    case SftpConstants.SSH_FXP_REALPATH_NO_CHECK:
-                        break;
-                    default:
-                        log.warn("doRealPath({}) unknown control value 0x{} for path={}",
-                                 getServerSession(), Integer.toHexString(control), p);
-                }
-            }
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendPath(BufferUtils.clear(buffer), id, result.getFirst(), attrs);
-    }
-
-    protected Pair<Path, Boolean> doRealPathV6(
-            int id, String path, Collection<String> extraPaths, Path p, LinkOption... options) throws IOException {
-        int numExtra = GenericUtils.size(extraPaths);
-        if (numExtra > 0) {
-            if (log.isDebugEnabled()) {
-                log.debug("doRealPathV6({})[id={}] path={}, extra={}",
-                          getServerSession(), id, path, extraPaths);
-            }
-            StringBuilder sb = new StringBuilder(GenericUtils.length(path) + numExtra * 8);
-            sb.append(path);
-
-            for (String p2 : extraPaths) {
-                p = p.resolve(p2);
-                options = getPathResolutionLinkOption(SftpConstants.SSH_FXP_REALPATH, "", p);
-                sb.append('/').append(p2);
-            }
-
-            path = sb.toString();
-        }
-
-        return validateRealPath(id, path, p, options);
-    }
-
-    protected Pair<Path, Boolean> doRealPathV345(int id, String path, Path p, LinkOption... options) throws IOException {
-        return validateRealPath(id, path, p, options);
-    }
-
-    /**
-     * @param id      The request identifier
-     * @param path    The original path
-     * @param f       The resolve {@link Path}
-     * @param options The {@link LinkOption}s to use to verify file existence and access
-     * @return A {@link Pair} whose left-hand is the <U>absolute <B>normalized</B></U>
-     * {@link Path} and right-hand is a {@link Boolean} indicating its status
-     * @throws IOException If failed to validate the file
-     * @see IoUtils#checkFileExists(Path, LinkOption...)
-     */
-    protected Pair<Path, Boolean> validateRealPath(int id, String path, Path f, LinkOption... options) throws IOException {
-        Path p = normalize(f);
-        Boolean status = IoUtils.checkFileExists(p, options);
-        return new Pair<>(p, status);
-    }
-
-    protected void doRemoveDirectory(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        try {
-            doRemoveDirectory(id, path, IoUtils.getLinkOptions(false));
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doRemoveDirectory(int id, String path, LinkOption... options) throws IOException {
-        Path p = resolveFile(path);
-        if (log.isDebugEnabled()) {
-            log.debug("doRemoveDirectory({})[id={}] SSH_FXP_RMDIR (path={})[{}]",
-                      getServerSession(), id, path, p);
-        }
-        if (Files.isDirectory(p, options)) {
-            doRemove(id, p);
-        } else {
-            throw new NotDirectoryException(p.toString());
-        }
-    }
-
-    /**
-     * Called when need to delete a file / directory - also informs the {@link SftpEventListener}
-     *
-     * @param id Deletion request ID
-     * @param p {@link Path} to delete
-     * @throws IOException If failed to delete
-     */
-    protected void doRemove(int id, Path p) throws IOException {
-        SftpEventListener listener = getSftpEventListenerProxy();
-        ServerSession session = getServerSession();
-        listener.removing(session, p);
-        try {
-            Files.delete(p);
-        } catch (IOException | RuntimeException e) {
-            listener.removed(session, p, e);
-            throw e;
-        }
-        listener.removed(session, p, null);
-    }
-
-    protected void doMakeDirectory(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-        try {
-            doMakeDirectory(id, path, attrs, IoUtils.getLinkOptions(false));
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doMakeDirectory(int id, String path, Map<String, ?> attrs, LinkOption... options) throws IOException {
-        Path p = resolveFile(path);
-        if (log.isDebugEnabled()) {
-            log.debug("doMakeDirectory({})[id={}] SSH_FXP_MKDIR (path={}[{}], attrs={})",
-                      getServerSession(), id, path, p, attrs);
-        }
-
-        Boolean status = IoUtils.checkFileExists(p, options);
-        if (status == null) {
-            throw new AccessDeniedException("Cannot validate make-directory existence for " + p);
-        }
-
-        if (status) {
-            if (Files.isDirectory(p, options)) {
-                throw new FileAlreadyExistsException(p.toString(), p.toString(), "Target directory already exists");
-            } else {
-                throw new FileNotFoundException(p.toString() + " already exists as a file");
-            }
-        } else {
-            SftpEventListener listener = getSftpEventListenerProxy();
-            ServerSession session = getServerSession();
-            listener.creating(session, p, attrs);
-            try {
-                Files.createDirectory(p);
-                doSetAttributes(p, attrs);
-            } catch (IOException | RuntimeException e) {
-                listener.created(session, p, attrs, e);
-                throw e;
-            }
-            listener.created(session, p, attrs, null);
-        }
-    }
-
-    protected void doRemove(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        try {
-            /*
-             * If 'filename' is a symbolic link, the link is removed,
-             * not the file it points to.
-             */
-            doRemove(id, path, IoUtils.getLinkOptions(false));
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doRemove(int id, String path, LinkOption... options) throws IOException {
-        Path p = resolveFile(path);
-        if (log.isDebugEnabled()) {
-            log.debug("doRemove({})[id={}] SSH_FXP_REMOVE (path={}[{}])",
-                      getServerSession(), id, path, p);
-        }
-
-        Boolean status = IoUtils.checkFileExists(p, options);
-        if (status == null) {
-            throw new AccessDeniedException("Cannot determine existence of remove candidate: " + p);
-        }
-        if (!status) {
-            throw new FileNotFoundException(p.toString());
-        } else if (Files.isDirectory(p, options)) {
-            throw new SftpException(SftpConstants.SSH_FX_FILE_IS_A_DIRECTORY, p.toString() + " is a folder");
-        } else {
-            doRemove(id, p);
-        }
-    }
-
+    @Override
     protected void doReadDir(Buffer buffer, int id) throws IOException {
         String handle = buffer.getString();
         Handle h = handles.get(handle);
@@ -1847,15 +752,15 @@ public class SftpSubsystem
                 getPathResolutionLinkOption(SftpConstants.SSH_FXP_READDIR, "", file);
             Boolean status = IoUtils.checkFileExists(file, options);
             if (status == null) {
-                throw new AccessDeniedException("Cannot determine existence of read-dir for " + file);
+                throw new AccessDeniedException(file.toString(), file.toString(), "Cannot determine existence of read-dir");
             }
 
             if (!status) {
-                throw new FileNotFoundException(file.toString());
+                throw new NoSuchFileException(file.toString(), file.toString(), "Non-existant directory");
             } else if (!Files.isDirectory(file, options)) {
                 throw new NotDirectoryException(file.toString());
             } else if (!Files.isReadable(file)) {
-                throw new AccessDeniedException("Not readable: " + file.toString());
+                throw new AccessDeniedException(file.toString(), file.toString(), "Not readable");
             }
 
             if (dh.isSendDot() || dh.isSendDotDot() || dh.hasNext()) {
@@ -1892,47 +797,26 @@ public class SftpSubsystem
 
             Objects.requireNonNull(reply, "No reply buffer created");
         } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
+            sendStatus(BufferUtils.clear(buffer), id, e, SftpConstants.SSH_FXP_READDIR, handle);
             return;
         }
 
         send(reply);
     }
 
-    protected void doOpenDir(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        String handle;
-
-        try {
-            Path p = resolveNormalizedLocation(path);
-            if (log.isDebugEnabled()) {
-                log.debug("doOpenDir({})[id={}] SSH_FXP_OPENDIR (path={})[{}]",
-                          getServerSession(), id, path, p);
-            }
-
-            LinkOption[] options =
-                getPathResolutionLinkOption(SftpConstants.SSH_FXP_OPENDIR, "", p);
-            handle = doOpenDir(id, path, p, options);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendHandle(BufferUtils.clear(buffer), id, handle);
-    }
-
+    @Override
     protected String doOpenDir(int id, String path, Path p, LinkOption... options) throws IOException {
         Boolean status = IoUtils.checkFileExists(p, options);
         if (status == null) {
-            throw new AccessDeniedException("Cannot determine open-dir existence for " + p);
+            throw new AccessDeniedException(p.toString(), p.toString(), "Cannot determine open-dir existence");
         }
 
         if (!status) {
-            throw new FileNotFoundException(path);
+            throw new NoSuchFileException(path, path, "Referenced target directory N/A");
         } else if (!Files.isDirectory(p, options)) {
             throw new NotDirectoryException(path);
         } else if (!Files.isReadable(p)) {
-            throw new AccessDeniedException("Not readable: " + p);
+            throw new AccessDeniedException(p.toString(), p.toString(), "Not readable");
         } else {
             String handle = generateFileHandle(p);
             DirectoryHandle dirHandle = new DirectoryHandle(this, p, handle);
@@ -1941,19 +825,7 @@ public class SftpSubsystem
         }
     }
 
-    protected void doFSetStat(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-        try {
-            doFSetStat(id, handle, attrs);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
+    @Override
     protected void doFSetStat(int id, String handle, Map<String, ?> attrs) throws IOException {
         Handle h = handles.get(handle);
         if (log.isDebugEnabled()) {
@@ -1964,102 +836,19 @@ public class SftpSubsystem
         doSetAttributes(validateHandle(handle, h, Handle.class).getFile(), attrs);
     }
 
-    protected void doSetStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        Map<String, Object> attrs = readAttrs(buffer);
-        try {
-            doSetStat(id, path, attrs);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
-    }
-
-    protected void doSetStat(int id, String path, Map<String, ?> attrs) throws IOException {
+    @Override
+    protected Map<String, Object> doFStat(int id, String handle, int flags) throws IOException {
+        Handle h = handles.get(handle);
         if (log.isDebugEnabled()) {
-            log.debug("doSetStat({})[id={}] SSH_FXP_SETSTAT (path={}, attrs={})",
-                      getServerSession(), id, path, attrs);
+            log.debug("doFStat({})[id={}] SSH_FXP_FSTAT (handle={}[{}], flags=0x{})",
+                      getServerSession(), id, handle, h, Integer.toHexString(flags));
         }
-        Path p = resolveFile(path);
-        doSetAttributes(p, attrs);
-    }
-
-    protected void doFStat(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL;
-        if (version >= SftpConstants.SFTP_V4) {
-            flags = buffer.getInt();
-        }
-
-        Map<String, ?> attrs;
-        try {
-            attrs = doFStat(id, handle, flags);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendAttrs(BufferUtils.clear(buffer), id, attrs);
-    }
-
-    protected Map<String, Object> doFStat(int id, String handle, int flags) throws IOException {
-        Handle h = handles.get(handle);
-        if (log.isDebugEnabled()) {
-            log.debug("doFStat({})[id={}] SSH_FXP_FSTAT (handle={}[{}], flags=0x{})",
-                      getServerSession(), id, handle, h, Integer.toHexString(flags));
-        }
-
-        return resolveFileAttributes(validateHandle(handle, h, Handle.class).getFile(), flags, IoUtils.getLinkOptions(true));
-    }
-
-    protected void doLStat(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        int flags = SftpConstants.SSH_FILEXFER_ATTR_ALL;
-        if (version >= SftpConstants.SFTP_V4) {
-            flags = buffer.getInt();
-        }
-
-        Map<String, ?> attrs;
-        try {
-            attrs = doLStat(id, path, flags);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendAttrs(BufferUtils.clear(buffer), id, attrs);
-    }
-
-    protected Map<String, Object> doLStat(int id, String path, int flags) throws IOException {
-        Path p = resolveFile(path);
-        if (log.isDebugEnabled()) {
-            log.debug("doLStat({})[id={}] SSH_FXP_LSTAT (path={}[{}], flags=0x{})",
-                      getServerSession(), id, path, p, Integer.toHexString(flags));
-        }
-
-        /*
-         * SSH_FXP_STAT and SSH_FXP_LSTAT only differ in that SSH_FXP_STAT
-         * follows symbolic links on the server, whereas SSH_FXP_LSTAT does not.
-         */
-        return resolveFileAttributes(p, flags, IoUtils.getLinkOptions(false));
-    }
-
-    protected void doWrite(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        int length = buffer.getInt();
-        try {
-            doWrite(id, handle, offset, length, buffer.array(), buffer.rpos(), buffer.available());
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
+
+        Handle fileHandle = validateHandle(handle, h, Handle.class);
+        return resolveFileAttributes(fileHandle.getFile(), flags, IoUtils.getLinkOptions(true));
     }
 
+    @Override
     protected void doWrite(int id, String handle, long offset, int length, byte[] data, int doff, int remaining) throws IOException {
         Handle h = handles.get(handle);
         if (log.isTraceEnabled()) {
@@ -2091,43 +880,7 @@ public class SftpSubsystem
         listener.written(getServerSession(), handle, fh, offset, data, doff, length, null);
     }
 
-    protected void doRead(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        long offset = buffer.getLong();
-        int requestedLength = buffer.getInt();
-        int maxAllowed = getServerSession().getIntProperty(MAX_READDATA_PACKET_LENGTH_PROP, DEFAULT_MAX_READDATA_PACKET_LENGTH);
-        int readLen = Math.min(requestedLength, maxAllowed);
-        if (log.isTraceEnabled()) {
-            log.trace("doRead({})[id={}]({})[offset={}] - req={}, max={}, effective={}",
-                      getServerSession(), id, handle, offset, requestedLength, maxAllowed, readLen);
-        }
-
-        try {
-            ValidateUtils.checkTrue(readLen >= 0, "Illegal requested read length: %d", readLen);
-
-            buffer.clear();
-            buffer.ensureCapacity(readLen + Long.SIZE /* the header */, IntUnaryOperator.identity());
-
-            buffer.putByte((byte) SftpConstants.SSH_FXP_DATA);
-            buffer.putInt(id);
-            int lenPos = buffer.wpos();
-            buffer.putInt(0);
-
-            int startPos = buffer.wpos();
-            int len = doRead(id, handle, offset, readLen, buffer.array(), startPos);
-            if (len < 0) {
-                throw new EOFException("Unable to read " + readLen + " bytes from offset=" + offset + " of " + handle);
-            }
-            buffer.wpos(startPos + len);
-            BufferUtils.updateLengthPlaceholder(buffer, lenPos, len);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        send(buffer);
-    }
-
+    @Override
     protected int doRead(int id, String handle, long offset, int length, byte[] data, int doff) throws IOException {
         Handle h = handles.get(handle);
         if (log.isTraceEnabled()) {
@@ -2151,18 +904,7 @@ public class SftpSubsystem
         return readLen;
     }
 
-    protected void doClose(Buffer buffer, int id) throws IOException {
-        String handle = buffer.getString();
-        try {
-            doClose(id, handle);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "", "");
-    }
-
+    @Override
     protected void doClose(int id, String handle) throws IOException {
         Handle h = handles.remove(handle);
         if (log.isDebugEnabled()) {
@@ -2175,81 +917,7 @@ public class SftpSubsystem
         listener.close(getServerSession(), handle, h);
     }
 
-    protected void doOpen(Buffer buffer, int id) throws IOException {
-        String path = buffer.getString();
-        /*
-         * Be consistent with FileChannel#open - if no mode specified then READ is assumed
-         */
-        int access = 0;
-        if (version >= SftpConstants.SFTP_V5) {
-            access = buffer.getInt();
-            if (access == 0) {
-                access = SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
-            }
-        }
-
-        int pflags = buffer.getInt();
-        if (pflags == 0) {
-            pflags = SftpConstants.SSH_FXF_READ;
-        }
-
-        if (version < SftpConstants.SFTP_V5) {
-            int flags = pflags;
-            pflags = 0;
-            switch (flags & (SftpConstants.SSH_FXF_READ | SftpConstants.SSH_FXF_WRITE)) {
-                case SftpConstants.SSH_FXF_READ:
-                    access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
-                    break;
-                case SftpConstants.SSH_FXF_WRITE:
-                    access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES;
-                    break;
-                default:
-                    access |= SftpConstants.ACE4_READ_DATA | SftpConstants.ACE4_READ_ATTRIBUTES;
-                    access |= SftpConstants.ACE4_WRITE_DATA | SftpConstants.ACE4_WRITE_ATTRIBUTES;
-                    break;
-            }
-            if ((flags & SftpConstants.SSH_FXF_APPEND) != 0) {
-                access |= SftpConstants.ACE4_APPEND_DATA;
-                pflags |= SftpConstants.SSH_FXF_APPEND_DATA | SftpConstants.SSH_FXF_APPEND_DATA_ATOMIC;
-            }
-            if ((flags & SftpConstants.SSH_FXF_CREAT) != 0) {
-                if ((flags & SftpConstants.SSH_FXF_EXCL) != 0) {
-                    pflags |= SftpConstants.SSH_FXF_CREATE_NEW;
-                } else if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) {
-                    pflags |= SftpConstants.SSH_FXF_CREATE_TRUNCATE;
-                } else {
-                    pflags |= SftpConstants.SSH_FXF_OPEN_OR_CREATE;
-                }
-            } else {
-                if ((flags & SftpConstants.SSH_FXF_TRUNC) != 0) {
-                    pflags |= SftpConstants.SSH_FXF_TRUNCATE_EXISTING;
-                } else {
-                    pflags |= SftpConstants.SSH_FXF_OPEN_EXISTING;
-                }
-            }
-        }
-
-        Map<String, Object> attrs = readAttrs(buffer);
-        String handle;
-        try {
-            handle = doOpen(id, path, pflags, access, attrs);
-        } catch (IOException | RuntimeException e) {
-            sendStatus(BufferUtils.clear(buffer), id, e);
-            return;
-        }
-
-        sendHandle(BufferUtils.clear(buffer), id, handle);
-    }
-
-    /**
-     * @param id     Request id
-     * @param path   Path
-     * @param pflags Open mode flags - see {@code SSH_FXF_XXX} flags
-     * @param access Access mode flags - see {@code ACE4_XXX} flags
-     * @param attrs  Requested attributes
-     * @return The assigned (opaque) handle
-     * @throws IOException if failed to execute
-     */
+    @Override
     protected String doOpen(int id, String path, int pflags, int access, Map<String, Object> attrs) throws IOException {
         if (log.isDebugEnabled()) {
             log.debug("doOpen({})[id={}] SSH_FXP_OPEN (path={}, access=0x{}, pflags=0x{}, attrs={})",
@@ -2320,1068 +988,7 @@ public class SftpSubsystem
         send(buffer);
     }
 
-    protected void appendExtensions(Buffer buffer, String supportedVersions) {
-        appendVersionsExtension(buffer, supportedVersions);
-        appendNewlineExtension(buffer, resolveNewlineValue(getServerSession()));
-        appendVendorIdExtension(buffer, VersionProperties.getVersionProperties());
-        appendOpenSSHExtensions(buffer);
-        appendAclSupportedExtension(buffer);
-
-        Map<String, OptionalFeature> extensions = getSupportedClientExtensions();
-        int numExtensions = GenericUtils.size(extensions);
-        List<String> extras = (numExtensions <= 0) ? Collections.emptyList() : new ArrayList<>(numExtensions);
-        if (numExtensions > 0) {
-            ServerSession session = getServerSession();
-            extensions.forEach((name, f) -> {
-                if (!f.isSupported()) {
-                    if (log.isDebugEnabled()) {
-                        log.debug("appendExtensions({}) skip unsupported extension={}", session, name);
-                    }
-                    return;
-                }
-
-                extras.add(name);
-            });
-        }
-        appendSupportedExtension(buffer, extras);
-        appendSupported2Extension(buffer, extras);
-    }
-
-    protected int appendAclSupportedExtension(Buffer buffer) {
-        ServerSession session = getServerSession();
-        Collection<Integer> maskValues = resolveAclSupportedCapabilities(session);
-        int mask = AclSupportedParser.AclCapabilities.constructAclCapabilities(maskValues);
-        if (mask != 0) {
-            if (log.isTraceEnabled()) {
-                log.trace("appendAclSupportedExtension({}) capabilities={}",
-                          session, AclSupportedParser.AclCapabilities.decodeAclCapabilities(mask));
-            }
-
-            buffer.putString(SftpConstants.EXT_ACL_SUPPORTED);
-
-            // placeholder for length
-            int lenPos = buffer.wpos();
-            buffer.putInt(0);
-            buffer.putInt(mask);
-            BufferUtils.updateLengthPlaceholder(buffer, lenPos);
-        }
-
-        return mask;
-    }
-
-    protected Collection<Integer> resolveAclSupportedCapabilities(ServerSession session) {
-        String override = session.getString(ACL_SUPPORTED_MASK_PROP);
-        if (override == null) {
-            return DEFAULT_ACL_SUPPORTED_MASK;
-        }
-
-        // empty means not supported
-        if (log.isDebugEnabled()) {
-            log.debug("resolveAclSupportedCapabilities({}) override='{}'", session, override);
-        }
-
-        if (override.length() == 0) {
-            return Collections.emptySet();
-        }
-
-        String[] names = GenericUtils.split(override, ',');
-        Set<Integer> maskValues = new HashSet<>(names.length);
-        for (String n : names) {
-            Integer v = ValidateUtils.checkNotNull(
-                    AclSupportedParser.AclCapabilities.getAclCapabilityValue(n), "Unknown ACL capability: %s", n);
-            maskValues.add(v);
-        }
-
-        return maskValues;
-    }
-
-    protected List<OpenSSHExtension> appendOpenSSHExtensions(Buffer buffer) {
-        List<OpenSSHExtension> extList = resolveOpenSSHExtensions(getServerSession());
-        if (GenericUtils.isEmpty(extList)) {
-            return extList;
-        }
-
-        for (OpenSSHExtension ext : extList) {
-            buffer.putString(ext.getName());
-            buffer.putString(ext.getVersion());
-        }
-
-        return extList;
-    }
-
-    protected List<OpenSSHExtension> resolveOpenSSHExtensions(ServerSession session) {
-        String value = session.getString(OPENSSH_EXTENSIONS_PROP);
-        if (value == null) {    // No override
-            return DEFAULT_OPEN_SSH_EXTENSIONS;
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("resolveOpenSSHExtensions({}) override='{}'", session, value);
-        }
-
-        String[] pairs = GenericUtils.split(value, ',');
-        int numExts = GenericUtils.length(pairs);
-        if (numExts <= 0) {     // User does not want to report ANY extensions
-            return Collections.emptyList();
-        }
-
-        List<OpenSSHExtension> extList = new ArrayList<>(numExts);
-        for (String nvp : pairs) {
-            nvp = GenericUtils.trimToEmpty(nvp);
-            if (GenericUtils.isEmpty(nvp)) {
-                continue;
-            }
-
-            int pos = nvp.indexOf('=');
-            ValidateUtils.checkTrue((pos > 0) && (pos < (nvp.length() - 1)), "Malformed OpenSSH extension spec: %s", nvp);
-            String name = GenericUtils.trimToEmpty(nvp.substring(0, pos));
-            String version = GenericUtils.trimToEmpty(nvp.substring(pos + 1));
-            extList.add(new OpenSSHExtension(name, ValidateUtils.checkNotNullAndNotEmpty(version, "No version specified for OpenSSH extension %s", name)));
-        }
-
-        return extList;
-    }
-
-    protected Map<String, OptionalFeature> getSupportedClientExtensions() {
-        ServerSession session = getServerSession();
-        String value = session.getString(CLIENT_EXTENSIONS_PROP);
-        if (value == null) {
-            return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS;
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("getSupportedClientExtensions({}) override='{}'", session, value);
-        }
-
-        if (value.length() <= 0) {  // means don't report any extensions
-            return Collections.emptyMap();
-        }
-
-        if (value.indexOf(',') <= 0) {
-            return Collections.singletonMap(value, OptionalFeature.TRUE);
-        }
-
-        String[] comps = GenericUtils.split(value, ',');
-        Map<String, OptionalFeature> result = new LinkedHashMap<>(comps.length);
-        for (String c : comps) {
-            result.put(c, OptionalFeature.TRUE);
-        }
-
-        return result;
-    }
-
-    /**
-     * Appends the &quot;versions&quot; extension to the buffer. <B>Note:</B>
-     * if overriding this method make sure you either do not append anything
-     * or use the correct extension name
-     *
-     * @param buffer The {@link Buffer} to append to
-     * @param value  The recommended value - ignored if {@code null}/empty
-     * @see SftpConstants#EXT_VERSIONS
-     */
-    protected void appendVersionsExtension(Buffer buffer, String value) {
-        if (GenericUtils.isEmpty(value)) {
-            return;
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("appendVersionsExtension({}) value={}", getServerSession(), value);
-        }
-
-        buffer.putString(SftpConstants.EXT_VERSIONS);
-        buffer.putString(value);
-    }
-
-    /**
-     * Appends the &quot;newline&quot; extension to the buffer. <B>Note:</B>
-     * if overriding this method make sure you either do not append anything
-     * or use the correct extension name
-     *
-     * @param buffer The {@link Buffer} to append to
-     * @param value  The recommended value - ignored if {@code null}/empty
-     * @see SftpConstants#EXT_NEWLINE
-     */
-    protected void appendNewlineExtension(Buffer buffer, String value) {
-        if (GenericUtils.isEmpty(value)) {
-            return;
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("appendNewlineExtension({}) value={}",
-                      getServerSession(), BufferUtils.toHex(':', value.getBytes(StandardCharsets.UTF_8)));
-        }
-
-        buffer.putString(SftpConstants.EXT_NEWLINE);
-        buffer.putString(value);
-    }
-
-    protected String resolveNewlineValue(ServerSession session) {
-        String value = session.getString(NEWLINE_VALUE);
-        if (value == null) {
-            return IoUtils.EOL;
-        } else {
-            return value;   // empty means disabled
-        }
-    }
-
-    /**
-     * Appends the &quot;vendor-id&quot; extension to the buffer. <B>Note:</B>
-     * if overriding this method make sure you either do not append anything
-     * or use the correct extension name
-     *
-     * @param buffer            The {@link Buffer} to append to
-     * @param versionProperties The currently available version properties - ignored
-     *                          if {@code null}/empty. The code expects the following values:
-     *                          <UL>
-     *                              <LI>{@code groupId} - as the vendor name</LI>
-     *                              <LI>{@code artifactId} - as the product name</LI>
-     *                              <LI>{@code version} - as the product version</LI>
-     *                          </UL>
-     * @see SftpConstants#EXT_VENDOR_ID
-     * @see <A HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt">DRAFT 09 - section 4.4</A>
-     */
-    protected void appendVendorIdExtension(Buffer buffer, Map<String, ?> versionProperties) {
-        if (GenericUtils.isEmpty(versionProperties)) {
-            return;
-        }
-
-        if (log.isDebugEnabled()) {
-            log.debug("appendVendorIdExtension({}): {}", getServerSession(), versionProperties);
-        }
-        buffer.putString(SftpConstants.EXT_VENDOR_ID);
-
-        PropertyResolver resolver = PropertyResolverUtils.toPropertyResolver(Collections.unmodifiableMap(versionProperties));
-        // placeholder for length
-        int lenPos = buffer.wpos();
-        buffer.putInt(0);
-        buffer.putString(resolver.getStringProperty("groupId", getClass().getPackage().getName()));   // vendor-name
-        buffer.putString(resolver.getStringProperty("artifactId", getClass().getSimpleName()));       // product-name
-        buffer.putString(resolver.getStringProperty("version", FactoryManager.DEFAULT_VERSION));      // product-version
-        buffer.putLong(0L); // product-build-number
-        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
-    }
-
-    /**
-     * Appends the &quot;supported&quot; extension to the buffer. <B>Note:</B>
-     * if overriding this method make sure you either do not append anything
-     * or use the correct extension name
-     *
-     * @param buffer The {@link Buffer} to append to
-     * @param extras The extra extensions that are available and can be reported
-     *               - may be {@code null}/empty
-     */
-    protected void appendSupportedExtension(Buffer buffer, Collection<String> extras) {
-        buffer.putString(SftpConstants.EXT_SUPPORTED);
-
-        int lenPos = buffer.wpos();
-        buffer.putInt(0); // length placeholder
-        // supported-attribute-mask
-        buffer.putInt(SftpConstants.SSH_FILEXFER_ATTR_SIZE | SftpConstants.SSH_FILEXFER_ATTR_PERMISSIONS
-                | SftpConstants.SSH_FILEXFER_ATTR_ACCESSTIME | SftpConstants.SSH_FILEXFER_ATTR_CREATETIME
-                | SftpConstants.SSH_FILEXFER_ATTR_MODIFYTIME | SftpCo

<TRUNCATED>