Repository: mina-sshd
Updated Branches:
  refs/heads/master 178c73e41 -> 77dfae2d4


[SSHD-517] Add support for SFTP vendor-id extension


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/77dfae2d
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/77dfae2d
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/77dfae2d

Branch: refs/heads/master
Commit: 77dfae2d431e4cebe101bfca6946626f5ee4e454
Parents: 178c73e
Author: Lyor Goldstein <[email protected]>
Authored: Thu Jul 2 15:53:00 2015 +0300
Committer: Lyor Goldstein <[email protected]>
Committed: Thu Jul 2 15:53:00 2015 +0300

----------------------------------------------------------------------
 .../org/apache/sshd/sshd-version.properties     |   5 +-
 .../sshd/client/session/ClientSessionImpl.java  |   2 +-
 .../subsystem/sftp/DefaultSftpClient.java       |  14 +-
 .../sshd/client/subsystem/sftp/SftpClient.java  |   6 +
 .../client/subsystem/sftp/SftpFileSystem.java   |   6 +
 .../sshd/common/AbstractFactoryManager.java     |  21 +-
 .../org/apache/sshd/common/FactoryManager.java  |  41 +--
 .../sshd/common/config/VersionProperties.java   |  90 +++++++
 .../sshd/common/session/AbstractSession.java    |   1 +
 .../common/subsystem/sftp/SftpConstants.java    |  14 +-
 .../sftp/extensions/AbstractParser.java         |  74 ++++++
 .../sftp/extensions/ExtensionParser.java        |  38 +++
 .../sftp/extensions/NewlineParser.java          |  73 ++++++
 .../subsystem/sftp/extensions/ParserUtils.java  | 158 ++++++++++++
 .../sftp/extensions/VendorIdParser.java         |  63 +++++
 .../sftp/extensions/VersionsParser.java         |  76 ++++++
 .../apache/sshd/common/util/buffer/Buffer.java  |  27 ++
 .../sshd/common/util/buffer/BufferUtils.java    |  45 +++-
 .../sshd/server/session/ServerSessionImpl.java  |   2 -
 .../server/subsystem/sftp/SftpSubsystem.java    | 250 +++++++++++++++----
 .../sshd/client/subsystem/sftp/SftpTest.java    |  34 ++-
 21 files changed, 942 insertions(+), 98 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/filtered-resources/org/apache/sshd/sshd-version.properties
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/filtered-resources/org/apache/sshd/sshd-version.properties 
b/sshd-core/src/main/filtered-resources/org/apache/sshd/sshd-version.properties
index 838508d..2c32c28 100644
--- 
a/sshd-core/src/main/filtered-resources/org/apache/sshd/sshd-version.properties
+++ 
b/sshd-core/src/main/filtered-resources/org/apache/sshd/sshd-version.properties
@@ -17,4 +17,7 @@
 ## under the License.
 ##
 
-version=${pom.artifactId}-${pom.version}
+groupId=${pom.groupId}
+artifactId=${pom.artifactId}
+version=${pom.version}
+sshd-version=${pom.artifactId}-${pom.version}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java 
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
index 936f24f..ef1a156 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java
@@ -509,7 +509,7 @@ public class ClientSessionImpl extends AbstractSession 
implements ClientSession
 
     protected void sendClientIdentification() {
         FactoryManager manager = getFactoryManager();
-        clientVersion = "SSH-2.0-" + manager.getVersion();
+        clientVersion = DEFAULT_SSH_VERSION_PREFIX + manager.getVersion();
         sendIdentification(clientVersion);
     }
 

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
index 6110900..2f743b9 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/DefaultSftpClient.java
@@ -97,9 +97,11 @@ import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.attribute.FileTime;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -129,7 +131,8 @@ public class DefaultSftpClient extends AbstractSftpClient {
     private final Buffer receiveBuffer = new ByteArrayBuffer();
     private boolean closing;
     private int version;
-    private final Map<String, byte[]> extensions = new HashMap<>();
+    private final Map<String,byte[]> extensions = new 
TreeMap<String,byte[]>(String.CASE_INSENSITIVE_ORDER);
+    private final Map<String,byte[]> exposedExtensions = 
Collections.unmodifiableMap(extensions);
 
     public DefaultSftpClient(ClientSession clientSession) throws IOException {
         this.clientSession = clientSession;
@@ -166,6 +169,11 @@ public class DefaultSftpClient extends AbstractSftpClient {
     }
 
     @Override
+    public Map<String, byte[]> getServerExtensions() {
+        return exposedExtensions;
+    }
+
+    @Override
     public boolean isClosing() {
         return closing;
     }
@@ -325,14 +333,16 @@ public class DefaultSftpClient extends AbstractSftpClient 
{
             buffer = messages.remove(messages.keySet().iterator().next());
 
         }
+
         int length = buffer.getInt();
-        int type = buffer.getByte();
+        int type = buffer.getByte() & 0xFF;
         int id = buffer.getInt();
         if (type == SSH_FXP_VERSION) {
             if (id < SFTP_V3) {
                 throw new SshException("Unsupported sftp version " + id);
             }
             version = id;
+
             while (buffer.available() > 0) {
                 String name = buffer.getString();
                 byte[] data = buffer.getBytes();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
index 2af762c..8b18c2f 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpClient.java
@@ -31,6 +31,7 @@ import java.nio.channels.Channel;
 import java.nio.file.attribute.FileTime;
 import java.util.Collection;
 import java.util.EnumSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
@@ -219,6 +220,11 @@ public interface SftpClient extends SubsystemClient {
 
     int getVersion();
 
+    /**
+     * @return An (unmodifiable) {@link Map} of the reported server extensions.
+     */
+    Map<String,byte[]> getServerExtensions();
+
     boolean isClosing();
 
     //

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
index 4b8f54c..f2abfa7 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/subsystem/sftp/SftpFileSystem.java
@@ -30,6 +30,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -191,6 +192,11 @@ public class SftpFileSystem extends 
BaseFileSystem<SftpPath> {
         }
 
         @Override
+        public Map<String, byte[]> getServerExtensions() {
+            return delegate.getServerExtensions();
+        }
+
+        @Override
         public boolean isClosing() {
             return false;
         }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/AbstractFactoryManager.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/AbstractFactoryManager.java 
b/sshd-core/src/main/java/org/apache/sshd/common/AbstractFactoryManager.java
index f021e25..d2afb7b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/AbstractFactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/AbstractFactoryManager.java
@@ -18,11 +18,9 @@
  */
 package org.apache.sshd.common;
 
-import java.io.InputStream;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -32,6 +30,7 @@ import org.apache.sshd.common.channel.Channel;
 import org.apache.sshd.common.channel.RequestHandler;
 import org.apache.sshd.common.cipher.Cipher;
 import org.apache.sshd.common.compression.Compression;
+import org.apache.sshd.common.config.VersionProperties;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.forward.TcpipForwarderFactory;
 import org.apache.sshd.common.io.DefaultIoServiceFactoryFactory;
@@ -68,7 +67,6 @@ public abstract class AbstractFactoryManager extends 
CloseableUtils.AbstractInne
     protected List<NamedFactory<Signature>> signatureFactories;
     protected Factory<Random> randomFactory;
     protected KeyPairProvider keyPairProvider;
-    protected String version;
     protected List<NamedFactory<Channel>> channelFactories;
     protected SshAgentFactory agentFactory;
     protected ScheduledExecutorService executor;
@@ -82,7 +80,7 @@ public abstract class AbstractFactoryManager extends 
CloseableUtils.AbstractInne
     protected ScheduledFuture<?> timeoutListenerFuture;
 
     protected AbstractFactoryManager() {
-        loadVersion();
+        super();
     }
 
     @Override
@@ -177,20 +175,7 @@ public abstract class AbstractFactoryManager extends 
CloseableUtils.AbstractInne
 
     @Override
     public String getVersion() {
-        return version;
-    }
-
-    protected void loadVersion() {
-        this.version = "SSHD-UNKNOWN";
-        try {
-            try (InputStream input = 
getClass().getClassLoader().getResourceAsStream("org/apache/sshd/sshd-version.properties"))
 {
-                Properties props = new Properties();
-                props.load(input);
-                this.version = props.getProperty("version").toUpperCase();
-            }
-        } catch (Exception e) {
-            log.warn("Unable to load version from resources. Missing 
org/apache/sshd/sshd-version.properties ?", e);
-        }
+        return 
FactoryManagerUtils.getStringProperty(VersionProperties.getVersionProperties(), 
"sshd-version", DEFAULT_VERSION).toUpperCase();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java 
b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
index b637597..bdb63d3 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/FactoryManager.java
@@ -50,44 +50,44 @@ public interface FactoryManager {
      * Key used to retrieve the value of the window size in the
      * configuration properties map.
      */
-    public static final String WINDOW_SIZE = "window-size";
+    String WINDOW_SIZE = "window-size";
 
     /**
      * Key used to retrieve timeout (msec.) to wait for data to
      * become available when reading from a channel. If not set
      * or non-positive then infinite value is assumed
      */
-    public static final String WINDOW_TIMEOUT = "window-timeout";
+    String WINDOW_TIMEOUT = "window-timeout";
 
 
     /**
      * Key used to retrieve the value of the maximum packet size
      * in the configuration properties map.
      */
-    public static final String MAX_PACKET_SIZE = "packet-size";
+    String MAX_PACKET_SIZE = "packet-size";
 
     /**
      * Number of NIO worker threads to use.
      */
-    public static final String NIO_WORKERS = "nio-workers";
+    String NIO_WORKERS = "nio-workers";
 
     /**
      * Default number of worker threads to use.
      */
-    public static final int DEFAULT_NIO_WORKERS = 
Runtime.getRuntime().availableProcessors() + 1;
+    int DEFAULT_NIO_WORKERS = Runtime.getRuntime().availableProcessors() + 1;
 
     /**
      * Key used to retrieve the value of the timeout after which
      * it will close the connection if the other side has not been
      * authenticated.
      */
-    public static final String AUTH_TIMEOUT = "auth-timeout";
+    String AUTH_TIMEOUT = "auth-timeout";
 
     /**
      * Key used to retrieve the value of idle timeout after which
      * it will close the connection.  In milliseconds.
      */
-    public static final String IDLE_TIMEOUT = "idle-timeout";
+    String IDLE_TIMEOUT = "idle-timeout";
 
     /**
      * Key used to retrieve the value of the disconnect timeout which
@@ -95,62 +95,62 @@ public interface FactoryManager {
      * message has not been sent before the timeout, the underlying socket
      * will be forcibly closed.
      */
-    public static final String DISCONNECT_TIMEOUT = "disconnect-timeout";
+    String DISCONNECT_TIMEOUT = "disconnect-timeout";
 
     /**
      * Key used to configure the timeout used when writing a close request
      * on a channel. If the message can not be written before the specified
      * timeout elapses, the channel will be immediately closed. In 
milliseconds.
      */
-    public static final String CHANNEL_CLOSE_TIMEOUT = "channel-close-timeout";
+    String CHANNEL_CLOSE_TIMEOUT = "channel-close-timeout";
 
     /**
      * Socket backlog.
      * See {@link 
java.nio.channels.AsynchronousServerSocketChannel#bind(java.net.SocketAddress, 
int)}
      */
-    public static final String SOCKET_BACKLOG = "socket-backlog";
+    String SOCKET_BACKLOG = "socket-backlog";
 
     /**
      * Socket keep-alive.
      * See {@link java.net.StandardSocketOptions#SO_KEEPALIVE}
      */
-    public static final String SOCKET_KEEPALIVE = "socket-keepalive";
+    String SOCKET_KEEPALIVE = "socket-keepalive";
 
     /**
      * Socket send buffer size.
      * See {@link java.net.StandardSocketOptions#SO_SNDBUF}
      */
-    public static final String SOCKET_SNDBUF = "socket-sndbuf";
+    String SOCKET_SNDBUF = "socket-sndbuf";
 
     /**
      * Socket receive buffer size.
      * See {@link java.net.StandardSocketOptions#SO_RCVBUF}
      */
-    public static final String SOCKET_RCVBUF = "socket-rcvbuf";
+    String SOCKET_RCVBUF = "socket-rcvbuf";
 
     /**
      * Socket reuse address.
      * See {@link java.net.StandardSocketOptions#SO_REUSEADDR}
      */
-    public static final String SOCKET_REUSEADDR = "socket-reuseaddr";
+    String SOCKET_REUSEADDR = "socket-reuseaddr";
 
     /**
      * Socket linger.
      * See {@link java.net.StandardSocketOptions#SO_LINGER}
      */
-    public static final String SOCKET_LINGER = "socket-linger";
+    String SOCKET_LINGER = "socket-linger";
 
     /**
      * Socket tcp no-delay.
      * See {@link java.net.StandardSocketOptions#TCP_NODELAY}
      */
-    public static final String TCP_NODELAY = "tcp-nodelay";
+    String TCP_NODELAY = "tcp-nodelay";
 
     /**
      * Read buffer size for NIO2 sessions
      * See {@link org.apache.sshd.common.io.nio2.Nio2Session}
      */
-    public static final String NIO2_READ_BUFFER_SIZE = "nio2-read-buf-size";
+    String NIO2_READ_BUFFER_SIZE = "nio2-read-buf-size";
 
     /**
      * <P>A map of properties that can be used to configure the SSH server
@@ -172,11 +172,16 @@ public interface FactoryManager {
     Map<String,Object> getProperties();
 
     /**
+     * The default reported version of {@link #getVersion()} if the built-in
+     * version information cannot be accessed
+     */
+    String DEFAULT_VERSION = "SSHD-UNKNOWN";
+
+    /**
      * An upper case string identifying the version of the
      * software used on client or server side.
      * This version includes the name of the software and usually
      * looks like: <code>SSHD-1.0</code>
-     *
      * @return the version of the software
      */
     String getVersion();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/config/VersionProperties.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/config/VersionProperties.java 
b/sshd-core/src/main/java/org/apache/sshd/common/config/VersionProperties.java
new file mode 100644
index 0000000..85bf2d3
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/config/VersionProperties.java
@@ -0,0 +1,90 @@
+/*
+ * 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.config;
+
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.threads.ThreadUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public final class VersionProperties {
+    private static class LazyHolder {
+        private static final Map<String,String> properties =
+                
Collections.unmodifiableMap(loadVersionProperties(LazyHolder.class));
+        
+        private static Map<String,String> loadVersionProperties(Class<?> 
anchor) {
+            return loadVersionProperties(anchor, 
ThreadUtils.resolveDefaultClassLoader(anchor));
+        }
+
+        private static Map<String,String> loadVersionProperties(Class<?> 
anchor, ClassLoader loader) {
+            Map<String,String> result = new 
TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+            try {
+                InputStream input = 
loader.getResourceAsStream("org/apache/sshd/sshd-version.properties");
+                if (input == null) {
+                    throw new FileNotFoundException("Resource does not 
exists");
+                }
+                
+                Properties props = new Properties();
+                try {
+                    props.load(input);
+                } finally {
+                    input.close();
+                }
+                
+                for (String key : props.stringPropertyNames()) {
+                    String value = 
GenericUtils.trimToEmpty(props.getProperty(key));
+                    if (GenericUtils.isEmpty(value)) {
+                        continue;   // we have no need for empty value
+                    }
+                    
+                    String prev = result.put(key, value);
+                    if (prev != null) {
+                        Logger log = LoggerFactory.getLogger(anchor);
+                        log.warn("Multiple values for key=" + key + ": 
current=" + value + ", previous=" + prev);
+                    }
+                }
+            } catch (Exception e) {
+                Logger log = LoggerFactory.getLogger(anchor);
+                log.warn("Failed (" + e.getClass().getSimpleName() + ") to 
load version properties: " + e.getMessage());
+            }
+            
+            return result;
+        }
+    }
+    
+    @SuppressWarnings("synthetic-access")
+    public static Map<String,String> getVersionProperties() {
+        return LazyHolder.properties;
+    }
+    
+    private VersionProperties() {
+        throw new UnsupportedOperationException("No instance");
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java 
b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
index e798f67..fd1655c 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java
@@ -85,6 +85,7 @@ import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
 public abstract class AbstractSession extends 
CloseableUtils.AbstractInnerCloseable implements Session {
+    public static final String  DEFAULT_SSH_VERSION_PREFIX="SSH-2.0-";
 
     /**
      * Name of the property where this session is stored in the attributes of 
the

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
index f2f005d..41c3a1a 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/SftpConstants.java
@@ -21,7 +21,7 @@ package org.apache.sshd.common.subsystem.sftp;
 /**
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
-public class SftpConstants {
+public final class SftpConstants {
     public static String SFTP_SUBSYSTEM_NAME = "sftp";
 
     public static final int SSH_FXP_INIT =             1;
@@ -220,4 +220,16 @@ public class SftpConstants {
     public static int SFTP_V4 = 4;
     public static int SFTP_V5 = 5;
     public static int SFTP_V6 = 6;
+    
+    // (Some) names of known extensions
+    public static final String EXT_VERSIONS = "versions";
+    public static final String EXT_NEWLINE = "newline";
+    public static final String EXT_VENDORID = "vendor-id";
+    public static final String EXT_SUPPORTED = "supported";
+    public static final String EXT_SUPPORTED2 = "supported2";
+    public static final String EXT_VERSELECT = "version-select";
+
+    private SftpConstants() {
+        throw new UnsupportedOperationException("No instance");
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
new file mode 100644
index 0000000..736a1bb
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/AbstractParser.java
@@ -0,0 +1,74 @@
+/*
+ * 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.subsystem.sftp.extensions;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractParser<T> implements ExtensionParser<T> {
+    private final String name;
+
+    protected AbstractParser(String name) {
+        this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No extension 
name", GenericUtils.EMPTY_OBJECT_ARRAY);
+    }
+
+    @Override
+    public final String getName() {
+        return name;
+    }
+
+    @Override   // TODO in JDK-8 make this a default method
+    public T transform(byte[] input) {
+        return parse(input);
+    }
+    
+    @Override   // TODO in JDK-8 make this a default method
+    public T parse(byte[] input) {
+        return parse(input, 0, GenericUtils.length(input));
+    }
+    
+    @Override   // TODO in JDK-8 make this a default method
+    public T parse(byte[] input, int offset, int len) {
+        return parse(new ByteArrayBuffer(input, offset, len));
+    }
+    
+    protected String parseStringBytes(Buffer buffer) {
+        return parseStringBytes(buffer, StandardCharsets.UTF_8);
+    }
+
+    protected String parseStringBytes(Buffer buffer, Charset charset) {
+        int available = buffer.available();
+        if (available <= 0) {
+            return "";
+        }
+        
+        byte[] buf = new byte[available];
+        buffer.getRawBytes(buf);
+        return new String(buf, charset);
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
new file mode 100644
index 0000000..fdd713d
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ExtensionParser.java
@@ -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.
+ */
+
+package org.apache.sshd.common.subsystem.sftp.extensions;
+
+import org.apache.sshd.common.NamedResource;
+import org.apache.sshd.common.util.Transformer;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public interface ExtensionParser<T> extends NamedResource, Transformer<byte[], 
T> {
+    T parse(byte[] input);
+    T parse(byte[] input, int offset, int len);
+
+    /**
+     * @param buffer A {@link Buffer} containing the encoded extension data
+     * @return The decode extension data
+     */
+    T parse(Buffer buffer);
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/NewlineParser.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/NewlineParser.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/NewlineParser.java
new file mode 100644
index 0000000..372408e
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/NewlineParser.java
@@ -0,0 +1,73 @@
+/*
+ * 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.subsystem.sftp.extensions;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.NewlineParser.Newline;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class NewlineParser extends AbstractParser<Newline> {
+    /**
+     * The &quot;newline&quot; extension information as per
+     * <A 
HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt";>DRAFT
 09 Section 4.3</A>
+     * @author <a href="mailto:[email protected]";>Apache MINA SSHD 
Project</a>
+     */
+    public static class Newline {
+        public String newline;
+        
+        @Override
+        public String toString() {
+            if (GenericUtils.isEmpty(newline)) {
+                return newline;
+            } else {
+                return BufferUtils.printHex(':', 
newline.getBytes(StandardCharsets.UTF_8));
+            }
+        }
+    }
+    
+    public static final NewlineParser INSTANCE = new NewlineParser();
+
+    public NewlineParser() {
+        super(SftpConstants.EXT_NEWLINE);
+    }
+
+    @Override
+    public Newline parse(Buffer buffer) {
+        return parse(parseStringBytes(buffer));
+    }
+    
+    @Override
+    public Newline parse(byte[] input, int offset, int len) {
+        return parse(new String(input, offset, len, StandardCharsets.UTF_8));
+    }
+
+    public Newline parse(String value) {
+        Newline nl = new Newline();
+        nl.newline = value;
+        return nl;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
new file mode 100644
index 0000000..0dd33ea
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/ParserUtils.java
@@ -0,0 +1,158 @@
+/*
+ * 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.subsystem.sftp.extensions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public final class ParserUtils {
+    public static final Collection<ExtensionParser<?>> BUILT_IN_PARSERS =
+            Collections.unmodifiableList(
+                    Arrays.<ExtensionParser<?>>asList(
+                            VendorIdParser.INSTANCE,
+                            NewlineParser.INSTANCE,
+                            VersionsParser.INSTANCE
+                    ));
+
+    private static final Map<String,ExtensionParser<?>> parsersMap = new 
TreeMap<String,ExtensionParser<?>>(String.CASE_INSENSITIVE_ORDER) {
+            private static final long serialVersionUID = 1L;    // we're not 
serializing it
+            
+            {
+                for (ExtensionParser<?> p : BUILT_IN_PARSERS) {
+                    put(p.getName(), p);
+                }
+            }
+        };
+
+    /**
+     * @param parser The {@link ExtensionParser} to register
+     * @return The replaced parser (by name) - {@code null} if no previous 
parser
+     * for this extension name
+     */
+    public static ExtensionParser<?> registerParser(ExtensionParser<?> parser) 
{
+        ValidateUtils.checkNotNull(parser, "No parser instance", 
GenericUtils.EMPTY_OBJECT_ARRAY);
+        
+        synchronized(parsersMap) {
+            return parsersMap.put(parser.getName(), parser);
+        }
+    }
+
+    /**
+     * @param name The extension name - ignored if {@code null}/empty
+     * @return The removed {@link ExtensionParser} - {@code null} if none 
registered
+     * for this extension name
+     */
+    public static ExtensionParser<?> unregisterParser(String name) {
+        if (GenericUtils.isEmpty(name)) {
+            return null;
+        }
+
+        synchronized(parsersMap) {
+            return parsersMap.remove(name);
+        }
+    }
+
+    /**
+     * @param name The extension name - ignored if {@code null}/empty
+     * @return The registered {@link ExtensionParser} - {@code null} if none 
registered
+     * for this extension name
+     */
+    public static ExtensionParser<?> getRegisteredParser(String name) {
+        if (GenericUtils.isEmpty(name)) {
+            return null;
+        }
+
+        synchronized(parsersMap) {
+            return parsersMap.get(name);
+        }
+    }
+
+    public static Set<String> getRegisteredParsersNames() {
+        synchronized(parsersMap) {
+            if (parsersMap.isEmpty()) {
+                return Collections.emptySet();
+            } else {    // return a copy in order to avoid concurrent 
modification issues
+                return GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, 
parsersMap.keySet());
+            }
+        }
+    }
+
+    public static final List<ExtensionParser<?>> getRegisteredParsers() {
+        synchronized(parsersMap) {
+            if (parsersMap.isEmpty()) {
+                return Collections.emptyList();
+            } else { // return a copy in order to avoid concurrent 
modification issues
+                return new ArrayList<ExtensionParser<?>>(parsersMap.values());
+            }
+        }
+    }
+
+    /**
+     * @param extensions The received extensions in encoded form
+     * @return A {@link Map} of all the successfully decoded extensions
+     * where key=extension name (same as in the original map), value=the
+     * decoded extension value. Extensions for which there is no registered
+     * parser are <U>ignored</U>
+     * @see #getRegisteredParser(String)
+     * @see ExtensionParser#transform(Object)
+     */
+    public static final Map<String,Object> parse(Map<String,byte[]> 
extensions) {
+        if (GenericUtils.isEmpty(extensions)) {
+            return Collections.emptyMap();
+        }
+        
+        Map<String,Object> data = new 
TreeMap<String,Object>(String.CASE_INSENSITIVE_ORDER);
+        for (Map.Entry<String,byte[]> ee : extensions.entrySet()) {
+            String name = ee.getKey();
+            Object result = parse(name, ee.getValue());
+            if (result == null) {
+                continue;
+            }
+            data.put(name, result);
+        }
+        
+        return data;
+    }
+
+    public static final Object parse(String name, byte ... encoded) {
+        ExtensionParser<?> parser = getRegisteredParser(name);
+        if (parser == null) {
+            return null;
+        } else {
+            return parser.transform(encoded);
+        }
+    }
+    
+    private ParserUtils() {
+        throw new UnsupportedOperationException("No instance");
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VendorIdParser.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VendorIdParser.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VendorIdParser.java
new file mode 100644
index 0000000..96ea0ae
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VendorIdParser.java
@@ -0,0 +1,63 @@
+/*
+ * 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.subsystem.sftp.extensions;
+
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import 
org.apache.sshd.common.subsystem.sftp.extensions.VendorIdParser.VendorId;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class VendorIdParser extends AbstractParser<VendorId> {
+    /**
+     * The &quot;vendor-id&quot; information as per
+     * <A 
HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt";>DRAFT
 09 - section 4.4</A> 
+     * @author <a href="mailto:[email protected]";>Apache MINA SSHD 
Project</a>
+     */
+    public static class VendorId {
+        public String vendorName;
+        public String productName;
+        public String productVersion;
+        public long productBuildNumber;
+
+        @Override
+        public String toString() {
+            return vendorName + "-" + productName + "-" + productVersion + "-" 
+ productBuildNumber;
+        }
+    }
+
+    public static final VendorIdParser INSTANCE = new VendorIdParser();
+
+    public VendorIdParser() {
+        super(SftpConstants.EXT_VENDORID);
+    }
+
+    @Override
+    public VendorId parse(Buffer buffer) {
+        VendorId id = new VendorId();
+        id.vendorName = buffer.getString();
+        id.productName = buffer.getString();
+        id.productVersion = buffer.getString();
+        id.productBuildNumber = buffer.getLong();
+        return id;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VersionsParser.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VersionsParser.java
 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VersionsParser.java
new file mode 100644
index 0000000..18f8335
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/subsystem/sftp/extensions/VersionsParser.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.subsystem.sftp.extensions;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import 
org.apache.sshd.common.subsystem.sftp.extensions.VersionsParser.Versions;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.buffer.Buffer;
+
+/**
+ * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
+ */
+public class VersionsParser extends AbstractParser<Versions> {
+    /**
+     * The &quot;versions&quot; extension data as per
+     * <A 
HREF="http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt";>DRAFT
 09 Section 4.6</A>
+     * @author <a href="mailto:[email protected]";>Apache MINA SSHD 
Project</a>
+     */
+    public static class Versions {
+        public static final char SEP = ',';
+
+        public Collection<String> versions;
+
+        @Override
+        public String toString() {
+            return GenericUtils.join(versions, ',');
+        }
+    }
+    
+    public static final VersionsParser INSTANCE = new VersionsParser();
+
+    public VersionsParser() {
+        super(SftpConstants.EXT_VERSIONS);
+    }
+
+    @Override
+    public Versions parse(Buffer buffer) {
+        return parse(parseStringBytes(buffer));
+    }
+    
+    @Override
+    public Versions parse(byte[] input, int offset, int len) {
+        return parse(new String(input, offset, len, StandardCharsets.UTF_8));
+    }
+
+    public Versions parse(String value) {
+        String[] comps = GenericUtils.split(value, Versions.SEP);
+        Versions v = new Versions();
+        v.versions = GenericUtils.isEmpty(comps)
+                    ? Collections.<String>emptyList()
+                    : Arrays.asList(comps);
+        return v;
+    }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java 
b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
index 9080958..b4c507f 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
@@ -43,6 +43,8 @@ import java.security.spec.ECPublicKeySpec;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.RSAPrivateCrtKeySpec;
 import java.security.spec.RSAPublicKeySpec;
+import java.util.Collection;
+import java.util.Objects;
 
 import org.apache.sshd.common.SshException;
 import org.apache.sshd.common.cipher.ECCurves;
@@ -383,6 +385,31 @@ public abstract class Buffer implements Readable {
         putRawBytes(b, off, len);
     }
 
+    /**
+     * Encodes the {@link Objects#toString(Object)} value of each member
+     * @param objects The objects to be encoded in the buffer
+     * @see #putStringList(Collection, Charset)
+     */
+    public void putStringList(Collection<?> objects) {
+        putStringList(objects, StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Encodes the {@link Objects#toString(Object)} value of each member
+     * @param objects The objects to be encoded in the buffer
+     * @param charset The {@link Charset} to use for encoding
+     * @see #putString(String, Charset)
+     */
+    public void putStringList(Collection<?> objects, Charset charset) {
+        if (GenericUtils.isEmpty(objects)) {
+            return;
+        }
+        
+        for (Object o : objects) {
+            putString(Objects.toString(o), charset);
+        }
+    }
+
     public void putString(String string) {
         putString(string, StandardCharsets.UTF_8);
     }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java 
b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
index 775f24e..12b3316 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java
@@ -93,23 +93,35 @@ public class BufferUtils {
     }
 
     /**
+     * Writes a 32-bit value in network order (i.e., MSB 1st)
      * @param value The 32-bit value 
-     * @param buf
-     * @return
+     * @param buf The buffer
+     * @return The number of bytes used in the buffer
+     * @throws IllegalArgumentException if not enough space available
+     * @see #putUInt(long, byte[], int, int)
      */
     public static int putUInt(long value, byte[] buf) {
         return putUInt(value, buf, 0, GenericUtils.length(buf));
     }
 
+    /**
+     * Writes a 32-bit value in network order (i.e., MSB 1st)
+     * @param value The 32-bit value 
+     * @param buf The buffer
+     * @param off The offset to write the value
+     * @param len The available space
+     * @return The number of bytes used in the buffer
+     * @throws IllegalArgumentException if not enough space available
+     */
     public static int putUInt(long value, byte[] buf, int off, int len) {
         // TODO use Integer.BYTES for JDK-8
         if (len < (Integer.SIZE / Byte.SIZE)) {
             throw new IllegalArgumentException("Not enough data for a UINT: 
required=" + (Integer.SIZE / Byte.SIZE) + ", available=" + len);
         }
 
-        buf[off]     = (byte) (value >> 24);
-        buf[off + 1] = (byte) (value >> 16);
-        buf[off + 2] = (byte) (value >>  8);
+        buf[off]     = (byte) ((value >> 24) & 0xFF);
+        buf[off + 1] = (byte) ((value >> 16) & 0xFF);
+        buf[off + 2] = (byte) ((value >>  8) & 0xFF);
         buf[off + 3] = (byte) (value & 0xFF);
 
         return (Integer.SIZE / Byte.SIZE);
@@ -147,4 +159,27 @@ public class BufferUtils {
         }
         return j;
     }
+    
+    /**
+     * Used for encodings where we don't know the data length before adding it
+     * to the buffer. The idea is to place a 32-bit &quot;placeholder&quot;,
+     * encode the data and then return back to the placeholder and update the
+     * length. The method calculates the encoded data length, moves the write
+     * position to the specified placeholder position, updates the length value
+     * and then moves the write position it back to its original value.
+     * @param buffer The {@link Buffer}
+     * @param lenPos The offset in the buffer where the length placeholder is
+     * to be update - <B>Note:</B> assumption is that the encoded data start
+     * <U>immediately</U> after the placeholder
+     * @return The amount of data that has been encoded
+     */
+    public static int updateLengthPlaceholder(Buffer buffer, int lenPos) {
+        int startPos = lenPos + (Integer.SIZE / Byte.SIZE);
+        int endPos = buffer.wpos();
+        int dataLength = endPos - startPos;
+        buffer.wpos(lenPos);
+        buffer.putInt(dataLength);
+        buffer.wpos(endPos);
+        return dataLength;
+    }
 }

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
----------------------------------------------------------------------
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java 
b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
index e5646a1..e3e5eb0 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionImpl.java
@@ -51,8 +51,6 @@ import org.apache.sshd.server.ServerFactoryManager;
  * @author <a href="mailto:[email protected]";>Apache MINA SSHD Project</a>
  */
 public class ServerSessionImpl extends AbstractSession implements 
ServerSession {
-    public static final String  DEFAULT_SSH_VERSION_PREFIX="SSH-2.0-";
-
     protected static final long MAX_PACKETS = (1l << 31);
 
     private long maxBytes = 1024 * 1024 * 1024;   // 1 GB

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/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 9788ca2..f5c8ba9 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
@@ -21,7 +21,6 @@ package org.apache.sshd.server.subsystem.sftp;
 import static org.apache.sshd.common.subsystem.sftp.SftpConstants.*;
 
 import java.io.DataInputStream;
-import java.io.DataOutputStream;
 import java.io.EOFException;
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -60,6 +59,7 @@ import java.nio.file.attribute.UserPrincipal;
 import java.nio.file.attribute.UserPrincipalLookupService;
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Collection;
 import java.util.Collections;
@@ -77,12 +77,16 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.common.FactoryManager;
 import org.apache.sshd.common.FactoryManagerUtils;
+import org.apache.sshd.common.config.VersionProperties;
 import org.apache.sshd.common.file.FileSystemAware;
+import org.apache.sshd.common.subsystem.sftp.SftpConstants;
 import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.SelectorUtils;
 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.IoUtils;
 import org.apache.sshd.common.util.logging.AbstractLoggingBean;
@@ -121,6 +125,29 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
     public static final String MAX_PACKET_LENGTH_PROP = 
"sftp-max-packet-length";
         public static final int  DEFAULT_MAX_PACKET_LENGTH = 1024 * 16;
 
+    /**
+     * 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 Set<String> 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 space-available - see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
+                // TODO home-directory - see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
+                Collections.unmodifiableSet(
+                        GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER,
+                                Arrays.asList(
+                                        SftpConstants.EXT_VERSELECT
+                                )));
+
     static {
         StringBuilder sb = new StringBuilder(2 * (1 + (HIGHER_SFTP_IMPL - 
LOWER_SFTP_IMPL)));
         for (int v = LOWER_SFTP_IMPL; v <= HIGHER_SFTP_IMPL; v++) {
@@ -142,7 +169,7 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
        private ExecutorService executors;
        private boolean shutdownExecutor;
        private Future<?> pendingFuture;
-
+       private final byte[] workBuf = new byte[Integer.SIZE / Byte.SIZE]; // 
TODO in JDK-8 use Integer.BYTES
     private FileSystem fileSystem = FileSystems.getDefault();
     private Path defaultDir = 
fileSystem.getPath(System.getProperty("user.dir"));
 
@@ -366,6 +393,10 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
         }
     }
 
+    public int getVersion() {
+        return version;
+    }
+
     public final UnsupportedAttributePolicy getUnsupportedAttributePolicy() {
         return unsupportedAttributePolicy;
     }
@@ -550,12 +581,21 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
     }
 
     protected void doExtended(Buffer buffer, int id) throws IOException {
-        String extension = buffer.getString();
+        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
+     */
+    protected void executeExtendedCommand(Buffer buffer, int id, String 
extension) throws IOException {
         switch (extension) {
             case "text-seek":
                 doTextSeek(buffer, id);
                 break;
-            case "version-select":
+            case SftpConstants.EXT_VERSELECT:
                 doVersionSelect(buffer, id);
                 break;
             default:
@@ -578,27 +618,47 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
     }
 
     protected void doVersionSelect(Buffer buffer, int id) throws IOException {
-        String ver = buffer.getString();
+        String proposed = buffer.getString();
+        Boolean result = validateProposedVersion(id, proposed);
+        if (result == null) {   // response sent internally
+            return;
+        } if (result.booleanValue()) {
+            version = Integer.parseInt(proposed);
+            sendStatus(id, SSH_FX_OK, "");
+        } else {
+            sendStatus(id, SSH_FX_FAILURE, "Unsupported version " + proposed);
+        }
+    }
+
+    /**
+     * @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(int id, String proposed) throws 
IOException {
         if (log.isDebugEnabled()) {
-            log.debug("Received SSH_FXP_EXTENDED(version-select) 
(version={})", Integer.valueOf(version));
+            log.debug("Received SSH_FXP_EXTENDED(version-select) 
(version={})", proposed);
         }
         
-        if (GenericUtils.length(ver) == 1) {
-            char digit = ver.charAt(0);
-            if ((digit >= '0') && (digit <= '9')) {
-                int value = digit - '0';
-                String all = checkVersionCompatibility(id, value, 
SSH_FX_FAILURE);
-                if (GenericUtils.isEmpty(all)) {    // validation failed
-                    return;
-                }
+        if (GenericUtils.length(proposed) != 1) {
+            return Boolean.FALSE;
+        }
 
-                version = value;
-                sendStatus(id, SSH_FX_OK, "");
-                return;
-            }
+        char digit = proposed.charAt(0);
+        if ((digit < '0') || (digit > '9')) {
+            return Boolean.FALSE;
         }
 
-        sendStatus(id, SSH_FX_FAILURE, "Unsupported version " + ver);
+        int value = digit - '0';
+        String all = checkVersionCompatibility(id, value, SSH_FX_FAILURE);
+        if (GenericUtils.isEmpty(all)) {    // validation failed
+            return null;
+        } else {
+            return Boolean.TRUE;
+        }
     }
 
     /**
@@ -631,7 +691,7 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
 
         if (log.isTraceEnabled()) {
             log.trace("checkVersionCompatibility(id={}) - proposed={}, 
available={}",
-                      new Object[] { Integer.valueOf(id), 
Integer.valueOf(proposed), available });
+                      Integer.valueOf(id), Integer.valueOf(proposed), 
available);
         }
 
         if ((proposed < low) || (proposed > hig)) {
@@ -1249,24 +1309,105 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
         }
 
         buffer.clear();
+
         buffer.putByte((byte) SSH_FXP_VERSION);
         buffer.putInt(version);
+        appendExtensions(buffer, all);
+
+        send(buffer);
+    }
 
-        // newline
-        buffer.putString("newline");
-        buffer.putString(System.getProperty("line.separator"));
+    protected void appendExtensions(Buffer buffer, String supportedVersions) {
+        appendVersionsExtension(buffer, supportedVersions);
+        appendNewlineExtension(buffer, System.getProperty("line.separator"));
+        appendVendorIdExtension(buffer, 
VersionProperties.getVersionProperties());
 
-        // versions
-        buffer.putString("versions");
-        buffer.putString(all);
+        /* TODO updateAvailableExtensions(extensions, 
appendAclSupportedExtension(...)
+            buffer.putString("acl-supported");
+            buffer.putInt(4);
+            // capabilities
+            buffer.putInt(0);
+        */
 
-        // TODO text-seek - see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-13.txt
-        // TODO space-available - see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
-        // TODO home-directory - see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
+        Collection<String> extras = getSupportedClientExtensions();
+        appendSupportedExtension(buffer, extras);
+        appendSupported2Extension(buffer, extras);
+    }
 
-        // supported
-        buffer.putString("supported");
-        buffer.putInt(5 * 4); // length of 5 integers
+    protected Collection<String> getSupportedClientExtensions() {
+        String value = FactoryManagerUtils.getString(session, 
CLIENT_EXTENSIONS_PROP);
+        if (value == null) {
+            return DEFAULT_SUPPORTED_CLIENT_EXTENSIONS;
+        }
+        
+        if (value.length() <= 0) {  // means don't report any extensions
+            return Collections.<String>emptyList();
+        }
+
+        String[] comps = GenericUtils.split(value, ',');
+        return Arrays.asList(comps);
+    }
+    /**
+     * 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
+     * @see SftpConstants#EXT_VERSIONS
+     */
+    protected void appendVersionsExtension(Buffer buffer, String value) {
+        buffer.putString(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
+     * @see SftpConstants#EXT_NEWLINE
+     */
+    protected void appendNewlineExtension(Buffer buffer, String value) {
+        buffer.putString(EXT_NEWLINE);
+        buffer.putString(value);
+    }
+    
+    /**
+     * 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
+     * @see SftpConstants#EXT_VENDORID
+     * @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) {
+        buffer.putString(EXT_VENDORID);
+        
+        // placeholder for length
+        int lenPos = buffer.wpos();
+        buffer.putInt(0);
+        
buffer.putString(FactoryManagerUtils.getStringProperty(versionProperties, 
"groupId", getClass().getPackage().getName()));   // vendor-name
+        
buffer.putString(FactoryManagerUtils.getStringProperty(versionProperties, 
"artifactId", getClass().getSimpleName()));       // product-name
+        
buffer.putString(FactoryManagerUtils.getStringProperty(versionProperties, 
"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(EXT_SUPPORTED);
+        
+        int lenPos = buffer.wpos();
+        buffer.putInt(0); // length placeholder
         // supported-attribute-mask
         buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
                 | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
@@ -1281,10 +1422,26 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
         buffer.putInt(0);
         // max-read-size
         buffer.putInt(0);
-
-        // supported2
-        buffer.putString("supported2");
-        buffer.putInt(8 * 4); // length of 7 integers + 2 shorts
+        // supported extensions
+        buffer.putStringList(extras);
+        
+        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
+    }
+    
+    /**
+     * Appends the &quot;supported2&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
+     * @see SftpConstants#EXT_SUPPORTED
+     */
+    protected void appendSupported2Extension(Buffer buffer, Collection<String> 
extras) {
+        buffer.putString(EXT_SUPPORTED2);
+        
+        int lenPos = buffer.wpos();
+        buffer.putInt(0); // length placeholder
         // supported-attribute-mask
         buffer.putInt(SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_PERMISSIONS
                 | SSH_FILEXFER_ATTR_ACCESSTIME | SSH_FILEXFER_ATTR_CREATETIME
@@ -1306,16 +1463,10 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
         buffer.putInt(0);
         // extension-count
         buffer.putInt(0);
+        // supported extensions
+        buffer.putStringList(extras);
 
-        // TODO vendor-id see 
http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/draft-ietf-secsh-filexfer-09.txt
-        /*
-        buffer.putString("acl-supported");
-        buffer.putInt(4);
-        // capabilities
-        buffer.putInt(0);
-        */
-
-        send(buffer);
+        BufferUtils.updateLengthPlaceholder(buffer, lenPos);
     }
 
     protected void sendHandle(int id, String handle) throws IOException {
@@ -2135,10 +2286,11 @@ public class SftpSubsystem extends AbstractLoggingBean 
implements Command, Runna
     }
 
     protected void send(Buffer buffer) throws IOException {
-        DataOutputStream dos = new DataOutputStream(out);
-        dos.writeInt(buffer.available());
-        dos.write(buffer.array(), buffer.rpos(), buffer.available());
-        dos.flush();
+        int len = buffer.available();
+        int used = BufferUtils.putUInt(len & 0xFFFFL, workBuf);
+        out.write(workBuf, 0, used);
+        out.write(buffer.array(), buffer.rpos(), len);
+        out.flush();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77dfae2d/sshd-core/src/test/java/org/apache/sshd/client/subsystem/sftp/SftpTest.java
----------------------------------------------------------------------
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 053aad9..b4a1cef 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
@@ -36,18 +36,19 @@ import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
+import java.util.Map;
 import java.util.Random;
 import java.util.Vector;
 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.common.NamedFactory;
 import org.apache.sshd.common.file.FileSystemFactory;
 import org.apache.sshd.common.file.root.RootedFileSystemProvider;
 import org.apache.sshd.common.session.Session;
 import org.apache.sshd.common.subsystem.sftp.SftpConstants;
+import org.apache.sshd.common.subsystem.sftp.extensions.ParserUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
 import org.apache.sshd.common.util.io.IoUtils;
@@ -576,6 +577,37 @@ public class SftpTest extends BaseTestSupport {
     }
 
     @Test
+    public void testServerExtensionsDeclarations() throws Exception {
+        try(SshClient client = SshClient.setUpDefaultClient()) {
+            client.start();
+            
+            try (ClientSession session = client.connect(getCurrentTestName(), 
"localhost", port).verify(7L, TimeUnit.SECONDS).getSession()) {
+                session.addPasswordIdentity(getCurrentTestName());
+                session.auth().verify(5L, TimeUnit.SECONDS);
+
+                try(SftpClient sftp = session.createSftpClient()) {
+                    Map<String,byte[]> extensions = sftp.getServerExtensions();
+                    for (String name : new String[] {
+                            SftpConstants.EXT_NEWLINE, 
SftpConstants.EXT_VERSIONS,
+                            // SftpConstants.EXT_VENDORID,
+                            SftpConstants.EXT_SUPPORTED, 
SftpConstants.EXT_SUPPORTED2
+                        }) {
+                        assertTrue("Missing extension=" + name, 
extensions.containsKey(name));
+                    }
+                    
+                    Map<String,?> data = ParserUtils.parse(extensions);
+                    for (Map.Entry<String,?> de : data.entrySet()) {
+                        System.out.append('\t').append(de.getKey()).append(": 
").println(de.getValue());
+                    }
+                        
+                }
+            } finally {
+                client.stop();
+            }
+        }
+    }
+
+    @Test
     public void testCreateSymbolicLink() throws Exception {
         // Do not execute on windows as the file system does not support 
symlinks
         Assume.assumeTrue("Skip non-Unix O/S", OsUtils.isUNIX());

Reply via email to