Repository: mina-sshd Updated Branches: refs/heads/master 92c24d13c -> 77af61d35
[SSHD-85] Port Forward closes connection before all bytes are sent Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/77af61d3 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/77af61d3 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/77af61d3 Branch: refs/heads/master Commit: 77af61d35d0fda4c8179c074088eb46fe1a3fbbe Parents: 92c24d1 Author: Bill Kuker <bku...@martellotech.com> Authored: Thu Jun 22 23:15:00 2017 +0300 Committer: Goldstein Lyor <l...@c-b4.com> Committed: Sun Jun 25 07:46:57 2017 +0300 ---------------------------------------------------------------------- .../sshd/common/config/keys/PublicKeyEntry.java | 4 +- .../apache/sshd/common/io/nio2/Nio2Session.java | 10 +- .../forward/AbstractServerCloseTestSupport.java | 233 +++++++++++++++++++ .../forward/ApacheServerApacheClientTest.java | 114 +++++++++ .../forward/ApacheServerJSchClientTest.java | 121 ++++++++++ .../common/forward/NoServerNoClientTest.java | 42 ++++ 6 files changed, 521 insertions(+), 3 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java index 41e7b42..acbd71a 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntry.java @@ -167,8 +167,8 @@ public class PublicKeyEntry implements Serializable { } /** - * @param data Assumed to contain at least {@code key-type base64-data} (anything - * beyond the BASE64 data is ignored) - ignored if {@code null}/empty + * @param encData Assumed to contain at least {@code key-type base64-data} + * (anything beyond the BASE64 data is ignored) - ignored if {@code null}/empty * @return A {@link PublicKeyEntry} or {@code null} if no data * @throws IllegalArgumentException if bad format found * @see #parsePublicKeyEntry(PublicKeyEntry, String) http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/main/java/org/apache/sshd/common/io/nio2/Nio2Session.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/io/nio2/Nio2Session.java b/sshd-core/src/main/java/org/apache/sshd/common/io/nio2/Nio2Session.java index 32d39b5..1384092 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/io/nio2/Nio2Session.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/io/nio2/Nio2Session.java @@ -179,7 +179,15 @@ public class Nio2Session extends AbstractCloseable implements IoSession { @Override protected CloseFuture doCloseGracefully() { - return builder().when(writes).build().close(false); + return builder().when(writes).run(() -> { + try { + AsynchronousSocketChannel socket = getSocket(); + socket.shutdownOutput(); + } catch (IOException e) { + log.info("doCloseGracefully({}) {} while shutting down output: {}", + this, e.getClass().getSimpleName(), e.getMessage()); + } + }).build().close(false); } @Override http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/test/java/org/apache/sshd/common/forward/AbstractServerCloseTestSupport.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/AbstractServerCloseTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/AbstractServerCloseTestSupport.java new file mode 100644 index 0000000..33a7c94 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/AbstractServerCloseTestSupport.java @@ -0,0 +1,233 @@ +/* + * 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.forward; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousServerSocketChannel; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.apache.sshd.util.test.BaseTestSupport; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Port forwarding tests + */ +public abstract class AbstractServerCloseTestSupport extends BaseTestSupport { + private static final String PAYLOAD = String.join("", Collections.nCopies(200, "This is significantly longer Test Data.")); + + protected int testServerPort; + private final Logger log; + private AsynchronousServerSocketChannel testServerSock; + + protected AbstractServerCloseTestSupport() { + log = LoggerFactory.getLogger(getClass()); + } + + /* + * Start a server to forward to. + * + * This server sends PAYLOAD and then closes. + */ + @Before + public void startTestServer() throws Exception { + InetSocketAddress sockAddr = new InetSocketAddress(TEST_LOCALHOST, 0); + testServerSock = AsynchronousServerSocketChannel.open().bind(sockAddr); + InetSocketAddress boundAddress = (InetSocketAddress) testServerSock.getLocalAddress(); + testServerPort = boundAddress.getPort(); + log.info("Listening on port {}", testServerPort); + // Accept a connection + testServerSock.accept(testServerSock, + new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() { + @Override + @SuppressWarnings("synthetic-access") + public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) { + // a connection is accepted, start to accept next connection + serverSock.accept(serverSock, this); + log.info("Accepted new incoming connection"); + + ByteBuffer buf = ByteBuffer.wrap(PAYLOAD.getBytes(StandardCharsets.UTF_8)); + // start to write payload to client + sockChannel.write(buf, sockChannel, + new CompletionHandler<Integer, AsynchronousSocketChannel>() { + @Override + public void completed(Integer result, AsynchronousSocketChannel channel) { + // Write has been completed, close the + // connection to the client + try { + channel.close(); + } catch (IOException e) { + log.warn("Failed ({}) to close channel after write complete: {}", + e.getClass().getSimpleName(), e.getMessage()); + } + } + + @Override + public void failed(Throwable exc, AsynchronousSocketChannel channel) { + log.error("Failed ({}) to write message to client: {}", exc.getClass().getSimpleName(), exc.getMessage()); + } + }); + } + + @Override + @SuppressWarnings("synthetic-access") + public void failed(Throwable exc, AsynchronousServerSocketChannel serverSock) { + log.error("Failed ({}) to accept incoming connection: {}", exc.getClass().getSimpleName(), exc.getMessage()); + } + }); + } + + @After + public void stopTestServer() throws Exception { + testServerSock.close(); + } + + private void readInLoop(int serverPort) throws Exception { + outputDebugMessage("readInLoop(port=%d)", serverPort); + + StringBuilder sb = new StringBuilder(PAYLOAD.length()); + try (Socket s = new Socket(TEST_LOCALHOST, serverPort)) { + s.setSoTimeout(300); + + try (InputStream inputStream = s.getInputStream()) { + byte b[] = new byte[PAYLOAD.length() / 10]; + while (true) { + int readLen = inputStream.read(b); + if (readLen == -1) { + break; + } + outputDebugMessage("readInLoop(port=%d) read %d bytes", serverPort, readLen); + + String fragment = new String(b, 0, readLen, StandardCharsets.UTF_8); + sb.append(fragment); + Thread.sleep(25L); + } + } + } catch (IOException e) { + String readData = sb.toString(); + assertEquals("Mismatched data length", PAYLOAD.length(), readData.length()); + assertEquals("Mismatched read data", PAYLOAD, readData); + } + } + + private void readInOneBuffer(int serverPort) throws Exception { + outputDebugMessage("readInOneBuffer(port=%d)", serverPort); + try (Socket s = new Socket(TEST_LOCALHOST, serverPort)) { + s.setSoTimeout(300); + + byte buf[] = new byte[PAYLOAD.length()]; + try (InputStream inputStream = s.getInputStream()) { + int readCount = inputStream.read(buf); + outputDebugMessage("readInOneBuffer(port=%d) - Got %d bytes from the server", serverPort, readCount); + + String actual = new String(buf, 0, readCount, StandardCharsets.UTF_8); + assertEquals("Mismatched read data", PAYLOAD, actual); + } + } + } + + private void readInTwoBuffersWithPause(int serverPort) throws Exception { + outputDebugMessage("readInTwoBuffersWithPause(port=%d)", serverPort); + try (Socket s = new Socket(TEST_LOCALHOST, serverPort)) { + s.setSoTimeout(300); + byte b1[] = new byte[PAYLOAD.length() / 2]; + byte b2[] = new byte[PAYLOAD.length()]; + + try (InputStream inputStream = s.getInputStream()) { + int read1 = inputStream.read(b1); + outputDebugMessage("readInTwoBuffersWithPause(port=%d) - 1st half is %d bytes", serverPort, read1); + String half1 = new String(b1, 0, read1, StandardCharsets.UTF_8); + + Thread.sleep(50L); + try { + int read2 = inputStream.read(b2); + outputDebugMessage("readInTwoBuffersWithPause(port=%d) - 2nd half is %d bytes", serverPort, read2); + + String half2 = new String(b2, 0, read2, StandardCharsets.UTF_8); + assertEquals("Mismatched read data", PAYLOAD, half1 + half2); + } catch (IOException e) { + log.error("Disconnected ({}) before all data read: {}", e.getClass().getSimpleName(), e.getMessage()); + throw e; + } + } + } + } + + protected abstract int startRemotePF() throws Exception; + + protected abstract int startLocalPF() throws Exception; + + /* + * Connect to test server via port forward and read real quick with one big + * buffer. + * + * PROVIDED AS TEST THAT HAS ALWAYS PASSED + */ + @Test + public void testRemotePortForwardOneBuffer() throws Exception { + readInOneBuffer(startRemotePF()); + } + + /* + * Connect to test server via port forward and read real quick with one big + * buffer. + * + * THIS IS THE TEST OF SSHD-85 + */ + @Test + public void testRemotePortForwardTwoBuffers() throws Exception { + readInTwoBuffersWithPause(startRemotePF()); + } + + @Test + public void testRemotePortForwardLoop() throws Exception { + readInLoop(startRemotePF()); + } + + @Test + public void testLocalPortForwardOneBuffer() throws Exception { + readInOneBuffer(startLocalPF()); + } + + /* + * Connect to test server via port forward and read with 2 buffers and a + * pause in between. + * + * THIS IS THE TEST OF SSHD-85 + */ + @Test + public void testLocalPortForwardTwoBuffers() throws Exception { + readInTwoBuffersWithPause(startLocalPF()); + } + + @Test + public void testLocalPortForwardLoop() throws Exception { + readInLoop(startLocalPF()); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerApacheClientTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerApacheClientTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerApacheClientTest.java new file mode 100644 index 0000000..f6ac886 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerApacheClientTest.java @@ -0,0 +1,114 @@ +/* + * 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.forward; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.net.SshdSocketAddress; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.forward.AcceptAllForwardingFilter; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Port forwarding tests, Apache server & client + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class ApacheServerApacheClientTest extends AbstractServerCloseTestSupport { + private static final Logger LOG = LoggerFactory.getLogger(ApacheServerApacheClientTest.class); + + private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10L); + + private static int sshServerPort; + private static SshServer server; + + private ClientSession session; + + public ApacheServerApacheClientTest() { + super(); + } + + @BeforeClass + public static void startSshServer() throws IOException { + LOG.info("Starting SSHD..."); + server = SshServer.setUpDefaultServer(); + server.setPasswordAuthenticator((u, p, s) -> true); + server.setTcpipForwardingFilter(AcceptAllForwardingFilter.INSTANCE); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); + server.start(); + sshServerPort = server.getPort(); + LOG.info("SSHD Running on port {}", server.getPort()); + } + + @AfterClass + public static void stopServer() throws IOException { + if (!server.close(true).await(TIMEOUT)) { + LOG.warn("Failed to close server within {} sec.", TimeUnit.MILLISECONDS.toSeconds(TIMEOUT)); + } + } + + @Before + public void createClient() throws IOException { + SshClient client = SshClient.setUpDefaultClient(); + client.setTcpipForwardingFilter(AcceptAllForwardingFilter.INSTANCE); + client.start(); + LOG.info("Connecting..."); + session = client.connect("user", TEST_LOCALHOST, sshServerPort).verify(TIMEOUT).getSession(); + LOG.info("Authenticating..."); + session.addPasswordIdentity("foo"); + session.auth().verify(TIMEOUT); + LOG.info("Authenticated"); + } + + @After + public void stopClient() throws Exception { + LOG.info("Disconnecting Client"); + try { + assertTrue("Failed to close session", session.close(true).await(TIMEOUT)); + } finally { + session = null; + } + } + + @Override + protected int startRemotePF() throws Exception { + SshdSocketAddress remote = new SshdSocketAddress(TEST_LOCALHOST, 0); + SshdSocketAddress local = new SshdSocketAddress(TEST_LOCALHOST, testServerPort); + SshdSocketAddress bound = session.startRemotePortForwarding(remote, local); + return bound.getPort(); + } + + @Override + protected int startLocalPF() throws Exception { + SshdSocketAddress remote = new SshdSocketAddress(TEST_LOCALHOST, 0); + SshdSocketAddress local = new SshdSocketAddress(TEST_LOCALHOST, testServerPort); + SshdSocketAddress bound = session.startLocalPortForwarding(remote, local); + return bound.getPort(); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerJSchClientTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerJSchClientTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerJSchClientTest.java new file mode 100644 index 0000000..3f15a91 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/ApacheServerJSchClientTest.java @@ -0,0 +1,121 @@ +/* + * 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.forward; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.concurrent.TimeUnit; + +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; + +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.forward.AcceptAllForwardingFilter; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.util.test.JSchLogger; +import org.apache.sshd.util.test.SimpleUserInfo; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Port forwarding tests - Apache server, JSch client + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class ApacheServerJSchClientTest extends AbstractServerCloseTestSupport { + private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(10L); + private static final Logger LOG = LoggerFactory.getLogger(ApacheServerJSchClientTest.class); + + private static int sshServerPort; + private static SshServer server; + + private Session session; + + public ApacheServerJSchClientTest() { + super(); + } + + private static int findFreePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + /* + * Starts an SSH Server + */ + @BeforeClass + public static void startSshServer() throws IOException { + LOG.info("Starting SSHD..."); + server = SshServer.setUpDefaultServer(); + server.setPasswordAuthenticator((u, p, s) -> true); + server.setTcpipForwardingFilter(AcceptAllForwardingFilter.INSTANCE); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider()); + server.start(); + sshServerPort = server.getPort(); + LOG.info("SSHD Running on port {}", server.getPort()); + } + + @BeforeClass + public static void jschInit() { + JSchLogger.init(); + } + + @AfterClass + public static void stopServer() throws IOException { + if (!server.close(true).await(TIMEOUT)) { + LOG.warn("Failed to close server within {} sec.", TimeUnit.MILLISECONDS.toSeconds(TIMEOUT)); + } + } + + @Before + public void createClient() throws Exception { + JSch client = new JSch(); + session = client.getSession("user", TEST_LOCALHOST, sshServerPort); + session.setUserInfo(new SimpleUserInfo("password")); + LOG.trace("Connecting session..."); + session.connect(); + LOG.trace("Client is running now..."); + } + + @After + public void stopClient() throws Exception { + LOG.info("Disconnecting Client"); + session.disconnect(); + } + + @Override + protected int startRemotePF() throws Exception { + int port = findFreePort(); + session.setPortForwardingR(TEST_LOCALHOST, port, TEST_LOCALHOST, testServerPort); + return port; + } + + @Override + protected int startLocalPF() throws Exception { + int port = findFreePort(); + session.setPortForwardingL(TEST_LOCALHOST, port, TEST_LOCALHOST, testServerPort); + return port; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/77af61d3/sshd-core/src/test/java/org/apache/sshd/common/forward/NoServerNoClientTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/NoServerNoClientTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/NoServerNoClientTest.java new file mode 100644 index 0000000..712114c --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/NoServerNoClientTest.java @@ -0,0 +1,42 @@ +/* + * 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.forward; + +import org.junit.FixMethodOrder; +import org.junit.runners.MethodSorters; + +/** + * Port forwarding tests - Control, direct connect. No SSH + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class NoServerNoClientTest extends AbstractServerCloseTestSupport { + public NoServerNoClientTest() { + super(); + } + + @Override + protected int startRemotePF() throws Exception { + return testServerPort; + } + + @Override + protected int startLocalPF() throws Exception { + return testServerPort; + } +} \ No newline at end of file