/* * Copyright 2015 Red Hat, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. * * * Copyright (c) 2015 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. * */ package io.vertx.ext.shell.term; import com.jcraft.jsch.Channel; import com.jcraft.jsch.ChannelShell; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import io.vertx.core.net.JksOptions; import io.vertx.ext.auth.AbstractUser; import io.vertx.ext.auth.AuthProvider; import io.vertx.ext.auth.shiro.ShiroAuthOptions; import io.vertx.ext.auth.shiro.ShiroAuthRealmType; import io.vertx.ext.shell.SSHTestBase; import io.vertx.ext.shell.term.impl.SSHExec; import io.vertx.ext.shell.term.impl.SSHServer; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import org.junit.After; import org.junit.Test; import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.net.URL; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> */ public class SSHServerTest extends SSHTestBase { TermServer server; Handler<Term> termHandler; Handler<SSHExec> execHandler; AuthProvider authProvider; @Override public void before() { super.before(); termHandler = term -> term.write("% "); } @After public void after() throws Exception { if (server != null) { CountDownLatch latch = new CountDownLatch(1); server.close(ar -> latch.countDown()); assertTrue(latch.await(10, TimeUnit.SECONDS)); } super.after(); } protected void startShell(SSHTermOptions options) throws ExecutionException, InterruptedException, TimeoutException { if (server != null) { throw new IllegalStateException(); } server = TermServer.createSSHTermServer(vertx, options); CompletableFuture<Void> fut = new CompletableFuture<>(); server.termHandler(termHandler); ((SSHServer)server).setExecHandler(execHandler); server.authProvider(authProvider); server.listen(ar -> { if (ar.succeeded()) { fut.complete(null); } else { fut.completeExceptionally(ar.cause()); } }); fut.get(10, TimeUnit.SECONDS); } @Test public void testRead(TestContext context) throws Exception { Async async = context.async(); termHandler = term -> { term.stdinHandler(s -> { context.assertEquals("hello", s); async.complete(); }); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); OutputStream out = channel.getOutputStream(); out.write("hello".getBytes()); out.flush(); channel.disconnect(); session.disconnect(); } @Test public void testWrite() throws Exception { termHandler = term -> { term.write("hello"); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); Reader in = new InputStreamReader(channel.getInputStream()); int count = 5; StringBuilder sb = new StringBuilder(); while (count > 0) { int code = in.read(); if (code == -1) { count = 0; } else { count--; sb.append((char)code); } } assertEquals("hello", sb.toString()); channel.disconnect(); session.disconnect(); } @Test public void testResizeHandler(TestContext context) throws Exception { Async async = context.async(); termHandler = term -> { term.resizehandler(v -> { context.assertEquals(20, term.width()); context.assertEquals(10, term.height()); async.complete(); }); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); ChannelShell channel = (ChannelShell) session.openChannel("shell"); channel.connect(); OutputStream out = channel.getOutputStream(); channel.setPtySize(20, 10, 20 * 8, 10 * 8); out.flush(); channel.disconnect(); session.disconnect(); } @Test public void testCloseHandler(TestContext context) throws Exception { Async async = context.async(); termHandler = term -> { term.closeHandler(v -> { async.complete(); }); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); channel.disconnect(); session.disconnect(); } @Test public void testClose(TestContext context) throws Exception { testClose(context, term -> { vertx.setTimer(10, id -> { term.close(); }); }); } @Test public void testCloseImmediatly(TestContext context) throws Exception { testClose(context, Term::close); } private void testClose(TestContext context, Consumer<Term> closer) throws Exception { Async async = context.async(); termHandler = term -> { term.closeHandler(v -> { async.complete(); }); closer.accept(term); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); while (channel.isClosed()) { Thread.sleep(10); } } @Test public void testType(TestContext context) throws Exception { Async async = context.async(); termHandler = term -> { context.assertEquals("vt100", term.type()); async.complete(); }; startShell(); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); } @Test public void testExternalAuthProvider(TestContext context) throws Exception { AtomicInteger count = new AtomicInteger(); authProvider = (authInfo, resultHandler) -> { count.incrementAndGet(); String username = authInfo.getString("username"); String password = authInfo.getString("password"); if (username.equals("paulo") && password.equals("anothersecret")) { resultHandler.handle(Future.succeededFuture(new AbstractUser() { @Override protected void doIsPermitted(String permission, Handler<AsyncResult<Boolean>> resultHandler) { resultHandler.handle(Future.succeededFuture(true)); } @Override public JsonObject principal() { return new JsonObject().put("username", username); } @Override public void setAuthProvider(AuthProvider authProvider) { } })); } else { resultHandler.handle(Future.failedFuture("not authenticated")); } }; Async async = context.async(); termHandler = term -> { context.assertEquals(1, count.get()); async.complete(); }; startShell(new SSHTermOptions().setPort(5000).setHost("localhost").setKeyPairOptions( new JksOptions().setPath("src/test/resources/server-keystore.jks").setPassword("wibble"))); Session session = createSession("paulo", "anothersecret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); } @Test public void testExternalAuthProviderFails(TestContext context) throws Exception { AtomicInteger count = new AtomicInteger(); authProvider = (authInfo, resultHandler) -> { count.incrementAndGet(); resultHandler.handle(Future.failedFuture("not authenticated")); }; termHandler = term -> { context.fail(); }; startShell(new SSHTermOptions().setPort(5000).setHost("localhost").setKeyPairOptions( new JksOptions().setPath("src/test/resources/server-keystore.jks").setPassword("wibble"))); Session session = createSession("paulo", "anothersecret", false); try { session.connect(); context.fail("Was not expected to login"); } catch (JSchException e) { assertEquals("Auth cancel", e.getMessage()); } context.assertEquals(1, count.get()); } @Test public void testDifferentCharset(TestContext context) throws Exception { termHandler = term -> { term.write("\u20AC"); term.close(); }; startShell(new SSHTermOptions().setDefaultCharset("ISO_8859_1").setPort(5000).setHost("localhost").setKeyPairOptions( new JksOptions().setPath("src/test/resources/server-keystore.jks").setPassword("wibble")). setAuthOptions(new ShiroAuthOptions().setType(ShiroAuthRealmType.PROPERTIES).setConfig( new JsonObject().put("properties_path", "classpath:test-auth.properties")))); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); InputStream in = channel.getInputStream(); int b = in.read(); context.assertEquals(63, b); } @Test public void testKeymapFromFilesystem() throws Exception { URL url = TermServer.class.getResource(SSHTermOptions.DEFAULT_INPUTRC); File f = new File(url.toURI()); termHandler = Term::close; startShell(new SSHTermOptions().setIntputrc(f.getAbsolutePath()).setPort(5000).setHost("localhost").setKeyPairOptions( new JksOptions().setPath("src/test/resources/server-keystore.jks").setPassword("wibble")). setAuthOptions(new ShiroAuthOptions().setType(ShiroAuthRealmType.PROPERTIES).setConfig( new JsonObject().put("properties_path", "classpath:test-auth.properties")))); Session session = createSession("paulo", "secret", false); session.connect(); Channel channel = session.openChannel("shell"); channel.connect(); } @Override public void testExec(TestContext context) throws Exception { execHandler = exec -> { context.assertNotNull(Vertx.currentContext()); context.assertEquals("the-command arg1 arg2", exec.command()); exec.write("the_output"); StringBuilder input = new StringBuilder(); context.assertEquals(-1, exec.width()); context.assertEquals(-1, exec.height()); exec.stdinHandler(data -> { input.append(data); if (input.toString().equals("the_input")) { exec.end(2); } }); }; super.testExec(context); } }