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 "newline" 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 "vendor-id" 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 "versions" 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 "placeholder", + * 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 "support" and "support2" 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 "versions" 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 "newline" 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 "vendor-id" 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 "supported" 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 "supported2" 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());
