Skip to content

Commit

Permalink
Added support for SSH exec. Tested with 1.5.8.RELEASE
Browse files Browse the repository at this point in the history
  • Loading branch information
anand1st committed Nov 13, 2017
1 parent 83f44f6 commit 3ee02af
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 58 deletions.
5 changes: 4 additions & 1 deletion README.md
Expand Up @@ -13,12 +13,15 @@ To import into Maven project, add the following dependency inside pom.xml:
<dependency>
<groupId>io.github.anand1st</groupId>
<artifactId>sshd-shell-spring-boot-starter</artifactId>
<version>2.5</version>
<version>2.6</version>
</dependency>

### Note
Versions < 2.1 are deprecated and unsupported. The artifact above supports the following functionalities:

### Version 2.6
In addition to SSH shell, SSH exec is now supported.

### Version 2.5
Upgraded to jline-3.5.1. Added post processor functionality to support searching and mailing of output. HealthIndicators are removed as spring-boot-actuator endpoints support them directly by an implementation in `EndpointCommand`. See examples below.

Expand Down
6 changes: 3 additions & 3 deletions sshd-shell-spring-boot-starter/pom.xml
Expand Up @@ -24,7 +24,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>io.github.anand1st</groupId>
<artifactId>sshd-shell-spring-boot-starter</artifactId>
<version>2.6-SNAPSHOT</version>
<version>2.6</version>
<name>SSH Shell starter for Spring Boot Application</name>
<description>
This artifact is a spring boot starter that provides SSH access to spring boot applications. It is to be used
Expand All @@ -51,7 +51,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring-boot.version>1.5.7.RELEASE</spring-boot.version>
<spring-boot.version>1.5.8.RELEASE</spring-boot.version>
<jline.version>3.5.1</jline.version>
</properties>
<dependencyManagement>
Expand Down Expand Up @@ -132,7 +132,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
<version>2.6</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Expand Up @@ -22,6 +22,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import org.apache.sshd.common.Factory;
import org.apache.sshd.server.ChannelSessionAware;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.ExitCallback;
Expand All @@ -38,7 +39,7 @@
*/
@lombok.extern.slf4j.Slf4j
@lombok.RequiredArgsConstructor(access = lombok.AccessLevel.PACKAGE)
class SshSessionInstance implements Command, ChannelSessionAware, Runnable {
class SshSessionInstance implements Command, Factory<Command>, ChannelSessionAware, Runnable {

private final Environment environment;
private final Banner shellBanner;
Expand Down Expand Up @@ -99,4 +100,9 @@ public void setOutputStream(OutputStream os) {
public void setChannelSession(ChannelSession session) {
this.session = session;
}

@Override
public Command create() {
return this;
}
}
Expand Up @@ -33,6 +33,7 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.AuthenticationProvider;
import sshd.shell.springboot.autoconfiguration.SshdShellProperties;
Expand Down Expand Up @@ -95,9 +96,16 @@ SshServer sshServer() {
server.setHost(props.getHost());
server.setPasswordAuthenticator(passwordAuthenticator());
server.setPort(props.getPort());
server.setShellFactory(() -> new SshSessionInstance(environment, shellBanner, terminalProcessor));
server.setShellFactory(() -> sshSessionInstance());
server.setCommandFactory(command -> sshSessionInstance());
return server;
}

@Bean
@Scope("prototype")
SshSessionInstance sshSessionInstance() {
return new SshSessionInstance(environment, shellBanner, terminalProcessor);
}

@PostConstruct
void startServer() throws IOException {
Expand Down
Expand Up @@ -15,7 +15,7 @@
*/
package sshd.shell.springboot.autoconfiguration;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
Expand All @@ -28,6 +28,7 @@
import java.util.Properties;
import static org.awaitility.Awaitility.await;
import org.awaitility.Duration;
import static org.junit.Assert.fail;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
Expand All @@ -40,38 +41,42 @@
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ConfigTest.class)
abstract class AbstractSshSupport {

@Autowired
protected SshdShellProperties props;

protected void sshCall(String username, String password, SshExecutor executor) throws JSchException, IOException {
JSch jsch = new JSch();
Session session = jsch.getSession(username, props.getShell().getHost(), props.getShell().getPort());
session.setPassword(password);
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect();
ChannelShell channel = (ChannelShell) session.openChannel("shell");
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
channel.setInputStream(new PipedInputStream(pos));
channel.setOutputStream(new PipedOutputStream(pis));
channel.connect();

protected void sshCall(String username, String password, SshExecutor executor, String channelType) {
try {
executor.execute(pis, pos);
} finally {
pis.close();
pos.close();
channel.disconnect();
session.disconnect();
JSch jsch = new JSch();
Session session = jsch.getSession(username, props.getShell().getHost(), props.getShell().getPort());
session.setPassword(password);
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect();
Channel channel = session.openChannel(channelType);
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
channel.setInputStream(new PipedInputStream(pos));
channel.setOutputStream(new PipedOutputStream(pis));
channel.connect();
try {
executor.execute(pis, pos);
} finally {
pis.close();
pos.close();
channel.disconnect();
session.disconnect();
}
} catch (JSchException | IOException ex) {
fail(ex.toString());
}
}
protected void sshCall(SshExecutor executor) throws JSchException, IOException {
sshCall(props.getShell().getUsername(), props.getShell().getPassword(), executor);

protected void sshCallShell(SshExecutor executor) throws JSchException, IOException {
sshCall(props.getShell().getUsername(), props.getShell().getPassword(), executor, "shell");
}

protected void verifyResponse(InputStream pis, String response) {
StringBuilder sb = new StringBuilder();
try {
Expand All @@ -96,10 +101,10 @@ protected void write(OutputStream os, String... input) throws IOException {
os.flush();
}
}

@FunctionalInterface
protected static interface SshExecutor {

void execute(InputStream is, OutputStream os) throws IOException;
}
}
Expand Up @@ -33,7 +33,7 @@ public class SshdShellAutoConfigurationAuthProviderTest extends AbstractSshSuppo

@Test
public void testDaoAuthWithoutRightPermission() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "test run bob");
verifyResponse(is, "Permission denied");
});
Expand All @@ -45,12 +45,12 @@ public void testDaoAuthWithoutRightPermission2() throws JSchException, IOExcepti
sshCall("alice", "alice", (is, os ) -> {
write(os, "test run");
verifyResponse(is, "Permission denied");
});
}, "exec");
}

@Test
public void testDaoAuthWithRightPermission() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "test execute bob");
verifyResponse(is, "test execute successful");
});
Expand Down
Expand Up @@ -49,7 +49,7 @@ public class SshdShellAutoConfigurationTest extends AbstractSshSupport {
@Ignore
@Test
public void testExitCommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "exit");
verifyResponse(is, "Exiting shell");
});
Expand All @@ -59,39 +59,39 @@ public void testExitCommand() throws JSchException, IOException {
@Ignore
@Test
public void testEmptyUserInput() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "");
verifyResponse(is, "app> app>");
});
}

@Test
public void testIAECommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "iae");
verifyResponse(is, "Error performing method invocation\r\r\nPlease check server logs for more information");
});
}

@Test
public void testUnsupportedCommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "xxx");
verifyResponse(is, "Unknown command. Enter 'help' for a list of supported commands");
});
}

@Test
public void testUnsupportedSubCommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "test nonexistent");
verifyResponse(is, "Unknown subcommand 'nonexistent'. Type 'test' for supported subcommands");
});
}

@Test
public void testSubcommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "test");
verifyResponse(is, "Supported subcommand for test\r\n\rexecute\t\ttest execute\r\n\rinteractive"
+ "\t\ttest interactive\r\n\rrun\t\ttest run");
Expand All @@ -100,7 +100,7 @@ public void testSubcommand() throws JSchException, IOException {

@Test
public void testHelp() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help");
StringBuilder format = new StringBuilder("Supported Commands");
for (int i = 0; i < 6; i++) {
Expand All @@ -121,15 +121,15 @@ public void testHelp() throws JSchException, IOException {

@Test
public void testInteractive() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "test interactive", "anand");
verifyResponse(is, "Name: anandHi anand");
});
}

@Test
public void testEndpointList() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "endpoint list");
verifyResponse(is, getEndpointList());
});
Expand All @@ -150,7 +150,7 @@ private String getEndpointList() {

@Test
public void testEndpointNullArg() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "endpoint invoke");
String response = "Null or unknown endpoint\r\n" + getEndpointList()
+ "\r\nUsage: endpoint invoke <endpoint>";
Expand All @@ -160,7 +160,7 @@ public void testEndpointNullArg() throws JSchException, IOException {

@Test
public void testEndpointInvalid() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "endpoint invoke invalid");
String response = "Null or unknown endpoint\r\n" + getEndpointList()
+ "\r\nUsage: endpoint invoke <endpoint>";
Expand All @@ -170,15 +170,15 @@ public void testEndpointInvalid() throws JSchException, IOException {

@Test
public void testEndpointDisabled() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "endpoint invoke shutdown");
verifyResponse(is, "Endpoint shutdown is not enabled");
});
}

@Test
public void testEndpointInvokeSuccess() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "endpoint invoke info");
verifyResponse(is, "{ }");
});
Expand All @@ -193,7 +193,7 @@ public void testMailProcessor() throws JSchException, IOException {
mailServer.start();
((JavaMailSenderImpl) mailSender).setPort(smtpPort);
assertEquals(0, mailServer.getReceivedMessages().length);
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help | m anand@test.com");
verifyResponse(is, "Output response sent to anand@test.com");
assertTrue(mailServer.waitForIncomingEmail(5000, 1));
Expand All @@ -216,15 +216,15 @@ public void testMailProcessor() throws JSchException, IOException {

@Test
public void testMailProcessorFail() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help | m anand@test.com");
verifyResponse(is, "Error sending mail, please check logs for more info");
});
}

@Test
public void testHighlightProcessor() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help | h <emailId>");
StringBuilder format = new StringBuilder("Supported Commands");
for (int i = 0; i < 6; i++) {
Expand All @@ -245,15 +245,15 @@ public void testHighlightProcessor() throws JSchException, IOException {

@Test
public void testInvalidCommand() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help | h bob@hope.com | m bob@hope.com");
verifyResponse(is, "Invalid command");
});
}

@Test
public void testInvalidCommand2() throws JSchException, IOException {
sshCall((is, os) -> {
sshCallShell((is, os) -> {
write(os, "help | x");
verifyResponse(is, "Invalid command");
});
Expand Down
4 changes: 2 additions & 2 deletions sshd-shell-spring-boot-test-app/pom.xml
Expand Up @@ -9,7 +9,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring-boot.version>1.5.7.RELEASE</spring-boot.version>
<spring-boot.version>1.5.8.RELEASE</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
Expand Down Expand Up @@ -46,7 +46,7 @@
<dependency>
<groupId>io.github.anand1st</groupId>
<artifactId>sshd-shell-spring-boot-starter</artifactId>
<version>2.6-SNAPSHOT</version>
<version>2.6</version>
</dependency>
</dependencies>
<build>
Expand Down

0 comments on commit 3ee02af

Please sign in to comment.