You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2012/10/29 04:48:25 UTC

svn commit: r1403155 - in /commons/proper/vfs/trunk/core/src: main/java/org/apache/commons/vfs2/provider/sftp/ test/java/org/apache/commons/vfs2/provider/sftp/test/

Author: ggregory
Date: Mon Oct 29 03:48:25 2012
New Revision: 1403155

URL: http://svn.apache.org/viewvc?rev=1403155&view=rev
Log:
[VFS-440] [SFTP] Stream (e.g. netcat) proxy for Sftp file system (aka ProxyCommand).

Added:
    commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java   (with props)
Modified:
    commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpClientFactory.java
    commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpFileSystemConfigBuilder.java
    commons/proper/vfs/trunk/core/src/test/java/org/apache/commons/vfs2/provider/sftp/test/SftpProviderTestCase.java

Modified: commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpClientFactory.java
URL: http://svn.apache.org/viewvc/commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpClientFactory.java?rev=1403155&r1=1403154&r2=1403155&view=diff
==============================================================================
--- commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpClientFactory.java (original)
+++ commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpClientFactory.java Mon Oct 29 03:48:25 2012
@@ -225,6 +225,23 @@ public final class SftpClientFactory
                         proxy = new ProxySOCKS5(proxyHost);
                     }
                 }
+                else if (SftpFileSystemConfigBuilder.PROXY_STREAM.equals(proxyType))
+                {
+                    // Use a stream proxy, i.e. it will use a remote host as a proxy
+                    // and run a command (e.g. netcat) that forwards input/output
+                    // to the target host.
+
+                    // Here we get the settings for connecting to the proxy:
+                    // user, password, options and a command
+                    String proxyUser = builder.getProxyUser(fileSystemOptions);
+                    String proxyPassword = builder.getProxyPassword(fileSystemOptions);
+                    FileSystemOptions proxyOptions = builder.getProxyOptions(fileSystemOptions);
+
+                    String proxyCommand = builder.getProxyCommand(fileSystemOptions);
+
+                    // Create the stream proxy
+                    proxy = new SftpStreamProxy(proxyCommand, proxyUser, proxyHost, proxyPort, proxyPassword, proxyOptions);
+                }
 
                 if (proxy != null)
                 {

Modified: commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpFileSystemConfigBuilder.java
URL: http://svn.apache.org/viewvc/commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpFileSystemConfigBuilder.java?rev=1403155&r1=1403154&r2=1403155&view=diff
==============================================================================
--- commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpFileSystemConfigBuilder.java (original)
+++ commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpFileSystemConfigBuilder.java Mon Oct 29 03:48:25 2012
@@ -101,15 +101,39 @@ public final class SftpFileSystemConfigB
     private static final String IDENTITY_REPOSITORY_FACTORY = _PREFIX + "IDENTITY_REPOSITORY_FACTORY";
     private static final String KNOWN_HOSTS = _PREFIX + ".KNOWN_HOSTS";
     private static final String PREFERRED_AUTHENTICATIONS = _PREFIX + ".PREFERRED_AUTHENTICATIONS";
-    private static final String PROXY_HOST = _PREFIX + ".PROXY_HOST";
 
     /** HTTP Proxy. */
     public static final ProxyType PROXY_HTTP = new ProxyType("http");
-    private static final String PROXY_PORT = _PREFIX + ".PROXY_PORT";
 
     /** SOCKS Proxy. */
     public static final ProxyType PROXY_SOCKS5 = new ProxyType("socks");
+
+    /**
+     * @brief Stream Proxy.
+     *
+     * <p>Connects to the SFTP server through a remote host reached by SSH.
+     * On this proxy host, a command
+     * (e.g. {@linkplain SftpStreamProxy#NETCAT_COMMAND}
+     * or {@linkplain SftpStreamProxy#NETCAT_COMMAND}) is run to forward
+     * input/output streams between the target host and the VFS host.</p>
+     * <p>
+     * When used, the proxy username ({@linkplain #setProxyUser}) and
+     * hostname ({@linkplain #setProxyHost}) <b>must</b> be set.
+     * Optionnaly, the command ({@linkplain #setProxyCommand}),
+     * password ({@linkplain #setProxyPassword}) and connection options
+     * ({@linkplain #setProxyOptions}) can be set.
+     * </p>
+     */
+    public static final ProxyType PROXY_STREAM = new ProxyType("stream");
+
+    private static final String PROXY_HOST = _PREFIX + ".PROXY_HOST";
+    private static final String PROXY_USER = _PREFIX + ".PROXY_USER";
+    private static final String PROXY_OPTIONS = _PREFIX + ".PROXY_OPTIONS";
     private static final String PROXY_TYPE = _PREFIX + ".PROXY_TYPE";
+    private static final String PROXY_PORT = _PREFIX + ".PROXY_PORT";
+    private static final String PROXY_PASSWORD = _PREFIX + ".PROXY_PASSWORD";
+    private static final String PROXY_COMMAND = _PREFIX + ".PROXY_COMMAND";
+
     private static final String STRICT_HOST_KEY_CHECKING = _PREFIX + ".STRICT_HOST_KEY_CHECKING";
     private static final String TIMEOUT = _PREFIX + ".TIMEOUT";
     private static final String USER_DIR_IS_ROOT = _PREFIX + ".USER_DIR_IS_ROOT";
@@ -198,6 +222,21 @@ public final class SftpFileSystemConfigB
         return (String) this.getParam(opts, PREFERRED_AUTHENTICATIONS);
     }
 
+
+    /**
+     * Gets the user name for the proxy used for the SFTP connection.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @return proxyUser
+     * @see #setProxyUser
+     * @since 2.1
+     */
+    public String getProxyUser(final FileSystemOptions opts)
+    {
+        return this.getString(opts, PROXY_USER);
+    }
+
     /**
      * Gets the proxy to use for the SFTP connection. You have to set the ProxyPort too if you would like to have the proxy
      * really used.
@@ -227,6 +266,55 @@ public final class SftpFileSystemConfigB
     }
 
     /**
+     * Gets the proxy options that are used to connect
+     * to the proxy host.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @return proxyOptions
+     * @see SftpStreamProxy
+     * @see #setProxyOptions
+     * @since 2.1
+     */
+    public FileSystemOptions getProxyOptions(final FileSystemOptions opts)
+    {
+        return (FileSystemOptions)this.getParam(opts, PROXY_OPTIONS);
+    }
+
+    /**
+     * Gets the proxy password that are used to connect
+     * to the proxy host.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @return proxyOptions
+     * @see SftpStreamProxy
+     * @see #setProxyPassword
+     * @since 2.1
+     */
+    public String getProxyPassword(final FileSystemOptions opts)
+    {
+        return this.getString(opts, PROXY_PASSWORD);
+    }
+
+    /**
+     * Gets the command that will be run on the proxy
+     * host when using a {@linkplain SftpStreamProxy}. The 
+     * command defaults to {@linkplain SftpStreamProxy#NETCAT_COMMAND}.
+     * 
+     * @param opts
+     *            The FileSystem options.
+     * @return proxyOptions
+     * @see SftpStreamProxy
+     * @see #setProxyOptions
+     * @since 2.1
+     */
+    public String getProxyCommand(final FileSystemOptions opts)
+    {
+        return this.getString(opts, PROXY_COMMAND, SftpStreamProxy.NETCAT_COMMAND);
+    }
+
+    /**
      * Gets the proxy type to use for the SFTP connection.
      *
      * @param opts
@@ -405,6 +493,13 @@ public final class SftpFileSystemConfigB
     /**
      * Sets the proxy type to use for the SFTP connection.
      *
+     * The possibles values are:
+     * <ul>
+     * <li>{@linkplain #PROXY_HTTP} connects using an HTTP proxy</li>
+     * <li>{@linkplain #PROXY_SOCKS5} connects using an Socket5 proxy</li>
+     * <li>{@linkplain #PROXY_STREAM} connects through a remote host stream command</li>
+     * </ul>
+     *
      * @param opts
      *            The FileSystem options.
      * @param proxyType
@@ -416,6 +511,69 @@ public final class SftpFileSystemConfigB
     }
 
     /**
+     * Sets the proxy username to use for the SFTP connection.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @param proxyUser
+     *            the username used to connect to the proxy
+     * @see #getProxyUser
+     * @since 2.1
+     */
+    public void setProxyUser(final FileSystemOptions opts, final String proxyUser)
+    {
+        this.setParam(opts, PROXY_USER, proxyUser);
+    }
+
+
+    /**
+     * Sets the proxy password to use for the SFTP connection.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @param proxyPassword
+     *            the username used to connect to the proxy
+     * @see #getProxyPassword
+     * @since 2.1
+     */
+    public void setProxyPassword(final FileSystemOptions opts, final String proxyPassword)
+    {
+        this.setParam(opts, PROXY_PASSWORD, proxyPassword);
+    }
+
+
+
+    /**
+     * Sets the proxy username to use for the SFTP connection.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @param proxyOptions
+     *            the options
+     * @see #getProxyOptions
+     * @since 2.1
+     */
+    public void setProxyOptions(final FileSystemOptions opts, final FileSystemOptions proxyOptions)
+    {
+        this.setParam(opts, PROXY_OPTIONS, proxyOptions);
+    }
+
+    /**
+     * Sets the proxy username to use for the SFTP connection.
+     *
+     * @param opts
+     *            The FileSystem options.
+     * @param proxyCommand
+     *            the port
+     * @see #getProxyOptions
+     * @since 2.1
+     */
+    public void setProxyCommand(final FileSystemOptions opts, final String proxyCommand)
+    {
+        this.setParam(opts, PROXY_COMMAND, proxyCommand);
+    }
+
+    /**
      * Configures the host key checking to use.
      * <p>
      * Valid arguments are: {@code "yes"}, {@code "no"} and {@code "ask"}.

Added: commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java
URL: http://svn.apache.org/viewvc/commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java?rev=1403155&view=auto
==============================================================================
--- commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java (added)
+++ commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java Mon Oct 29 03:48:25 2012
@@ -0,0 +1,189 @@
+/*
+ * 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.commons.vfs2.provider.sftp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+
+import org.apache.commons.vfs2.FileSystemOptions;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.Proxy;
+import com.jcraft.jsch.Session;
+import com.jcraft.jsch.SocketFactory;
+
+/**
+ * Stream based proxy for JSch.
+ *
+ * <p>
+ * Use a command on the proxy that will forward the SSH stream to the target host and port.
+ * </p>
+ *
+ * @since 2.1
+ */
+public class SftpStreamProxy implements Proxy
+{
+    /**
+     * Command format using bash built-in TCP stream.
+     */
+    public final static String BASH_TCP_COMMAND = "/bin/bash -c 'exec 3<>/dev/tcp/%s/%d; cat <&3 & cat >&3; kill $!";
+
+    /**
+     * Command format using netcat command.
+     */
+    public final static String NETCAT_COMMAND = "nc -q 0 %s %d";
+
+    private ChannelExec channel;
+
+    /**
+     * Command pattern to execute on the proxy host.
+     *
+     * <p>
+     * When run, the command output should be forwarded to the target host and port, and its input should be forwarded
+     * from the target host and port.
+     * </p>
+     *
+     * <p>
+     * The command will be created for each host/port pair by using {@linkplain String#format(String, Object...)} with
+     * two objects: the target host name ({@linkplain String}) and the target port ({@linkplain Integer}).
+     * </p>
+     * <p/>
+     * <p>
+     * Here are two examples (that can be easily used by using the static members of this class):
+     * <ul>
+     * <li><code>nc -q 0 %s %d</code> to use the netcat command ({@linkplain #NETCAT_COMMAND})</li>
+     * <li><code>/bin/bash -c 'exec 3<>/dev/tcp/%s/%d; cat <&3 & cat >&3; kill $!</code> will use bash built-in TCP
+     * stream, which can be useful when there is no netcat available. ({@linkplain #BASH_TCP_COMMAND})</li>
+     * </ul>
+     * </p>
+     */
+    private final String commandFormat;
+
+    /**
+     * Hostname used to connect to the proxy host.
+     */
+    private final String proxyHost;
+
+    /**
+     * The options for connection.
+     */
+    private FileSystemOptions proxyOptions;
+
+    /**
+     * The password to be used for connection.
+     */
+    private final String proxyPassword;
+
+    /**
+     * Port used to connect to the proxy host.
+     */
+    private final int proxyPort;
+
+    /**
+     * Username used to connect to the proxy host.
+     */
+    private final String proxyUser;
+
+    private Session session;
+
+    /**
+     * Creates a stream proxy.
+     *
+     * @param commandFormat
+     *            A format string that will be used to create the command to execute on the proxy host using
+     *            {@linkplain String#format(String, Object...)}. Two parameters are given to the format command, the
+     *            target host name (String) and port (Integer).
+     * @param proxyUser
+     *            The proxy user
+     * @param proxyPassword
+     *            The proxy password
+     * @param proxyHost
+     *            The proxy host
+     * @param proxyPort
+     *            The port to connect to on the proxy
+     * @param proxyOptions
+     *            Options used when connecting to the proxy
+     */
+    public SftpStreamProxy(String commandFormat, String proxyUser, String proxyHost, int proxyPort,
+            String proxyPassword, FileSystemOptions proxyOptions)
+    {
+        this.proxyHost = proxyHost;
+        this.proxyPort = proxyPort;
+        this.proxyUser = proxyUser;
+        this.proxyPassword = proxyPassword;
+        this.commandFormat = commandFormat;
+        this.proxyOptions = proxyOptions;
+    }
+
+    @Override
+    public void close()
+    {
+        if (channel != null)
+        {
+            channel.disconnect();
+        }
+        if (session != null)
+        {
+            session.disconnect();
+        }
+    }
+
+    @Override
+    public void connect(SocketFactory socketFactory, String targetHost, int targetPort, int timeout) throws Exception
+    {
+        session = SftpClientFactory.createConnection(proxyHost, proxyPort, proxyUser.toCharArray(),
+                proxyPassword.toCharArray(), proxyOptions);
+        channel = (ChannelExec) session.openChannel("exec");
+        channel.setCommand(String.format(commandFormat, targetHost, targetPort));
+        channel.connect(timeout);
+    }
+
+    @Override
+    public InputStream getInputStream()
+    {
+        try
+        {
+            return channel.getInputStream();
+        }
+        catch (IOException e)
+        {
+            throw new IllegalStateException("IOException getting the SSH proxy input stream", e);
+        }
+    }
+
+    @Override
+    public OutputStream getOutputStream()
+    {
+        try
+        {
+            return channel.getOutputStream();
+        }
+        catch (IOException e)
+        {
+            throw new IllegalStateException("IOException getting the SSH proxy output stream", e);
+        }
+    }
+
+    @Override
+    public Socket getSocket()
+    {
+        return null;
+    }
+}

Propchange: commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/vfs/trunk/core/src/main/java/org/apache/commons/vfs2/provider/sftp/SftpStreamProxy.java
------------------------------------------------------------------------------
    svn:keywords = Id

Modified: commons/proper/vfs/trunk/core/src/test/java/org/apache/commons/vfs2/provider/sftp/test/SftpProviderTestCase.java
URL: http://svn.apache.org/viewvc/commons/proper/vfs/trunk/core/src/test/java/org/apache/commons/vfs2/provider/sftp/test/SftpProviderTestCase.java?rev=1403155&r1=1403154&r2=1403155&view=diff
==============================================================================
--- commons/proper/vfs/trunk/core/src/test/java/org/apache/commons/vfs2/provider/sftp/test/SftpProviderTestCase.java (original)
+++ commons/proper/vfs/trunk/core/src/test/java/org/apache/commons/vfs2/provider/sftp/test/SftpProviderTestCase.java Mon Oct 29 03:48:25 2012
@@ -16,35 +16,44 @@
  */
 package org.apache.commons.vfs2.provider.sftp.test;
 
-import java.io.*;
-import java.net.InetSocketAddress;
-import java.security.PublicKey;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.TreeMap;
-
 import com.jcraft.jsch.SftpATTRS;
+import com.jcraft.jsch.TestIdentityRepositoryFactory;
+import junit.extensions.TestSetup;
 import junit.framework.Test;
-
+import junit.framework.TestSuite;
 import org.apache.commons.AbstractVfsTestCase;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.FileSystemManager;
 import org.apache.commons.vfs2.FileSystemOptions;
 import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
-import org.apache.commons.vfs2.test.PermissionsTests;
 import org.apache.commons.vfs2.provider.sftp.SftpFileProvider;
+import org.apache.commons.vfs2.provider.sftp.SftpFileSystem;
 import org.apache.commons.vfs2.provider.sftp.SftpFileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.sftp.SftpStreamProxy;
 import org.apache.commons.vfs2.provider.sftp.TrustEveryoneUserInfo;
 import org.apache.commons.vfs2.test.AbstractProviderTestConfig;
+import org.apache.commons.vfs2.test.PermissionsTests;
+import org.apache.commons.vfs2.test.ProviderReadTests;
+import org.apache.commons.vfs2.test.ProviderTestConfig;
 import org.apache.commons.vfs2.test.ProviderTestSuite;
 import org.apache.commons.vfs2.util.FreeSocketPortUtil;
 import org.apache.ftpserver.ftplet.FtpException;
 import org.apache.sshd.SshServer;
 import org.apache.sshd.common.NamedFactory;
 import org.apache.sshd.common.Session;
+import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.session.AbstractSession;
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.common.util.SecurityUtils;
-import org.apache.sshd.server.*;
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+import org.apache.sshd.server.ExitCallback;
+import org.apache.sshd.server.FileSystemFactory;
+import org.apache.sshd.server.FileSystemView;
+import org.apache.sshd.server.ForwardingFilter;
+import org.apache.sshd.server.PasswordAuthenticator;
+import org.apache.sshd.server.PublickeyAuthenticator;
+import org.apache.sshd.server.SshFile;
 import org.apache.sshd.server.auth.UserAuthNone;
 import org.apache.sshd.server.command.ScpCommandFactory;
 import org.apache.sshd.server.filesystem.NativeSshFile;
@@ -53,7 +62,21 @@ import org.apache.sshd.server.keyprovide
 import org.apache.sshd.server.session.ServerSession;
 import org.apache.sshd.server.sftp.SftpSubsystem;
 
-import com.jcraft.jsch.TestIdentityRepositoryFactory;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.URI;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Tests cases for the SFTP provider.
@@ -64,6 +87,11 @@ import com.jcraft.jsch.TestIdentityRepos
 public class SftpProviderTestCase extends AbstractProviderTestConfig
 {
     /**
+     * The underlying filesystem
+     */
+    private SftpFileSystem filesystem;
+
+    /**
      * Implements FileSystemFactory because SSHd does not know about users and home directories.
      */
     static final class TestFileSystemFactory implements FileSystemFactory
@@ -156,6 +184,9 @@ public class SftpProviderTestCase extend
 
     private static final String TEST_URI = "test.sftp.uri";
 
+    /** True if we are testing the SFTP stream proxy */
+    private boolean streamProxyMode;
+
     private static String getSystemTestUriOverride()
     {
         return System.getProperty(TEST_URI);
@@ -167,8 +198,12 @@ public class SftpProviderTestCase extend
      * @throws FtpException
      * @throws IOException
      */
-    private static void setUpClass() throws FtpException, IOException
+    private static void setUpClass() throws FtpException, IOException, InterruptedException
     {
+        SocketPort = FreeSocketPortUtil.findFreeLocalPort();
+        // Use %40 for @ in a URL
+        ConnectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, SocketPort);
+
         if (Server != null)
         {
             return;
@@ -179,7 +214,14 @@ public class SftpProviderTestCase extend
         Server.setPort(SocketPort);
         if (SecurityUtils.isBouncyCastleRegistered())
         {
-            Server.setKeyPairProvider(new PEMGeneratorHostKeyProvider(tmpDir + "/key.pem"));
+            // A temporary file will hold the key
+            final File keyFile = File.createTempFile("key", ".pem", new File(tmpDir));
+            keyFile.deleteOnExit();
+            // It has to be deleted in order to be generated
+            keyFile.delete();
+
+            final PEMGeneratorHostKeyProvider keyProvider = new PEMGeneratorHostKeyProvider(keyFile.getAbsolutePath());
+            Server.setKeyPairProvider(keyProvider);
         } else
         {
             Server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(tmpDir + "/key.ser"));
@@ -256,6 +298,27 @@ public class SftpProviderTestCase extend
         // Do this after we start the server to simplify this set up code.
         Server.getUserAuthFactories().add(new UserAuthNone.Factory());
         // HACK End
+    }
+
+
+    static private class BaseTest extends ProviderTestSuite {
+
+        public BaseTest(ProviderTestConfig providerConfig) throws Exception
+        {
+            super(providerConfig);
+        }
+
+        @Override
+        protected void tearDown() throws Exception
+        {
+            // Close all active sessions
+            // Note that it should be done by super.tearDown()
+            // while closing
+            for (AbstractSession session : Server.getActiveSessions()) {
+                session.close(true);
+            }
+            super.tearDown();
+        }
 
     }
 
@@ -264,7 +327,38 @@ public class SftpProviderTestCase extend
      */
     public static Test suite() throws Exception
     {
-        final ProviderTestSuite suite = new ProviderTestSuite(new SftpProviderTestCase())
+        // The test suite to be returned        
+        TestSuite suite = new TestSuite();
+
+        // --- Standard VFS test suite
+        final SftpProviderTestCase standardTestCase = new SftpProviderTestCase(false);
+        final ProviderTestSuite sftpSuite = new BaseTest(standardTestCase);
+        
+        // VFS-405: set/get permissions
+        sftpSuite.addTests(PermissionsTests.class);
+
+        suite.addTest(sftpSuite);
+
+
+        // --- VFS-440: stream proxy test suite
+        // We override the addBaseTests method so that only
+        // one test is run (we just test that the input/output are correctly forwarded, and
+        // hence if the reading test succeeds/fails the other will also succeed/fail)
+        final SftpProviderTestCase streamProxyTestCase = new SftpProviderTestCase(true);
+        final ProviderTestSuite sftpStreamSuite = new BaseTest(streamProxyTestCase)
+        {
+            @Override
+            protected void addBaseTests() throws Exception
+            {
+                // Just tries to read
+                addTests(ProviderReadTests.class);
+            }
+        };
+        suite.addTest(sftpStreamSuite);
+
+
+        // Decorate the test suite to set up the Sftp server
+        final TestSetup setup = new TestSetup(suite)
         {
             @Override
             protected void setUp() throws Exception
@@ -279,17 +373,14 @@ public class SftpProviderTestCase extend
             @Override
             protected void tearDown() throws Exception
             {
+                // Close SFTP server if needed
                 tearDownClass();
                 super.tearDown();
             }
         };
 
 
-        // VFS-405: set/get permissions
-        suite.addTests(PermissionsTests.class);
-
-        return suite;
-
+        return setup;
     }
 
     /**
@@ -305,11 +396,9 @@ public class SftpProviderTestCase extend
         }
     }
 
-    public SftpProviderTestCase() throws IOException
+    public SftpProviderTestCase(boolean streamProxyMode) throws IOException
     {
-        SocketPort = FreeSocketPortUtil.findFreeLocalPort();
-        // Use %40 for @ in a URL
-        ConnectionUri = String.format("sftp://%s@localhost:%d", DEFAULT_USER, SocketPort);
+        this.streamProxyMode = streamProxyMode;
     }
 
     /**
@@ -330,7 +419,33 @@ public class SftpProviderTestCase extend
         builder.setUserInfo(fileSystemOptions, new TrustEveryoneUserInfo());
         builder.setIdentityRepositoryFactory(fileSystemOptions, new TestIdentityRepositoryFactory());
 
-        return manager.resolveFile(uri, fileSystemOptions);
+        if (streamProxyMode)
+        {
+            final FileSystemOptions proxyOptions = (FileSystemOptions) fileSystemOptions.clone();
+
+            URI parsedURI = new URI(uri);
+            final String userInfo = parsedURI.getUserInfo();
+            final String[] userFields = userInfo.split(":", 2);
+
+            builder.setProxyType(fileSystemOptions, SftpFileSystemConfigBuilder.PROXY_STREAM);
+            builder.setProxyUser(fileSystemOptions, userFields[0]);
+            if (userFields.length > 1)
+            {
+                builder.setProxyPassword(fileSystemOptions, userFields[1]);
+            }
+            builder.setProxyHost(fileSystemOptions, parsedURI.getHost());
+            builder.setProxyPort(fileSystemOptions, parsedURI.getPort());
+            builder.setProxyCommand(fileSystemOptions, SftpStreamProxy.NETCAT_COMMAND);
+            builder.setProxyOptions(fileSystemOptions, proxyOptions);
+            builder.setProxyPassword(fileSystemOptions, parsedURI.getAuthority());
+
+            // Set up the new URI
+            uri = String.format("sftp://%s@localhost:%d", userInfo, parsedURI.getPort());
+        }
+
+        final FileObject fileObject = manager.resolveFile(uri, fileSystemOptions);
+        this.filesystem = (SftpFileSystem) fileObject.getFileSystem();
+        return fileObject;
     }
 
     /**
@@ -345,34 +460,44 @@ public class SftpProviderTestCase extend
 
     /**
      * The command factory for the SSH server:
-     * Handles two commands: id -u and id -G
+     * Handles these commands
+     * <p>
+     * <li><code>id -u</code> (permissions test)</li>
+     * <li><code>id -G</code> (permission tests)</li>
+     * <li><code>nc -q 0 localhost port</code> (Stream proxy tests)</li>
+     * </p>
      */
     private static class TestCommandFactory extends ScpCommandFactory
     {
+
+        public static final Pattern NETCAT_COMMAND = Pattern.compile("nc -q 0 localhost (\\d+)");
+
         @Override
         public Command createCommand(final String command)
         {
             return new Command()
             {
                 public ExitCallback callback = null;
-                public PrintStream out = null;
-                public PrintStream err = null;
+                public OutputStream out = null;
+                public OutputStream err = null;
+                public InputStream in = null;
 
                 @Override
                 public void setInputStream(InputStream in)
                 {
+                    this.in = in;
                 }
 
                 @Override
                 public void setOutputStream(OutputStream out)
                 {
-                    this.out = new PrintStream(out);
+                    this.out = out;
                 }
 
                 @Override
                 public void setErrorStream(OutputStream err)
                 {
-                    this.err = new PrintStream(err);
+                    this.err = err;
                 }
 
                 @Override
@@ -388,15 +513,36 @@ public class SftpProviderTestCase extend
                     int code = 0;
                     if (command.equals("id -G") || command.equals("id -u"))
                     {
-                        out.println(0);
+                        new PrintStream(out).println(0);
+                    } else if (NETCAT_COMMAND.matcher(command).matches())
+                    {
+                        final Matcher matcher = NETCAT_COMMAND.matcher(command);
+                        matcher.matches();
+                        final int port = Integer.parseInt(matcher.group(1));
+
+                        Socket socket = new Socket((String) null, port);
+
+                        if (out != null)
+                        {
+                            connect("from nc", socket.getInputStream(), out, null);
+                        }
+
+                        if (in != null)
+                        {
+                            connect("to nc", in, socket.getOutputStream(), callback);
+                        }
+
+                        return;
+
                     } else
                     {
                         if (err != null)
                         {
-                            err.format("Unknown command %s%n", command);
+                            new PrintStream(err).format("Unknown command %s%n", command);
                         }
                         code = -1;
                     }
+
                     if (out != null)
                     {
                         out.flush();
@@ -416,6 +562,54 @@ public class SftpProviderTestCase extend
         }
     }
 
+    /**
+     * Creates a pipe thread that connects an input to an output
+     * @param name The name of the thread (for debugging purposes)
+     * @param in The input stream
+     * @param out The output stream
+     * @param callback An object whose method {@linkplain ExitCallback#onExit(int)} will be
+     *                 called when the pipe is broken. The integer argument is 0 if everything
+     *                 went well.
+     */
+    private static void connect(final String name, final InputStream in, final OutputStream out, final ExitCallback callback)
+    {
+        Thread thread = new Thread(new Runnable()
+        {
+            public void run()
+            {
+                int code = 0;
+                try
+                {
+                    byte buffer[] = new byte[1024];
+                    int len;
+                    while ((len = in.read(buffer, 0, buffer.length)) != -1)
+                    {
+                        out.write(buffer, 0, len);
+                        out.flush();
+                    }
+                }
+                catch (SshException ex)
+                {
+                    // Nothing to do, this occurs when the connection
+                    // is closed on the remote side
+                }
+                catch (IOException ex)
+                {
+                    if (!ex.getMessage().equals("Pipe closed"))
+                    {
+                        code = -1;
+                    }
+                }
+                if (callback != null)
+                {
+                    callback.onExit(code);
+                }
+            }
+        }, name);
+        thread.setDaemon(true);
+        thread.start();
+    }
+
 
     private static class SftpAttrs
     {