Updated Branches: refs/heads/master 36446ae8d -> ade49e489
[SSHD-283] Support key re-exchange Make main message handler common between ServerSession and ClientSessionImpl. Replace the Session#state with an internal kex state variable and send events on SessionListener instead. Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/8d226dd8 Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/8d226dd8 Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/8d226dd8 Branch: refs/heads/master Commit: 8d226dd8a2f7f234924f95e88b2206808567e681 Parents: 36446ae Author: Guillaume Nodet <[email protected]> Authored: Wed Jan 29 22:21:45 2014 +0100 Committer: Guillaume Nodet <[email protected]> Committed: Wed Jan 29 22:21:45 2014 +0100 ---------------------------------------------------------------------- .../sshd/client/session/ClientSessionImpl.java | 156 +++++---------- .../java/org/apache/sshd/common/Session.java | 17 +- .../org/apache/sshd/common/SessionListener.java | 12 +- .../org/apache/sshd/common/SshConstants.java | 35 ++++ .../session/AbstractConnectionService.java | 39 ++-- .../sshd/common/session/AbstractSession.java | 192 +++++++++++++++---- .../sshd/server/session/ServerSession.java | 154 +-------------- .../session/ServerSessionTimeoutListener.java | 3 +- .../org/apache/sshd/AbstractSessionTest.java | 21 +- .../org/apache/sshd/AuthenticationTest.java | 3 - .../java/org/apache/sshd/KeyReExchangeTest.java | 148 ++++++++++++++ .../test/java/org/apache/sshd/ServerTest.java | 4 +- .../file/virtualfs/VirtualFileSystemTest.java | 17 +- 13 files changed, 479 insertions(+), 322 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java index ce3e030..42af0ff 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionImpl.java @@ -24,7 +24,6 @@ import java.security.KeyPair; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.apache.sshd.ClientChannel; import org.apache.sshd.ClientSession; @@ -46,9 +45,9 @@ import org.apache.sshd.client.future.DefaultAuthFuture; import org.apache.sshd.client.scp.DefaultScpClient; import org.apache.sshd.client.sftp.DefaultSftpClient; import org.apache.sshd.common.KeyPairProvider; -import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.Service; import org.apache.sshd.common.ServiceFactory; +import org.apache.sshd.common.SessionListener; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; import org.apache.sshd.common.SshdSocketAddress; @@ -69,15 +68,16 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession */ private Map<Object, Object> metadataMap = new HashMap<Object, Object>(); + // TODO: clean service support a bit + private boolean initialServiceRequestSent; private ServiceFactory currentServiceFactory; - private Service nextService; private ServiceFactory nextServiceFactory; protected AuthFuture authFuture; public ClientSessionImpl(ClientFactoryManager client, IoSession session) throws Exception { - super(client, session); + super(false, client, session); log.info("Session created..."); // Need to set the initial service early as calling code likes to start trying to // manipulate it before the connection has even been established. For instance, to @@ -97,6 +97,7 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession authFuture = new DefaultAuthFuture(lock); authFuture.setAuthed(false); sendClientIdentification(); + kexState = KEX_STATE_INIT; sendKexInit(); } @@ -225,99 +226,7 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession protected void handleMessage(Buffer buffer) throws Exception { synchronized (lock) { - doHandleMessage(buffer); - } - } - - protected void doHandleMessage(Buffer buffer) throws Exception { - SshConstants.Message cmd = buffer.getCommand(); - log.debug("Received packet {}", cmd); - switch (cmd) { - case SSH_MSG_DISCONNECT: { - int code = buffer.getInt(); - String msg = buffer.getString(); - log.info("Received SSH_MSG_DISCONNECT (reason={}, msg={})", code, msg); - close(true); - break; - } - case SSH_MSG_UNIMPLEMENTED: { - int code = buffer.getInt(); - log.info("Received SSH_MSG_UNIMPLEMENTED #{}", code); - break; - } - case SSH_MSG_DEBUG: { - boolean display = buffer.getBoolean(); - String msg = buffer.getString(); - log.info("Received SSH_MSG_DEBUG (display={}) '{}'", display, msg); - break; - } - case SSH_MSG_IGNORE: - log.info("Received SSH_MSG_IGNORE"); - break; - default: - switch (getState()) { - case ReceiveKexInit: - if (cmd != SshConstants.Message.SSH_MSG_KEXINIT) { - log.error("Ignoring command {} while waiting for {}", cmd, SshConstants.Message.SSH_MSG_KEXINIT); - break; - } - log.info("Received SSH_MSG_KEXINIT"); - receiveKexInit(buffer); - negociate(); - kex = NamedFactory.Utils.create(factoryManager.getKeyExchangeFactories(), negociated[SshConstants.PROPOSAL_KEX_ALGS]); - kex.init(this, serverVersion.getBytes(), clientVersion.getBytes(), I_S, I_C); - setState(State.Kex); - break; - case Kex: - buffer.rpos(buffer.rpos() - 1); - if (kex.next(buffer)) { - checkHost(); - sendNewKeys(); - setState(State.ReceiveNewKeys); - } - break; - case ReceiveNewKeys: - if (cmd != SshConstants.Message.SSH_MSG_NEWKEYS) { - disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Protocol error: expected packet SSH_MSG_NEWKEYS, got " + cmd); - return; - } - log.info("Received SSH_MSG_NEWKEYS"); - receiveNewKeys(false); - log.info("Send SSH_MSG_SERVICE_REQUEST for {}", currentServiceFactory.getName()); - Buffer request = createBuffer(SshConstants.Message.SSH_MSG_SERVICE_REQUEST, 0); - request.putString(currentServiceFactory.getName()); - writePacket(request); - setState(State.ServiceRequestSent); - // Assuming that MINA-SSHD only implements "explicit server authentication" it is permissible - // for the client's service to start sending data before the service-accept has been received. - // If "implicit authentication" were to ever be supported, then this would need to be - // called after service-accept comes back. See SSH-TRANSPORT. - currentService.start(); - break; - case ServiceRequestSent: - if (cmd != SshConstants.Message.SSH_MSG_SERVICE_ACCEPT) { - disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Protocol error: expected packet SSH_MSG_SERVICE_ACCEPT, got " + cmd); - return; - } - log.info("Received SSH_MSG_SERVICE_ACCEPT"); - setState(State.Running); - break; - case Running: - switch (cmd) { - case SSH_MSG_REQUEST_SUCCESS: - requestSuccess(buffer); - break; - case SSH_MSG_REQUEST_FAILURE: - requestFailure(buffer); - break; - default: - currentService.process(cmd, buffer); - break; - } - break; - default: - throw new IllegalStateException("Unsupported state: " + getState()); - } + super.handleMessage(buffer); } } @@ -362,13 +271,6 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession } } - public void setState(State newState) { - synchronized (lock) { - super.setState(newState); - lock.notifyAll(); - } - } - protected boolean readIdentification(Buffer buffer) throws IOException { serverVersion = doReadIdentification(buffer); if (serverVersion == null) { @@ -387,17 +289,18 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession sendIdentification(clientVersion); } - private void sendKexInit() throws Exception { + protected void sendKexInit() throws IOException { clientProposal = createProposal(KeyPairProvider.SSH_RSA + "," + KeyPairProvider.SSH_DSS); I_C = sendKexInit(clientProposal); } - private void receiveKexInit(Buffer buffer) throws Exception { + protected void receiveKexInit(Buffer buffer) throws IOException { serverProposal = new String[SshConstants.PROPOSAL_MAX]; I_S = receiveKexInit(buffer, serverProposal); } - private void checkHost() throws SshException { + @Override + protected void checkKeys() throws SshException { ServerKeyVerifier serverKeyVerifier = getClientFactoryManager().getServerKeyVerifier(); SocketAddress remoteAddress = ioSession.getRemoteAddress(); @@ -406,7 +309,44 @@ public class ClientSessionImpl extends AbstractSession implements ClientSession } } - public Map<Object, Object> getMetadataMap() { + @Override + protected void sendEvent(SessionListener.Event event) throws IOException { + if (event == SessionListener.Event.KeyEstablished) { + sendInitialServiceRequest(); + } + synchronized (lock) { + lock.notifyAll(); + } + super.sendEvent(event); + } + + protected void sendInitialServiceRequest() throws IOException { + if (initialServiceRequestSent) { + return; + } + initialServiceRequestSent = true; + log.info("Send SSH_MSG_SERVICE_REQUEST for {}", currentServiceFactory.getName()); + Buffer request = createBuffer(SshConstants.Message.SSH_MSG_SERVICE_REQUEST, 0); + request.putString(currentServiceFactory.getName()); + writePacket(request); + // Assuming that MINA-SSHD only implements "explicit server authentication" it is permissible + // for the client's service to start sending data before the service-accept has been received. + // If "implicit authentication" were to ever be supported, then this would need to be + // called after service-accept comes back. See SSH-TRANSPORT. + currentService.start(); + } + + @Override + public void startService(String name) throws Exception { + throw new IllegalStateException("Starting services is not supported on the client side"); + } + + @Override + public void resetIdleTimeout() { + + } + + public Map<Object, Object> getMetadataMap() { return metadataMap; } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/common/Session.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/Session.java b/sshd-core/src/main/java/org/apache/sshd/common/Session.java index cc968e3..9b8bcae 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/Session.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/Session.java @@ -20,6 +20,7 @@ package org.apache.sshd.common; import java.io.IOException; +import org.apache.sshd.common.future.SshFuture; import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.util.Buffer; @@ -30,17 +31,6 @@ import org.apache.sshd.common.util.Buffer; */ public interface Session { - public enum State { - ReceiveKexInit, Kex, ReceiveNewKeys, ServiceRequestSent, WaitForServiceRequest, Running, Closed - } - - /** - * Retrieve the state of this session. - * @return the session's state - * @see SessionListener - */ - State getState(); - /** * Returns the value of the user-defined attribute of this session. * @@ -153,6 +143,11 @@ public interface Session { void removeListener(SessionListener listener); /** + * Initiate a new key exchange. + */ + SshFuture reExchangeKeys() throws IOException; + + /** * Type safe key for storage within the user attributes of {@link org.apache.sshd.common.session.AbstractSession}. * Typically it is used as a static variable that is shared between the producer * and the consumer. To further restrict access the setting or getting it from http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/common/SessionListener.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/SessionListener.java b/sshd-core/src/main/java/org/apache/sshd/common/SessionListener.java index 96c96eb..dd30b48 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/SessionListener.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/SessionListener.java @@ -25,6 +25,10 @@ package org.apache.sshd.common; */ public interface SessionListener { + enum Event { + KeyEstablished, Authenticated + } + /** * A new session just been created * @param session @@ -32,11 +36,11 @@ public interface SessionListener { void sessionCreated(Session session); /** - * A session state has changed - * @param session - * @see org.apache.sshd.common.Session#getState() + * An event has been triggered + * @param sesssion + * @param event */ - void sessionChanged(Session session); + void sessionEvent(Session sesssion, Event event); /** * A session has been closed http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/common/SshConstants.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/SshConstants.java b/sshd-core/src/main/java/org/apache/sshd/common/SshConstants.java index 241c61e..26148fd 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/SshConstants.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/SshConstants.java @@ -97,6 +97,41 @@ public interface SshConstants { } } + static final int SSH_MSG_DISCONNECT= 1; + static final int SSH_MSG_IGNORE= 2; + static final int SSH_MSG_UNIMPLEMENTED= 3; + static final int SSH_MSG_DEBUG= 4; + static final int SSH_MSG_SERVICE_REQUEST= 5; + static final int SSH_MSG_SERVICE_ACCEPT= 6; + static final int SSH_MSG_KEXINIT= 20; + static final int SSH_MSG_NEWKEYS= 21; + + static final int SSH_MSG_KEX_FIRST= 30; + static final int SSH_MSG_KEX_LAST= 49; + + static final int SSH_MSG_KEXDH_INIT= 30; + static final int SSH_MSG_KEXDH_REPLY= 31; + + static final int SSH_MSG_KEX_DH_GEX_GROUP= 31; + static final int SSH_MSG_KEX_DH_GEX_INIT= 32; + static final int SSH_MSG_KEX_DH_GEX_REPLY= 33; + static final int SSH_MSG_KEX_DH_GEX_REQUEST= 34; + + static final int SSH_MSG_GLOBAL_REQUEST= 80; + static final int SSH_MSG_REQUEST_SUCCESS= 81; + static final int SSH_MSG_REQUEST_FAILURE= 82; + static final int SSH_MSG_CHANNEL_OPEN= 90; + static final int SSH_MSG_CHANNEL_OPEN_CONFIRMATION= 91; + static final int SSH_MSG_CHANNEL_OPEN_FAILURE= 92; + static final int SSH_MSG_CHANNEL_WINDOW_ADJUST= 93; + static final int SSH_MSG_CHANNEL_DATA= 94; + static final int SSH_MSG_CHANNEL_EXTENDED_DATA= 95; + static final int SSH_MSG_CHANNEL_EOF= 96; + static final int SSH_MSG_CHANNEL_CLOSE= 97; + static final int SSH_MSG_CHANNEL_REQUEST= 98; + static final int SSH_MSG_CHANNEL_SUCCESS= 99; + static final int SSH_MSG_CHANNEL_FAILURE= 100; + // // Values for the algorithms negociation // http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractConnectionService.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractConnectionService.java b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractConnectionService.java index 0149adb..ce68594 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractConnectionService.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractConnectionService.java @@ -182,6 +182,12 @@ public abstract class AbstractConnectionService implements ConnectionService { case SSH_MSG_GLOBAL_REQUEST: globalRequest(buffer); break; + case SSH_MSG_REQUEST_SUCCESS: + requestSuccess(buffer); + break; + case SSH_MSG_REQUEST_FAILURE: + requestFailure(buffer); + break; default: throw new IllegalStateException("Unsupported command: " + cmd); } @@ -356,18 +362,21 @@ public abstract class AbstractConnectionService implements ConnectionService { buf.putInt(channel.getLocalWindow().getSize()); buf.putInt(channel.getLocalWindow().getPacketSize()); session.writePacket(buf); - } else if (future.getException() != null) { - Buffer buf = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_OPEN_FAILURE, 0); - buf.putInt(id); - if (future.getException() instanceof OpenChannelException) { - buf.putInt(((OpenChannelException) future.getException()).getReasonCode()); - buf.putString(future.getException().getMessage()); - } else { - buf.putInt(0); - buf.putString("Error opening channel: " + future.getException().getMessage()); + } else { + Throwable exception = future.getException(); + if (exception != null) { + Buffer buf = session.createBuffer(SshConstants.Message.SSH_MSG_CHANNEL_OPEN_FAILURE, 0); + buf.putInt(id); + if (exception instanceof OpenChannelException) { + buf.putInt(((OpenChannelException) exception).getReasonCode()); + buf.putString(exception.getMessage()); + } else { + buf.putInt(0); + buf.putString("Error opening channel: " + exception.getMessage()); + } + buf.putString(""); + session.writePacket(buf); } - buf.putString(""); - session.writePacket(buf); } } catch (IOException e) { session.exceptionCaught(e); @@ -421,4 +430,12 @@ public abstract class AbstractConnectionService implements ConnectionService { } } + protected void requestSuccess(Buffer buffer) throws Exception { + ((AbstractSession) session).requestSuccess(buffer); + } + + protected void requestFailure(Buffer buffer) throws Exception { + ((AbstractSession) session).requestFailure(buffer); + } + } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java index 942809e..b703dff 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/AbstractSession.java @@ -24,6 +24,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.common.Cipher; @@ -41,6 +42,8 @@ import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; import org.apache.sshd.common.future.CloseFuture; import org.apache.sshd.common.future.DefaultCloseFuture; +import org.apache.sshd.common.future.DefaultSshFuture; +import org.apache.sshd.common.future.SshFuture; import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.io.IoCloseFuture; import org.apache.sshd.common.io.IoSession; @@ -72,6 +75,14 @@ public abstract class AbstractSession implements Session { * and {@link #attachSession(IoSession, AbstractSession)}. */ public static final String SESSION = "org.apache.sshd.session"; + + protected static final int KEX_STATE_INIT = 1; + protected static final int KEX_STATE_RUN = 2; + protected static final int KEX_STATE_KEYS = 3; + protected static final int KEX_STATE_DONE = 4; + + /** Client or server side */ + protected final boolean isServer; /** Our logger */ protected final Logger log = LoggerFactory.getLogger(getClass()); /** The factory manager used to retrieve factories of Ciphers, Macs and other objects */ @@ -94,7 +105,7 @@ public abstract class AbstractSession implements Session { protected String username; /** Session listener */ - protected final List<SessionListener> listeners = new ArrayList<SessionListener>(); + protected final List<SessionListener> listeners = new CopyOnWriteArrayList<SessionListener>(); // // Key exchange support @@ -108,6 +119,8 @@ public abstract class AbstractSession implements Session { protected byte[] I_C; // the payload of the client's SSH_MSG_KEXINIT protected byte[] I_S; // the payload of the factoryManager's SSH_MSG_KEXINIT protected KeyExchange kex; + protected int kexState; + protected DefaultSshFuture reexchangeFuture; // // SSH packets encoding / decoding support @@ -135,15 +148,14 @@ public abstract class AbstractSession implements Session { protected Service currentService; - private volatile State state = State.ReceiveKexInit; - /** * Create a new session. * * @param factoryManager the factory manager * @param ioSession the underlying MINA session */ - public AbstractSession(FactoryManager factoryManager, IoSession ioSession) { + public AbstractSession(boolean isServer, FactoryManager factoryManager, IoSession ioSession) { + this.isServer = isServer; this.factoryManager = factoryManager; this.ioSession = ioSession; this.random = factoryManager.getRandomFactory().create(); @@ -191,18 +203,6 @@ public abstract class AbstractSession implements Session { ioSession.setAttribute(SESSION, session); } - public State getState() { - return state; - } - - protected void setState(State state) { - this.state = state; - final ArrayList<SessionListener> l = new ArrayList<SessionListener>(listeners); - for (SessionListener sl : l) { - sl.sessionChanged(this); - } - } - public String getServerVersion() { return serverVersion; } @@ -242,9 +242,10 @@ public abstract class AbstractSession implements Session { return authed; } - public void setAuthenticated(String username) { + public void setAuthenticated(String username) throws IOException { this.authed = true; this.username = username; + sendEvent(SessionListener.Event.Authenticated); } /** @@ -285,7 +286,102 @@ public abstract class AbstractSession implements Session { * @param buffer the buffer containing the packet * @throws Exception if an exeption occurs while handling this packet. */ - protected abstract void handleMessage(Buffer buffer) throws Exception; + protected void handleMessage(Buffer buffer) throws Exception { + SshConstants.Message cmd = buffer.getCommand(); + switch (cmd) { + case SSH_MSG_DISCONNECT: { + int code = buffer.getInt(); + String msg = buffer.getString(); + log.debug("Received SSH_MSG_DISCONNECT (reason={}, msg={})", code, msg); + close(true); + break; + } + case SSH_MSG_IGNORE: { + log.debug("Received SSH_MSG_IGNORE"); + break; + } + case SSH_MSG_UNIMPLEMENTED: { + int code = buffer.getInt(); + log.debug("Received SSH_MSG_UNIMPLEMENTED #{}", code); + break; + } + case SSH_MSG_DEBUG: { + boolean display = buffer.getBoolean(); + String msg = buffer.getString(); + log.debug("Received SSH_MSG_DEBUG (display={}) '{}'", display, msg); + break; + } + case SSH_MSG_SERVICE_REQUEST: + String service = buffer.getString(); + log.debug("Received SSH_MSG_SERVICE_REQUEST '{}'", service); + if (kexState != KEX_STATE_DONE) { + throw new IllegalStateException("Received command " + cmd + " before key exchange is finished"); + } + try { + startService(service); + } catch (Exception e) { + log.debug("Service " + service + " rejected", e); + disconnect(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, "Bad service request: " + service); + break; + } + log.debug("Accepted service {}", service); + Buffer response = createBuffer(SshConstants.Message.SSH_MSG_SERVICE_ACCEPT, 0); + response.putString(service); + writePacket(response); + break; + case SSH_MSG_SERVICE_ACCEPT: + log.debug("Received SSH_MSG_SERVICE_ACCEPT"); + if (kexState != KEX_STATE_DONE) { + throw new IllegalStateException("Received command " + cmd + " before key exchange is finished"); + } + serviceAccept(); + break; + case SSH_MSG_KEXINIT: + log.debug("Received SSH_MSG_KEXINIT"); + receiveKexInit(buffer); + if (kexState == KEX_STATE_DONE) { + sendKexInit(); + } else if (kexState != KEX_STATE_INIT) { + throw new IllegalStateException("Received SSH_MSG_KEXINIT while key exchange is running"); + } + kexState = KEX_STATE_RUN; + negociate(); + kex = NamedFactory.Utils.create(factoryManager.getKeyExchangeFactories(), negociated[SshConstants.PROPOSAL_KEX_ALGS]); + kex.init(this, serverVersion.getBytes(), clientVersion.getBytes(), I_S, I_C); + break; + case SSH_MSG_NEWKEYS: + log.debug("Received SSH_MSG_NEWKEYS"); + if (kexState != KEX_STATE_KEYS) { + throw new IllegalStateException("Received command " + cmd + " before key exchange is finished"); + } + receiveNewKeys(); + kexState = KEX_STATE_DONE; + if (reexchangeFuture != null) { + reexchangeFuture.setValue(true); + } + sendEvent(SessionListener.Event.KeyEstablished); + break; + default: + log.debug("Received {}", cmd); + if (cmd.toByte() >= SshConstants.SSH_MSG_KEX_FIRST && cmd.toByte() <= SshConstants.SSH_MSG_KEX_LAST) { + if (kexState != KEX_STATE_RUN) { + throw new IllegalStateException("Received kex command " + cmd.toByte() + " while not in key exchange"); + } + buffer.rpos(buffer.rpos() - 1); + if (kex.next(buffer)) { + checkKeys(); + sendNewKeys(); + kexState = KEX_STATE_KEYS; + } + } else if (currentService != null) { + currentService.process(cmd, buffer); + resetIdleTimeout(); + } else { + throw new IllegalStateException("Unsupported command " + cmd); + } + break; + } + } /** * Handle any exceptions that occured on this session. @@ -331,8 +427,7 @@ public abstract class AbstractSession implements Session { closeFuture.setClosed(); lock.notifyAll(); } - state = State.Closed; - log.info("SMessession {}@{} closed", s.getUsername(), s.getIoSession().getRemoteAddress()); + log.info("Session {}@{} closed", s.getUsername(), s.getIoSession().getRemoteAddress()); // Fire 'close' event final ArrayList<SessionListener> l = new ArrayList<SessionListener>(listeners); for (SessionListener sl : l) { @@ -373,12 +468,16 @@ public abstract class AbstractSession implements Session { * @throws java.io.IOException if an error occured when encoding sending the packet */ public IoWriteFuture writePacket(Buffer buffer) throws IOException { - // Synchronize all write requests as needed by the encoding algorithm - // and also queue the write request in this synchronized block to ensure - // packets are sent in the correct order - synchronized (encodeLock) { - encode(buffer); - return ioSession.write(buffer); + try { + // Synchronize all write requests as needed by the encoding algorithm + // and also queue the write request in this synchronized block to ensure + // packets are sent in the correct order + synchronized (encodeLock) { + encode(buffer); + return ioSession.write(buffer); + } + } finally { + resetIdleTimeout(); } } @@ -768,10 +867,9 @@ public abstract class AbstractSession implements Session { * This method will intialize the ciphers, digests, macs and compression * according to the negociated server and client proposals. * - * @param isServer boolean indicating if this session is on the server or the client side * @throws Exception if an error occurs */ - protected void receiveNewKeys(boolean isServer) throws Exception { + protected void receiveNewKeys() throws Exception { byte[] IVc2s; byte[] IVs2c; byte[] Ec2s; @@ -1050,19 +1148,47 @@ public abstract class AbstractSession implements Session { if (listener == null) { throw new IllegalArgumentException(); } + this.listeners.add(listener); + } + + /** + * {@inheritDoc} + */ + public void removeListener(SessionListener listener) { + this.listeners.remove(listener); + } - synchronized (this.listeners) { - this.listeners.add(listener); + protected void sendEvent(SessionListener.Event event) throws IOException { + for (SessionListener sl : listeners) { + sl.sessionEvent(this, event); } } + /** * {@inheritDoc} */ - public void removeListener(SessionListener listener) { - synchronized (this.listeners) { - this.listeners.remove(listener); + public SshFuture reExchangeKeys() throws IOException { + if (kexState != KEX_STATE_DONE) { + throw new IllegalStateException("Can not perform key re-exchange while key exchange is already running"); } + kexState = KEX_STATE_INIT; + sendKexInit(); + reexchangeFuture = new DefaultSshFuture(null); + return reexchangeFuture; } + protected abstract void sendKexInit() throws IOException; + + protected abstract void checkKeys() throws IOException; + + protected abstract void receiveKexInit(Buffer buffer) throws IOException; + + protected void serviceAccept() throws IOException { + } + + public abstract void startService(String name) throws Exception; + + public abstract void resetIdleTimeout(); + } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java index 9fc2a14..30c8311 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSession.java @@ -20,33 +20,16 @@ package org.apache.sshd.server.session; import java.io.IOException; import java.security.KeyPair; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import org.apache.sshd.SshServer; -import org.apache.sshd.agent.common.AgentForwardSupport; -import org.apache.sshd.agent.local.ChannelAgentForwarding; -import org.apache.sshd.client.future.OpenFuture; -import org.apache.sshd.common.Channel; import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.Service; import org.apache.sshd.common.ServiceFactory; import org.apache.sshd.common.SshConstants; import org.apache.sshd.common.SshException; -import org.apache.sshd.common.SshdSocketAddress; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.future.SshFutureListener; import org.apache.sshd.common.io.IoSession; import org.apache.sshd.common.io.IoWriteFuture; import org.apache.sshd.common.session.AbstractSession; import org.apache.sshd.common.util.Buffer; import org.apache.sshd.server.ServerFactoryManager; -import org.apache.sshd.server.UserAuth; -import org.apache.sshd.server.channel.OpenChannelException; -import org.apache.sshd.server.x11.X11ForwardSupport; /** * @@ -68,12 +51,13 @@ public class ServerSession extends AbstractSession { private int idleTimeoutMs = 10 * 60 * 1000; // 10 minutes in milliseconds public ServerSession(ServerFactoryManager server, IoSession ioSession) throws Exception { - super(server, ioSession); + super(true, server, ioSession); authTimeoutMs = getIntProperty(ServerFactoryManager.AUTH_TIMEOUT, authTimeoutMs); authTimeoutTimestamp = System.currentTimeMillis() + authTimeoutMs; idleTimeoutMs = getIntProperty(ServerFactoryManager.IDLE_TIMEOUT, idleTimeoutMs); log.info("Session created from {}", ioSession.getRemoteAddress()); sendServerIdentification(); + kexState = KEX_STATE_INIT; sendKexInit(); } @@ -85,135 +69,17 @@ public class ServerSession extends AbstractSession { return (ServerFactoryManager) factoryManager; } - protected ScheduledExecutorService getScheduledExecutorService() { - return getServerFactoryManager().getScheduledExecutorService(); - } - - @Override - public IoWriteFuture writePacket(Buffer buffer) throws IOException { - boolean rescheduleIdleTimer = getState() == State.Running; - if (rescheduleIdleTimer) { - resetIdleTimeout(); - } - IoWriteFuture future = super.writePacket(buffer); - if (rescheduleIdleTimer) { - resetIdleTimeout(); - } - return future; - } - - protected void handleMessage(Buffer buffer) throws Exception { - SshConstants.Message cmd = buffer.getCommand(); - log.debug("Received packet {}", cmd); - switch (cmd) { - case SSH_MSG_DISCONNECT: { - int code = buffer.getInt(); - String msg = buffer.getString(); - log.debug("Received SSH_MSG_DISCONNECT (reason={}, msg={})", code, msg); - close(true); - break; - } - case SSH_MSG_UNIMPLEMENTED: { - int code = buffer.getInt(); - log.debug("Received SSH_MSG_UNIMPLEMENTED #{}", code); - break; - } - case SSH_MSG_DEBUG: { - boolean display = buffer.getBoolean(); - String msg = buffer.getString(); - log.debug("Received SSH_MSG_DEBUG (display={}) '{}'", display, msg); - break; - } - case SSH_MSG_IGNORE: - log.debug("Received SSH_MSG_IGNORE"); - break; - default: - switch (getState()) { - case ReceiveKexInit: - if (cmd != SshConstants.Message.SSH_MSG_KEXINIT) { - log.warn("Ignoring command " + cmd + " while waiting for " + SshConstants.Message.SSH_MSG_KEXINIT); - break; - } - log.debug("Received SSH_MSG_KEXINIT"); - receiveKexInit(buffer); - negociate(); - kex = NamedFactory.Utils.create(factoryManager.getKeyExchangeFactories(), negociated[SshConstants.PROPOSAL_KEX_ALGS]); - kex.init(this, serverVersion.getBytes(), clientVersion.getBytes(), I_S, I_C); - setState(State.Kex); - break; - case Kex: - buffer.rpos(buffer.rpos() - 1); - if (kex.next(buffer)) { - sendNewKeys(); - setState(State.ReceiveNewKeys); - } - break; - case ReceiveNewKeys: - if (cmd != SshConstants.Message.SSH_MSG_NEWKEYS) { - disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Protocol error: expected packet " + SshConstants.Message.SSH_MSG_NEWKEYS + ", got " + cmd); - return; - } - log.debug("Received SSH_MSG_NEWKEYS"); - receiveNewKeys(true); - setState(State.WaitForServiceRequest); - break; - case WaitForServiceRequest: - if (cmd != SshConstants.Message.SSH_MSG_SERVICE_REQUEST) { - log.debug("Expecting a {}, but received {}", SshConstants.Message.SSH_MSG_SERVICE_REQUEST, cmd); - notImplemented(); - } else { - String service = buffer.getString(); - log.debug("Received SSH_MSG_SERVICE_REQUEST '{}'", service); - try { - startService(service); - } catch (Exception e) { - log.debug("Service " + service + " rejected", e); - disconnect(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, "Bad service request: " + service); - break; - } - log.debug("Accepted service {}", service); - Buffer response = createBuffer(SshConstants.Message.SSH_MSG_SERVICE_ACCEPT, 0); - response.putString(service); - writePacket(response); - setState(State.Running); - } - break; - case Running: - running(cmd, buffer); - resetIdleTimeout(); - break; - default: - throw new IllegalStateException("Unsupported state: " + getState()); - } - } + protected void checkKeys() { } public void startService(String name) throws Exception { currentService = ServiceFactory.Utils.create(getFactoryManager().getServiceFactories(), name, this); } - private void running(SshConstants.Message cmd, Buffer buffer) throws Exception { - switch (cmd) { - case SSH_MSG_KEXINIT: - receiveKexInit(buffer); - sendKexInit(); - negociate(); - kex = NamedFactory.Utils.create(factoryManager.getKeyExchangeFactories(), negociated[SshConstants.PROPOSAL_KEX_ALGS]); - kex.init(this, serverVersion.getBytes(), clientVersion.getBytes(), I_S, I_C); - break; - case SSH_MSG_KEXDH_INIT: - buffer.rpos(buffer.rpos() - 1); - if (kex.next(buffer)) { - sendNewKeys(); - } - break; - case SSH_MSG_NEWKEYS: - receiveNewKeys(true); - break; - default: - currentService.process(cmd, buffer); - break; - } + @Override + protected void serviceAccept() throws IOException { + // TODO: can services be initiated by the server-side ? + disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Unsupported packet: SSH_MSG_SERVICE_ACCEPT"); } /** @@ -223,7 +89,7 @@ public class ServerSession extends AbstractSession { * @throws IOException */ protected void checkForTimeouts() throws IOException { - if (getState() != State.Closed) { + if (!closing) { long now = System.currentTimeMillis(); if (!authed && now > authTimeoutTimestamp) { disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Session has timed out waiting for authentication after " + authTimeoutMs + " ms."); @@ -247,7 +113,7 @@ public class ServerSession extends AbstractSession { sendIdentification(serverVersion); } - private void sendKexInit() throws IOException { + protected void sendKexInit() throws IOException { serverProposal = createProposal(factoryManager.getKeyPairProvider().getKeyTypes()); I_S = sendKexInit(serverProposal); } @@ -265,7 +131,7 @@ public class ServerSession extends AbstractSession { return true; } - private void receiveKexInit(Buffer buffer) throws IOException { + protected void receiveKexInit(Buffer buffer) throws IOException { clientProposal = new String[SshConstants.PROPOSAL_MAX]; I_C = receiveKexInit(buffer, clientProposal); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionTimeoutListener.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionTimeoutListener.java b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionTimeoutListener.java index 580249c..d85315f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionTimeoutListener.java +++ b/sshd-core/src/main/java/org/apache/sshd/server/session/ServerSessionTimeoutListener.java @@ -44,8 +44,7 @@ public class ServerSessionTimeoutListener implements SessionListener, Runnable { } } - public void sessionChanged(Session session) { - // ignore + public void sessionEvent(Session sesssion, Event event) { } public void sessionClosed(Session s) { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/test/java/org/apache/sshd/AbstractSessionTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/AbstractSessionTest.java b/sshd-core/src/test/java/org/apache/sshd/AbstractSessionTest.java index 1c4486a..8943be5 100644 --- a/sshd-core/src/test/java/org/apache/sshd/AbstractSessionTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/AbstractSessionTest.java @@ -18,6 +18,8 @@ */ package org.apache.sshd; +import java.io.IOException; + import org.apache.mina.core.buffer.IoBuffer; import org.apache.sshd.common.session.AbstractSession; import org.apache.sshd.common.util.Buffer; @@ -102,9 +104,7 @@ public class AbstractSessionTest { public static class MySession extends AbstractSession { public MySession() { - super(SshServer.setUpDefaultServer(), null); - } - public void messageReceived(IoBuffer byteBuffer) throws Exception { + super(true, SshServer.setUpDefaultServer(), null); } protected void handleMessage(Buffer buffer) throws Exception { } @@ -114,5 +114,20 @@ public class AbstractSessionTest { public String doReadIdentification(Buffer buffer) { return super.doReadIdentification(buffer); } + @Override + protected void sendKexInit() throws IOException { + } + @Override + protected void checkKeys() { + } + @Override + protected void receiveKexInit(Buffer buffer) throws IOException { + } + @Override + public void startService(String name) throws Exception { + } + @Override + public void resetIdleTimeout() { + } } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/test/java/org/apache/sshd/AuthenticationTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/AuthenticationTest.java b/sshd-core/src/test/java/org/apache/sshd/AuthenticationTest.java index bbbd863..1d429c3 100644 --- a/sshd-core/src/test/java/org/apache/sshd/AuthenticationTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/AuthenticationTest.java @@ -136,9 +136,6 @@ public class AuthenticationTest { public TestSession(ServerFactoryManager server, IoSession ioSession) throws Exception { super(server, ioSession); } - public void setState(State state) { - super.setState(state); - } public void handleMessage(Buffer buffer) throws Exception { super.handleMessage(buffer); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/test/java/org/apache/sshd/KeyReExchangeTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/KeyReExchangeTest.java b/sshd-core/src/test/java/org/apache/sshd/KeyReExchangeTest.java new file mode 100644 index 0000000..e6b1c54 --- /dev/null +++ b/sshd-core/src/test/java/org/apache/sshd/KeyReExchangeTest.java @@ -0,0 +1,148 @@ +/* + * 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.sshd; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Collections; + +import com.jcraft.jsch.JSch; +import org.apache.sshd.client.channel.ChannelShell; +import org.apache.sshd.client.kex.DHG1; +import org.apache.sshd.client.kex.DHG14; +import org.apache.sshd.client.kex.DHGEX; +import org.apache.sshd.client.kex.DHGEX256; +import org.apache.sshd.client.kex.ECDHP256; +import org.apache.sshd.client.kex.ECDHP384; +import org.apache.sshd.client.kex.ECDHP521; +import org.apache.sshd.common.KeyExchange; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.util.SecurityUtils; +import org.apache.sshd.util.BogusPasswordAuthenticator; +import org.apache.sshd.util.EchoShellFactory; +import org.apache.sshd.util.JSchLogger; +import org.apache.sshd.util.SimpleUserInfo; +import org.apache.sshd.util.TeeOutputStream; +import org.apache.sshd.util.Utils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Test key exchange algorithms. + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class KeyReExchangeTest { + + private SshServer sshd; + private int port; + + @Before + public void setUp() throws Exception { + port = Utils.getFreePort(); + + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(port); + sshd.setKeyPairProvider(Utils.createTestHostKeyProvider()); + sshd.setShellFactory(new EchoShellFactory()); + sshd.setPasswordAuthenticator(new BogusPasswordAuthenticator()); + sshd.start(); + } + + @After + public void tearDown() throws Exception { + sshd.stop(); + } + + @Test + public void testReExchangeFromClient() throws Exception { + JSchLogger.init(); + JSch.setConfig("kex", "diffie-hellman-group-exchange-sha1"); + JSch sch = new JSch(); + com.jcraft.jsch.Session s = sch.getSession("smx", "localhost", port); + s.setUserInfo(new SimpleUserInfo("smx")); + s.connect(); + com.jcraft.jsch.Channel c = s.openChannel("shell"); + c.connect(); + OutputStream os = c.getOutputStream(); + InputStream is = c.getInputStream(); + for (int i = 0; i < 10; i++) { + os.write("this is my command\n".getBytes()); + os.flush(); + byte[] data = new byte[512]; + int len = is.read(data); + String str = new String(data, 0, len); + assertEquals("this is my command\n", str); + s.rekey(); + } + c.disconnect(); + s.disconnect(); + } + + @Test + public void testReExchangeFromNativeClient() throws Exception { + SshClient client = SshClient.setUpDefaultClient(); + client.start(); + ClientSession session = client.connect("localhost", port).await().getSession(); + session.authPassword("smx", "smx").await(); + ChannelShell channel = session.createShellChannel(); + + ByteArrayOutputStream sent = new ByteArrayOutputStream(); + PipedOutputStream pipedIn = new PipedOutputStream(); + channel.setIn(new PipedInputStream(pipedIn)); + OutputStream teeOut = new TeeOutputStream(sent, pipedIn); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + channel.setOut(out); + channel.setErr(err); + channel.open(); + + teeOut.write("this is my command\n".getBytes()); + teeOut.flush(); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append("0123456789"); + } + sb.append("\n"); + + for (int i = 0; i < 10; i++) { + teeOut.write(sb.toString().getBytes()); + teeOut.flush(); + session.reExchangeKeys().await(); + } + teeOut.write("exit\n".getBytes()); + teeOut.flush(); + + channel.waitFor(ClientChannel.CLOSED, 0); + + channel.close(false); + client.stop(); + + assertArrayEquals(sent.toByteArray(), out.toByteArray()); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/test/java/org/apache/sshd/ServerTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/ServerTest.java b/sshd-core/src/test/java/org/apache/sshd/ServerTest.java index d7fea87..6cd2a08 100644 --- a/sshd-core/src/test/java/org/apache/sshd/ServerTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/ServerTest.java @@ -150,8 +150,8 @@ public class ServerTest { public void sessionCreated(Session session) { System.out.println("Session created"); } - public void sessionChanged(Session session) { - System.out.println("Session changed"); + public void sessionEvent(Session sesssion, Event event) { + System.out.println("Session event: " + event); } public void sessionClosed(Session session) { System.out.println("Session closed"); http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/8d226dd8/sshd-core/src/test/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemTest.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/test/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemTest.java b/sshd-core/src/test/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemTest.java index 222fce0..c9e887e 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/file/virtualfs/VirtualFileSystemTest.java @@ -77,7 +77,7 @@ public class VirtualFileSystemTest { static class TestSession extends AbstractSession { TestSession() { - super(SshServer.setUpDefaultServer(), null); + super(true, SshServer.setUpDefaultServer(), null); this.username = "userName"; } @Override @@ -87,6 +87,21 @@ public class VirtualFileSystemTest { protected boolean readIdentification(Buffer buffer) throws IOException { return false; } + @Override + protected void sendKexInit() throws IOException { + } + @Override + protected void checkKeys() { + } + @Override + protected void receiveKexInit(Buffer buffer) throws IOException { + } + @Override + public void startService(String name) throws Exception { + } + @Override + public void resetIdleTimeout() { + } } static class TestFactoryManager extends AbstractFactoryManager {
