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"); +} }