diff --git a/pom.xml b/pom.xml index b45b05bc..80c60494 100644 --- a/pom.xml +++ b/pom.xml @@ -59,8 +59,8 @@ 1.7.12 - 4.1.36.Final - 2.6.1 + 4.1.81.Final + 2.7.4 2.4.0 4.12 diff --git a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/util/Helper.java b/terminal-api/src/main/java/org/aesh/terminal/utils/Helper.java similarity index 97% rename from terminal-telnet/src/main/java/org/aesh/terminal/telnet/util/Helper.java rename to terminal-api/src/main/java/org/aesh/terminal/utils/Helper.java index 3858f56f..ae54b177 100644 --- a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/util/Helper.java +++ b/terminal-api/src/main/java/org/aesh/terminal/utils/Helper.java @@ -17,7 +17,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.aesh.terminal.telnet.util; +package org.aesh.terminal.utils; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; diff --git a/terminal-ssh/hostkey.ser b/terminal-ssh/hostkey.ser index 19135c77..a1a10e80 100644 Binary files a/terminal-ssh/hostkey.ser and b/terminal-ssh/hostkey.ser differ diff --git a/terminal-ssh/pom.xml b/terminal-ssh/pom.xml index 015a5876..a4bc6bc1 100644 --- a/terminal-ssh/pom.xml +++ b/terminal-ssh/pom.xml @@ -49,6 +49,9 @@ org.aesh.terminal.ssh 1.7.12 + 2.2.4 + 0.1.54 + 2.14.0 @@ -72,6 +75,11 @@ netty-common ${netty.version} + + io.netty + netty-common + ${netty.version} + io.netty netty-transport @@ -93,7 +101,7 @@ org.apache.sshd sshd-core - 1.0.0 + ${sshd-core.version} true @@ -102,6 +110,18 @@ + + org.apache.sshd + sshd-netty + ${sshd-core.version} + true + + + org.slf4j + slf4j-api + + + org.aesh @@ -127,15 +147,21 @@ com.jcraft jsch - 0.1.54 + ${jsch.version} test org.apache.mina mina-core - 2.1.10 + ${mina-core.version} test + + org.aesh + readline + ${project.version} + test + diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/TtyCommand.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/TtyCommand.java index efc573cb..f180a5f5 100644 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/TtyCommand.java +++ b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/TtyCommand.java @@ -34,14 +34,16 @@ import org.apache.sshd.common.io.IoInputStream; import org.apache.sshd.common.io.IoOutputStream; import org.apache.sshd.common.io.IoWriteFuture; +import org.apache.sshd.common.io.WritePendingException; import org.apache.sshd.common.util.buffer.ByteArrayBuffer; -import org.apache.sshd.server.AsyncCommand; -import org.apache.sshd.server.ChannelSessionAware; +import org.apache.sshd.server.command.AsyncCommand; +import org.apache.sshd.server.channel.ChannelSessionAware; import org.apache.sshd.server.Environment; import org.apache.sshd.server.ExitCallback; import org.apache.sshd.server.channel.ChannelDataReceiver; import org.apache.sshd.server.channel.ChannelSession; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -120,21 +122,22 @@ public void setIoInputStream(IoInputStream in) { @Override public void setIoOutputStream(IoOutputStream out) { - this.ioOut = out; - this.out = (byte[] bytes) -> { - if(writeFuture == null) - writeFuture = out.write(new ByteArrayBuffer(bytes)); - else if(writeFuture.isWritten()) - writeFuture = out.write(new ByteArrayBuffer(bytes)); - else { - try { - writeFuture.await(); - writeFuture = out.write(new ByteArrayBuffer(bytes)); - } catch (IOException e) { - e.printStackTrace(); - } - } - }; + this.ioOut = out; + this.out = bytes -> { + ByteArrayBuffer byteArrayBuffer = new ByteArrayBuffer(bytes); + // the loop is only needed if we catch a WritePendingException, to retry the write and clear the buffer + while (byteArrayBuffer.available() > 0) { + try { + IoWriteFuture ioWriteFuture = out.writeBuffer(byteArrayBuffer); + // await the write so that we do not lose bytes + ioWriteFuture.verify(1, TimeUnit.SECONDS); + } catch (WritePendingException | EOFException ignored) { + // WritePendingException is only cought if the verify() method timeouts + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; } @Override @@ -148,7 +151,7 @@ public void setExitCallback(ExitCallback callback) { } @Override - public void start(final Environment env) throws IOException { + public void start(ChannelSession channelSession, Environment env) throws IOException { String lcctype = env.getEnv().get("LC_CTYPE"); if (lcctype != null) { charset = parseCharset(lcctype); @@ -156,7 +159,7 @@ public void start(final Environment env) throws IOException { if (charset == null) { charset = defaultCharset; } - env.addSignalListener(signal -> updateSize(env), EnumSet.of(org.apache.sshd.server.Signal.WINCH)); + env.addSignalListener((ch, signal) -> updateSize(env), EnumSet.of(org.apache.sshd.server.Signal.WINCH)); updateSize(env); // Event handling @@ -205,7 +208,7 @@ public void updateSize(Environment env) { @Override public void close() throws IOException { - close(0); + close(0); } private void close(int exit) throws IOException { @@ -223,8 +226,8 @@ private void close(int exit) throws IOException { } @Override - public void destroy() { - // Test this + public void destroy(ChannelSession channelSession) throws Exception { + // Test this } protected void execute(Runnable task) { diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncAuth.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncAuth.java deleted file mode 100644 index 93e8d927..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncAuth.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import java.util.function.Consumer; - -/** - * @author Julien Viet - */ -public class AsyncAuth extends RuntimeException { - - private volatile Consumer listener; - private volatile Boolean authed; - - public void setAuthed(boolean authed) { - Consumer listener; - synchronized (this) { - if (this.authed != null) { - return; - } - this.authed = authed; - listener = this.listener; - } - if (listener != null) { - listener.accept(authed); - } - } - - public void setListener(Consumer listener) { - Boolean result; - synchronized (this) { - this.listener = listener; - result = this.authed; - } - if (result != null) { - listener.accept(result); - } - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthService.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthService.java deleted file mode 100644 index 525a6ada..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthService.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.common.FactoryManagerUtils; -import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.NamedResource; -import org.apache.sshd.common.Service; -import org.apache.sshd.common.SshConstants; -import org.apache.sshd.common.SshException; -import org.apache.sshd.common.session.Session; -import org.apache.sshd.common.util.CloseableUtils; -import org.apache.sshd.common.util.GenericUtils; -import org.apache.sshd.common.util.ValidateUtils; -import org.apache.sshd.common.util.buffer.Buffer; -import org.apache.sshd.server.ServerFactoryManager; -import org.apache.sshd.server.auth.UserAuth; -import org.apache.sshd.server.session.ServerSession; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author Julien Viet - */ -public class AsyncUserAuthService extends CloseableUtils.AbstractCloseable implements Service { - - public static final int DEFAULT_MAX_AUTH_REQUESTS = 20; - private final ServerSession session; - private List> userAuthFactories; - private List> authMethods; - private String authUserName; - private String authMethod; - private String authService; - private UserAuth currentAuth; - private AsyncAuth async; - - private int maxAuthRequests; - private int nbAuthRequests; - - private static final Logger LOGGER = Logger.getLogger(AsyncUserAuthService.class.getName()); - - - public AsyncUserAuthService(Session s) throws SshException { - ValidateUtils.checkTrue(s instanceof ServerSession, "Server side service used on client side"); - if (s.isAuthenticated()) { - throw new SshException("Session already authenticated"); - } - - this.session = (ServerSession) s; - maxAuthRequests = session.getIntProperty(ServerFactoryManager.MAX_AUTH_REQUESTS, DEFAULT_MAX_AUTH_REQUESTS); - - ServerFactoryManager manager = getFactoryManager(); - userAuthFactories = new ArrayList<>(manager.getUserAuthFactories()); - // Get authentication methods - authMethods = new ArrayList<>(); - - String mths = FactoryManagerUtils.getString(manager, ServerFactoryManager.AUTH_METHODS); - if (GenericUtils.isEmpty(mths)) { - for (NamedFactory uaf : manager.getUserAuthFactories()) { - authMethods.add(new ArrayList<>(Collections.singletonList(uaf.getName()))); - } - } - else { - for (String mthl : mths.split("\\s")) { - authMethods.add(new ArrayList<>(Arrays.asList(mthl.split(",")))); - } - } - // Verify all required methods are supported - for (List l : authMethods) { - for (String m : l) { - NamedFactory factory = NamedResource.Utils.findByName(m, String.CASE_INSENSITIVE_ORDER, userAuthFactories); - if (factory == null) { - throw new SshException("Configured method is not supported: " + m); - } - } - } - - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Authorized authentication methods: "+ NamedResource.Utils.getNames(userAuthFactories)); - } - } - - @Override - public void start() { - // do nothing - } - - @Override - public ServerSession getSession() { - return session; - } - - @Override - public void process(int cmd, Buffer buffer) throws Exception { - Boolean authed = Boolean.FALSE; - - if (cmd == SshConstants.SSH_MSG_USERAUTH_REQUEST) { - LOGGER.fine("Received SSH_MSG_USERAUTH_REQUEST"); - if (this.currentAuth != null) { - this.currentAuth.destroy(); - this.currentAuth = null; - } - - String username = buffer.getString(); - String service = buffer.getString(); - String method = buffer.getString(); - if (this.authUserName == null || this.authService == null) { - this.authUserName = username; - this.authService = service; - } - else if (!this.authUserName.equals(username) || !this.authService.equals(service)) { - session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, - "Change of username or service is not allowed (" + this.authUserName + ", " + this.authService + ") -> (" - + username + ", " + service + ")"); - return; - } - // TODO: verify that the service is supported - this.authMethod = method; - if (nbAuthRequests++ > maxAuthRequests) { - session.disconnect(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR, "Too many authentication failures"); - return; - } - - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Authenticating user '{"+username+"}' with service '{"+service+"}' and method '{"+method+"}'"); - } - - NamedFactory factory = NamedResource.Utils.findByName(method, String.CASE_INSENSITIVE_ORDER, userAuthFactories); - if (factory != null) { - currentAuth = factory.create(); - try { - authed = currentAuth.auth(session, username, service, buffer); - } catch (Exception e) { - if (asyncAuth(buffer, e)) { - return; - } - - // Continue - LOGGER.fine("Authentication failed: "+ e.getMessage()); - } - } - } - else { - assert async == null; - if (this.currentAuth == null) { - // This should not happen - throw new IllegalStateException("No current authentication mechanism for cmd=" + (cmd & 0xFF)); - } - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Received authentication message: "+ Integer.valueOf(cmd & 0xFF)); - } - buffer.rpos(buffer.rpos() - 1); - try { - authed = currentAuth.next(buffer); - } catch (Exception e) { - if (asyncAuth(buffer, e)) { - return; - } - - // Continue - LOGGER.log(Level.FINE, "Failed ({}) to authenticate: "+ e.getClass().getSimpleName(), e.getMessage()); - } - } - - if (authed == null) { - // authentication is still ongoing - LOGGER.fine("Authentication not finished"); - } - else { - sendAuthenticationResult(buffer, authed); - } - } - - private boolean asyncAuth(Buffer buffer, Exception e) { - if (e instanceof AsyncAuth) { - async = (AsyncAuth) e; - async.setListener(authenticated -> { - async = null; - try { - sendAuthenticationResult(buffer, authenticated); - } catch (Exception e1) { - // HANDLE THIS BETTER - e1.printStackTrace(); - } - }); - return true; - } - else { - return false; - } - } - - private void sendAuthenticationResult(Buffer buffer, boolean authed) throws Exception { - if (authed) { - LOGGER.fine("Authentication succeeded"); - String username = currentAuth.getUserName(); - - boolean success = false; - for (List l : authMethods) { - if ((GenericUtils.size(l) > 0) && l.get(0).equals(authMethod)) { - l.remove(0); - success |= l.isEmpty(); - } - } - - if (success) { - FactoryManager manager = getFactoryManager(); - Integer maxSessionCount = FactoryManagerUtils.getInteger(manager, ServerFactoryManager.MAX_CONCURRENT_SESSIONS); - if (maxSessionCount != null) { - int currentSessionCount = session.getActiveSessionCountForUser(username); - if (currentSessionCount >= maxSessionCount) { - session.disconnect(SshConstants.SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE, - "Too many concurrent connections (" + currentSessionCount + ") - max. allowed: " + maxSessionCount); - return; - } - } - - String welcomeBanner = FactoryManagerUtils.getString(manager, ServerFactoryManager.WELCOME_BANNER); - if (welcomeBanner != null) { - buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_BANNER); - buffer.putString(welcomeBanner); - buffer.putString("en"); - session.writePacket(buffer); - } - - buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_SUCCESS); - session.setUsername(username); - session.setAuthenticated(); - session.startService(authService); - - // Important: write packet after setting service, otherwise this service may process wrong messages - session.writePacket(buffer); - session.resetIdleTimeout(); - LOGGER.fine("Session {"+username+"}@{+"+session.getIoSession().getRemoteAddress()+"} authenticated"); - - } - else { - buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE); - StringBuilder sb = new StringBuilder(); - for (List l : authMethods) { - if (GenericUtils.size(l) > 0) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(l.get(0)); - } - } - buffer.putString(sb.toString()); - buffer.putBoolean(true); - session.writePacket(buffer); - } - - currentAuth.destroy(); - currentAuth = null; - } - else { - LOGGER.fine("Authentication failed"); - - buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_FAILURE); - StringBuilder sb = new StringBuilder(); - for (List l : authMethods) { - if (GenericUtils.size(l) > 0) { - String m = l.get(0); - if (!"none".equals(m)) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(l.get(0)); - } - } - } - buffer.putString(sb.toString()); - buffer.putByte((byte) 0); - session.writePacket(buffer); - - if (currentAuth != null) { - currentAuth.destroy(); - currentAuth = null; - } - } - } - - private ServerFactoryManager getFactoryManager() { - return session.getFactoryManager(); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthServiceFactory.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthServiceFactory.java deleted file mode 100644 index 758c5fa7..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/AsyncUserAuthServiceFactory.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import org.apache.sshd.common.Service; -import org.apache.sshd.common.ServiceFactory; -import org.apache.sshd.common.session.Session; - -import java.io.IOException; - -/** - * @author Julien Viet - */ -public class AsyncUserAuthServiceFactory implements ServiceFactory { - public static final AsyncUserAuthServiceFactory INSTANCE = new AsyncUserAuthServiceFactory(); - - public AsyncUserAuthServiceFactory() { - super(); - } - - @Override - public String getName() { - return "ssh-userauth"; - } - - @Override - public Service create(Session session) throws IOException { - return new AsyncUserAuthService(session); - } -} \ No newline at end of file diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/Helper.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/Helper.java deleted file mode 100644 index 967f300f..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/Helper.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import java.io.IOException; -import java.io.InterruptedIOException; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -/** - * @author Julien Viet - */ -class Helper { - - static IOException toIOException(Exception e) { - if (e instanceof InterruptedException) { - InterruptedIOException ioe = new InterruptedIOException(); - ioe.initCause(e); - return ioe; - } else { - return new IOException(e); - } - } - - public static Consumer startedHandler(CompletableFuture fut) { - return err -> { - if (err == null) { - fut.complete(null); - } else { - fut.completeExceptionally(err); - } - }; - } - - public static Consumer stoppedHandler(CompletableFuture fut) { - return err -> { - fut.complete(null); - }; - } - -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoAcceptor.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoAcceptor.java deleted file mode 100644 index 0058fa74..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoAcceptor.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelOption; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.group.ChannelGroup; -import io.netty.channel.group.DefaultChannelGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioServerSocketChannel; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; -import io.netty.util.concurrent.GlobalEventExecutor; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.future.DefaultCloseFuture; -import org.apache.sshd.common.io.IoAcceptor; -import org.apache.sshd.common.io.IoHandler; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.util.CloseableUtils; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * @author Julien Viet - */ -public class NettyIoAcceptor extends CloseableUtils.AbstractCloseable implements IoAcceptor { - - final NettyIoServiceFactory factory; - final ChannelGroup channelGroup; - final NettyIoService ioService = new NettyIoService(); - private final ServerBootstrap bootstrap = new ServerBootstrap(); - private final DefaultCloseFuture closeFuture = new DefaultCloseFuture(null); - private final Map boundAddresses = new HashMap<>(); - private final IoHandler handler; - - public NettyIoAcceptor(NettyIoServiceFactory factory, IoHandler handler) { - this.factory = factory; - this.handler = handler; - channelGroup = new DefaultChannelGroup("sshd-acceptor-channels", GlobalEventExecutor.INSTANCE);; - bootstrap.group(factory.eventLoopGroup) - .channel(NioServerSocketChannel.class) - .option(ChannelOption.SO_BACKLOG, 100) - .handler(new LoggingHandler(LogLevel.INFO)) - .childHandler(new ChannelInitializer() { - @Override - public void initChannel(SocketChannel ch) throws Exception { - ChannelPipeline p = ch.pipeline(); - p.addLast(new NettyIoSession(NettyIoAcceptor.this, handler).adapter); - } - }); - } - - @Override - public void bind(Collection addresses) throws IOException { - for (SocketAddress address : addresses) { - bind(address); - } - } - - @Override - public void bind(SocketAddress address) throws IOException { - InetSocketAddress inetAddress = (InetSocketAddress) address; - ChannelFuture f = bootstrap.bind(inetAddress); - Channel channel = f.channel(); - channelGroup.add(channel); - try { - f.sync(); - SocketAddress bound = channel.localAddress(); - boundAddresses.put(bound, channel); - channel.closeFuture().addListener(fut -> { - boundAddresses.remove(bound); - }); - } catch (Exception e) { - throw Helper.toIOException(e); - } - } - - @Override - public void unbind(Collection addresses) { - throw new UnsupportedOperationException(); - } - - @Override - public void unbind(SocketAddress address) { - Channel channel = boundAddresses.get(address); - if (channel != null) { - ChannelFuture fut; - if (channel.isOpen()) { - fut = channel.close(); - } else { - fut = channel.closeFuture(); - } - try { - fut.sync(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - - @Override - public void unbind() { - throw new UnsupportedOperationException(); - } - - @Override - public Set getBoundAddresses() { - return new HashSet<>(boundAddresses.keySet()); - } - - @Override - public Map getManagedSessions() { - return ioService.sessions; - } - - @Override - protected CloseFuture doCloseGracefully() { - channelGroup.close().addListener(fut -> closeFuture.setClosed()); - return closeFuture; - } - - @Override - protected void doCloseImmediately() { - doCloseGracefully(); - super.doCloseImmediately(); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoHandlerBridge.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoHandlerBridge.java deleted file mode 100644 index 3e37a0b2..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoHandlerBridge.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import org.apache.sshd.common.io.IoHandler; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.util.Readable; - -/** - * @author Julien Viet - */ -public class NettyIoHandlerBridge { - - public void sessionCreated(IoHandler handler, IoSession session) throws Exception { - handler.sessionCreated(session); - } - - public void sessionClosed(IoHandler handler, IoSession session) throws Exception { - handler.sessionClosed(session); - } - - public void messageReceived(IoHandler handler, IoSession session, Readable message) throws Exception { - handler.messageReceived(session, message); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoService.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoService.java deleted file mode 100644 index 184c0376..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoService.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import org.apache.sshd.common.io.IoService; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.util.CloseableUtils; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author Julien Viet - */ -public class NettyIoService extends CloseableUtils.AbstractCloseable implements IoService { - - final AtomicLong sessionSeq = new AtomicLong(); - final Map sessions = new ConcurrentHashMap<>(); - - @Override - public Map getManagedSessions() { - return null; - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactory.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactory.java deleted file mode 100644 index 0cc44321..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactory.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.util.concurrent.Future; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.io.IoAcceptor; -import org.apache.sshd.common.io.IoConnector; -import org.apache.sshd.common.io.IoHandler; -import org.apache.sshd.common.io.IoServiceFactory; -import org.apache.sshd.common.util.CloseableUtils; - -/** - * @author Julien Viet - */ -public class NettyIoServiceFactory extends CloseableUtils.AbstractCloseable implements IoServiceFactory { - - final NettyIoHandlerBridge handlerBridge; - final EventLoopGroup eventLoopGroup; - final boolean closeEventLoopGroup; - - public NettyIoServiceFactory() { - this(null); - } - - public NettyIoServiceFactory(EventLoopGroup group) { - this(group, new NettyIoHandlerBridge()); - } - - public NettyIoServiceFactory(EventLoopGroup group, NettyIoHandlerBridge handlerBridge) { - this.handlerBridge = handlerBridge; - this.closeEventLoopGroup = group == null; - this.eventLoopGroup = group == null ? new NioEventLoopGroup() : group; - } - - @Override - public IoConnector createConnector(IoHandler handler) { - throw new UnsupportedOperationException("Only implement server for now"); - } - - @Override - public IoAcceptor createAcceptor(IoHandler handler) { - return new NettyIoAcceptor(this, handler); - } - - @Override - protected CloseFuture doCloseGracefully() { - if (closeEventLoopGroup) { - eventLoopGroup.shutdownGracefully().addListener((Future fut) -> { - closeFuture.setClosed(); - }); - } else { - closeFuture.setClosed(); - } - return closeFuture; - } - - @Override - protected void doCloseImmediately() { - doCloseGracefully(); - super.doCloseImmediately(); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactoryFactory.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactoryFactory.java deleted file mode 100644 index 413a0058..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoServiceFactoryFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import io.netty.channel.EventLoopGroup; -import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.common.io.IoServiceFactory; -import org.apache.sshd.common.io.IoServiceFactoryFactory; - -/** - * @author Julien Viet - */ -public class NettyIoServiceFactoryFactory implements IoServiceFactoryFactory { - - final EventLoopGroup eventLoopGroup; - final NettyIoHandlerBridge handlerBridge; - - public NettyIoServiceFactoryFactory() { - this.eventLoopGroup = null; - this.handlerBridge = new NettyIoHandlerBridge(); - } - - public NettyIoServiceFactoryFactory(EventLoopGroup eventLoopGroup) { - this.eventLoopGroup = eventLoopGroup; - this.handlerBridge = new NettyIoHandlerBridge(); - } - - public NettyIoServiceFactoryFactory(EventLoopGroup eventLoopGroup, NettyIoHandlerBridge handlerBridge) { - this.eventLoopGroup = eventLoopGroup; - this.handlerBridge = handlerBridge; - } - - @Override - public IoServiceFactory create(FactoryManager manager) { - return new NettyIoServiceFactory(eventLoopGroup, handlerBridge); - } - -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoSession.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoSession.java deleted file mode 100644 index 65db55e3..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoSession.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.future.DefaultCloseFuture; -import org.apache.sshd.common.io.IoHandler; -import org.apache.sshd.common.io.IoService; -import org.apache.sshd.common.io.IoSession; -import org.apache.sshd.common.io.IoWriteFuture; -import org.apache.sshd.common.util.CloseableUtils; -import org.apache.sshd.common.util.buffer.Buffer; -import org.apache.sshd.common.util.buffer.ByteArrayBuffer; - -import java.net.SocketAddress; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * @author Julien Viet - */ -public class NettyIoSession extends CloseableUtils.AbstractCloseable implements IoSession { - - private final Map attributes = new HashMap<>(); - private final NettyIoAcceptor acceptor; - private final IoHandler handler; - private ChannelHandlerContext context; - private SocketAddress remoteAddr; - private ChannelFuture prev; - private final DefaultCloseFuture closeFuture = new DefaultCloseFuture(null); - private final long id; - - private static final Logger LOGGER = Logger.getLogger(NettyIoSession.class.getName()); - - public NettyIoSession(NettyIoAcceptor acceptor, IoHandler handler) { - this.acceptor = acceptor; - this.handler = handler; - this.id = acceptor.ioService.sessionSeq.incrementAndGet(); - } - - final ChannelInboundHandlerAdapter adapter = new ChannelInboundHandlerAdapter() { - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - context = ctx; - acceptor.channelGroup.add(ctx.channel()); - acceptor.ioService.sessions.put(id, NettyIoSession.this); - prev = context.newPromise().setSuccess(); - remoteAddr = context.channel().remoteAddress(); - acceptor.factory.handlerBridge.sessionCreated(handler, NettyIoSession.this); - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - acceptor.ioService.sessions.remove(id); - acceptor.factory.handlerBridge.sessionClosed(handler, NettyIoSession.this); - context = null; - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - ByteBuf buf = (ByteBuf) msg; - byte[] bytes = new byte[buf.readableBytes()]; - buf.getBytes(0, bytes); - acceptor.factory.handlerBridge.messageReceived(handler, NettyIoSession.this, new ByteArrayBuffer(bytes)); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - LOGGER.log(Level.WARNING, "exception during io, closing.", cause); - ctx.close(); - } - }; - - public void execute(Runnable task) { - context.channel().eventLoop().execute(task); - } - - public void schedule(Runnable task, long delay, TimeUnit unit) { - context.channel().eventLoop().schedule(task, delay, unit); - } - - @Override - public long getId() { - return id; - } - - @Override - public Object getAttribute(Object key) { - return attributes.get(key); - } - - @Override - public Object setAttribute(Object key, Object value) { - return attributes.put(key, value); - } - - @Override - public SocketAddress getRemoteAddress() { - return remoteAddr; - } - - @Override - public SocketAddress getLocalAddress() { - return context.channel().localAddress(); - } - - @Override - public IoWriteFuture write(Buffer buffer) { - ByteBuf buf = Unpooled.buffer(buffer.available()); - buf.writeBytes(buffer.array(), buffer.rpos(), buffer.available()); - NettyIoWriteFuture msg = new NettyIoWriteFuture(); - ChannelPromise next = context.newPromise(); - prev.addListener(whatever -> { - if (context != null) { - context.writeAndFlush(buf, next); - } - }); - prev = next; - next.addListener(fut -> { - if (fut.isSuccess()) { - msg.setValue(Boolean.TRUE); - } else { - msg.setValue(fut.cause()); - } - }); - return msg; - } - - @Override - public IoService getService() { - return acceptor.ioService; - } - - @Override - protected CloseFuture doCloseGracefully() { - context. - writeAndFlush(Unpooled.EMPTY_BUFFER). - addListener(ChannelFutureListener.CLOSE). - addListener(fut -> { - closeFuture.setClosed(); - }); - return closeFuture; - } - - @Override - protected void doCloseImmediately() { - context.close(); - super.doCloseImmediately(); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoWriteFuture.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoWriteFuture.java deleted file mode 100644 index 8dc3d85e..00000000 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettyIoWriteFuture.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.netty; - -import org.apache.sshd.common.io.AbstractIoWriteFuture; - -/** - * @author Julien Viet - */ -public class NettyIoWriteFuture extends AbstractIoWriteFuture { - - public NettyIoWriteFuture() { - super(null); - } -} diff --git a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettySshTtyBootstrap.java b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettySshTtyBootstrap.java index 5bb57e50..350edbbc 100644 --- a/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettySshTtyBootstrap.java +++ b/terminal-ssh/src/main/java/org/aesh/terminal/ssh/netty/NettySshTtyBootstrap.java @@ -23,7 +23,9 @@ import io.netty.channel.nio.NioEventLoopGroup; import org.aesh.terminal.Connection; import org.aesh.terminal.ssh.TtyCommand; +import org.aesh.terminal.utils.Helper; import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.netty.NettyIoServiceFactoryFactory; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; @@ -119,10 +121,8 @@ public void start(Consumer factory, Consumer doneHandler) server.setHost(host); server.setKeyPairProvider(keyPairProvider); server.setPasswordAuthenticator(passwordAuthenticator); - if (publicKeyAuthenticator != null) { - server.setPublickeyAuthenticator(publicKeyAuthenticator); - } - server.setShellFactory(() -> new TtyCommand(charset, factory)); + server.setPublickeyAuthenticator(publicKeyAuthenticator); + server.setShellFactory(channelSession -> new TtyCommand(charset, factory)); try { server.start(); } catch (Exception e) { diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthInteractiveTest.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthInteractiveTest.java deleted file mode 100644 index 7693f7a8..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthInteractiveTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh; - -import com.jcraft.jsch.ChannelShell; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UserInfo; - -/** - * @author Julien Viet - */ -public class AsyncAuthInteractiveTest extends AsyncAuthTestBase { - - protected boolean authenticate() throws Exception { - - JSch jsch = new JSch(); - Session session; - ChannelShell channel; - - session = jsch.getSession("whatever", "localhost", 5000); - session.setUserInfo(new UserInfo() { - @Override - public String getPassphrase() { - throw new UnsupportedOperationException(); - } - - @Override - public String getPassword() { - return "whocares"; - } - - @Override - public boolean promptPassword(String s) { - return true; - } - - @Override - public boolean promptPassphrase(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean promptYesNo(String s) { - return true; - } - - @Override - public void showMessage(String s) { - } - }); - try { - session.connect(); - } catch (JSchException e) { - switch (e.getMessage()) { - case "Auth cancel": - case "Auth fail": - return false; - default: - throw e; - } - } - channel = (ChannelShell) session.openChannel("shell"); - channel.connect(); - - if (channel != null) { - try { channel.disconnect(); } catch (Exception ignore) {} - } - if (session != null) { - try { session.disconnect(); } catch (Exception ignore) {} - } - - return true; - } -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTest.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTest.java index 99e11b53..28bcd250 100644 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTest.java +++ b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTest.java @@ -19,74 +19,171 @@ */ package org.aesh.terminal.ssh; -import com.jcraft.jsch.ChannelShell; -import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UserInfo; +import org.aesh.terminal.TestBase; +import org.apache.sshd.util.test.EchoShellFactory; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.core.CoreModuleProperties; +import org.apache.sshd.server.SshServer; +import org.apache.sshd.server.auth.AsyncAuthException; +import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerConnectionServiceFactory; +import org.apache.sshd.server.session.ServerUserAuthServiceFactory; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; /** * @author Julien Viet */ -public class AsyncAuthTest extends AsyncAuthTestBase { - - protected boolean authenticate() throws Exception { - - JSch jsch = new JSch(); - Session session; - ChannelShell channel; - - session = jsch.getSession("whatever", "localhost", 5000); - session.setPassword("whocares"); - session.setUserInfo(new UserInfo() { - @Override - public String getPassphrase() { - return null; - } - - @Override - public String getPassword() { - return null; - } - - @Override - public boolean promptPassword(String s) { - return false; - } - - @Override - public boolean promptPassphrase(String s) { - return false; - } - - @Override - public boolean promptYesNo(String s) { - return true; - } // Accept all server keys - - @Override - public void showMessage(String s) { - } - }); - try { - session.connect(); - } catch (JSchException e) { - if (e.getMessage().equals("Auth cancel")) { - return false; - } else { - throw e; - } +public class AsyncAuthTest extends TestBase { + + SshServer server; + + private PasswordAuthenticator authenticator; + + public void startServer() throws Exception { + startServer(null); + } + + public void startServer(Integer timeout) throws Exception { + if (server != null) { + throw failure("Server already started"); + } + server = SshServer.setUpDefaultServer(); + if (timeout != null) { + server.getProperties().put(CoreModuleProperties.AUTH_TIMEOUT.getName(), timeout.toString()); + } + server.setPort(5000); + server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); + server.setPasswordAuthenticator((username, password, sess) -> authenticator.authenticate(username, password, sess)); + server.setShellFactory(new EchoShellFactory()); + server.setServiceFactories(Arrays.asList(ServerConnectionServiceFactory.INSTANCE, ServerUserAuthServiceFactory.INSTANCE)); + server.start(); + } + + @After + public void stopServer() throws Exception { + if (server != null) { + server.stop(); + } + } + + @Test + public void testSyncAuthFailed() throws Exception { + startServer(); + authenticator = (username, password, sess) -> false; + Assert.assertFalse(authenticate()); } - channel = (ChannelShell) session.openChannel("shell"); - channel.connect(); - if (channel != null) { - try { channel.disconnect(); } catch (Exception ignore) {} + @Test + public void testSyncAuthSucceeded() throws Exception { + startServer(); + authenticator = (username, password, sess) -> true; + Assert.assertTrue(authenticate()); } - if (session != null) { - try { session.disconnect(); } catch (Exception ignore) {} + + @Test + public void testAsyncAuthFailed() throws Exception { + startServer(); + authenticator = (username, password, sess) -> { + AsyncAuthException auth = new AsyncAuthException(); + new Thread() { + @Override + public void run() { + try { + Thread.sleep(200); + } catch (InterruptedException ignore) { + } finally { + auth.setAuthed(false); + } + } + }.start(); + throw auth; + }; + Assert.assertFalse(authenticate()); + } + + @Test + public void testAsyncAuthSucceeded() throws Exception { + startServer(); + authenticator = (username, password, sess) -> { + AsyncAuthException auth = new AsyncAuthException(); + new Thread() { + @Override + public void run() { + try { + Thread.sleep(200); + } catch (InterruptedException ignore) { + } finally { + auth.setAuthed(true); + } + } + }.start(); + throw auth; + }; + Assert.assertTrue(authenticate()); + } + + @Test + public void testAsyncAuthTimeout() throws Exception { + startServer(500); + authenticator = (username, password, sess) -> { + throw new AsyncAuthException(); + }; + try { + authenticate(); + } catch (JSchException e) { + Assert.assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); + } + } + + @Test + public void testAsyncAuthSucceededAfterTimeout() throws Exception { + startServer(500); + authenticator = (username, password, sess) -> { + AsyncAuthException auth = new AsyncAuthException(); + new Thread() { + @Override + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException ignore) { + } finally { + auth.setAuthed(true); + } + } + }.start(); + throw auth; + }; + try { + authenticate(); + } catch (JSchException e) { + Assert.assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); + } + } + + protected boolean authenticate() throws Exception { + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + ClientSession sess = client + .connect("whatever", "localhost", 5000) + .verify(TimeUnit.SECONDS.toMillis(5)) + .getSession(); + // Only use password authentication, don't try to load SSH keys + sess.setKeyIdentityProvider(null); + sess.addPasswordIdentity("whocares"); + sess.auth().verify(TimeUnit.SECONDS.toMillis(5)); + return true; + } catch (Exception e) { + return false; + } } - return true; - } } diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTestBase.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTestBase.java deleted file mode 100644 index c306c6ce..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/AsyncAuthTestBase.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh; - -import com.jcraft.jsch.JSchException; -import org.aesh.terminal.TestBase; -import org.aesh.terminal.ssh.netty.AsyncAuth; -import org.aesh.terminal.ssh.netty.AsyncUserAuthServiceFactory; -import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.auth.password.PasswordAuthenticator; -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.apache.sshd.server.session.ServerConnectionServiceFactory; -import org.apache.sshd.util.EchoShellFactory; -import org.junit.After; -import org.junit.Test; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; - -/** - * @author Julien Viet - */ -public abstract class AsyncAuthTestBase extends TestBase { - - SshServer server; - - private PasswordAuthenticator authenticator; - - public void startServer() throws Exception { - startServer(null); - } - - public void startServer(Integer timeout) throws Exception { - if (server != null) { - throw failure("Server already started"); - } - server = SshServer.setUpDefaultServer(); - if (timeout != null) { - server.setProperties(Collections.singletonMap(FactoryManager.AUTH_TIMEOUT, timeout.toString())); - } - server.setPort(5000); - server.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); - server.setPasswordAuthenticator((username, password, sess) -> authenticator.authenticate(username, password, sess)); - server.setShellFactory(new EchoShellFactory()); - server.setServiceFactories(Arrays.asList(ServerConnectionServiceFactory.INSTANCE, AsyncUserAuthServiceFactory.INSTANCE)); - server.start(); - } - - @After - public void stopServer() throws Exception { - if (server != null) { - server.stop(); - } - } - - @Test - public void testSyncAuthFailed() throws Exception { - startServer(); - authenticator = (username, password, sess) -> false; - assertFalse(authenticate()); - } - - @Test - public void testSyncAuthSucceeded() throws Exception { - startServer(); - authenticator = (username, password, sess) -> true; - assertTrue(authenticate()); - } - - @Test - public void testAsyncAuthFailed() throws Exception { - startServer(); - authenticator = (username, password, sess) -> { - AsyncAuth auth = new AsyncAuth(); - new Thread() { - @Override - public void run() { - try { - Thread.sleep(200); - } catch (InterruptedException ignore) { - } finally { - auth.setAuthed(false); - } - } - }.start(); - throw auth; - }; - assertFalse(authenticate()); - } - - @Test - public void testAsyncAuthSucceeded() throws Exception { - startServer(); - authenticator = (username, password, sess) -> { - AsyncAuth auth = new AsyncAuth(); - new Thread() { - @Override - public void run() { - try { - Thread.sleep(200); - } catch (InterruptedException ignore) { - } finally { - auth.setAuthed(true); - } - } - }.start(); - throw auth; - }; - assertTrue(authenticate()); - } - - @Test - public void testAsyncAuthTimeout() throws Exception { - startServer(500); - authenticator = (username, password, sess) -> { - throw new AsyncAuth(); - }; - try { - authenticate(); - } catch (JSchException e) { - assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); - } - } - - @Test - public void testAsyncAuthSucceededAfterTimeout() throws Exception { - startServer(500); - authenticator = (username, password, sess) -> { - AsyncAuth auth = new AsyncAuth(); - new Thread() { - @Override - public void run() { - try { - Thread.sleep(1000); - } catch (InterruptedException ignore) { - } finally { - auth.setAuthed(true); - } - } - }.start(); - throw auth; - }; - try { - authenticate(); - } catch (JSchException e) { - assertTrue("Unexpected failure " + e.getMessage(), e.getMessage().startsWith("SSH_MSG_DISCONNECT")); - } - } - - protected abstract boolean authenticate() throws Exception; - -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHSession.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHSession.java new file mode 100644 index 00000000..373745af --- /dev/null +++ b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHSession.java @@ -0,0 +1,110 @@ +package org.aesh.terminal.ssh; + +import org.aesh.terminal.TestBase; +import org.apache.sshd.client.channel.ChannelShell; +import org.apache.sshd.client.session.ClientSession; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.TimeUnit; + +public class SSHSession { + + ClientSession session; + ChannelShell channel; + InputStream in; + OutputStream out; + + private String termType; + + public String termType() { + return termType; + } + + public SSHSession termType(String termType) { + this.termType = termType; + return this; + } + + public void disconnect() throws IOException { + session.disconnect(0, "bye"); + } + + public SSHSession write(byte[] bytes) throws IOException { + out.write(bytes); + out.flush(); + return this; + } + + public byte[] read(int len) throws IOException { + byte[] buf = new byte[len]; + while (len > 0) { + int count = in.read(buf, buf.length - len, len); + if (count == -1) { + throw new AssertionError("Could not read enough"); + } + len -= count; + } + return buf; + } + + public boolean isClosed() { + return channel.isClosed(); + } + + public int exitStatus() { + return channel.getExitStatus(); + } + + public boolean checkDisconnect() { + try { + return in != null && in.read() == -1; + } catch (IOException e) { + throw TestBase.failure(e); + } + } + + public void connect() throws Exception { + org.apache.sshd.client.SshClient client = org.apache.sshd.client.SshClient.setUpDefaultClient(); + client.start(); + ClientSession sess = client + .connect("whatever", "localhost", 5000) + .verify() + .getSession(); + sess.addPasswordIdentity("whocares"); + sess.auth().verify(TimeUnit.SECONDS.toMillis(5000)); + session = sess; + channel = session.createShellChannel(); + if (termType != null) { + channel.setPtyType(termType); + } + PipedInputStream p1 = new PipedInputStream(); + PipedOutputStream p2 = new PipedOutputStream(p1); + PipedInputStream p3 = new PipedInputStream(); + PipedOutputStream p4 = new PipedOutputStream(p3); + channel.setIn(p1); + channel.setOut(p4); + channel.open().verify(); + in = p3; + out = p2; + } + + public void close() { + if (out != null) { + try { out.close(); } catch (Exception ignore) {} + } + if (in != null) { + try { in.close(); } catch (Exception ignore) {} + } + if (channel != null) { +// try { channel.disconnect(); } catch (Exception ignore) {} + } + if (session != null) { +// try { session.disconnect(); } catch (Exception ignore) {} + } + } + +} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHTtyCommandTest.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHTtyCommandTest.java new file mode 100644 index 00000000..83d350a5 --- /dev/null +++ b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/SSHTtyCommandTest.java @@ -0,0 +1,101 @@ +package org.aesh.terminal.ssh; + +import org.aesh.terminal.ssh.netty.NettySshTtyBootstrap; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.channel.ChannelShell; +import org.apache.sshd.client.channel.ClientChannelEvent; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.common.util.io.output.NoCloseOutputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +@RunWith(Parameterized.class) +public class SSHTtyCommandTest { + + public static final int TIMEOUT_SECS = 5; + public static final int REPETITIONS = 2; + + @Parameterized.Parameters + public static Object[][] data() { + return new Object[REPETITIONS][0]; + } + + + @Test + public void runCommandViaSSHTest() { + // Configura il server SSH (basato su SshShellExample) + NettySshTtyBootstrap bootstrap = new NettySshTtyBootstrap() + .setPort(5000) + .setHost("localhost"); + try { + + bootstrap.start(new ShellExample()).get(10, TimeUnit.SECONDS); + + } catch (Exception e) { + fail(e.getMessage()); + } + + String result = ""; + + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + + try (ClientSession session = client.connect("user", "localhost", 5000) + .verify(TIMEOUT_SECS, TimeUnit.SECONDS) + .getClientSession()) { + session.addPasswordIdentity("password"); + session.auth().verify(TIMEOUT_SECS, TimeUnit.SECONDS); + try ( + ByteArrayOutputStream outputStream = new ByteArrayOutputStream() + ) { + try (ChannelShell channel = session.createShellChannel()) { + channel.setOut(outputStream); + + channel.setErr(new NoCloseOutputStream(System.err)); + channel.open().verify(TIMEOUT_SECS, TimeUnit.SECONDS); + + OutputStream pipedIn = channel.getInvertedIn(); + + StringBuilder expected = new StringBuilder(); + + String expectedString = ""; + + // resets all data in the output stream + for (int i = 0; i < REPETITIONS; i++) { + + String expectedRes = "hello world " + (i + 1); + pipedIn.write(("echo " + expectedRes + "\n").getBytes()); + pipedIn.flush(); + channel.waitFor(Arrays.asList( + ClientChannelEvent.STDOUT_DATA, + ClientChannelEvent.EOF + ), TimeUnit.SECONDS.toMillis(2L)); + + result = outputStream.toString(); + expected.append("echo ").append(expectedRes).append("\n").append(expectedRes).append("\n"); + + expectedString = expected.toString().replaceAll("(\r|\n)", ""); + String actual = result.replaceAll("(\r|\n)", ""); + + assertEquals(expectedString, actual); + } + } + } + } + + bootstrap.stop(); + } catch (Exception e) { + fail(e.getMessage()); + } + + } +} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/ShellExample.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/ShellExample.java new file mode 100644 index 00000000..fdfa502d --- /dev/null +++ b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/ShellExample.java @@ -0,0 +1,351 @@ +package org.aesh.terminal.ssh; + +import org.aesh.readline.Prompt; +import org.aesh.readline.Readline; +import org.aesh.readline.completion.Completion; +import org.aesh.readline.editing.EditMode; +import org.aesh.readline.editing.EditModeBuilder; +import org.aesh.readline.util.LoggerUtil; +import org.aesh.terminal.Attributes; +import org.aesh.terminal.Connection; +import org.aesh.terminal.tty.Point; +import org.aesh.terminal.tty.Signal; +import org.aesh.terminal.utils.Config; + +import java.io.InterruptedIOException; +import java.util.ArrayList; +import java.util.Formatter; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ShellExample implements Consumer{ + + private static final Pattern splitter = Pattern.compile("\\w+"); + + private static final Logger LOGGER = LoggerUtil.getLogger(ShellExample.class.getName()); + + private boolean stopped = false; + + @Override + public void accept(Connection connection) { + connection.setSignalHandler( signal -> { + connection.write("\nlets quit\n"); + connection.close(); + }); + + connection.setCloseHandler(close->{ + stopped = true; + }); + + Readline readline = new Readline(EditModeBuilder.builder(EditMode.Mode.EMACS).create()); + read(connection, readline); + connection.openBlocking(); + } + + /** + * Use {@link Readline} to openBlocking a user input and then process it + * + * @param conn the tty connection + * @param readline the readline object + */ + public void read(final Connection conn, final Readline readline) { + // Just call readline and get a callback when line is openBlocking + Prompt prompt = new Prompt(""); + + //suspend reader asap since we're creating commands in a new thread + //this is not needed when running single threaded, eg as examples.Example + readline.readline(conn, prompt, line -> { + //we got eof or quit + if (line == null) { + conn.close(); + return; + } + + //LOGGER.info("got: " + line); + + Matcher matcher = splitter.matcher(line); + if (matcher.find()) { + String cmd = matcher.group(); + + // Gather args + List args = new ArrayList<>(); + while (matcher.find()) { + args.add(matcher.group()); + } + + try { + new Task(conn, readline, Command.valueOf(cmd), args).start(); + return; + } catch (IllegalArgumentException e) { + conn.write(line + ": command not found\n"); + } + } + read(conn, readline); + }, getCompletions()); + } + + private List getCompletions() { + List completions = new ArrayList<>(); + completions.add(completeOperation -> { + if("exit".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("exit"); + if("sleep".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("sleep"); + if("echo".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("echo"); + if("window".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("window"); + if("help".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("help"); + if("keyscan".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("keyscan"); + if("linescan".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("linescan"); + if("top".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("top"); + if("cursor".startsWith(completeOperation.getBuffer())) + completeOperation.addCompletionCandidate("cursor"); + }); + return completions; + } + + + /** + * A blocking interruptable task. + */ + class Task extends Thread implements Consumer { + + final Connection conn; + final Readline readline; + final Command command; + final List args; + + Task(Connection conn, Readline readline, Command command, List args) { + this.conn = conn; + this.readline = readline; + this.command = command; + this.args = args; + } + + @Override + public void accept(Signal signal) { + switch (signal) { + case INT: + // Ctrl-C interrupt : we use Thread interrupts to signal the command to stop + LOGGER.info("got interrupted in Task"); + interrupt(); + } + } + + @Override + public void run() { + // Subscribe to events, in particular Ctrl-C + conn.setSignalHandler(this); + try { + command.execute(conn, args); + } + catch (InterruptedException | InterruptedIOException e) { + // Ctlr-C interrupt + } + catch (Exception e) { + e.printStackTrace(); + } + finally { + conn.setSignalHandler(null); + // Readline again + if(!stopped) + read(conn, readline); + } + } + } + + /** + * The shell app commands. + */ + enum Command { + + sleep() { + @Override + public void execute(Connection conn, List args) throws Exception { + if (args.isEmpty()) { + conn.write("usage: sleep seconds\n"); + return; + } + int time = -1; + try { + time = Integer.parseInt(args.get(0)); + } catch (NumberFormatException ignore) { + } + if (time > 0) { + // Sleep until timeout or Ctrl-C interrupted + Thread.sleep(time * 1000); + } + } + }, + + exit() { + @Override + public void execute(Connection conn, List args) throws Exception { + conn.close(); + } + }, + + + echo() { + @Override + public void execute(Connection conn, List args) throws Exception { + for (int i = 0;i < args.size();i++) { + if (i > 0) { + conn.write(" "); + } + conn.write(args.get(i)); + } + conn.write("\n"); + } + }, + + window() { + @Override + public void execute(Connection conn, List args) throws Exception { + conn.write("Current window size " + conn.size() + ", try resize it\n"); + + // Refresh the screen with the new size + conn.setSizeHandler(size -> { + conn.write("Window resized " + size + "\n"); + }); + + try { + // Wait until interrupted + new CountDownLatch(1).await(); + } finally { + conn.setSizeHandler(null); + } + } + }, + + help() { + @Override + public void execute(Connection conn, List args) throws Exception { + StringBuilder msg = new StringBuilder("Demo term, try commands: "); + Command[] commands = Command.values(); + for (int i = 0;i < commands.length;i++) { + if (i > 0) { + msg.append(","); + } + msg.append(" ").append(commands[i].name()); + } + msg.append("...\n"); + conn.write(msg.toString()); + } + }, + + keyscan() { + @Override + public void execute(Connection conn, List args) throws Exception { + //we need to enter raw mode to get each keystroke + Attributes attributes = conn.enterRawMode(); + // Subscribe to key events and print them + conn.setStdinHandler(keys -> { + for (int key : keys) { + conn.write(key + " pressed\n"); + } + }); + try { + // Wait until interrupted + new CountDownLatch(1).await(); + } + finally { + conn.setStdinHandler(null); + conn.setAttributes(attributes); + } + } + }, + + linescan() { + @Override + public void execute(Connection conn, List args) throws Exception { + String line = readLine("[myprompt]", conn); + + conn.write("we got: "+line+Config.getLineSeparator()); + } + + private String readLine(String prompt, Connection conn) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + Readline readline = new Readline(); + String[] out = new String[1]; + readline.readline(conn, "[myprompt]: ", event -> { + out[0] = event; + latch.countDown(); + }); + try { + // Wait until interrupted + latch.await(); + } + finally { + conn.setStdinHandler(null); + } + + return out[0]; + } + + }, + + top() { + @Override + public void execute(Connection conn, List args) throws Exception { + while (true) { + + StringBuilder buf = new StringBuilder(); + Formatter formatter = new Formatter(buf); + + List threads = new ArrayList<>(Thread.getAllStackTraces().keySet()); + for (int i = 1;i <= conn.size().getHeight();i++) { + + // Change cursor position and erase line with ANSI escape code magic + buf.append("\033[").append(i).append(";1H\033[K"); + + // + String format = " %1$-5s %2$-10s %3$-50s %4$s"; + if (i == 1) { + formatter.format(format, + "ID", + "STATE", + "NAME", + "GROUP"); + } else { + int index = i - 2; + if (index < threads.size()) { + Thread thread = threads.get(index); + formatter.format(format, + thread.getId(), + thread.getState().name(), + thread.getName(), + thread.getThreadGroup().getName()); + } + } + } + + conn.write(buf.toString()); + // Sleep until we refresh the list of interrupted + Thread.sleep(1000); + } + } + }, + + cursor() { + @Override + public void execute(Connection conn, List args) throws Exception { + conn.write("cursor position is: "); + Point p = conn.getCursorPosition(); + conn.write(p.toString()+Config.getLineSeparator()); + } + + }; + + abstract void execute(Connection conn, List args) throws Exception; + } +} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestIoServiceFactoryFactory.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestIoServiceFactoryFactory.java deleted file mode 100644 index c43ca529..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestIoServiceFactoryFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh; - -import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.common.io.IoServiceFactory; -import org.apache.sshd.common.io.nio2.Nio2ServiceFactoryFactory; - -/** - * @author Julien Viet - */ -public class TestIoServiceFactoryFactory extends Nio2ServiceFactoryFactory { - - @Override - public IoServiceFactory create(FactoryManager manager) { - return new TestServiceFactory(manager, getExecutorService(), isShutdownOnExit()); - } -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestServiceFactory.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestServiceFactory.java deleted file mode 100644 index 164e0be6..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/TestServiceFactory.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh; - -import org.aesh.terminal.ssh.netty.NettyIoAcceptor; -import org.aesh.terminal.ssh.netty.NettyIoServiceFactory; -import org.apache.sshd.common.FactoryManager; -import org.apache.sshd.common.future.CloseFuture; -import org.apache.sshd.common.io.IoAcceptor; -import org.apache.sshd.common.io.IoHandler; -import org.apache.sshd.common.io.nio2.Nio2ServiceFactory; - -import java.util.concurrent.ExecutorService; - -/** - * @author Julien Viet - */ -public class TestServiceFactory extends Nio2ServiceFactory { - - private final NettyIoServiceFactory factory = new NettyIoServiceFactory(); - - public TestServiceFactory(FactoryManager factoryManager, ExecutorService service, boolean shutdownOnExit) { - super(factoryManager, service, shutdownOnExit); - } - - @Override - public IoAcceptor createAcceptor(IoHandler handler) { - return new NettyIoAcceptor(factory, handler); - } - - @Override - public CloseFuture close(boolean immediately) { - factory.close(immediately); - return super.close(immediately); - } -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/DefaultSshTtyTest.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/DefaultSshTtyTest.java deleted file mode 100644 index d286c734..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/DefaultSshTtyTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.tty; - -import org.apache.sshd.server.SshServer; - -/** - * @author Julien Viet - */ -public class DefaultSshTtyTest extends SshTtyTestBase { - - @Override - protected SshServer createServer() { - return SshServer.setUpDefaultServer(); - } -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/NettySshTtyTest.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/NettySshTtyTest.java deleted file mode 100644 index fc3637a9..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/NettySshTtyTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.tty; - -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.util.concurrent.Future; -import org.aesh.terminal.Connection; -import org.aesh.terminal.ssh.TtyCommand; -import org.aesh.terminal.ssh.netty.NettyIoServiceFactoryFactory; -import org.aesh.terminal.ssh.netty.NettyIoSession; -import org.apache.sshd.common.session.Session; -import org.apache.sshd.server.SshServer; -import org.junit.After; -import org.junit.Before; - -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; - -/** - * @author Julien Viet - */ -public class NettySshTtyTest extends SshTtyTestBase { - - private EventLoopGroup eventLoopGroup; - - @Before - public void before() { - eventLoopGroup = new NioEventLoopGroup(); - } - - @After - public void after() throws Exception { - Future future = eventLoopGroup.shutdownGracefully(); - assertTrue(future.await(30, TimeUnit.SECONDS)); - } - - @Override - protected SshServer createServer() { - SshServer sshd = SshServer.setUpDefaultServer(); - sshd.setIoServiceFactoryFactory(new NettyIoServiceFactoryFactory(eventLoopGroup)); - return sshd; - } - - @Override - protected TtyCommand createConnection(Consumer onConnect) { - return new TtyCommand(charset, onConnect) { - @Override - public void execute(Runnable task) { - Session session = this.session.getSession(); - NettyIoSession ioSession = (NettyIoSession) session.getIoSession(); - ioSession.execute(task); - } - }; - } - - @Override - protected void assertThreading(Thread connThread, Thread schedulerThread) throws Exception { - assertTrue(connThread.getName().startsWith("nioEventLoopGroup")); - assertEquals(connThread, schedulerThread); - } -} diff --git a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/SshTtyTestBase.java b/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/SshTtyTestBase.java deleted file mode 100644 index 2f6d77ae..00000000 --- a/terminal-ssh/src/test/java/org/aesh/terminal/ssh/tty/SshTtyTestBase.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * JBoss, Home of Professional Open Source - * Copyright 2017 Red Hat Inc. and/or its affiliates and other contributors - * as indicated by the @authors tag. All rights reserved. - * See the copyright.txt in the distribution for a - * full listing of individual contributors. - * - * Licensed 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.aesh.terminal.ssh.tty; - -import com.jcraft.jsch.ChannelShell; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UserInfo; -import org.aesh.terminal.Connection; -import org.aesh.terminal.ssh.TtyCommand; -import org.aesh.terminal.tty.TtyTestBase; -import org.apache.sshd.server.SshServer; -import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; -import org.junit.After; -import org.junit.Test; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.Socket; -import java.util.function.Consumer; - -/** - * @author Julien Viet - */ -public abstract class SshTtyTestBase extends TtyTestBase { - - JSch jsch = new JSch(); - Session session; - ChannelShell channel; - InputStream in; - OutputStream out; - - @Override - protected void assertConnect(String term) throws Exception { - if (session != null) { - throw failure("Already a session"); - } - Exception cause = new Exception("Could not connect"); - for (int i = 0;i < 10;i++) { - try { - Session sess = jsch.getSession("whatever", "localhost", 5000); - sess.setPassword("whocares"); - sess.setUserInfo(new UserInfo() { - public String getPassphrase() { - return null; - } - public String getPassword() { - return null; - } - public boolean promptPassword(String s) { - return false; - } - public boolean promptPassphrase(String s) { - return false; - } - public boolean promptYesNo(String s) { - return true; /* Accept all server keys */ - } - public void showMessage(String s) { - } - }); - sess.connect(); - session = sess; - break; - } - catch (JSchException e) { - // Retry up to 10 times (see https://sourceforge.net/p/jsch/bugs/111/) - // Save cause for throwing it later - cause = e; - } - } - if (session == null) { - throw cause; - } - channel = (ChannelShell) session.openChannel("shell"); - if (term != null) { - channel.setPtyType(term); - } - channel.connect(); - in = channel.getInputStream(); - out = channel.getOutputStream(); - } - - @Override - public boolean checkDisconnected() { - try { - return in != null && in.read() == -1; - } catch (IOException e) { - throw failure(e); - } - } - - @Override - protected void assertDisconnect(boolean clean) throws Exception { - if (clean) { - session.disconnect(); - } else { - Field socketField = session.getClass().getDeclaredField("socket"); - socketField.setAccessible(true); - Socket socket = (Socket) socketField.get(session); - socket.close(); - } - } - - @Override - protected void resize(int width, int height) { - channel.setPtySize(width, height, width * 8, height * 8); - } - - @Override - protected void assertWrite(String s) throws Exception { - out.write(s.getBytes(charset)); - out.flush(); - } - - @Override - protected String assertReadString(int len) throws Exception { - byte[] buf = new byte[len]; - while (len > 0) { - int count = in.read(buf, buf.length - len, len); - if (count == -1) { - throw failure("Could not read enough"); - } - len -= count; - } - return new String(buf, "UTF-8"); - } - - @Override - protected void assertWriteln(String s) throws Exception { - assertWrite((s + "\r")); - } - - private SshServer sshd; - - protected abstract SshServer createServer(); - - protected TtyCommand createConnection(Consumer onConnect) { - return new TtyCommand(charset, onConnect); - } - - @Override - protected void server(Consumer onConnect) { - if (sshd != null) { - throw failure("Already a server"); - } - try { - sshd = createServer(); - sshd.setPort(5000); - sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(new File("hostkey.ser").toPath())); - sshd.setPasswordAuthenticator((username, password, session) -> true); - sshd.setShellFactory(() -> createConnection(onConnect)); - sshd.start(); - } catch (Exception e) { - throw failure(e); - } - } - - @Test - public void testExitCode() throws Exception { - server(conn -> { - conn.setStdinHandler(bytes -> { - conn.close(25); - }); - }); - assertConnect(); - assertWrite("whatever"); - long timeout = System.currentTimeMillis() + 5000; - while (!channel.isClosed()) { - assertTrue(System.currentTimeMillis() < timeout); - Thread.sleep(10); - } - assertEquals(25, channel.getExitStatus()); - } - - @After - public void after() throws Exception { - if (out != null) { - try { out.close(); } catch (Exception ignore) {} - } - if (in != null) { - try { in.close(); } catch (Exception ignore) {} - } - if (channel != null) { - try { channel.disconnect(); } catch (Exception ignore) {} - } - if (session != null) { - try { session.disconnect(); } catch (Exception ignore) {} - } - if (sshd != null && !sshd.isClosed()) { - try { - sshd.close(); - } catch (Exception ignore) { - } - } - } -} diff --git a/terminal-ssh/src/test/java/org/apache/sshd/util/EchoShellFactory.java b/terminal-ssh/src/test/java/org/apache/sshd/util/EchoShellFactory.java deleted file mode 100644 index fa1021ab..00000000 --- a/terminal-ssh/src/test/java/org/apache/sshd/util/EchoShellFactory.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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.util; - -import org.apache.sshd.common.Factory; -import org.apache.sshd.server.Command; -import org.apache.sshd.server.Environment; -import org.apache.sshd.server.ExitCallback; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.InterruptedIOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; - -/** - * TODO Add javadoc - * - * @author Apache MINA SSHD Project - */ -public class EchoShellFactory implements Factory { - - @Override - public Command create() { - return new EchoShell(); - } - - public static class EchoShell implements Command, Runnable { - - private InputStream in; - private OutputStream out; - private OutputStream err; - private ExitCallback callback; - private Environment environment; - private Thread thread; - - public EchoShell() { - super(); - } - - public InputStream getIn() { - return in; - } - - public OutputStream getOut() { - return out; - } - - public OutputStream getErr() { - return err; - } - - public Environment getEnvironment() { - return environment; - } - - @Override - public void setInputStream(InputStream in) { - this.in = in; - } - - @Override - public void setOutputStream(OutputStream out) { - this.out = out; - } - - @Override - public void setErrorStream(OutputStream err) { - this.err = err; - } - - @Override - public void setExitCallback(ExitCallback callback) { - this.callback = callback; - } - - @Override - public void start(Environment env) throws IOException { - environment = env; - thread = new Thread(this, "EchoShell"); - thread.setDaemon(true); - thread.start(); - } - - @Override - public void destroy() { - thread.interrupt(); - } - - @Override - public void run() { - BufferedReader r = new BufferedReader(new InputStreamReader(in)); - try { - for (; ; ) { - String s = r.readLine(); - if (s == null) { - return; - } - out.write((s + "\n").getBytes(StandardCharsets.UTF_8)); - out.flush(); - if ("exit".equals(s)) { - return; - } - } - } catch (InterruptedIOException e) { - // Ignore - } catch (Exception e) { - e.printStackTrace(); - } finally { - callback.onExit(0); - } - } - } -} diff --git a/terminal-ssh/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java b/terminal-ssh/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java new file mode 100644 index 00000000..d0203260 --- /dev/null +++ b/terminal-ssh/src/test/java/org/apache/sshd/util/test/CommandExecutionHelper.java @@ -0,0 +1,67 @@ +package org.apache.sshd.util.test; + +import org.apache.sshd.server.command.AbstractCommandSupport; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +/** + * @author Apache MINA SSHD Project + */ +public abstract class CommandExecutionHelper extends AbstractCommandSupport { + protected CommandExecutionHelper() { + this(null); + } + + protected CommandExecutionHelper(String command) { + super(command, null); + } + + @Override + public void run() { + String command = getCommand(); + try { + if (command == null) { + try (BufferedReader r = new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8))) { + for (;;) { + command = r.readLine(); + if (command == null) { + return; + } + + if (!handleCommandLine(command)) { + return; + } + } + } + } else { + handleCommandLine(command); + } + } catch (InterruptedIOException e) { + // Ignore - signaled end + } catch (Exception e) { + String message = "Failed (" + e.getClass().getSimpleName() + ") to handle '" + command + "': " + e.getMessage(); + try { + OutputStream stderr = getErrorStream(); + stderr.write(message.getBytes(StandardCharsets.US_ASCII)); + } catch (IOException ioe) { + log.warn("Failed ({}) to write error message={}: {}", + e.getClass().getSimpleName(), message, ioe.getMessage()); + } finally { + onExit(-1, message); + } + } finally { + onExit(0); + } + } + + /** + * @param command The command line + * @return {@code true} if continue accepting command + * @throws Exception If failed to handle the command line + */ + protected abstract boolean handleCommandLine(String command) throws Exception; +} diff --git a/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShell.java b/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShell.java new file mode 100644 index 00000000..35d3b6dd --- /dev/null +++ b/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShell.java @@ -0,0 +1,23 @@ +package org.apache.sshd.util.test; + +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * @author Apache MINA SSHD Project + */ +public class EchoShell extends CommandExecutionHelper { + public EchoShell() { + super(); + } + + @Override + protected boolean handleCommandLine(String command) throws Exception { + OutputStream out = getOutputStream(); + out.write((command + "\n").getBytes(StandardCharsets.UTF_8)); + out.flush(); + + return !"exit".equals(command); + + } +} \ No newline at end of file diff --git a/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShellFactory.java b/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShellFactory.java new file mode 100644 index 00000000..d70052e1 --- /dev/null +++ b/terminal-ssh/src/test/java/org/apache/sshd/util/test/EchoShellFactory.java @@ -0,0 +1,21 @@ +package org.apache.sshd.util.test; + +import org.apache.sshd.server.channel.ChannelSession; +import org.apache.sshd.server.command.Command; +import org.apache.sshd.server.shell.ShellFactory; + +/** + * @author Apache MINA SSHD Project + */ +public class EchoShellFactory implements ShellFactory { + public static final EchoShellFactory INSTANCE = new EchoShellFactory(); + + public EchoShellFactory() { + super(); + } + + @Override + public Command createShell(ChannelSession channel) { + return new EchoShell(); + } +} diff --git a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/TelnetBootstrap.java b/terminal-telnet/src/main/java/org/aesh/terminal/telnet/TelnetBootstrap.java index 48a32e7d..acfcab81 100644 --- a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/TelnetBootstrap.java +++ b/terminal-telnet/src/main/java/org/aesh/terminal/telnet/TelnetBootstrap.java @@ -19,7 +19,7 @@ */ package org.aesh.terminal.telnet; -import org.aesh.terminal.telnet.util.Helper; +import org.aesh.terminal.utils.Helper; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; diff --git a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/netty/NettyTelnetTtyBootstrap.java b/terminal-telnet/src/main/java/org/aesh/terminal/telnet/netty/NettyTelnetTtyBootstrap.java index 664d662b..a1411f27 100644 --- a/terminal-telnet/src/main/java/org/aesh/terminal/telnet/netty/NettyTelnetTtyBootstrap.java +++ b/terminal-telnet/src/main/java/org/aesh/terminal/telnet/netty/NettyTelnetTtyBootstrap.java @@ -21,7 +21,7 @@ import org.aesh.terminal.Connection; import org.aesh.terminal.telnet.TelnetTtyConnection; -import org.aesh.terminal.telnet.util.Helper; +import org.aesh.terminal.utils.Helper; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets;