Add remote shell implementation based on crsh

This commit adds a new starter named spring-boot-starter-shell-crsh and auto configuration support to embed a system shell within Spring Boot applications.

The embedded shell allows clients to connect via ssh or telnet to the Boot app and execute commands. Commands can be implemented and embedded with app.

For sample usage see spring-boot-samples-actuator.
This commit is contained in:
Christian Dupuis 2013-11-04 17:16:17 +01:00
parent 90a2bf38da
commit 6b599b8483
16 changed files with 1802 additions and 1 deletions

View File

@ -72,6 +72,11 @@
<artifactId>tomcat-embed-core</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.embed.spring</artifactId>
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework</groupId>

View File

@ -0,0 +1,462 @@
/*
* Copyright 2013 the original author or authors.
*
* 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.springframework.boot.actuate.autoconfigure;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.crsh.auth.AuthenticationPlugin;
import org.crsh.plugin.CRaSHPlugin;
import org.crsh.plugin.PluginContext;
import org.crsh.plugin.PluginDiscovery;
import org.crsh.plugin.PluginLifeCycle;
import org.crsh.plugin.PropertyDescriptor;
import org.crsh.plugin.ServiceLoaderDiscovery;
import org.crsh.vfs.FS;
import org.crsh.vfs.spi.AbstractFSDriver;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.properties.CrshProperties;
import org.springframework.boot.actuate.properties.CrshProperties.AuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.JaasAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.KeyAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.SimpleAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.SpringAuthenticationProperties;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.SpringVersion;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for embedding an extensible shell into a Spring
* Boot enabled application. By default a SSH daemon is started on port 2000 with a default username
* <code>user</code> and password (default password is logged during application startup).
*
* <p>
* This configuration will auto detect the existence of a Spring Security {@link AuthenticationManager}
* and will delegate authentication requests for shell access to this detected instance.
*
* <p>
* To add customizations to the shell simply define beans of type {@link CRaSHPlugin} in the
* application context. Those beans will get auto detected during startup and registered with the
* underlying shell infrastructure.
*
* <p>
* Additional shell commands can be implemented using the guide and documentation at
* <a href="http://www.crashub.org">crashub.org</a>. By default Boot will search for commands using
* the following classpath scanning pattern <code>classpath*:/commands/**</code>. To add different
* locations or override the default use <code>shell.command_path_patterns</code> in your application
* configuration.
*
* @author Christian Dupuis
*/
@Configuration
@ConditionalOnClass({ PluginLifeCycle.class })
@EnableConfigurationProperties({ CrshProperties.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class CrshAutoConfiguration {
@Autowired
private CrshProperties properties;
@Bean
@ConditionalOnExpression("#{environment['shell.auth'] == 'jaas'}")
@ConditionalOnMissingBean({ AuthenticationProperties.class })
public AuthenticationProperties jaasAuthenticationProperties() {
return new JaasAuthenticationProperties();
}
@Bean
@ConditionalOnExpression("#{environment['shell.auth'] == 'key'}")
@ConditionalOnMissingBean({ AuthenticationProperties.class })
public AuthenticationProperties keyAuthenticationProperties() {
return new KeyAuthenticationProperties();
}
@Bean
@ConditionalOnExpression("#{environment['shell.auth'] == 'simple'}")
@ConditionalOnMissingBean({ AuthenticationProperties.class })
public AuthenticationProperties simpleAuthenticationProperties() {
return new SimpleAuthenticationProperties();
}
@Bean
@ConditionalOnExpression("#{environment['shell.auth'] == 'spring'}")
@ConditionalOnMissingBean({ AuthenticationProperties.class })
public AuthenticationProperties SpringAuthenticationProperties() {
return new SpringAuthenticationProperties();
}
@Bean
@ConditionalOnBean({ AuthenticationManager.class })
public CRaSHPlugin<?> shellAuthenticationManager() {
return new AuthenticationManagerAdapter();
}
@Bean
@ConditionalOnMissingBean({ PluginLifeCycle.class })
public PluginLifeCycle shellBootstrap() {
CrshBootstrap bs = new CrshBootstrap();
bs.setConfig(properties.mergeProperties(new Properties()));
return bs;
}
public static class CrshBootstrap extends PluginLifeCycle {
@Autowired
private ListableBeanFactory beanFactory;
@Autowired
private CrshProperties properties;
@Autowired
private ResourcePatternResolver resourceLoader;
@PreDestroy
public void destroy() {
stop();
}
@PostConstruct
public void init() throws Exception {
FS commandFileSystem = createFileSystem(properties.getCommandPathPatterns());
FS confFileSystem = createFileSystem(properties.getConfigPathPatterns());
PluginDiscovery discovery = new BeanFactoryFilteringPluginDiscovery(resourceLoader.getClassLoader(),
beanFactory, properties.getDisabledPlugins());
PluginContext context = new PluginContext(discovery, createPluginContextAttributes(),
commandFileSystem, confFileSystem, resourceLoader.getClassLoader());
context.refresh();
start(context);
}
protected FS createFileSystem(String[] pathPatterns) throws IOException, URISyntaxException {
Assert.notNull(pathPatterns);
FS cmdFS = new FS();
for (String pathPattern : pathPatterns) {
cmdFS.mount(new SimpleFileSystemDriver(new DirectoryHandle(pathPattern, resourceLoader)));
}
return cmdFS;
}
protected Map<String, Object> createPluginContextAttributes() {
Map<String, Object> attributes = new HashMap<String, Object>();
String bootVersion = CrshAutoConfiguration.class.getPackage().getImplementationVersion();
if (bootVersion != null) {
attributes.put("spring.boot.version", bootVersion);
}
attributes.put("spring.version", SpringVersion.getVersion());
if (beanFactory != null) {
attributes.put("spring.beanfactory", beanFactory);
}
return attributes;
}
}
@SuppressWarnings("rawtypes")
private static class AuthenticationManagerAdapter extends CRaSHPlugin<AuthenticationPlugin> implements
AuthenticationPlugin<String> {
private static final PropertyDescriptor<String> ROLES = PropertyDescriptor.create(
"auth.spring.roles", "ADMIN", "Comma separated list of roles required to access the shell");
@Autowired(required=false)
private AccessDecisionManager accessDecisionManager;
@Autowired
private AuthenticationManager authenticationManager;
private String[] roles = new String[] { "ROLE_ADMIN" };
@Override
public boolean authenticate(String username, String password) throws Exception {
// Authenticate first to make credentials are valid
Authentication token = new UsernamePasswordAuthenticationToken(username, password);
try {
token = authenticationManager.authenticate(token);
}
catch (AuthenticationException ae) {
return false;
}
// Test access rights if a Spring Security AccessDecisionManager is installed
if (accessDecisionManager != null && token.isAuthenticated() && roles != null) {
try {
accessDecisionManager.decide(token, this, SecurityConfig.createList(roles));
}
catch (AccessDeniedException e) {
return false;
}
}
return token.isAuthenticated();
}
@Override
public Class<String> getCredentialType() {
return String.class;
}
@Override
public AuthenticationPlugin<String> getImplementation() {
return this;
}
@Override
public String getName() {
return "spring";
}
@Override
public void init() {
String rolesPropertyValue = getContext().getProperty(ROLES);
if (rolesPropertyValue != null) {
this.roles = StringUtils.commaDelimitedListToStringArray(rolesPropertyValue);
}
}
@Override
protected Iterable<PropertyDescriptor<?>> createConfigurationCapabilities() {
return Arrays.<PropertyDescriptor<?>>asList(ROLES);
}
}
private static class BeanFactoryFilteringPluginDiscovery extends ServiceLoaderDiscovery {
private ListableBeanFactory beanFactory;
private String[] disabledPlugins;
public BeanFactoryFilteringPluginDiscovery(ClassLoader classLoader, ListableBeanFactory beanFactory,
String[] disabledPlugins)
throws NullPointerException {
super(classLoader);
this.beanFactory = beanFactory;
this.disabledPlugins = disabledPlugins;
}
@Override
@SuppressWarnings("rawtypes")
public Iterable<CRaSHPlugin<?>> getPlugins() {
List<CRaSHPlugin<?>> plugins = new ArrayList<CRaSHPlugin<?>>();
for (CRaSHPlugin<?> p : super.getPlugins()) {
if (!shouldFilter(p)) {
plugins.add(p);
}
}
Collection<CRaSHPlugin> springPlugins = beanFactory.getBeansOfType(CRaSHPlugin.class).values();
for (CRaSHPlugin<?> p : springPlugins) {
if (!shouldFilter(p)) {
plugins.add(p);
}
}
return plugins;
}
@SuppressWarnings("rawtypes")
protected boolean shouldFilter(CRaSHPlugin<?> plugin) {
Assert.notNull(plugin);
Set<Class> classes = ClassUtils.getAllInterfacesAsSet(plugin);
classes.add(plugin.getClass());
for (Class<?> clazz : classes) {
if (disabledPlugins != null && disabledPlugins.length > 0) {
for (String disabledPlugin : disabledPlugins) {
if (ClassUtils.getShortName(clazz).equalsIgnoreCase(disabledPlugin)
|| ClassUtils.getQualifiedName(clazz).equalsIgnoreCase(disabledPlugin)) {
return true;
}
}
}
}
return false;
}
}
private static class SimpleFileSystemDriver extends AbstractFSDriver<ResourceHandle> {
private ResourceHandle root;
public SimpleFileSystemDriver(ResourceHandle handle) {
this.root = handle;
}
@Override
public Iterable<ResourceHandle> children(ResourceHandle handle) throws IOException {
if (handle instanceof DirectoryHandle) {
return ((DirectoryHandle) handle).members();
}
return Collections.emptySet();
}
@Override
public long getLastModified(ResourceHandle handle) throws IOException {
if (handle instanceof FileHandle) {
return ((FileHandle) handle).getLastModified();
}
return -1;
}
@Override
public boolean isDir(ResourceHandle handle) throws IOException {
return handle instanceof DirectoryHandle;
}
@Override
public String name(ResourceHandle handle) throws IOException {
return handle.getName();
}
@Override
public Iterator<InputStream> open(ResourceHandle handle) throws IOException {
if (handle instanceof FileHandle) {
return Collections.singletonList(((FileHandle) handle).openStream()).iterator();
}
return Collections.<InputStream>emptyList().iterator();
}
@Override
public ResourceHandle root() throws IOException {
return root;
}
}
private static class DirectoryHandle extends ResourceHandle {
private ResourcePatternResolver resourceLoader;
public DirectoryHandle(String name, ResourcePatternResolver resourceLoader) {
super(name);
this.resourceLoader = resourceLoader;
}
public List<ResourceHandle> members() throws IOException {
Resource[] resources = resourceLoader.getResources(getName());
List<ResourceHandle> files = new ArrayList<ResourceHandle>();
for (Resource resource : resources) {
if (!resource.getURL().getPath().endsWith("/")) {
files.add(new FileHandle(resource.getFilename(), resource));
}
}
return files;
}
}
private static class FileHandle extends ResourceHandle {
private Resource resource;
public FileHandle(String name, Resource resource) {
super(name);
this.resource = resource;
}
public InputStream openStream() throws IOException {
return this.resource.getInputStream();
}
public long getLastModified() {
try {
return this.resource.lastModified();
}
catch (IOException e) {}
return -1;
}
}
private abstract static class ResourceHandle {
private String name;
public ResourceHandle(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}

View File

@ -0,0 +1,355 @@
/*
* Copyright 2013 the original author or authors.
*
* 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.springframework.boot.actuate.properties;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Configuration properties for the shell subsystem.
*
* @author Christian Dupuis
*/
@ConfigurationProperties(name = "shell", ignoreUnknownFields = true)
public class CrshProperties {
protected static final String CRASH_AUTH = "crash.auth";
protected static final String CRASH_AUTH_JAAS_DOMAIN = "crash.auth.jaas.domain";
protected static final String CRASH_AUTH_KEY_PATH = "crash.auth.key.path";
protected static final String CRASH_AUTH_SIMPLE_PASSWORD = "crash.auth.simple.password";
protected static final String CRASH_AUTH_SIMPLE_USERNAME = "crash.auth.simple.username";
protected static final String CRASH_AUTH_SPRING_ROLES = "crash.auth.spring.roles";
protected static final String CRASH_SSH_KEYPATH = "crash.ssh.keypath";
protected static final String CRASH_SSH_PORT = "crash.ssh.port";
protected static final String CRASH_TELNET_PORT = "crash.telnet.port";
protected static final String CRASH_VFS_REFRESH_PERIOD = "crash.vfs.refresh_period";
private String auth = "simple";
@Autowired(required = false)
private AuthenticationProperties authenticationProperties;
private int commandRefreshInterval = -1;
private String[] commandPathPatterns = new String[] { "classpath*:/commands/**",
"classpath*:/crash/commands/**" };
private String[] configPathPatterns = new String[] { "classpath*:/crash/*" };
private String[] disabledPlugins = new String[0];
private Ssh ssh = new Ssh();
private Telnet telnet = new Telnet();
public String getAuth() {
return this.auth;
}
public AuthenticationProperties getAuthenticationProperties() {
return this.authenticationProperties;
}
public int getCommandRefreshInterval() {
return this.commandRefreshInterval;
}
public String[] getCommandPathPatterns() {
return this.commandPathPatterns;
}
public String[] getConfigPathPatterns() {
return this.configPathPatterns;
}
public String[] getDisabledPlugins() {
return this.disabledPlugins;
}
public Ssh getSsh() {
return this.ssh;
}
public Telnet getTelnet() {
return this.telnet;
}
public Properties mergeProperties(Properties properties) {
properties = ssh.mergeProperties(properties);
properties = telnet.mergeProperties(properties);
properties.put(CRASH_AUTH, auth);
if (authenticationProperties != null) {
properties = authenticationProperties.mergeProperties(properties);
}
if (this.commandRefreshInterval > 0) {
properties.put(CRASH_VFS_REFRESH_PERIOD, String.valueOf(this.commandRefreshInterval));
}
// special handling for disabling Ssh and Telnet support
List<String> dp = new ArrayList<String>(Arrays.asList(this.disabledPlugins));
if (!ssh.isEnabled()) {
dp.add("org.crsh.ssh.SSHPlugin");
}
if (!telnet.isEnabled()) {
dp.add("org.crsh.telnet.TelnetPlugin");
}
this.disabledPlugins = dp.toArray(new String[dp.size()]);
return properties;
}
public void setAuth(String auth) {
Assert.hasLength(auth);
this.auth = auth;
}
public void setAuthenticationProperties(AuthenticationProperties authenticationProperties) {
Assert.notNull(authenticationProperties);
this.authenticationProperties = authenticationProperties;
}
public void setCommandRefreshInterval(int commandRefreshInterval) {
this.commandRefreshInterval = commandRefreshInterval;
}
public void setCommandPathPatterns(String[] commandPathPatterns) {
Assert.notEmpty(commandPathPatterns);
this.commandPathPatterns = commandPathPatterns;
}
public void setConfigPathPatterns(String[] configPathPatterns) {
Assert.notEmpty(configPathPatterns);
this.configPathPatterns = configPathPatterns;
}
public void setDisabledPlugins(String[] disabledPlugins) {
Assert.notEmpty(disabledPlugins);
this.disabledPlugins = disabledPlugins;
}
public void setSsh(Ssh ssh) {
Assert.notNull(ssh);
this.ssh = ssh;
}
public void setTelnet(Telnet telnet) {
Assert.notNull(telnet);
this.telnet = telnet;
}
public interface AuthenticationProperties extends PropertiesProvider {
}
@ConfigurationProperties(name = "shell.auth.jaas", ignoreUnknownFields = false)
public static class JaasAuthenticationProperties implements AuthenticationProperties {
private String domain = "my-domain";
@Override
public Properties mergeProperties(Properties properties) {
properties.put(CRASH_AUTH_JAAS_DOMAIN, this.domain);
return properties;
}
public void setDomain(String domain) {
Assert.hasText(domain);
this.domain = domain;
}
}
@ConfigurationProperties(name = "shell.auth.key", ignoreUnknownFields = false)
public static class KeyAuthenticationProperties implements AuthenticationProperties {
private String path;
@Override
public Properties mergeProperties(Properties properties) {
if (this.path != null) {
properties.put(CRASH_AUTH_KEY_PATH, this.path);
}
return properties;
}
public void setPath(String path) {
Assert.hasText(path);
this.path = path;
}
}
public interface PropertiesProvider {
Properties mergeProperties(Properties properties);
}
@ConfigurationProperties(name = "shell.auth.simple", ignoreUnknownFields = false)
public static class SimpleAuthenticationProperties implements AuthenticationProperties {
private static Log logger = LogFactory.getLog(SimpleAuthenticationProperties.class);
private String username = "user";
private String password = UUID.randomUUID().toString();
private boolean defaultPassword = true;
public boolean isDefaultPassword() {
return this.defaultPassword;
}
@Override
public Properties mergeProperties(Properties properties) {
properties.put(CRASH_AUTH_SIMPLE_USERNAME, this.username);
properties.put(CRASH_AUTH_SIMPLE_PASSWORD, this.password);
if (this.defaultPassword) {
logger.info("Using default password for shell access: " + this.password);
}
return properties;
}
public void setPassword(String password) {
if (password.startsWith("${") && password.endsWith("}") || !StringUtils.hasLength(password)) {
return;
}
this.password = password;
this.defaultPassword = false;
}
public void setUsername(String username) {
Assert.hasLength(username);
this.username = username;
}
}
@ConfigurationProperties(name = "shell.auth.spring", ignoreUnknownFields = false)
public static class SpringAuthenticationProperties implements AuthenticationProperties {
private String[] roles = new String[] { "ROLE_ADMIN" };
@Override
public Properties mergeProperties(Properties properties) {
if (this.roles != null) {
properties.put(CRASH_AUTH_SPRING_ROLES, StringUtils.arrayToCommaDelimitedString(this.roles));
}
return properties;
}
public void setRoles(String[] roles) {
Assert.notNull(roles);
this.roles = roles;
}
}
public static class Ssh implements PropertiesProvider {
private boolean enabled = true;
private String keyPath = null;
private String port = "2000";
public boolean isEnabled() {
return this.enabled;
}
@Override
public Properties mergeProperties(Properties properties) {
if (this.enabled) {
properties.put(CRASH_SSH_PORT, this.port);
if (this.keyPath != null) {
properties.put(CRASH_SSH_KEYPATH, this.keyPath);
}
}
return properties;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setKeyPath(String keyPath) {
Assert.hasText(keyPath);
this.keyPath = keyPath;
}
public void setPort(Integer port) {
Assert.notNull(port);
this.port = port.toString();
}
}
public static class Telnet implements PropertiesProvider {
private boolean enabled = false;
private String port = "5000";
public boolean isEnabled() {
return this.enabled;
}
@Override
public Properties mergeProperties(Properties properties) {
if (this.enabled) {
properties.put(CRASH_TELNET_PORT, this.port);
}
return properties;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setPort(Integer port) {
Assert.notNull(port);
this.port = port.toString();
}
}
}

View File

@ -1,5 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.actuate.autoconfigure.AuditAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration,\
org.springframework.boot.actuate.autoconfigure.ErrorMvcAutoConfiguration,\

View File

@ -0,0 +1,333 @@
/*
* Copyright 2013 the original author or authors.
*
* 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.springframework.boot.actuate.autoconfigure;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.crsh.auth.AuthenticationPlugin;
import org.crsh.auth.JaasAuthenticationPlugin;
import org.crsh.lang.groovy.GroovyREPL;
import org.crsh.plugin.PluginContext;
import org.crsh.plugin.PluginLifeCycle;
import org.crsh.plugin.ResourceKind;
import org.crsh.processor.term.ProcessorIOHandler;
import org.crsh.vfs.Resource;
import org.junit.After;
import org.junit.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
/**
* Tests for {@link CrshAutoConfiguration}.
*
* @author Christian Dupuis
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
public class CrshAutoConfigurationTests {
private AnnotationConfigWebApplicationContext context;
@After
public void tearDown() {
if (this.context != null) {
this.context.close();
this.context = null;
}
}
@Test
public void testDisabledPlugins() throws Exception {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.disabled_plugins", "GroovyREPL, termIOHandler, org.crsh.auth.AuthenticationPlugin");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertNotNull(lifeCycle);
assertNull(lifeCycle.getContext().getPlugin(GroovyREPL.class));
assertNull(lifeCycle.getContext().getPlugin(ProcessorIOHandler.class));
assertNull(lifeCycle.getContext().getPlugin(JaasAuthenticationPlugin.class));
}
@Test
public void testAttributes() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
Map<String, Object> attributes = lifeCycle.getContext().getAttributes();
assertTrue(attributes.containsKey("spring.version"));
assertTrue(attributes.containsKey("spring.beanfactory"));
assertEquals(this.context.getBeanFactory(), attributes.get("spring.beanfactory"));
}
@Test
public void testSshConfiguration() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.ssh.enabled", "true");
env.setProperty("shell.ssh.port", "3333");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertEquals(lifeCycle.getConfig().getProperty("crash.ssh.port"), "3333");
}
@Test
public void testSshConfigurationWithKeyPath() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.ssh.enabled", "true");
env.setProperty("shell.ssh.key_path", "~/.ssh/id.pem");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertEquals(lifeCycle.getConfig().getProperty("crash.ssh.keypath"), "~/.ssh/id.pem");
}
@Test
public void testCommandResolution() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
int count = 0;
Iterator<Resource> resources = lifeCycle.getContext().loadResources("login", ResourceKind.LIFECYCLE).iterator();
while (resources.hasNext()) {
count++;
resources.next();
}
assertEquals(1, count);
count = 0;
resources = lifeCycle.getContext().loadResources("help.java", ResourceKind.COMMAND).iterator();
while (resources.hasNext()) {
count++;
resources.next();
}
assertEquals(1, count);
}
@Test
public void testAuthenticationProvidersAreInstalled() {
this.context = new AnnotationConfigWebApplicationContext();
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityConfiguration.class);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
PluginContext pluginContext = lifeCycle.getContext();
int count = 0;
Iterator<AuthenticationPlugin> plugins = pluginContext.getPlugins(AuthenticationPlugin.class).iterator();
while (plugins.hasNext()) {
count++;
plugins.next();
}
assertEquals(3, count);
}
@Test
public void testJaasAuthenticationProvider() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.auth", "jaas");
env.setProperty("shell.auth.jaas.domain", "my-test-domain");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityConfiguration.class);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertEquals(lifeCycle.getConfig().get("crash.auth"), "jaas");
assertEquals(lifeCycle.getConfig().get("crash.auth.jaas.domain"), "my-test-domain");
}
@Test
public void testKeyAuthenticationProvider() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.auth", "key");
env.setProperty("shell.auth.key.path", "~/test.pem");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityConfiguration.class);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertEquals(lifeCycle.getConfig().get("crash.auth"), "key");
assertEquals(lifeCycle.getConfig().get("crash.auth.key.path"), "~/test.pem");
}
@Test
public void testSimpleAuthenticationProvider() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.auth", "simple");
env.setProperty("shell.auth.simple.username", "user");
env.setProperty("shell.auth.simple.password", "password");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityConfiguration.class);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
assertEquals(lifeCycle.getConfig().get("crash.auth"), "simple");
AuthenticationPlugin<String> authenticationPlugin = null;
String authentication = lifeCycle.getConfig().getProperty("crash.auth");
assertNotNull(authentication);
for (AuthenticationPlugin plugin : lifeCycle.getContext().getPlugins(AuthenticationPlugin.class)) {
if (authentication.equals(plugin.getName())) {
authenticationPlugin = plugin;
break;
}
}
assertNotNull(authenticationPlugin);
try {
assertTrue(authenticationPlugin.authenticate("user", "password"));
}
catch (Exception e) {
fail();
}
try {
assertFalse(authenticationPlugin.authenticate(UUID.randomUUID().toString(),
"password"));
}
catch (Exception e) {
fail();
}
}
@Test
public void testSpringAuthenticationProvider() {
MockEnvironment env = new MockEnvironment();
env.setProperty("shell.auth", "spring");
this.context = new AnnotationConfigWebApplicationContext();
this.context.setEnvironment(env);
this.context.setServletContext(new MockServletContext());
this.context.register(SecurityConfiguration.class);
this.context.register(CrshAutoConfiguration.class);
this.context.refresh();
PluginLifeCycle lifeCycle = this.context.getBean(PluginLifeCycle.class);
AuthenticationPlugin<String> authenticationPlugin = null;
String authentication = lifeCycle.getConfig().getProperty("crash.auth");
assertNotNull(authentication);
for (AuthenticationPlugin plugin : lifeCycle.getContext().getPlugins(AuthenticationPlugin.class)) {
if (authentication.equals(plugin.getName())) {
authenticationPlugin = plugin;
break;
}
}
assertNotNull(authenticationPlugin);
try {
assertTrue(authenticationPlugin.authenticate(SecurityConfiguration.USERNAME,
SecurityConfiguration.PASSWORD));
}
catch (Exception e) {
fail();
}
try {
assertFalse(authenticationPlugin.authenticate(UUID.randomUUID().toString(),
SecurityConfiguration.PASSWORD));
}
catch (Exception e) {
fail();
}
}
@Configuration
public static class SecurityConfiguration {
public static final String USERNAME = UUID.randomUUID().toString();
public static final String PASSWORD = UUID.randomUUID().toString();
@Bean
public AuthenticationManager authenticationManager() {
return new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication.getName().equals(USERNAME) && authentication.getCredentials().equals(PASSWORD)) {
authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
authentication.getCredentials(), Collections.singleton(new SimpleGrantedAuthority("ROLE_ADMIN")));
}
else {
throw new BadCredentialsException("Invalid username and password");
}
return authentication;
}
};
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
voters.add(new RoleVoter());
AccessDecisionManager result = new UnanimousBased(voters);
return result;
}
}
}

View File

@ -0,0 +1,282 @@
/*
* Copyright 2013 the original author or authors.
*
* 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.springframework.boot.actuate.properties;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.boot.actuate.properties.CrshProperties.JaasAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.KeyAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.SimpleAuthenticationProperties;
import org.springframework.boot.actuate.properties.CrshProperties.SpringAuthenticationProperties;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.core.convert.support.DefaultConversionService;
/**
* Tests for {@link CrshProperties}.
*
* @author Christian Dupuis
*/
public class CrshPropertiesTests {
@Test
public void testBindingAuth() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth", "spring")));
assertFalse(binder.getBindingResult().hasErrors());
assertEquals("spring", props.getAuth());
}
@Test
public void testBindingAuthIfEmpty() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth", "")));
assertTrue(binder.getBindingResult().hasErrors());
assertEquals("simple", props.getAuth());
}
@Test
public void testBindingCommandRefreshInterval() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.command_refresh_interval", "1")));
assertFalse(binder.getBindingResult().hasErrors());
assertEquals(1, props.getCommandRefreshInterval());
}
@Test
public void testBindingCommandPathPatterns() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.command_path_patterns",
"pattern1, pattern2")));
assertFalse(binder.getBindingResult().hasErrors());
assertEquals(2, props.getCommandPathPatterns().length);
Assert.assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getCommandPathPatterns());
}
@Test
public void testBindingConfigPathPatterns() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.config_path_patterns",
"pattern1, pattern2")));
assertFalse(binder.getBindingResult().hasErrors());
assertEquals(2, props.getConfigPathPatterns().length, 2);
Assert.assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getConfigPathPatterns());
}
@Test
public void testBindingDisabledPlugins() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.disabled_plugins",
"pattern1, pattern2")));
assertFalse(binder.getBindingResult().hasErrors());
assertEquals(2, props.getDisabledPlugins().length, 2);
assertArrayEquals(new String[] { "pattern1", "pattern2" }, props.getDisabledPlugins());
}
@Test
public void testBindingSsh() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.ssh.enabled", "true");
map.put("shell.ssh.port", "2222");
map.put("shell.ssh.key_path", "~/.ssh/test.pem");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("2222", p.get(CrshProperties.CRASH_SSH_PORT));
assertEquals("~/.ssh/test.pem", p.get(CrshProperties.CRASH_SSH_KEYPATH));
}
@Test
public void testBindingSshIgnored() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.ssh.enabled", "false");
map.put("shell.ssh.port", "2222");
map.put("shell.ssh.key_path", "~/.ssh/test.pem");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertNull(p.get(CrshProperties.CRASH_SSH_PORT));
assertNull(p.get(CrshProperties.CRASH_SSH_KEYPATH));
}
@Test
public void testBindingTelnet() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.telnet.enabled", "true");
map.put("shell.telnet.port", "2222");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("2222", p.get(CrshProperties.CRASH_TELNET_PORT));
}
@Test
public void testBindingTelnetIgnored() {
CrshProperties props = new CrshProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.telnet.enabled", "false");
map.put("shell.telnet.port", "2222");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertNull(p.get(CrshProperties.CRASH_TELNET_PORT));
}
@Test
public void testBindingJaas() {
JaasAuthenticationProperties props = new JaasAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.jaas");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.auth.jaas.domain", "my-test-domain");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("my-test-domain", p.get(CrshProperties.CRASH_AUTH_JAAS_DOMAIN));
}
@Test
public void testBindingKey() {
KeyAuthenticationProperties props = new KeyAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.key");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.auth.key.path", "~/.ssh/test.pem");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("~/.ssh/test.pem", p.get(CrshProperties.CRASH_AUTH_KEY_PATH));
}
@Test
public void testBindingKeyIgnored() {
KeyAuthenticationProperties props = new KeyAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.key");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertNull(p.get(CrshProperties.CRASH_AUTH_KEY_PATH));
}
@Test
public void testBindingSimple() {
SimpleAuthenticationProperties props = new SimpleAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.simple");
binder.setConversionService(new DefaultConversionService());
Map<String, String> map = new HashMap<String, String>();
map.put("shell.auth.simple.username", "username123");
map.put("shell.auth.simple.password", "password123");
binder.bind(new MutablePropertyValues(map));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("username123", p.get(CrshProperties.CRASH_AUTH_SIMPLE_USERNAME));
assertEquals("password123", p.get(CrshProperties.CRASH_AUTH_SIMPLE_PASSWORD));
}
@Test
public void testDefaultPasswordAutogeneratedIfUnresolovedPlaceholder() {
SimpleAuthenticationProperties security = new SimpleAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(security, "security");
binder.bind(new MutablePropertyValues(Collections.singletonMap(
"shell.auth.simple.password", "${ADMIN_PASSWORD}")));
assertFalse(binder.getBindingResult().hasErrors());
assertTrue(security.isDefaultPassword());
}
@Test
public void testDefaultPasswordAutogeneratedIfEmpty() {
SimpleAuthenticationProperties security = new SimpleAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(security, "security");
binder.bind(new MutablePropertyValues(Collections.singletonMap(
"shell.auth.simple.password", "")));
assertFalse(binder.getBindingResult().hasErrors());
assertTrue(security.isDefaultPassword());
}
@Test
public void testBindingSpring() {
SpringAuthenticationProperties props = new SpringAuthenticationProperties();
RelaxedDataBinder binder = new RelaxedDataBinder(props, "shell.auth.spring");
binder.bind(new MutablePropertyValues(Collections.singletonMap("shell.auth.spring.roles", "role1, role2")));
assertFalse(binder.getBindingResult().hasErrors());
Properties p = new Properties();
p = props.mergeProperties(p);
assertEquals("role1, role2", p.get(CrshProperties.CRASH_AUTH_SPRING_ROLES));
}
}

View File

@ -47,6 +47,7 @@
<thymeleaf-extras-springsecurity3.version>2.0.1</thymeleaf-extras-springsecurity3.version>
<thymeleaf-layout-dialect.version>1.1.3</thymeleaf-layout-dialect.version>
<tomcat.version>7.0.42</tomcat.version>
<crashub.version>1.3.0-beta8</crashub.version>
</properties>
<dependencyManagement>
<dependencies>
@ -481,6 +482,31 @@
<artifactId>geronimo-jms_1.1_spec</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.cli</artifactId>
<version>${crashub.version}</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.connectors.ssh</artifactId>
<version>${crashub.version}</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.connectors.telnet</artifactId>
<version>${crashub.version}</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.embed.spring</artifactId>
<version>${crashub.version}</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.shell</artifactId>
<version>${crashub.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>

View File

@ -32,6 +32,10 @@
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter-shell-crsh</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

View File

@ -6,4 +6,12 @@ server.port: 8080
server.tomcat.basedir: target/tomcat
server.tomcat.access_log_pattern: %h %t "%r" %s %b
security.require_ssl: false
service.name: Phil
service.name: Phil
shell.ssh.enabled: true
shell.ssh.port: 2222
shell.telnet.enabled: false
#shell.telnet.port: 1111
shell.auth: spring
#shell.auth: key
#shell.auth.key.path: ${user.home}/test/id_rsa.pub.pem
#shell.auth: simple

View File

@ -26,6 +26,7 @@
<module>spring-boot-starter-actuator</module>
<module>spring-boot-starter-parent</module>
<module>spring-boot-starter-security</module>
<module>spring-boot-starter-shell-crsh</module>
<module>spring-boot-starter-test</module>
<module>spring-boot-starter-tomcat</module>
<module>spring-boot-starter-web</module>

View File

@ -98,6 +98,11 @@
<artifactId>spring-boot-starter-security</artifactId>
<version>0.5.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-shell-crsh</artifactId>
<version>0.5.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>0.5.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-starter-shell-crsh</artifactId>
<packaging>jar</packaging>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.cli</artifactId>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.connectors.ssh</artifactId>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.connectors.telnet</artifactId>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.embed.spring</artifactId>
</dependency>
<dependency>
<groupId>org.crashub</groupId>
<artifactId>crash.shell</artifactId>
<exclusions>
<exclusion>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,58 @@
package crash.commands.base;
import org.crsh.cli.Command;
import org.crsh.cli.Usage;
import org.crsh.command.BaseCommand;
import org.crsh.command.DescriptionFormat;
import org.crsh.command.InvocationContext;
import org.crsh.command.ShellCommand;
import org.crsh.shell.impl.command.CRaSH;
import org.crsh.text.Color;
import org.crsh.text.Decoration;
import org.crsh.text.Style;
import org.crsh.text.ui.LabelElement;
import org.crsh.text.ui.RowElement;
import org.crsh.text.ui.TableElement;
import java.io.IOException;
/** @author Julien Viet */
public class help extends BaseCommand {
@Usage("provides basic help")
@Command
public void main(InvocationContext<Object> context) throws IOException {
//
TableElement table = new TableElement().rightCellPadding(1);
table.add(
new RowElement().
add(new LabelElement("NAME").style(Style.style(Decoration.bold))).
add(new LabelElement("DESCRIPTION")));
//
CRaSH crash = (CRaSH)context.getSession().get("crash");
Iterable<String> names = crash.getCommandNames();
for (String name : names) {
try {
ShellCommand cmd = crash.getCommand(name);
if (cmd != null) {
String desc = cmd.describe(name, DescriptionFormat.DESCRIBE);
if (desc == null) {
desc = "";
}
table.add(
new RowElement().
add(new LabelElement(name).style(Style.style(Color.red))).
add(new LabelElement(desc)));
}
} catch (Exception ignore) {
//
}
}
//
context.provide(new LabelElement("Try one of these commands with the -h or --help switch:\n"));
context.provide(table);
}
}

View File

@ -0,0 +1,123 @@
package crash.commands.base;
import org.crsh.cli.Argument;
import org.crsh.cli.Command;
import org.crsh.cli.Option;
import org.crsh.cli.Usage;
import org.crsh.command.BaseCommand;
import org.crsh.command.InvocationContext;
import org.crsh.command.PipeCommand;
import org.crsh.command.ScriptException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** @author Julien Viet */
@Usage("Java Management Extensions")
public class jmx extends BaseCommand {
@Usage("find mbeans")
@Command
public void find(
InvocationContext<ObjectName> context,
@Usage("The object name pattern")
@Option(names = {"p", "pattern"})
String pattern) throws Exception {
//
ObjectName patternName = pattern != null ? ObjectName.getInstance(pattern) : null;
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
Set<ObjectInstance> instances = server.queryMBeans(patternName, null);
for (ObjectInstance instance : instances) {
context.provide(instance.getObjectName());
}
/*
if (context.piped) {
} else {
UIBuilder ui = new UIBuilder()
ui.table(columns: [1,3]) {
row(bold: true, fg: black, bg: white) {
label("CLASS NAME"); label("OBJECT NAME")
}
instances.each { instance ->
row() {
label(foreground: red, instance.getClassName()); label(instance.objectName)
}
}
}
out << ui;
}
*/
}
@Command
@Usage("return the attributes info of an MBean")
public void attributes(InvocationContext<Map> context, @Argument ObjectName name) throws IOException {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
try {
MBeanInfo info = server.getMBeanInfo(name);
for (MBeanAttributeInfo attributeInfo : info.getAttributes()) {
HashMap<String, Object> tuple = new HashMap<String, Object>();
tuple.put("name", attributeInfo.getName());
tuple.put("type", attributeInfo.getType());
tuple.put("description", attributeInfo.getDescription());
context.provide(tuple);
}
}
catch (JMException e) {
throw new ScriptException("Could not access MBean meta data", e);
}
}
@Usage("get attributes of an MBean")
@Command
public PipeCommand<ObjectName, Map> get(@Argument final List<String> attributes) {
// Determine common attributes from all names
if (attributes == null || attributes.isEmpty()) {
throw new ScriptException("Must provide JMX attributes");
}
//
return new PipeCommand<ObjectName, Map>() {
/** . */
private MBeanServer server;
@Override
public void open() throws ScriptException {
server = ManagementFactory.getPlatformMBeanServer();
}
@Override
public void provide(ObjectName name) throws IOException {
try {
HashMap<String, Object> tuple = new HashMap<String, Object>();
for (String attribute : attributes) {
String prop = name.getKeyProperty(attribute);
if (prop != null) {
tuple.put(attribute, prop);
}
else {
tuple.put(attribute, server.getAttribute(name, attribute));
}
}
context.provide(tuple);
}
catch (JMException ignore) {
//
}
}
};
}
}

View File

@ -0,0 +1,22 @@
welcome = { ->
def hostName;
try {
hostName = java.net.InetAddress.getLocalHost().getHostName();
} catch (java.net.UnknownHostException ignore) {
hostName = "localhost";
}
def version = crash.context.attributes.get("spring.boot.version")
return """\
. ____ _ __ _ _
/\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\
( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\
\\\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v$version) on $hostName
""";
}
prompt = { ->
return "> ";
}

View File

@ -0,0 +1,49 @@
package commands
import org.crsh.text.ui.UIBuilder
import org.springframework.boot.actuate.endpoint.MetricsEndpoint
class metrics {
@Usage("Display metrics provided by Spring Boot")
@Command
public void main(InvocationContext context) {
context.takeAlternateBuffer();
try {
while (!Thread.interrupted()) {
out.cls()
out.show(new UIBuilder().table(columns:[1]) {
header {
table(columns:[1], separator: dashed) {
header(bold: true, fg: black, bg: white) { label("metrics"); }
}
}
row {
table(columns:[1, 1]) {
header(bold: true, fg: black, bg: white) {
label("NAME")
label("VALUE")
}
context.attributes['spring.beanfactory'].getBeansOfType(MetricsEndpoint.class).each { name, metrics ->
metrics.invoke().each { k, v ->
row {
label(k)
label(v)
}
}
}
}
}
}
);
out.flush();
Thread.sleep(1000);
}
}
finally {
context.releaseAlternateBuffer();
}
}
}