[SSHD-665] allow disable some features in SftpSubsystem (sshd-core)
diff --git a/pom.xml b/pom.xml
index e2f2d61..92dc6c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,7 +99,7 @@
         <min.required.maven.version>3.0</min.required.maven.version>
 
         <bouncycastle.version>1.54</bouncycastle.version>
-        <slf4j.version>1.7.16</slf4j.version>
+        <slf4j.version>1.7.21</slf4j.version>
         <spring.version>3.0.6.RELEASE</spring.version>
         <jgit.version>3.4.1.201406201815-r</jgit.version>
         <junit.version>4.12</junit.version>
@@ -627,8 +627,10 @@
                                     </module>
                                     <!--<module name="RedundantThrows" />-->
                                     <module name="PackageDeclaration" />
+                                    <!-- see http://checkstyle.sourceforge.net/config_coding.html#ReturnCount -->
                                     <module name="ReturnCount">
                                         <property name="max" value="15" />
+                                        <property name="maxForVoid" value="15" />
                                     </module>
 
                                     <module name="IllegalType">
@@ -745,7 +747,7 @@
 						<dependency>
 							<groupId>com.puppycrawl.tools</groupId>
 							<artifactId>checkstyle</artifactId>
-							<version>6.15</version>
+							<version>6.19</version>
 							<exclusions>
 								<!-- MCHECKSTYLE-156 -->
 								<exclusion>
@@ -985,6 +987,7 @@
         <module>sshd-core</module>
         <module>sshd-ldap</module>
         <module>sshd-git</module>
+        <module>sshd-contrib</module>
         <module>assembly</module>
     </modules>
 </project>
diff --git a/sshd-contrib/pom.xml b/sshd-contrib/pom.xml
new file mode 100644
index 0000000..b403737
--- /dev/null
+++ b/sshd-contrib/pom.xml
@@ -0,0 +1,93 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+
+    <!--
+
+        Licensed to the Apache Software Foundation (ASF) under one or more
+        contributor license agreements.  See the NOTICE file distributed with
+        this work for additional information regarding copyright ownership.
+        The ASF licenses this file to You under the Apache License, Version 2.0
+        (the "License"); you may not use this file except in compliance with
+        the License.  You may obtain a copy of the License at
+
+           http://www.apache.org/licenses/LICENSE-2.0
+
+        Unless required by applicable law or agreed to in writing, software
+        distributed under the License is distributed on an "AS IS" BASIS,
+        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+        See the License for the specific language governing permissions and
+        limitations under the License.
+    -->
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.apache.sshd</groupId>
+        <artifactId>sshd</artifactId>
+        <version>1.2.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sshd-contrib</artifactId>
+    <name>Apache Mina SSHD :: Contributions</name>
+    <packaging>jar</packaging>
+    <inceptionYear>2016</inceptionYear>
+
+    <properties>
+        <projectRoot>${basedir}/..</projectRoot>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sshd</groupId>
+            <artifactId>sshd-core</artifactId>
+            <version>${project.version}</version>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-log4j12</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <additionalparam>-Xdoclint:none</additionalparam>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java b/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java
new file mode 100644
index 0000000..527b48d
--- /dev/null
+++ b/sshd-contrib/src/main/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListener.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.scp;
+
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.scp.AbstractScpTransferEventListenerAdapter;
+
+/**
+ * Provides a simple access control by making a distinction between methods
+ * that upload data and ones that download it
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class SimpleAccessControlScpEventListener extends AbstractScpTransferEventListenerAdapter {
+    public static final SimpleAccessControlScpEventListener READ_ONLY_ACCESSOR =
+        new SimpleAccessControlScpEventListener() {
+            @Override
+            protected boolean isFileUploadAllowed(Path path) throws IOException {
+                return false;
+            }
+
+            @Override
+            protected boolean isFileDownloadAllowed(Path path) throws IOException {
+                return true;
+            }
+    };
+
+    protected SimpleAccessControlScpEventListener() {
+        super();
+    }
+
+    @Override
+    public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms)
+            throws IOException {
+        super.startFileEvent(op, file, length, perms);
+        switch(op) {
+            case SEND:
+                if (!isFileDownloadAllowed(file)) {
+                    throw new AccessDeniedException(file.toString());
+                }
+                break;
+
+            case RECEIVE:
+                if (!isFileUploadAllowed(file)) {
+                    throw new AccessDeniedException(file.toString());
+                }
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown file operation: " + op);
+        }
+    }
+
+    @Override
+    public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+        super.startFolderEvent(op, file, perms);
+        switch(op) {
+            case SEND:
+                if (!isFileDownloadAllowed(file)) {
+                    throw new AccessDeniedException(file.toString());
+                }
+                break;
+
+            case RECEIVE:
+                if (!isFileUploadAllowed(file)) {
+                    throw new AccessDeniedException(file.toString());
+                }
+                break;
+            default:
+                throw new UnsupportedOperationException("Unknown file operation: " + op);
+        }
+    }
+
+    /**
+     * @param path The local file/folder path
+     * @return {@code true} if client is allowed to read from the specified local path
+     * @throws IOException If failed to handle the call
+     */
+    protected abstract boolean isFileDownloadAllowed(Path path) throws IOException;
+
+    /**
+     * @param path The local file/folder path
+     * @return {@code true} if client is allowed to write to the specified local path
+     * @throws IOException If failed to handle the call
+     */
+    protected abstract boolean isFileUploadAllowed(Path path) throws IOException;
+}
diff --git a/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java b/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java
new file mode 100644
index 0000000..68fdb82
--- /dev/null
+++ b/sshd-contrib/src/main/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListener.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.subsystem.sftp;
+
+import java.io.IOException;
+import java.nio.file.AccessDeniedException;
+import java.nio.file.CopyOption;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.sshd.server.session.ServerSession;
+
+/**
+ * Provides a simple access control by making a distinction between methods
+ * that provide information - including reading data - and those that modify it
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class SimpleAccessControlSftpEventListener extends AbstractSftpEventListenerAdapter {
+    public static final SimpleAccessControlSftpEventListener READ_ONLY_ACCESSOR =
+        new SimpleAccessControlSftpEventListener() {
+            @Override
+            protected boolean isAccessAllowed(ServerSession session, String remoteHandle, Path localPath)
+                    throws IOException {
+                return true;
+            }
+
+            @Override
+            protected boolean isModificationAllowed(ServerSession session, String remoteHandle, Path localPath)
+                    throws IOException {
+                return false;
+            }
+    };
+
+    protected SimpleAccessControlSftpEventListener() {
+        super();
+    }
+
+    @Override
+    public void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries)
+            throws IOException {
+        super.read(session, remoteHandle, localHandle, entries);
+        if (!isAccessAllowed(session, remoteHandle, localHandle)) {
+            throw new AccessDeniedException(remoteHandle);
+        }
+    }
+
+    @Override
+    public void reading(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data,
+            int dataOffset, int dataLen) throws IOException {
+        super.reading(session, remoteHandle, localHandle, offset, data, dataOffset, dataLen);
+        if (!isAccessAllowed(session, remoteHandle, localHandle)) {
+            throw new AccessDeniedException(remoteHandle);
+        }
+    }
+
+    /**
+     * @param session The {@link ServerSession} throw which the request was made
+     * @param remoteHandle The remote handle value
+     * @param localHandle The local handle
+     * @return {@code true} if allowed to access the handle
+     * @throws IOException If failed to handle the call
+     */
+    protected boolean isAccessAllowed(ServerSession session, String remoteHandle, Handle localHandle) throws IOException {
+        return isAccessAllowed(session, remoteHandle, localHandle.getFile());
+    }
+
+    /**
+     * @param session The {@link ServerSession} throw which the request was made
+     * @param remoteHandle The remote handle value
+     * @param localPath The local {@link Path}
+     * @return {@code true} if allowed to access the path
+     * @throws IOException If failed to handle the call
+     */
+    protected abstract boolean isAccessAllowed(ServerSession session, String remoteHandle, Path localPath) throws IOException;
+
+    @Override
+    public void writing(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data,
+            int dataOffset, int dataLen) throws IOException {
+        super.writing(session, remoteHandle, localHandle, offset, data, dataOffset, dataLen);
+        if (!isModificationAllowed(session, remoteHandle, localHandle.getFile())) {
+            throw new AccessDeniedException(remoteHandle);
+        }
+    }
+
+    @Override
+    public void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask) throws IOException {
+        super.blocking(session, remoteHandle, localHandle, offset, length, mask);
+        if (!isModificationAllowed(session, remoteHandle, localHandle.getFile())) {
+            throw new AccessDeniedException(remoteHandle);
+        }
+    }
+
+    @Override
+    public void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length)
+            throws IOException {
+        super.unblocking(session, remoteHandle, localHandle, offset, length);
+        if (!isModificationAllowed(session, remoteHandle, localHandle.getFile())) {
+            throw new AccessDeniedException(remoteHandle);
+        }
+    }
+
+    @Override
+    public void creating(ServerSession session, Path path, Map<String, ?> attrs) throws IOException {
+        super.creating(session, path, attrs);
+        if (!isModificationAllowed(session, path.toString(), path)) {
+            throw new AccessDeniedException(path.toString());
+        }
+    }
+
+    @Override
+    public void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts)
+            throws IOException {
+        super.moving(session, srcPath, dstPath, opts);
+        if (!isModificationAllowed(session, srcPath.toString(), srcPath)) {
+            throw new AccessDeniedException(srcPath.toString());
+        }
+    }
+
+    @Override
+    public void removing(ServerSession session, Path path) throws IOException {
+        super.removing(session, path);
+        if (!isModificationAllowed(session, path.toString(), path)) {
+            throw new AccessDeniedException(path.toString());
+        }
+    }
+
+    @Override
+    public void linking(ServerSession session, Path source, Path target, boolean symLink) throws IOException {
+        super.linking(session, source, target, symLink);
+        if (!isModificationAllowed(session, source.toString(), source)) {
+            throw new AccessDeniedException(source.toString());
+        }
+    }
+
+    @Override
+    public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) throws IOException {
+        super.modifyingAttributes(session, path, attrs);
+        if (!isModificationAllowed(session, path.toString(), path)) {
+            throw new AccessDeniedException(path.toString());
+        }
+    }
+
+    /**
+     * @param session The {@link ServerSession} throw which the request was made
+     * @param remoteHandle The remote handle value
+     * @param localPath The local {@link Path}
+     * @return {@code true} if allowed to modify the path
+     * @throws IOException If failed to handle the call
+     */
+    protected abstract boolean isModificationAllowed(ServerSession session, String remoteHandle, Path localPath) throws IOException;
+
+}
diff --git a/sshd-contrib/src/main/resources/.gitignore b/sshd-contrib/src/main/resources/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sshd-contrib/src/main/resources/.gitignore
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
new file mode 100644
index 0000000..70bcd44
--- /dev/null
+++ b/sshd-contrib/src/test/java/org/apache/sshd/server/scp/SimpleAccessControlScpEventListenerTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.scp;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.EnumSet;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.scp.ScpClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.scp.ScpException;
+import org.apache.sshd.common.scp.ScpHelper;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SimpleAccessControlScpEventListenerTest extends BaseTestSupport {
+    private SshServer sshd;
+    private int port;
+    private final FileSystemFactory fileSystemFactory;
+
+    public SimpleAccessControlScpEventListenerTest() {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd = setupTestServer();
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.setFileSystemFactory(fileSystemFactory);
+        sshd.start();
+        port = sshd.getPort();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            try {
+                sshd.stop(true);
+            } finally {
+                sshd = null;
+            }
+        }
+    }
+
+    @Test
+    public void testReadOnlyScpTransferEventListener() throws Exception {
+        sshd.setCommandFactory(new ScpCommandFactory.Builder()
+                .addEventListener(SimpleAccessControlScpEventListener.READ_ONLY_ACCESSOR)
+                .build());
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                ScpClient scp = session.createScpClient();
+                Path targetPath = detectTargetFolder();
+                Path parentPath = targetPath.getParent();
+                Path scpRoot = Utils.resolve(targetPath, ScpHelper.SCP_COMMAND_PREFIX, getClass().getSimpleName(), getCurrentTestName());
+                Utils.deleteRecursive(scpRoot);
+
+                Path remoteDir = assertHierarchyTargetFolderExists(scpRoot.resolve("remote"));
+                Path remoteFile = remoteDir.resolve("file.txt");
+                String remotePath = Utils.resolveRelativeRemotePath(parentPath, remoteFile);
+                byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+                Files.write(remoteFile, data);
+                byte[] downloaded = scp.downloadBytes(remotePath);
+                assertArrayEquals("Mismatched downloaded data", data, downloaded);
+
+                try {
+                    scp.upload(data, remotePath, EnumSet.allOf(PosixFilePermission.class), null);
+                    fail("Unexpected upload success");
+                } catch (ScpException e) {
+                    // expected - ignored
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListenerTest.java b/sshd-contrib/src/test/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListenerTest.java
new file mode 100644
index 0000000..1a75871
--- /dev/null
+++ b/sshd-contrib/src/test/java/org/apache/sshd/server/subsystem/sftp/SimpleAccessControlSftpEventListenerTest.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.server.subsystem.sftp;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.client.subsystem.sftp.SftpClient;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.CloseableHandle;
+import org.apache.sshd.client.subsystem.sftp.SftpClient.OpenMode;
+import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.file.FileSystemFactory;
+import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.server.scp.ScpCommandFactory;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class SimpleAccessControlSftpEventListenerTest extends BaseTestSupport {
+    private SshServer sshd;
+    private int port;
+    private final FileSystemFactory fileSystemFactory;
+
+    public SimpleAccessControlSftpEventListenerTest() {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        fileSystemFactory = new VirtualFileSystemFactory(parentPath);
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        sshd = setupTestServer();
+        SftpSubsystemFactory.Builder builder =
+                new SftpSubsystemFactory.Builder();
+        builder.addSftpEventListener(SimpleAccessControlSftpEventListener.READ_ONLY_ACCESSOR);
+        sshd.setSubsystemFactories(
+                Collections.<NamedFactory<Command>>singletonList(builder.build()));
+        sshd.setCommandFactory(new ScpCommandFactory());
+        sshd.setFileSystemFactory(fileSystemFactory);
+        sshd.start();
+        port = sshd.getPort();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sshd != null) {
+            try {
+                sshd.stop(true);
+            } finally {
+                sshd = null;
+            }
+        }
+    }
+
+    @Test
+    public void testReadOnlyFileAccess() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Path testFile = assertHierarchyTargetFolderExists(lclSftp).resolve("file.txt");
+        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+        Files.deleteIfExists(testFile);
+        Files.write(testFile, data);
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try (SftpClient sftp = session.createSftpClient()) {
+                    String file = Utils.resolveRelativeRemotePath(parentPath, testFile);
+                    try (CloseableHandle handle = sftp.open(file, OpenMode.Read)) {
+                        byte[] actual = new byte[data.length];
+                        int readLen = sftp.read(handle, 0L, actual);
+                        assertEquals("Mismatched read data length", data.length, readLen);
+                        assertArrayEquals("Mismatched read file contents", data, actual);
+                    }
+
+                    try (CloseableHandle handle = sftp.open(file, OpenMode.Create, OpenMode.Write, OpenMode.Read, OpenMode.Append)) {
+                        sftp.write(handle, 0L, data);
+                        fail("Unexpected file write success");
+                    } catch (SftpException e) {
+                        int status = e.getStatus();
+                        assertEquals("Unexpected write SFTP status code", SftpConstants.SSH_FX_PERMISSION_DENIED, status);
+                    }
+
+                    SftpClient.Attributes attrs = sftp.stat(file);
+                    attrs.modifyTime(System.currentTimeMillis());
+                    try {
+                        sftp.setStat(file, attrs);
+                        fail("Unexpected attributes modification success");
+                    } catch (SftpException e) {
+                        int status = e.getStatus();
+                        assertEquals("Unexpected setAttributes SFTP status code", SftpConstants.SSH_FX_PERMISSION_DENIED, status);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
+    public void testReadOnlyDirectoryAccess() throws Exception {
+        Path targetPath = detectTargetFolder();
+        Path parentPath = targetPath.getParent();
+        Path lclSftp = Utils.resolve(targetPath, SftpConstants.SFTP_SUBSYSTEM_NAME, getClass().getSimpleName(), getCurrentTestName());
+        Path testFile = assertHierarchyTargetFolderExists(lclSftp).resolve("file.txt");
+        byte[] data = (getClass().getName() + "#" + getCurrentTestName()).getBytes(StandardCharsets.UTF_8);
+        Files.deleteIfExists(testFile);
+        Files.write(testFile, data);
+
+        try (SshClient client = setupTestClient()) {
+            client.start();
+
+            try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try (SftpClient sftp = session.createSftpClient()) {
+                    String folder = Utils.resolveRelativeRemotePath(parentPath, targetPath);
+                    for (SftpClient.DirEntry entry : sftp.readDir(folder)) {
+                        assertNotNull("No entry", entry);
+                    }
+
+                    String file = Utils.resolveRelativeRemotePath(parentPath, testFile);
+                    try {
+                        sftp.remove(file);
+                        fail("Unexpected file remove success");
+                    } catch (SftpException e) {
+                        int status = e.getStatus();
+                        assertEquals("Unexpected remove SFTP status code", SftpConstants.SSH_FX_PERMISSION_DENIED, status);
+                    }
+
+                    try {
+                        sftp.mkdir(folder + "/writeAttempt");
+                        fail("Unexpected folder creation success");
+                    } catch (SftpException e) {
+                        int status = e.getStatus();
+                        assertEquals("Unexpected mkdir SFTP status code", SftpConstants.SSH_FX_PERMISSION_DENIED, status);
+                    }
+
+                    try {
+                        sftp.rmdir(folder);
+                        fail("Unexpected folder creation success");
+                    } catch (SftpException e) {
+                        int status = e.getStatus();
+                        assertEquals("Unexpected rmdir SFTP status code", SftpConstants.SSH_FX_PERMISSION_DENIED, status);
+                    }
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+}
diff --git a/sshd-contrib/src/test/resources/log4j.properties b/sshd-contrib/src/test/resources/log4j.properties
new file mode 100644
index 0000000..18ea57c
--- /dev/null
+++ b/sshd-contrib/src/test/resources/log4j.properties
@@ -0,0 +1,38 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed  under the  License is distributed on an "AS IS" BASIS,
+# WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
+# implied.
+#
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+
+#
+# The logging properties used during tests..
+#
+log4j.rootLogger=INFO, stdout, logfile
+#log4j.logger.org.apache.sshd=TRACE
+#log4j.logger.org.apache.sshd.common.channel.Window=DEBUG
+
+# CONSOLE appender
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n
+
+# File appender
+log4j.appender.logfile=org.apache.log4j.FileAppender
+log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
+log4j.appender.logfile.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c{1} - %m%n
+log4j.appender.logfile.file=target/sshd-contrib-tests.log
+log4j.appender.logfile.append=true
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
index 0f74a97..6c20da3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/AbstractSftpClient.java
@@ -52,10 +52,11 @@
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
  */
 public abstract class AbstractSftpClient extends AbstractSubsystemClient implements SftpClient, RawSftpClient {
+    private final Attributes fileOpenAttributes = new Attributes();
     private final AtomicReference<Map<String, Object>> parsedExtensionsHolder = new AtomicReference<>(null);
 
     protected AbstractSftpClient() {
-        super();
+        fileOpenAttributes.setType(SftpConstants.SSH_FILEXFER_TYPE_REGULAR);
     }
 
     @Override
@@ -700,7 +701,7 @@
             }
         }
         buffer.putInt(mode);
-        writeAttributes(buffer, new Attributes());
+        writeAttributes(buffer, fileOpenAttributes);
 
         CloseableHandle handle = new DefaultCloseableHandle(this, path, checkHandle(SftpConstants.SSH_FXP_OPEN, buffer));
         if (log.isTraceEnabled()) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
new file mode 100644
index 0000000..6273dfd
--- /dev/null
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/AbstractScpTransferEventListenerAdapter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.scp;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.Set;
+
+import org.apache.sshd.common.util.logging.AbstractLoggingBean;
+
+/**
+ * A no-op implementation of {@link ScpTransferEventListener} for those who wish to
+ * implement only a small number of methods. By default, all non-overridden methods
+ * simply log at TRACE level their invocation parameters
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractScpTransferEventListenerAdapter
+        extends AbstractLoggingBean
+        implements ScpTransferEventListener {
+    protected AbstractScpTransferEventListenerAdapter() {
+        super();
+    }
+
+    @Override
+    public void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms)
+            throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("startFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")");
+        }
+    }
+
+    @Override
+    public void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown)
+            throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("endFileEvent(op=" + op + ", file=" + file + ", length=" + length + ", permissions=" + perms + ")"
+                    + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+
+        }
+    }
+
+    @Override
+    public void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("startFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")");
+        }
+    }
+
+    @Override
+    public void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown)
+            throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("endFolderEvent(op=" + op + ", file=" + file + ", permissions=" + perms + ")"
+                    + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+
+        }
+    }
+}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
index 09ea724..1b14e2b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpHelper.java
@@ -303,8 +303,8 @@
         ack();
 
         time = null;
+        listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
         try {
-            listener.startFolderEvent(FileOperation.RECEIVE, path, perms);
             for (;;) {
                 header = readLine();
                 if (log.isDebugEnabled()) {
@@ -330,6 +330,7 @@
             listener.endFolderEvent(FileOperation.RECEIVE, path, perms, e);
             throw e;
         }
+        listener.endFolderEvent(FileOperation.RECEIVE, path, perms, null);
     }
 
     public void receiveFile(String header, Path local, ScpTimestamp time, boolean preserve, int bufferSize) throws IOException {
@@ -383,14 +384,14 @@
             ack();
 
             Path file = resolver.getEventListenerFilePath();
+            listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
             try {
-                listener.startFileEvent(FileOperation.RECEIVE, file, length, perms);
                 IoUtils.copy(is, os, bufSize);
-                listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
             } catch (IOException | RuntimeException e) {
                 listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, e);
                 throw e;
             }
+            listener.endFileEvent(FileOperation.RECEIVE, file, length, perms, null);
         }
 
         resolver.postProcessReceivedData(name, preserve, perms, time);
@@ -625,14 +626,14 @@
 
         try (InputStream in = resolver.resolveSourceStream(getSession())) {
             Path path = resolver.getEventListenerFilePath();
+            listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
             try {
-                listener.startFileEvent(FileOperation.SEND, path, fileSize, perms);
                 IoUtils.copy(in, out, bufSize);
-                listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
             } catch (IOException | RuntimeException e) {
                 listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, e);
                 throw e;
             }
+            listener.endFileEvent(FileOperation.SEND, path, fileSize, perms, null);
         }
         ack();
 
@@ -728,12 +729,12 @@
                         sendDir(child, preserve, bufferSize);
                     }
                 }
-
-                listener.endFolderEvent(FileOperation.SEND, path, perms, null);
             } catch (IOException | RuntimeException e) {
                 listener.endFolderEvent(FileOperation.SEND, path, perms, e);
                 throw e;
             }
+
+            listener.endFolderEvent(FileOperation.SEND, path, perms, null);
         }
 
         if (log.isDebugEnabled()) {
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
index 1763b9f..ebe2787 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/scp/ScpTransferEventListener.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.common.scp;
 
+import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.attribute.PosixFilePermission;
 import java.util.EventListener;
@@ -69,8 +70,9 @@
      * @param length Size (in bytes) of transfered data
      * @param perms  A {@link Set} of {@link PosixFilePermission}s to be applied
      *               once transfer is complete
+     * @throws IOException If failed to handle the event
      */
-    void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms);
+    void startFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms) throws IOException;
 
     /**
      * @param op     The {@link FileOperation}
@@ -80,16 +82,18 @@
      *               once transfer is complete
      * @param thrown The result of the operation attempt - if {@code null} then
      *               reception was successful
+     * @throws IOException If failed to handle the event
      */
-    void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown);
+    void endFileEvent(FileOperation op, Path file, long length, Set<PosixFilePermission> perms, Throwable thrown) throws IOException;
 
     /**
      * @param op    The {@link FileOperation}
      * @param file  The <U>local</U> referenced folder {@link Path}
      * @param perms A {@link Set} of {@link PosixFilePermission}s to be applied
      *              once transfer is complete
+     * @throws IOException If failed to handle the event
      */
-    void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms);
+    void startFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms) throws IOException;
 
     /**
      * @param op     The {@link FileOperation}
@@ -98,7 +102,7 @@
      *               once transfer is complete
      * @param thrown The result of the operation attempt - if {@code null} then
      *               reception was successful
+     * @throws IOException If failed to handle the event
      */
-    void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown);
-
+    void endFolderEvent(FileOperation op, Path file, Set<PosixFilePermission> perms, Throwable thrown) throws IOException;
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
index f15f5ca..3e0aa2a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpHelper.java
@@ -468,6 +468,16 @@
         }
     }
 
+    public static String resolveStatusMessage(Throwable t) {
+        if (t == null) {
+            return "";
+        } else if (t instanceof SftpException) {
+            return t.toString();
+        } else {
+            return "Internal " + t.getClass().getSimpleName() + ": " + t.getMessage();
+        }
+    }
+
     public static Map<String, Object> readAttrs(Buffer buffer, int version) {
         Map<String, Object> attrs = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER);
         int flags = buffer.getInt();
@@ -483,12 +493,16 @@
                 case SftpConstants.SSH_FILEXFER_TYPE_SYMLINK:
                     attrs.put("isSymbolicLink", Boolean.TRUE);
                     break;
-                case SftpConstants.SSH_FILEXFER_TYPE_UNKNOWN:
+                case SftpConstants.SSH_FILEXFER_TYPE_SOCKET:
+                case SftpConstants.SSH_FILEXFER_TYPE_CHAR_DEVICE:
+                case SftpConstants.SSH_FILEXFER_TYPE_BLOCK_DEVICE:
+                case SftpConstants.SSH_FILEXFER_TYPE_FIFO:
                     attrs.put("isOther", Boolean.TRUE);
                     break;
                 default:    // ignored
             }
         }
+
         if ((flags & SftpConstants.SSH_FILEXFER_ATTR_SIZE) != 0) {
             attrs.put("size", buffer.getLong());
         }
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
index ff0e1b3..cd3ddf6 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/io/IoUtils.java
@@ -31,6 +31,7 @@
 import java.nio.file.LinkOption;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
+import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.UserPrincipal;
 import java.util.Arrays;
@@ -53,6 +54,7 @@
     public static final OpenOption[] EMPTY_OPEN_OPTIONS = new OpenOption[0];
     public static final CopyOption[] EMPTY_COPY_OPTIONS = new CopyOption[0];
     public static final LinkOption[] EMPTY_LINK_OPTIONS = new LinkOption[0];
+    public static final FileAttribute<?>[] EMPTY_FILE_ATTRIBUTES = new FileAttribute<?>[0];
 
     public static final List<String> WINDOWS_EXECUTABLE_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(".bat", ".exe", ".cmd"));
 
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerAdapter.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerAdapter.java
index 4c6cf66..a3063f8 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerAdapter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/AbstractSftpEventListenerAdapter.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.server.subsystem.sftp;
 
+import java.io.IOException;
 import java.nio.file.CopyOption;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -63,7 +64,8 @@
     }
 
     @Override
-    public void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries) {
+    public void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries)
+            throws IOException {
         int numEntries = GenericUtils.size(entries);
         if (log.isDebugEnabled()) {
             log.debug("read(" + session + ")[" + localHandle.getFile() + "] " + numEntries + " entries");
@@ -77,23 +79,47 @@
     }
 
     @Override
-    public void read(ServerSession session, String remoteHandle, FileHandle localHandle,
-                     long offset, byte[] data, int dataOffset, int dataLen, int readLen) {
+    public void reading(ServerSession session, String remoteHandle, FileHandle localHandle,
+                     long offset, byte[] data, int dataOffset, int dataLen)
+                        throws IOException {
         if (log.isTraceEnabled()) {
-            log.trace("read(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen + ", read=" + readLen);
+            log.trace("reading(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen);
         }
     }
 
     @Override
-    public void write(ServerSession session, String remoteHandle, FileHandle localHandle,
-                      long offset, byte[] data, int dataOffset, int dataLen) {
+    public void read(ServerSession session, String remoteHandle, FileHandle localHandle,
+                     long offset, byte[] data, int dataOffset, int dataLen, int readLen, Throwable thrown)
+                        throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("read(" + session + ")[" + localHandle.getFile() + "] offset=" + offset
+                    + ", requested=" + dataLen + ", read=" + readLen
+                    + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+        }
+    }
+
+    @Override
+    public void writing(ServerSession session, String remoteHandle, FileHandle localHandle,
+                      long offset, byte[] data, int dataOffset, int dataLen)
+                              throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("write(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen);
         }
     }
 
     @Override
-    public void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask) {
+    public void written(ServerSession session, String remoteHandle, FileHandle localHandle,
+                      long offset, byte[] data, int dataOffset, int dataLen, Throwable thrown)
+                              throws IOException {
+        if (log.isTraceEnabled()) {
+            log.trace("written(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen
+                    + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
+        }
+    }
+
+    @Override
+    public void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("blocking(" + session + ")[" + localHandle.getFile() + "]"
                    + " offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask));
@@ -102,7 +128,8 @@
 
     @Override
     public void blocked(ServerSession session, String remoteHandle, FileHandle localHandle,
-                        long offset, long length, int mask, Throwable thrown) {
+                        long offset, long length, int mask, Throwable thrown)
+                                throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("blocked(" + session + ")[" + localHandle.getFile() + "]"
                     + " offset=" + offset + ", length=" + length + ", mask=0x" + Integer.toHexString(mask)
@@ -111,7 +138,8 @@
     }
 
     @Override
-    public void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length) {
+    public void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("unblocking(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", length=" + length);
         }
@@ -119,10 +147,11 @@
 
     @Override
     public void unblocked(ServerSession session, String remoteHandle, FileHandle localHandle,
-                          long offset, long length, Boolean result, Throwable thrown) {
+                          long offset, long length, Throwable thrown)
+                                  throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("unblocked(" + session + ")[" + localHandle.getFile() + "]"
-                    + " offset=" + offset + ", length=" + length + ", result=" + result
+                    + " offset=" + offset + ", length=" + length
                     + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
         }
     }
@@ -136,14 +165,16 @@
     }
 
     @Override
-    public void creating(ServerSession session, Path path, Map<String, ?> attrs) {
+    public void creating(ServerSession session, Path path, Map<String, ?> attrs)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("creating(" + session + ") " + (Files.isDirectory(path) ? "directory" : "file") + " " + path);
         }
     }
 
     @Override
-    public void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
+    public void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("created(" + session + ") " + (Files.isDirectory(path) ? "directory" : "file") + " " + path
                    + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
@@ -151,14 +182,16 @@
     }
 
     @Override
-    public void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts) {
+    public void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("moving(" + session + ")[" + opts + "]" + srcPath + " => " + dstPath);
         }
     }
 
     @Override
-    public void moved(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts, Throwable thrown) {
+    public void moved(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts, Throwable thrown)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("moved(" + session + ")[" + opts + "]" + srcPath + " => " + dstPath
                     + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
@@ -166,14 +199,16 @@
     }
 
     @Override
-    public void removing(ServerSession session, Path path) {
+    public void removing(ServerSession session, Path path)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("removing(" + session + ") " + path);
         }
     }
 
     @Override
-    public void removed(ServerSession session, Path path, Throwable thrown) {
+    public void removed(ServerSession session, Path path, Throwable thrown)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("removed(" + session + ") " + path
                   + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
@@ -181,14 +216,16 @@
     }
 
     @Override
-    public void linking(ServerSession session, Path source, Path target, boolean symLink) {
+    public void linking(ServerSession session, Path source, Path target, boolean symLink)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("linking(" + session + ")[" + symLink + "]" + source + " => " + target);
         }
     }
 
     @Override
-    public void linked(ServerSession session, Path source, Path target, boolean symLink, Throwable thrown) {
+    public void linked(ServerSession session, Path source, Path target, boolean symLink, Throwable thrown)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("linked(" + session + ")[" + symLink + "]" + source + " => " + target
                     + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
@@ -196,14 +233,16 @@
     }
 
     @Override
-    public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs) {
+    public void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("modifyingAttributes(" + session + ") " + path + ": " + attrs);
         }
     }
 
     @Override
-    public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown) {
+    public void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown)
+            throws IOException {
         if (log.isTraceEnabled()) {
             log.trace("modifiedAttributes(" + session + ") " + path
                   + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
index faed6a0..9299151 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/FileHandle.java
@@ -22,18 +22,22 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
-import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.FileAttribute;
 import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.Collection;
+import java.util.EnumSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.SftpException;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.io.IoUtils;
 
 
 /**
@@ -50,14 +54,16 @@
         super(file);
         this.access = access;
 
-        Set<OpenOption> options = new HashSet<>();
+        Set<StandardOpenOption> options = EnumSet.noneOf(StandardOpenOption.class);
         if (((access & SftpConstants.ACE4_READ_DATA) != 0) || ((access & SftpConstants.ACE4_READ_ATTRIBUTES) != 0)) {
             options.add(StandardOpenOption.READ);
         }
         if (((access & SftpConstants.ACE4_WRITE_DATA) != 0) || ((access & SftpConstants.ACE4_WRITE_ATTRIBUTES) != 0)) {
             options.add(StandardOpenOption.WRITE);
         }
-        switch (flags & SftpConstants.SSH_FXF_ACCESS_DISPOSITION) {
+
+        int accessDisposition = flags & SftpConstants.SSH_FXF_ACCESS_DISPOSITION;
+        switch (accessDisposition) {
             case SftpConstants.SSH_FXF_CREATE_NEW:
                 options.add(StandardOpenOption.CREATE_NEW);
                 break;
@@ -78,26 +84,25 @@
         if ((flags & SftpConstants.SSH_FXF_APPEND_DATA) != 0) {
             options.add(StandardOpenOption.APPEND);
         }
-        FileAttribute<?>[] attributes = new FileAttribute<?>[attrs.size()];
-        int index = 0;
-        for (Map.Entry<String, Object> attr : attrs.entrySet()) {
-            final String key = attr.getKey();
-            final Object val = attr.getValue();
-            attributes[index++] = new FileAttribute<Object>() {
-                @Override
-                public String name() {
-                    return key;
-                }
 
-                @Override
-                public Object value() {
-                    return val;
-                }
-            };
+        Collection<FileAttribute<?>> attributes = null;
+        for (Map.Entry<String, Object> attr : attrs.entrySet()) {
+            FileAttribute<?> fileAttr = toFileAttribute(attr.getKey(), attr.getValue());
+            if (fileAttr == null) {
+                continue;
+            }
+            if (attributes == null) {
+                attributes = new LinkedList<>();
+            }
+            attributes.add(fileAttr);
         }
+
+        FileAttribute<?>[] fileAttrs = GenericUtils.isEmpty(attributes)
+                ? IoUtils.EMPTY_FILE_ATTRIBUTES
+                : attributes.toArray(new FileAttribute<?>[attributes.size()]);
         FileChannel channel;
         try {
-            channel = FileChannel.open(file, options, attributes);
+            channel = FileChannel.open(file, options, fileAttrs);
         } catch (UnsupportedOperationException e) {
             channel = FileChannel.open(file, options);
             sftpSubsystem.doSetAttributes(file, attrs);
@@ -171,27 +176,67 @@
         FileChannel channel = getFileChannel();
         long size = (length == 0L) ? channel.size() - offset : length;
         FileLock lock = channel.tryLock(offset, size, false);
+        if (lock == null) {
+            throw new SftpException(SftpConstants.SSH_FX_BYTE_RANGE_LOCK_REFUSED,
+                "Overlapping lock held by another program on range [" + offset + "-" + (offset + length));
+        }
+
         synchronized (locks) {
             locks.add(lock);
         }
     }
 
-    public boolean unlock(long offset, long length) throws IOException {
+    public void unlock(long offset, long length) throws IOException {
         FileChannel channel = getFileChannel();
-        long size = (length == 0) ? channel.size() - offset : length;
+        long size = (length == 0L) ? channel.size() - offset : length;
         FileLock lock = null;
         for (Iterator<FileLock> iterator = locks.iterator(); iterator.hasNext();) {
             FileLock l = iterator.next();
-            if (l.position() == offset && l.size() == size) {
+            if ((l.position() == offset) && (l.size() == size)) {
                 iterator.remove();
                 lock = l;
                 break;
             }
         }
-        if (lock != null) {
-            lock.release();
-            return true;
+        if (lock == null) {
+            throw new SftpException(SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK,
+                    "No mtahcing lock found on range [" + offset + "-" + (offset + length));
         }
-        return false;
+
+        lock.release();
+    }
+
+    public static FileAttribute<?> toFileAttribute(final String key, final Object val) {
+        // Some ignored attributes sent by the SFTP client
+        if ("isOther".equals(key)) {
+            if (((Boolean) val).booleanValue()) {
+                throw new IllegalArgumentException("Not allowed to use " + key + "=" + val);
+            }
+            return null;
+        } else if ("isRegular".equals(key)) {
+            if (!((Boolean) val).booleanValue()) {
+                throw new IllegalArgumentException("Not allowed to use " + key + "=" + val);
+            }
+            return null;
+        }
+
+        return new FileAttribute<Object>() {
+            private final String s = key + "=" + val;
+
+            @Override
+            public String name() {
+                return key;
+            }
+
+            @Override
+            public Object value() {
+                return val;
+            }
+
+            @Override
+            public String toString() {
+                return s;
+            }
+        };
     }
 }
diff --git a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
index a52f820..d026b4a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
+++ b/sshd-core/src/main/java/org/apache/sshd/server/subsystem/sftp/SftpEventListener.java
@@ -19,6 +19,7 @@
 
 package org.apache.sshd.server.subsystem.sftp;
 
+import java.io.IOException;
 import java.nio.file.CopyOption;
 import java.nio.file.Path;
 import java.util.Collection;
@@ -56,8 +57,10 @@
      * @param session      The {@link ServerSession} through which the request was handled
      * @param remoteHandle The (opaque) assigned handle for the file / directory
      * @param localHandle  The associated file / directory {@link Handle}
+     * @throws IOException If failed to handle the call
      */
-    void open(ServerSession session, String remoteHandle, Handle localHandle);
+    void open(ServerSession session, String remoteHandle, Handle localHandle)
+            throws IOException;
 
     /**
      * Result of reading entries from a directory - <B>Note:</B> it may be a
@@ -69,8 +72,25 @@
      * @param localHandle  The associated {@link DirectoryHandle}
      * @param entries      A {@link Map} of the listed entries - key = short name,
      *                     value = {@link Path} of the sub-entry
+     * @throws IOException If failed to handle the call
      */
-    void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries);
+    void read(ServerSession session, String remoteHandle, DirectoryHandle localHandle, Map<String, Path> entries)
+            throws IOException;
+
+    /**
+     * Preparing to read from a file
+     *
+     * @param session      The {@link ServerSession} through which the request was handled
+     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file from which to read
+     * @param data         Buffer holding the read data
+     * @param dataOffset   Offset of read data in buffer
+     * @param dataLen      Requested read length
+     * @throws IOException If failed to handle the call
+     */
+    void reading(ServerSession session, String remoteHandle, FileHandle localHandle,
+            long offset, byte[] data, int dataOffset, int dataLen) throws IOException;
 
     /**
      * Result of reading from a file
@@ -82,13 +102,16 @@
      * @param data         Buffer holding the read data
      * @param dataOffset   Offset of read data in buffer
      * @param dataLen      Requested read length
-     * @param readLen      Actual read length
+     * @param readLen      Actual read length - negative if thrown exception provided
+     * @param thrown       Non-{@code null} if read failed due to this exception
+     * @throws IOException If failed to handle the call
      */
     void read(ServerSession session, String remoteHandle, FileHandle localHandle,
-              long offset, byte[] data, int dataOffset, int dataLen, int readLen);
+              long offset, byte[] data, int dataOffset, int dataLen, int readLen, Throwable thrown)
+                      throws IOException;
 
     /**
-     * Result of writing to a file
+     * Preparing to write to file
      *
      * @param session      The {@link ServerSession} through which the request was handled
      * @param remoteHandle The (opaque) assigned handle for the file
@@ -97,9 +120,28 @@
      * @param data         Buffer holding the written data
      * @param dataOffset   Offset of write data in buffer
      * @param dataLen      Requested write length
+     * @throws IOException If failed to handle the call
      */
-    void write(ServerSession session, String remoteHandle, FileHandle localHandle,
-               long offset, byte[] data, int dataOffset, int dataLen);
+    void writing(ServerSession session, String remoteHandle, FileHandle localHandle,
+               long offset, byte[] data, int dataOffset, int dataLen)
+                       throws IOException;
+
+    /**
+     * Finished to writing to file
+     *
+     * @param session      The {@link ServerSession} through which the request was handled
+     * @param remoteHandle The (opaque) assigned handle for the file
+     * @param localHandle  The associated {@link FileHandle}
+     * @param offset       Offset in file to which to write
+     * @param data         Buffer holding the written data
+     * @param dataOffset   Offset of write data in buffer
+     * @param dataLen      Requested write length
+     * @param thrown       The reason for failing to write - {@code null} if successful
+     * @throws IOException If failed to handle the call
+     */
+    void written(ServerSession session, String remoteHandle, FileHandle localHandle,
+               long offset, byte[] data, int dataOffset, int dataLen, Throwable thrown)
+                       throws IOException;
 
     /**
      * Called <U>prior</U> to blocking a file section
@@ -110,9 +152,11 @@
      * @param offset       Offset in file for locking
      * @param length       Section size for locking
      * @param mask         Lock mask flags - see {@code SSH_FXP_BLOCK} message
+     * @throws IOException If failed to handle the call
      * @see #blocked(ServerSession, String, FileHandle, long, long, int, Throwable)
      */
-    void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask);
+    void blocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask)
+            throws IOException;
 
     /**
      * Called <U>after</U> blocking a file section
@@ -124,8 +168,10 @@
      * @param length       Section size for locking
      * @param mask         Lock mask flags - see {@code SSH_FXP_BLOCK} message
      * @param thrown       If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void blocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask, Throwable thrown);
+    void blocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, int mask, Throwable thrown)
+            throws IOException;
 
     /**
      * Called <U>prior</U> to un-blocking a file section
@@ -135,8 +181,10 @@
      * @param localHandle  The associated {@link FileHandle}
      * @param offset       Offset in file for un-locking
      * @param length       Section size for un-locking
+     * @throws IOException If failed to handle the call
      */
-    void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length);
+    void unblocking(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length)
+            throws IOException;
 
     /**
      * Called <U>prior</U> to un-blocking a file section
@@ -146,11 +194,11 @@
      * @param localHandle  The associated {@link FileHandle}
      * @param offset       Offset in file for un-locking
      * @param length       Section size for un-locking
-     * @param result       If successful (i.e., <tt>thrown</tt> is {@code null}, then whether
-     *                     section was un-blocked
      * @param thrown       If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void unblocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, Boolean result, Throwable thrown);
+    void unblocked(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, long length, Throwable thrown)
+            throws IOException;
 
     /**
      * Specified file / directory has been closed
@@ -167,9 +215,11 @@
      * @param session The {@link ServerSession} through which the request was handled
      * @param path    Directory {@link Path} to be created
      * @param attrs   Requested associated attributes to set
+     * @throws IOException If failed to handle the call
      * @see #created(ServerSession, Path, Map, Throwable)
      */
-    void creating(ServerSession session, Path path, Map<String, ?> attrs);
+    void creating(ServerSession session, Path path, Map<String, ?> attrs)
+            throws IOException;
 
     /**
      * Called <U>after</U> creating a directory
@@ -178,8 +228,10 @@
      * @param path    Directory {@link Path} to be created
      * @param attrs   Requested associated attributes to set
      * @param thrown  If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown);
+    void created(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown)
+            throws IOException;
 
     /**
      * Called <U>prior</U> to renaming a file / directory
@@ -188,9 +240,11 @@
      * @param srcPath The source {@link Path}
      * @param dstPath The target {@link Path}
      * @param opts    The resolved renaming options
+     * @throws IOException If failed to handle the call
      * @see #moved(ServerSession, Path, Path, Collection, Throwable)
      */
-    void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts);
+    void moving(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts)
+            throws IOException;
 
     /**
      * Called <U>after</U> renaming a file / directory
@@ -200,17 +254,20 @@
      * @param dstPath The target {@link Path}
      * @param opts    The resolved renaming options
      * @param thrown  If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void moved(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts, Throwable thrown);
+    void moved(ServerSession session, Path srcPath, Path dstPath, Collection<CopyOption> opts, Throwable thrown)
+            throws IOException;
 
     /**
      * Called <U>prior</U> to removing a file / directory
      *
      * @param session The {@link ServerSession} through which the request was handled
      * @param path    The {@link Path} about to be removed
+     * @throws IOException If failed to handle the call
      * @see #removed(ServerSession, Path, Throwable)
      */
-    void removing(ServerSession session, Path path);
+    void removing(ServerSession session, Path path) throws IOException;
 
     /**
      * Called <U>after</U> a file / directory has been removed
@@ -218,8 +275,9 @@
      * @param session The {@link ServerSession} through which the request was handled
      * @param path    The {@link Path} to be removed
      * @param thrown  If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void removed(ServerSession session, Path path, Throwable thrown);
+    void removed(ServerSession session, Path path, Throwable thrown) throws IOException;
 
     /**
      * Called <U>prior</U> to creating a link
@@ -228,9 +286,11 @@
      * @param source  The source {@link Path}
      * @param target  The target {@link Path}
      * @param symLink {@code true} = symbolic link
+     * @throws IOException If failed to handle the call
      * @see #linked(ServerSession, Path, Path, boolean, Throwable)
      */
-    void linking(ServerSession session, Path source, Path target, boolean symLink);
+    void linking(ServerSession session, Path source, Path target, boolean symLink)
+            throws IOException;
 
     /**
      * Called <U>after</U> creating a link
@@ -240,8 +300,10 @@
      * @param target  The target {@link Path}
      * @param symLink {@code true} = symbolic link
      * @param thrown  If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void linked(ServerSession session, Path source, Path target, boolean symLink, Throwable thrown);
+    void linked(ServerSession session, Path source, Path target, boolean symLink, Throwable thrown)
+            throws IOException;
 
     /**
      * Called <U>prior</U> to modifying the attributes of a file / directory
@@ -250,9 +312,11 @@
      * @param path    The file / directory {@link Path} to be modified
      * @param attrs   The attributes {@link Map} - names and values depend on the
      *                O/S, view, type, etc...
+     * @throws IOException If failed to handle the call
      * @see #modifiedAttributes(ServerSession, Path, Map, Throwable)
      */
-    void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs);
+    void modifyingAttributes(ServerSession session, Path path, Map<String, ?> attrs)
+            throws IOException;
 
     /**
      * Called <U>after</U> modifying the attributes of a file / directory
@@ -262,6 +326,8 @@
      * @param attrs   The attributes {@link Map} - names and values depend on the
      *                O/S, view, type, etc...
      * @param thrown  If not-{@code null} then the reason for the failure to execute
+     * @throws IOException If failed to handle the call
      */
-    void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown);
+    void modifiedAttributes(ServerSession session, Path path, Map<String, ?> attrs, Throwable thrown)
+            throws IOException;
 }
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 40ff2a2..8fe54d2 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
@@ -1197,29 +1197,28 @@
         listener.blocking(session, handle, fileHandle, offset, length, mask);
         try {
             fileHandle.lock(offset, length, mask);
-            listener.blocked(session, handle, fileHandle, offset, length, mask, null);
         } catch (IOException | RuntimeException e) {
             listener.blocked(session, handle, fileHandle, offset, length, mask, e);
             throw e;
         }
+        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();
-        boolean found;
         try {
-            found = doUnblock(id, handle, offset, length);
+            doUnblock(id, handle, offset, length);
         } catch (IOException | RuntimeException e) {
             sendStatus(BufferUtils.clear(buffer), id, e);
             return;
         }
 
-        sendStatus(BufferUtils.clear(buffer), id, found ? SftpConstants.SSH_FX_OK : SftpConstants.SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK, "");
+        sendStatus(BufferUtils.clear(buffer), id, SftpConstants.SSH_FX_OK, "");
     }
 
-    protected boolean doUnblock(int id, String handle, long offset, long length) throws IOException {
+    protected void doUnblock(int id, String handle, long offset, long length) throws IOException {
         Handle p = handles.get(handle);
         if (log.isDebugEnabled()) {
             log.debug("doUnblock({})[id={}] SSH_FXP_UNBLOCK (handle={}[{}], offset={}, length={})",
@@ -1231,13 +1230,12 @@
         ServerSession session = getServerSession();
         listener.unblocking(session, handle, fileHandle, offset, length);
         try {
-            boolean result = fileHandle.unlock(offset, length);
-            listener.unblocked(session, handle, fileHandle, offset, length, Boolean.valueOf(result), null);
-            return result;
+            fileHandle.unlock(offset, length);
         } catch (IOException | RuntimeException e) {
-            listener.unblocked(session, handle, fileHandle, offset, length, null, e);
+            listener.unblocked(session, handle, fileHandle, offset, length, e);
             throw e;
         }
+        listener.unblocked(session, handle, fileHandle, offset, length, null);
     }
 
     protected void doLink(Buffer buffer, int id) throws IOException {
@@ -1302,11 +1300,11 @@
             } else {
                 Files.createLink(link, existing);
             }
-            listener.linked(session, link, existing, symLink, null);
         } 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 {
@@ -1382,11 +1380,11 @@
         listener.moving(session, o, n, opts);
         try {
             Files.move(o, n, GenericUtils.isEmpty(opts) ? IoUtils.EMPTY_COPY_OPTIONS : opts.toArray(new CopyOption[opts.size()]));
-            listener.moved(session, o, n, opts, null);
         } 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
@@ -1716,11 +1714,11 @@
         listener.removing(session, p);
         try {
             Files.delete(p);
-            listener.removed(session, p, null);
         } 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 {
@@ -1761,11 +1759,11 @@
             try {
                 Files.createDirectory(p);
                 doSetAttributes(p, attrs);
-                listener.created(session, p, attrs, null);
             } catch (IOException | RuntimeException e) {
                 listener.created(session, p, attrs, e);
                 throw e;
             }
+            listener.created(session, p, attrs, null);
         }
     }
 
@@ -2043,14 +2041,19 @@
             throw new IllegalStateException("Not enough buffer data for writing to " + fh + ": required=" + length + ", available=" + remaining);
         }
 
-        if (fh.isOpenAppend()) {
-            fh.append(data, doff, length);
-        } else {
-            fh.write(data, doff, length, offset);
-        }
-
         SftpEventListener listener = getSftpEventListenerProxy();
-        listener.write(getServerSession(), handle, fh, offset, data, doff, length);
+        listener.writing(getServerSession(), handle, fh, offset, data, doff, length);
+        try {
+            if (fh.isOpenAppend()) {
+                fh.append(data, doff, length);
+            } else {
+                fh.write(data, doff, length, offset);
+            }
+        } catch (IOException | RuntimeException e) {
+            listener.written(getServerSession(), handle, fh, offset, data, doff, length, e);
+            throw e;
+        }
+        listener.written(getServerSession(), handle, fh, offset, data, doff, length, null);
     }
 
     protected void doRead(Buffer buffer, int id) throws IOException {
@@ -2100,9 +2103,17 @@
 
         ValidateUtils.checkTrue(length > 0L, "Invalid read length: %d", length);
         FileHandle fh = validateHandle(handle, h, FileHandle.class);
-        int readLen = fh.read(data, doff, length, offset);
         SftpEventListener listener = getSftpEventListenerProxy();
-        listener.read(getServerSession(), handle, fh, offset, data, doff, length, readLen);
+        ServerSession serverSession = getServerSession();
+        int readLen;
+        listener.reading(serverSession, handle, fh, offset, data, doff, length);
+        try {
+            readLen = fh.read(data, doff, length, offset);
+        } catch (IOException | RuntimeException e) {
+            listener.read(serverSession, handle, fh, offset, data, doff, length, -1, e);
+            throw e;
+        }
+        listener.read(serverSession, handle, fh, offset, data, doff, length, readLen, null);
         return readLen;
     }
 
@@ -2985,11 +2996,11 @@
         listener.modifyingAttributes(session, file, attributes);
         try {
             setFileAttributes(file, attributes, IoUtils.getLinkOptions(false));
-            listener.modifiedAttributes(session, file, attributes, null);
         } catch (IOException | RuntimeException e) {
             listener.modifiedAttributes(session, file, attributes, e);
             throw e;
         }
+        listener.modifiedAttributes(session, file, attributes, null);
     }
 
     protected void setFileAttributes(Path file, Map<String, ?> attributes, LinkOption ... options) throws IOException {
@@ -3310,7 +3321,7 @@
 
     protected void sendStatus(Buffer buffer, int id, Throwable e) throws IOException {
         int substatus = SftpHelper.resolveSubstatus(e);
-        sendStatus(buffer, id, substatus, e.toString());
+        sendStatus(buffer, id, substatus, SftpHelper.resolveStatusMessage(e));
     }
 
     protected void sendStatus(Buffer buffer, int id, int substatus, String msg) throws IOException {
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
index 680ae6d..3ddfe18 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
@@ -89,7 +89,6 @@
 import org.apache.sshd.server.subsystem.sftp.DirectoryHandle;
 import org.apache.sshd.server.subsystem.sftp.FileHandle;
 import org.apache.sshd.server.subsystem.sftp.Handle;
-import org.apache.sshd.server.subsystem.sftp.SftpEventListener;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystem;
 import org.apache.sshd.server.subsystem.sftp.SftpSubsystemFactory;
 import org.apache.sshd.util.test.JSchLogger;
@@ -102,8 +101,6 @@
 import org.junit.FixMethodOrder;
 import org.junit.Test;
 import org.junit.runners.MethodSorters;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
@@ -539,9 +536,7 @@
         final AtomicInteger modifyingCount = new AtomicInteger(0);
         final AtomicInteger modifiedCount = new AtomicInteger(0);
 
-        factory.addSftpEventListener(new SftpEventListener() {
-            private final Logger log = LoggerFactory.getLogger(SftpEventListener.class);
-
+        factory.addSftpEventListener(new AbstractSftpEventListenerAdapter() {
             @Override
             public void initialized(ServerSession session, int version) {
                 log.info("initialized(" + session + ") version: " + version);
@@ -557,7 +552,8 @@
             }
 
             @Override
-            public void write(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data, int dataOffset, int dataLen) {
+            public void written(ServerSession session, String remoteHandle, FileHandle localHandle,
+                    long offset, byte[] data, int dataOffset, int dataLen, Throwable thrown) {
                 writeSize.addAndGet(dataLen);
                 if (log.isDebugEnabled()) {
                     log.debug("write(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen);
@@ -591,7 +587,8 @@
             }
 
             @Override
-            public void read(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data, int dataOffset, int dataLen, int readLen) {
+            public void read(ServerSession session, String remoteHandle, FileHandle localHandle, long offset, byte[] data,
+                    int dataOffset, int dataLen, int readLen, Throwable thrown) {
                 readSize.addAndGet(readLen);
                 if (log.isDebugEnabled()) {
                     log.debug("read(" + session + ")[" + localHandle.getFile() + "] offset=" + offset + ", requested=" + dataLen + ", read=" + readLen);
@@ -677,9 +674,9 @@
 
             @Override
             public void unblocked(ServerSession session, String remoteHandle, FileHandle localHandle,
-                                  long offset, long length, Boolean result, Throwable thrown) {
+                                  long offset, long length, Throwable thrown) {
                 log.info("unblocked(" + session + ")[" + localHandle.getFile() + "]"
-                       + " offset=" + offset + ", length=" + length + ", result=" + result
+                       + " offset=" + offset + ", length=" + length
                        + ((thrown == null) ? "" : (": " + thrown.getClass().getSimpleName() + ": " + thrown.getMessage())));
             }