Hi all, so I wrote my own class that looks like this (stripped down):
public class SftpServerImpl implements PasswordAuthenticator {
private SshServer sshd;
/** holds user/pass authentication info for each SFTP session */
private final ConcurrentMap<String, String> authMap = new
ConcurrentHashMap<String, String>();
/* constructor */
public SftpServerImpl( ... params ... ) {
// configure server with sane defaults
sshd = SshServer.setUpDefaultServer();
// the port we listen on
sshd.setPort(port);
// the private RSA key
sshd.setKeyPairProvider(new FileKeyPairProvider(new
String[]{privateKey.getAbsolutePath()}));
// SFTP subsystem
sshd.setSubsystemFactories(Arrays.<NamedFactory<Command>>asList(new
SftpSubsystem.Factory()));
// echo shell
sshd.setShellFactory(new EchoShellFactory());
// this class manages authentication
sshd.setPasswordAuthenticator(this);
// file system view - uses our custom SFTP file system view
sshd.setFileSystemFactory(new SftpFileSystemFactory(rootDir));
}
It implements the PasswordAuthenticator interface =>
public boolean authenticate(String username, String password, ServerSession
session) {
System.err.println(String.format("AUTH: u=%s, p=%s, map=> %s,
map-hashcode=%s, this-hashcode=%s", username, password, authMap,
System.identityHashCode(authMap), System.identityHashCode(this)));
if(authMap.containsKey(username)) {
return authMap.get(username).equals(password);
}
return false;
}
}
So, this class holds an instance of the Ssh server, and it implements
PasswordAuthenticator interface. I have removed a lot of other code that is
not relevant for this example. You'll see that I have a private final instance
variable (ConcurrentHashMap) that holds the username/password for
authentication.
While writing test cases for this class, I encountered a troubling issue. In
my JUnit 4 @Before, I spin up a new instance of this class so I can test
against it:
private SftpServerImpl server;
@Before
public void setUp() throws Exception {
server = new SftpServerImpl(port, privateKey, rootDir, timeoutSeconds,
hostname);
}
I also properly shutdown the server on @After.
If I write a test case that uses a single session, and upload a file,
everything works fine. Here's a sample test case:
@Test
public void testCreateSessionAndUploadFile() throws Exception {
// server should be up and allow me to connect and upload a file or two
String url = server.createSession(new SessionRequest(username,
password));
assertEquals("url is incorrect ", "sftp://"+hostname+":"+port, url);
// you can breakpoint this test here, and try to connect with an
external client like FileZilla
System.out.println("SFTP server available at "+url);
// connect and upload a file
session = getSession(username, password, hostname);
sendFile(session, uploadFile1, targetFile1.getName());
assertTrue(targetFile1.getAbsolutePath()+" target file was not written
", targetFile1.exists());
assertFilesAreEqual(uploadFile1, targetFile1);
}
The method createSession( ) will end up adding a new entry in the map, so later
when authenticate( ) is called when the client session connects, it will work.
I am using JSch as the client here just like the Apache SftpTest.java
However, if I try to create two client sessions, I am running into an issue.
Here is an example:
@Test
public void testTwoSessionAndUploadTwoFiles() throws Exception {
// create the usual session
server.createSession(new SessionRequest(username, password));
final Session session1 = getSession(username, password, hostname);
// create a new session for a different user
String username2 = "larry";
String password2 = "david";
server.createSession(new SessionRequest(username2, password2));
final Session session2 = getSession(username2, password2, hostname);
<= FAILS AUTHENTICATION
}
The last line there fails authentication. If I print/inspect the map, I can
see that the entry is not there. But I also print/inspect the map on the line
above it and the entry IS there.
The map itself is private final so it cannot be changed, and I am not
explicitly changing it anywhere...which means that the actual SftpServerImpl
instance object itself is DIFFERENT.
This is why I'm printing System.identityHashcode(this) in that class, and
indeed, the hashcode changes between calls to the authenticate method, which
means the Sshd.java class itself is swapping instances internally.
Sshd.java has the getPasswordAuthenticator( ) / setPasswordAuthenticator( )
methods. I only ever set it once.
In UserAuthPassword.java, I see this method:
private boolean checkPassword(ServerSession session, String username, String
password) throws Exception {
PasswordAuthenticator auth =
session.getServerFactoryManager().getPasswordAuthenticator();
if (auth != null) {
return auth.authenticate(username, password, session);
}
throw new Exception("No PasswordAuthenticator configured");
}
Somehow, the first line there is delivering a different instance of my class
across successive calls - it is not consistent. I have only set the
PasswordAuthenticator one time and one time only, but it seems to have more
than one copy of my class in different states, which is very bizarre. I'm not
sure if it is a different ServerFactoryManager or not, but it is not returning
the same instance of my own SftpServerImpl.java class - I have validated this
by printing out the System.identityHashcode(this) in that class itself in
multiple places.
Any ideas on what is going on here. I aim to debug this further, but I'd love
to hear any viable explanation for why this is happening.
I can, of course, hack-fix it by making my map static, but this is ugly, and it
means I can only have one instance of this class in any JVM, and it can
potentially have other bad side effects.
Regards,
Davis