diff --git a/src/main/java/com/trilead/ssh2/Connection.java b/src/main/java/com/trilead/ssh2/Connection.java
index 0de952a0..ad06d2fa 100644
--- a/src/main/java/com/trilead/ssh2/Connection.java
+++ b/src/main/java/com/trilead/ssh2/Connection.java
@@ -9,6 +9,8 @@
import java.net.SocketTimeoutException;
import java.security.KeyPair;
import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.List;
import java.util.Vector;
import com.trilead.ssh2.auth.AuthenticationManager;
@@ -495,6 +497,37 @@ private void checkRequirements(String user)
}
}
+ /**
+ * Returns the {@code SSH_MSG_USERAUTH_BANNER} messages sent by the server.
+ *
+ * The server may send banners at any time before authentication completes
+ * (typically used to display login notices or, in the case of Tailscale SSH,
+ * a web URL the user must visit to finish authenticating). Banners received
+ * during a blocking {@code authenticateWith*} call are accumulated and can
+ * be read either while the call is in progress (from another thread) or
+ * after it returns.
+ *
+ * @return list of banner strings in the order received, never {@code null};
+ * empty if no banners have been received or authentication has not
+ * been attempted yet.
+ * @see com.trilead.ssh2.auth.AuthenticationManager#getBanners()
+ */
+ public List getBanners()
+ {
+ // Intentionally NOT synchronized on this Connection. The other authentication
+ // entry points (authenticateWithNone, authenticateWithPassword, ...) hold the
+ // Connection monitor for the entire duration of the blocking auth call. A
+ // caller that wants to display banners live — e.g. Tailscale SSH's web-login
+ // URL, which arrives in a banner BEFORE USERAUTH_SUCCESS — must be able to
+ // read them from another thread while that call is still blocked.
+ AuthenticationManager local = am;
+ if (local == null)
+ {
+ return Collections.emptyList();
+ }
+ return local.getBanners();
+ }
+
/**
* Add a {@link ConnectionMonitor} to this connection. Can be invoked at any
* time, but it is best to add connection monitors before invoking
diff --git a/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java b/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java
index 71c51163..b3963096 100644
--- a/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java
+++ b/src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java
@@ -54,7 +54,7 @@ public class AuthenticationManager implements MessageHandler
List packets = new ArrayList<>();
boolean connectionClosed = false;
- String banner;
+ List banners = new ArrayList<>();
String[] remainingMethods = new String[0];
boolean isPartialSuccess = false;
@@ -114,7 +114,33 @@ byte[] getNextMessage() throws IOException
PacketUserauthBanner sb = new PacketUserauthBanner(msg, 0, msg.length);
- banner = sb.getBanner();
+ synchronized (banners)
+ {
+ banners.add(sb.getBanner());
+ }
+ }
+ }
+
+ /**
+ * Returns the {@code SSH_MSG_USERAUTH_BANNER} messages sent by the server.
+ * The server may send banners at any point before authentication completes
+ * (typically used to display login notices or, in the case of Tailscale SSH,
+ * a web URL the user must visit to finish authenticating).
+ *
+ * Returns a defensive snapshot, so the caller is free to iterate without
+ * holding any locks. Synchronizes only on the {@code banners} list itself,
+ * NOT on the {@code AuthenticationManager} or {@code Connection} monitor,
+ * so it remains callable from another thread while a blocking
+ * {@code authenticateWith*} call is in progress.
+ *
+ * @return list of banner strings in the order received, never {@code null};
+ * empty if the server has not sent any.
+ */
+ public List getBanners()
+ {
+ synchronized (banners)
+ {
+ return new ArrayList<>(banners);
}
}
diff --git a/src/test/java/com/trilead/ssh2/ConnectionTest.java b/src/test/java/com/trilead/ssh2/ConnectionTest.java
index 500270a8..3e4e7112 100644
--- a/src/test/java/com/trilead/ssh2/ConnectionTest.java
+++ b/src/test/java/com/trilead/ssh2/ConnectionTest.java
@@ -310,4 +310,14 @@ public void testSetAlgorithmMethods() {
String[] testKexAlgos = {"diffie-hellman-group1-sha1"}; // Basic DH group
connection.setKeyExchangeAlgorithms(testKexAlgos);
}
+
+@Test
+public void testGetBannersReturnsEmptyBeforeAuthentication() {
+ // Before any authentication attempt, AuthenticationManager has not been created
+ // yet, so the accessor must return an empty list rather than throwing or
+ // returning null.
+ assertNotNull(connection.getBanners(), "getBanners() must never return null");
+ assertTrue(connection.getBanners().isEmpty(),
+ "getBanners() must be empty before authentication");
+}
}