This is an automated email from the ASF dual-hosted git repository.

bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 0c4bcc5bf9 ssh, attach inputstream on connection, fixes #9159 (#9160)
0c4bcc5bf9 is described below

commit 0c4bcc5bf903aecc0dd6a601ae6975a4e0027381
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Oct 30 07:41:54 2026 +0100

    ssh, attach inputstream on connection, fixes #9159 (#9160)
---
 .../hop/vfs/sftp/provider/SftpStreamProxy.java     | 12 ++--
 .../hop/vfs/sftp/provider/SftpStreamProxyTest.java | 71 ++++++++++++++++++++++
 .../hop/pipeline/transforms/ssh/SessionResult.java | 16 +++--
 .../pipeline/transforms/ssh/SessionResultTest.java | 22 +++++++
 4 files changed, 111 insertions(+), 10 deletions(-)

diff --git 
a/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxy.java
 
b/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxy.java
index 5d648df6dc..5d8b074155 100644
--- 
a/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxy.java
+++ 
b/plugins/tech/sftp/src/main/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxy.java
@@ -44,6 +44,9 @@ public class SftpStreamProxy implements Proxy {
 
   private ChannelExec channel;
 
+  /** The output of the proxy command, attached before the channel connects. */
+  private InputStream inputStream;
+
   /**
    * Command pattern to execute on the proxy host.
    *
@@ -135,16 +138,15 @@ public class SftpStreamProxy implements Proxy {
             proxyOptions);
     channel = (ChannelExec) session.openChannel("exec");
     channel.setCommand(String.format(commandFormat, targetHost, targetPort));
+    // JSch drops output that arrives before a stream is attached, such as the 
target server's SSH
+    // greeting: attach it before connecting
+    inputStream = channel.getInputStream();
     channel.connect(timeout);
   }
 
   @Override
   public InputStream getInputStream() {
-    try {
-      return channel.getInputStream();
-    } catch (final IOException e) {
-      throw new IllegalStateException("IOException getting the SSH proxy input 
stream", e);
-    }
+    return inputStream;
   }
 
   @Override
diff --git 
a/plugins/tech/sftp/src/test/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxyTest.java
 
b/plugins/tech/sftp/src/test/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxyTest.java
new file mode 100644
index 0000000000..ff1c6bc498
--- /dev/null
+++ 
b/plugins/tech/sftp/src/test/java/org/apache/hop/vfs/sftp/provider/SftpStreamProxyTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.hop.vfs.sftp.provider;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.Session;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.MockedStatic;
+
+class SftpStreamProxyTest {
+
+  @Test
+  void connectAttachesTheInputStreamBeforeConnectingTheChannel() throws 
Exception {
+    // JSch drops output that arrives before a stream is attached: the target 
server's SSH greeting
+    // could be lost before the session read it
+    Session session = mock(Session.class);
+    ChannelExec channel = mock(ChannelExec.class);
+    InputStream input = new ByteArrayInputStream(new byte[0]);
+    when(session.openChannel("exec")).thenReturn(channel);
+    when(channel.getInputStream()).thenReturn(input);
+
+    SftpStreamProxy proxy =
+        new SftpStreamProxy(
+            SftpStreamProxy.NETCAT_COMMAND, "user", "proxy", 22, "secret", new 
FileSystemOptions());
+    try (MockedStatic<SftpClientFactory> factory = 
mockStatic(SftpClientFactory.class)) {
+      factory
+          .when(
+              () ->
+                  SftpClientFactory.createConnection(
+                      anyString(), anyInt(), any(char[].class), 
any(char[].class), any()))
+          .thenReturn(session);
+
+      proxy.connect(null, "target", 2222, 1000);
+    }
+
+    verify(channel).setCommand("nc -q 0 target 2222");
+    InOrder order = inOrder(channel);
+    order.verify(channel).getInputStream();
+    order.verify(channel).connect(1000);
+    assertSame(input, proxy.getInputStream());
+  }
+}
diff --git 
a/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SessionResult.java
 
b/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SessionResult.java
index 666bcfbdf5..e3e77f40da 100644
--- 
a/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SessionResult.java
+++ 
b/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SessionResult.java
@@ -75,11 +75,14 @@ public class SessionResult {
     try {
       channel = (ChannelExec) session.openChannel("exec");
       channel.setCommand(command);
+      // JSch drops output that arrives before a stream is attached: attach 
them before connecting
+      InputStream isOut = channel.getInputStream();
+      InputStream isErr = channel.getErrStream();
       channel.connect();
       SessionResult result = new SessionResult();
-      result.readFromChannel(channel, commandTimeoutMs, stopped, clock);
+      result.readFromChannel(channel, isOut, isErr, commandTimeoutMs, stopped, 
clock);
       return result;
-    } catch (JSchException e) {
+    } catch (JSchException | IOException e) {
       throw new HopException(e);
     } finally {
       if (channel != null) {
@@ -100,11 +103,14 @@ public class SessionResult {
   }
 
   private void readFromChannel(
-      ChannelExec channel, long commandTimeoutMs, BooleanSupplier stopped, 
LongSupplier clock)
+      ChannelExec channel,
+      InputStream isOut,
+      InputStream isErr,
+      long commandTimeoutMs,
+      BooleanSupplier stopped,
+      LongSupplier clock)
       throws HopException {
     try {
-      InputStream isOut = channel.getInputStream();
-      InputStream isErr = channel.getErrStream();
       byte[] buffer = new byte[8192];
       StringBuilder stdout = new StringBuilder();
       StringBuilder stderr = new StringBuilder();
diff --git 
a/plugins/transforms/ssh/src/test/java/org/apache/hop/pipeline/transforms/ssh/SessionResultTest.java
 
b/plugins/transforms/ssh/src/test/java/org/apache/hop/pipeline/transforms/ssh/SessionResultTest.java
index 5fc5625c9b..b2ed25274e 100644
--- 
a/plugins/transforms/ssh/src/test/java/org/apache/hop/pipeline/transforms/ssh/SessionResultTest.java
+++ 
b/plugins/transforms/ssh/src/test/java/org/apache/hop/pipeline/transforms/ssh/SessionResultTest.java
@@ -23,6 +23,7 @@ import static 
org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -37,6 +38,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.LongSupplier;
 import org.apache.hop.core.exception.HopException;
 import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
 
 class SessionResultTest {
 
@@ -68,6 +70,26 @@ class SessionResultTest {
     verify(channel).disconnect();
   }
 
+  @Test
+  void executeCommandAttachesTheStreamsBeforeConnecting() throws Exception {
+    // JSch drops output that arrives before a stream is attached: a fast 
command lost its output
+    Session session = mock(Session.class);
+    ChannelExec channel = mock(ChannelExec.class);
+    when(session.openChannel("exec")).thenReturn(channel);
+    when(channel.getInputStream()).thenReturn(stream("out\n"));
+    when(channel.getErrStream()).thenReturn(stream(""));
+    when(channel.isClosed()).thenReturn(true);
+
+    SessionResult.executeCommand(session, "echo out");
+
+    InOrder order = inOrder(channel);
+    order.verify(channel).getInputStream();
+    order.verify(channel).connect();
+    order = inOrder(channel);
+    order.verify(channel).getErrStream();
+    order.verify(channel).connect();
+  }
+
   @Test
   void executeCommandWrapsJSchException() throws Exception {
     Session session = mock(Session.class);

Reply via email to