Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/main/java/com/trilead/ssh2/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -495,6 +497,37 @@ private void checkRequirements(String user)
}
}

/**
* Returns the {@code SSH_MSG_USERAUTH_BANNER} messages sent by the server.
* <p>
* 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<String> 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
Expand Down
30 changes: 28 additions & 2 deletions src/main/java/com/trilead/ssh2/auth/AuthenticationManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public class AuthenticationManager implements MessageHandler
List<byte[]> packets = new ArrayList<>();
boolean connectionClosed = false;

String banner;
List<String> banners = new ArrayList<>();

String[] remainingMethods = new String[0];
boolean isPartialSuccess = false;
Expand Down Expand Up @@ -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).
* <p>
* 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<String> getBanners()
{
synchronized (banners)
{
return new ArrayList<>(banners);
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/test/java/com/trilead/ssh2/ConnectionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}