diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedCompletionProposal.java b/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedCompletionProposal.java index b8acf307..196e5fb1 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedCompletionProposal.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedCompletionProposal.java @@ -14,34 +14,40 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell; + package com.github.fonimus.ssh.shell; -import lombok.Getter; -import lombok.Setter; -import org.springframework.shell.CompletionProposal; - -/** - * Extended completion proposal to be able to set complete attribute of proposal - */ -public class ExtendedCompletionProposal extends CompletionProposal { - - /** - * If should add space after proposed proposal - */ - @Getter - @Setter - private boolean complete; - - /** - * Default constructor - * - * @param value string value - * @param complete true if should add space after proposed proposal (true is default value when not using - * extended completion proposal) - */ - public ExtendedCompletionProposal(String value, boolean complete) { - super(value); - this.complete = complete; - } - -} + import lombok.Getter; + import lombok.Setter; + import org.springframework.shell.CompletionProposal; + + /** + * Extended completion proposal to allow customization of completion behavior. + */ + public class ExtendedCompletionProposal extends CompletionProposal { + + /** + * The string value of the proposal. + */ + @Getter + private final String value; + + /** + * Indicates if a space should be added after the proposed completion. + */ + @Getter + @Setter + private boolean complete; + + /** + * Default constructor. + * + * @param value string value + * @param complete true if a space should be added after the proposed completion + */ + public ExtendedCompletionProposal(String value, boolean complete) { + super(value); + this.value = value; + this.complete = complete; + } + } + \ No newline at end of file diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedShell.java b/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedShell.java index ab2decb7..2f1b647d 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedShell.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/ExtendedShell.java @@ -14,165 +14,216 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell; + package com.github.fonimus.ssh.shell; -import com.github.fonimus.ssh.shell.postprocess.PostProcessor; -import com.github.fonimus.ssh.shell.postprocess.PostProcessorObject; -import com.github.fonimus.ssh.shell.postprocess.provided.SavePostProcessor; -import lombok.extern.slf4j.Slf4j; -import org.jline.terminal.Terminal; -import org.springframework.context.annotation.Primary; -import org.springframework.shell.*; -import org.springframework.shell.command.CommandCatalog; -import org.springframework.shell.context.ShellContext; -import org.springframework.shell.exit.ExitCodeMappings; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import static com.github.fonimus.ssh.shell.ExtendedInput.*; -import static com.github.fonimus.ssh.shell.SshShellCommandFactory.SSH_THREAD_CONTEXT; - -/** - * Extended shell which takes in account special characters - */ -@Slf4j -@Component -@Primary -public class ExtendedShell extends Shell { - - private final ResultHandlerService resultHandlerService; - private final List postProcessorNames = new ArrayList<>(); - - /** - * Extended shell to handle post processors - * - * @param resultHandlerService result handler service - * @param commandRegistry command registry - * @param terminal terminal - * @param shellContext shell context - * @param exitCodeMappings exit code mappipngs - * @param postProcessors post processors - */ - protected ExtendedShell( - ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, - Terminal terminal, ShellContext shellContext, ExitCodeMappings exitCodeMappings, - List> postProcessors - ) { - super(resultHandlerService, commandRegistry, terminal, shellContext, exitCodeMappings); - this.resultHandlerService = resultHandlerService; - if (postProcessors != null) { - this.postProcessorNames.addAll(postProcessors.stream().map(PostProcessor::getName).toList()); - } - } - - @Override - public void run(InputProvider inputProvider) { - run(inputProvider, () -> false); - } - - /** - * Run shell - * - * @param inputProvider input provider - * @param shellNotifier shell notifier - */ - public void run(InputProvider inputProvider, ShellNotifier shellNotifier) { - Object result = null; - // Handles ExitRequest thrown from Quit command - while (!(result instanceof ExitRequest) && !shellNotifier.shouldStop()) { - Input input; - try { - input = inputProvider.readInput(); - } catch (ExitRequest e) { - // Handles ExitRequest thrown from hitting CTRL-C - break; - } catch (Exception e) { - resultHandlerService.handle(e); - continue; - } - if (input == null) { - break; - } - - result = evaluate(input); - if (result != NO_INPUT && !(result instanceof ExitRequest)) { - resultHandlerService.handle(result); - } - } - } - - @Override - public Object evaluate(Input input) { - List words = input.words(); - Object toReturn = super.evaluate(new ExtendedInput(input)); - SshContext ctx = SSH_THREAD_CONTEXT.get(); - if (ctx != null) { - if (!ctx.isBackground()) { - // clear potential post processors from previous commands - ctx.getPostProcessorsList().clear(); - } - if (isKeyCharInList(words)) { - List indexes = - IntStream.range(0, words.size()).filter(i -> KEY_CHARS.contains(words.get(i))).boxed().toList(); - for (Integer index : indexes) { - if (words.size() > index + 1) { - String keyChar = words.get(index); - if (keyChar.equals(PIPE)) { - String postProcessorName = words.get(index + 1); - int currentIndex = 2; - String word = words.size() > index + currentIndex ? words.get(index + currentIndex) : null; - List params = new ArrayList<>(); - while (word != null && !KEY_CHARS.contains(word)) { - params.add(word); - currentIndex++; - word = words.size() > index + currentIndex ? words.get(index + currentIndex) : null; - } - ctx.getPostProcessorsList().add(new PostProcessorObject(postProcessorName, params)); - } else if (keyChar.equals(ARROW)) { - ctx.getPostProcessorsList().add(new PostProcessorObject(SavePostProcessor.SAVE, - Collections.singletonList(words.get(index + 1)))); - } - } - } - LOGGER.debug("Found {} post processors", ctx.getPostProcessorsList().size()); - } - } - return toReturn; - } - - @Override - public List complete(CompletionContext context) { - if (context.getWords().contains("|")) { - return postProcessorNames.stream().map(CompletionProposal::new).collect(Collectors.toList()); - } - return super.complete(context); - } - - private static boolean isKeyCharInList(List strList) { - for (String key : KEY_CHARS) { - if (strList.contains(key)) { - return true; - } - } - return false; - } - - /** - * Shell notifier interface - */ - @FunctionalInterface - public interface ShellNotifier { - - /** - * Method used to break loop if shell should be stopped - * - * @return if shell should stop or not - */ - boolean shouldStop(); - } -} + import com.github.fonimus.ssh.shell.postprocess.PostProcessor; + import com.github.fonimus.ssh.shell.postprocess.PostProcessorObject; + import com.github.fonimus.ssh.shell.postprocess.provided.SavePostProcessor; + import lombok.extern.slf4j.Slf4j; + import org.jline.terminal.Terminal; + import org.springframework.context.annotation.Primary; + import org.springframework.shell.*; + import org.springframework.shell.command.CommandCatalog; + import org.springframework.shell.context.ShellContext; + import org.springframework.shell.exit.ExitCodeMappings; + import org.springframework.stereotype.Component; + + import java.util.ArrayList; + import java.util.Collections; + import java.util.List; + import java.util.stream.Collectors; + import java.util.stream.IntStream; + + import static com.github.fonimus.ssh.shell.ExtendedInput.*; + import static com.github.fonimus.ssh.shell.SshShellCommandFactory.SSH_THREAD_CONTEXT; + + /** + * Extended shell which takes into account special characters. + */ + @Slf4j + @Component + @Primary + public class ExtendedShell extends Shell { + + private final ResultHandlerService resultHandlerService; + private final List postProcessorNames = new ArrayList<>(); + + /** + * Extended shell to handle post processors. + * + * @param resultHandlerService result handler service + * @param commandRegistry command registry + * @param terminal terminal + * @param shellContext shell context + * @param exitCodeMappings exit code mappings + * @param postProcessors post processors + */ + protected ExtendedShell( + ResultHandlerService resultHandlerService, CommandCatalog commandRegistry, + Terminal terminal, ShellContext shellContext, ExitCodeMappings exitCodeMappings, + List> postProcessors + ) { + super(resultHandlerService, commandRegistry, terminal, shellContext, exitCodeMappings); + this.resultHandlerService = resultHandlerService; + if (postProcessors != null) { + this.postProcessorNames.addAll(postProcessors.stream().map(PostProcessor::getName).toList()); + } + } + + @Override + public void run(InputProvider inputProvider) { + run(inputProvider, () -> false); + } + + /** + * Run shell. + * + * @param inputProvider input provider + * @param shellNotifier shell notifier + */ + public void run(InputProvider inputProvider, ShellNotifier shellNotifier) { + Object result = null; + // Handles ExitRequest thrown from Quit command + while (!(result instanceof ExitRequest) && !shellNotifier.shouldStop()) { + Input input; + try { + input = inputProvider.readInput(); + } catch (ExitRequest e) { + // Handles ExitRequest thrown from hitting CTRL-C + break; + } catch (Exception e) { + resultHandlerService.handle(e); + continue; + } + if (input == null) { + break; + } + + result = evaluate(input); + if (result != NO_INPUT && !(result instanceof ExitRequest)) { + resultHandlerService.handle(result); + } + } + } + + @Override + public Object evaluate(Input input) { + List words = input.words(); + Object toReturn = super.evaluate(new ExtendedInput(input)); + SshContext ctx = SSH_THREAD_CONTEXT.get(); + + if (ctx != null) { + clearPostProcessorsIfNotBackground(ctx); + processKeyCharacters(words, ctx); + } + + return toReturn; + } + + /** + * Clears post processors from previous commands if the context is not in the background. + * + * @param ctx the SSH context + */ + private void clearPostProcessorsIfNotBackground(SshContext ctx) { + if (!ctx.isBackground()) { + ctx.getPostProcessorsList().clear(); + } + } + + /** + * Processes key characters in the input words and updates the post processors. + * + * @param words the input words + * @param ctx the SSH context + */ + private void processKeyCharacters(List words, SshContext ctx) { + if (!isKeyCharInList(words)) { + return; + } + + List indexes = IntStream.range(0, words.size()) + .filter(i -> KEY_CHARS.contains(words.get(i))) + .boxed() + .toList(); + + for (Integer index : indexes) { + if (words.size() > index + 1) { + processKeyCharacter(words, index, ctx); + } + } + + LOGGER.debug("Found {} post processors", ctx.getPostProcessorsList().size()); + } + + /** + * Processes a single key character and updates the post processors in the context. + * + * @param words the input words + * @param index the index of the key character + * @param ctx the SSH context + */ + private void processKeyCharacter(List words, Integer index, SshContext ctx) { + String keyChar = words.get(index); + if (keyChar.equals(PIPE)) { + processPipeKey(words, index, ctx); + } else if (keyChar.equals(ARROW)) { + ctx.getPostProcessorsList().add(new PostProcessorObject(SavePostProcessor.SAVE, + Collections.singletonList(words.get(index + 1)))); + } + } + + /** + * Processes the pipe ('|') key character and updates the post processors in the context. + * + * @param words the input words + * @param index the index of the pipe key + * @param ctx the SSH context + */ + private void processPipeKey(List words, Integer index, SshContext ctx) { + String postProcessorName = words.get(index + 1); + int currentIndex = 2; + String word = words.size() > index + currentIndex ? words.get(index + currentIndex) : null; + List params = new ArrayList<>(); + + while (word != null && !KEY_CHARS.contains(word)) { + params.add(word); + currentIndex++; + word = words.size() > index + currentIndex ? words.get(index + currentIndex) : null; + } + + ctx.getPostProcessorsList().add(new PostProcessorObject(postProcessorName, params)); + } + + @Override + public List complete(CompletionContext context) { + if (context.getWords().contains("|")) { + return postProcessorNames.stream().map(CompletionProposal::new).collect(Collectors.toList()); + } + return super.complete(context); + } + + private static boolean isKeyCharInList(List strList) { + for (String key : KEY_CHARS) { + if (strList.contains(key)) { + return true; + } + } + return false; + } + + /** + * Shell notifier interface. + */ + @FunctionalInterface + public interface ShellNotifier { + + /** + * Method used to break the loop if the shell should be stopped. + * + * @return if the shell should stop or not + */ + boolean shouldStop(); + } + } + diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/ProposalFormatter.java b/starter/src/main/java/com/github/fonimus/ssh/shell/ProposalFormatter.java new file mode 100644 index 00000000..0f900c08 --- /dev/null +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/ProposalFormatter.java @@ -0,0 +1,19 @@ +/* + * Utility class for formatting completion proposals. + */ + + package com.github.fonimus.ssh.shell; + + public class ProposalFormatter { + + /** + * Formats the completion proposal based on the `complete` flag. + * + * @param proposal The extended completion proposal + * @return Formatted proposal value + */ + public static String formatProposal(ExtendedCompletionProposal proposal) { + return proposal.isComplete() ? proposal.getValue() + " " : proposal.getValue(); + } + } + \ No newline at end of file diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/SimpleTable.java b/starter/src/main/java/com/github/fonimus/ssh/shell/SimpleTable.java index 614bf00e..6a2af0f5 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/SimpleTable.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/SimpleTable.java @@ -14,61 +14,120 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell; + package com.github.fonimus.ssh.shell; -import lombok.Builder; -import lombok.Data; -import lombok.NonNull; -import lombok.Singular; -import org.springframework.shell.table.Aligner; -import org.springframework.shell.table.BorderStyle; -import org.springframework.shell.table.TableBuilder; - -import java.util.List; - -/** - * Simple data builder, with header names, and list of lines, containing map with header names. - * Optionally set aligner, and style - */ -@Data -@Builder -public class SimpleTable { - - @Singular - private List columns; - - @Builder.Default - private boolean displayHeaders = true; - - @Singular - private List headerAligners; - - @NonNull - @Singular - private List> lines; - - @Singular - private List lineAligners; - - @Builder.Default - private boolean useFullBorder = true; - - @Builder.Default - private BorderStyle borderStyle = BorderStyle.fancy_light; - - private SimpleTableBuilderListener tableBuilderListener; - - /** - * Listener to add some properties to table builder before it is rendered - */ - @FunctionalInterface - public interface SimpleTableBuilderListener { - - /** - * Method called before render - * - * @param tableBuilder table builder - */ - void onBuilt(TableBuilder tableBuilder); - } -} + import lombok.Builder; + import lombok.Data; + import lombok.NonNull; + import lombok.Singular; + import org.springframework.shell.table.Aligner; + import org.springframework.shell.table.BorderStyle; + import org.springframework.shell.table.TableBuilder; + import org.springframework.shell.table.TableModelBuilder; + + import java.util.ArrayList; + import java.util.List; + + /** + * Simple data builder, with header names, and list of lines, containing map with header names. + * Optionally set aligner, and style. Now includes dynamic methods for better modularization. + */ + @Data + @Builder + public class SimpleTable { + + @Singular + private List columns; + + @Builder.Default + private boolean displayHeaders = true; + + @Singular + private List headerAligners; + + @NonNull + @Singular + private List> lines; + + @Singular + private List lineAligners; + + @Builder.Default + private boolean useFullBorder = true; + + @Builder.Default + private BorderStyle borderStyle = BorderStyle.fancy_light; + + private SimpleTableBuilderListener tableBuilderListener; + + /** + * Adds a row to the table. + * + * @param row A list of objects representing a row. Must match the number of columns. + */ + public void addRow(List row) { + if (columns != null && row.size() != columns.size()) { + throw new IllegalArgumentException("Row size must match the number of columns."); + } + if (lines == null) { + lines = new ArrayList<>(); + } + lines.add(row); + } + + /** + * Renders the table into a string format using Spring Shell's TableBuilder. + * + * @return String representation of the table. + */ + public String render() { + TableModelBuilder modelBuilder = new TableModelBuilder<>(); + + // Add headers if required + if (displayHeaders && columns != null) { + modelBuilder.addRow(); + columns.forEach(modelBuilder::addValue); + } + + // Add rows + if (lines != null) { + for (List line : lines) { + modelBuilder.addRow(); + line.forEach(modelBuilder::addValue); + } + } + + TableBuilder tableBuilder = new TableBuilder(modelBuilder.build()); + tableBuilder.addFullBorder(borderStyle); // Corrected line + + if (tableBuilderListener != null) { + tableBuilderListener.onBuilt(tableBuilder); + } + + return tableBuilder.build().render(80); + } + + /** + * Resets the table by clearing all rows. + */ + public void reset() { + if (lines != null) { + lines.clear(); + } + } + + /** + * Listener to add some properties to table builder before it is rendered. + */ + @FunctionalInterface + public interface SimpleTableBuilderListener { + + /** + * Method called before render + * + * @param tableBuilder table builder + */ + void onBuilt(TableBuilder tableBuilder); + } + } + \ No newline at end of file diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellAutoConfiguration.java b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellAutoConfiguration.java index 6fd98393..a7a29f6b 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellAutoConfiguration.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/SshShellAutoConfiguration.java @@ -14,185 +14,190 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.fonimus.ssh.shell.auth.SshShellAuthenticationProvider; -import com.github.fonimus.ssh.shell.auth.SshShellPasswordAuthenticationProvider; -import com.github.fonimus.ssh.shell.auth.SshShellSecurityAuthenticationProvider; -import com.github.fonimus.ssh.shell.listeners.SshShellListener; -import com.github.fonimus.ssh.shell.listeners.SshShellListenerService; -import com.github.fonimus.ssh.shell.postprocess.provided.*; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.sshd.server.SshServer; -import org.jline.terminal.Terminal; -import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStyle; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationListener; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.shell.boot.LineReaderAutoConfiguration; -import org.springframework.shell.boot.SpringShellAutoConfiguration; -import org.springframework.shell.boot.SpringShellProperties; -import org.springframework.shell.context.InteractionMode; -import org.springframework.shell.context.ShellContext; -import org.springframework.shell.jline.PromptProvider; -import org.springframework.shell.standard.ValueProvider; - -import javax.annotation.PostConstruct; -import java.util.List; - -import static com.github.fonimus.ssh.shell.SshShellProperties.SSH_SHELL_ENABLE; -import static com.github.fonimus.ssh.shell.SshShellProperties.SSH_SHELL_PREFIX; - -/** - *

Ssh shell auto configuration

- *

Can be disabled by property ssh.shell.enable=false

- */ -@Slf4j -@Configuration -@ConditionalOnClass(SshServer.class) -@ConditionalOnProperty(name = SSH_SHELL_ENABLE, havingValue = "true", matchIfMissing = true) -@EnableConfigurationProperties({SshShellProperties.class}) -@AutoConfigureAfter(value = { - SpringShellAutoConfiguration.class, LineReaderAutoConfiguration.class -}, name = { - "org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.jolokia.JolokiaEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration", - "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration" -}) -@ComponentScan(basePackages = {"com.github.fonimus.ssh.shell"}) -@AllArgsConstructor -public class SshShellAutoConfiguration { - - private final ApplicationContext context; - private final SshShellProperties properties; - private final SpringShellProperties springShellProperties; - private final ShellContext shellContext; - - /** - * Initialize ssh shell auto config - */ - @PostConstruct - public void init() { - // override some spring shell properties - springShellProperties.getHistory().setName(properties.getHistoryFile().getAbsolutePath()); - // set interactive mode so that ThrowableResultHandler.showShortError() returns true - shellContext.setInteractionMode(InteractionMode.INTERACTIVE); - } - - @Bean - @ConditionalOnProperty(value = "spring.main.lazy-initialization", havingValue = "true") - public ApplicationListener lazyInitApplicationListener() { - return event -> { - LOGGER.info("Lazy initialization enabled, calling configuration beans explicitly to start ssh server and initialize shell correctly"); - context.getBean(SshShellConfiguration.SshServerLifecycle.class); - context.getBeansOfType(Terminal.class); - context.getBeansOfType(ValueProvider.class); - }; - } - - // post processors - - @Bean - @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") - public JsonPointerPostProcessor jsonPointerPostProcessor(ObjectMapper mapper) { - return new JsonPointerPostProcessor(mapper); - } - - @Bean - @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") - public PrettyJsonPostProcessor prettyJsonPostProcessor(ObjectMapper mapper) { - return new PrettyJsonPostProcessor(mapper); - } - - @Bean - public SavePostProcessor savePostProcessor() { - return new SavePostProcessor(); - } - - @Bean - public GrepPostProcessor grepPostProcessor() { - return new GrepPostProcessor(); - } - - @Bean - public HighlightPostProcessor highlightPostProcessor() { - return new HighlightPostProcessor(); - } - - @Bean - public SshShellHelper sshShellHelper() { - return new SshShellHelper(properties.getConfirmationWords()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = "org.springframework.security.authentication.AuthenticationManager") - @ConditionalOnProperty(value = SSH_SHELL_PREFIX + ".authentication", havingValue = "security") - public SshShellAuthenticationProvider sshShellSecurityAuthenticationProvider() { - return new SshShellSecurityAuthenticationProvider(context, properties.getAuthProviderBeanName()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnProperty(value = SSH_SHELL_PREFIX + ".authentication", havingValue = "simple", matchIfMissing = true) - public SshShellAuthenticationProvider sshShellSimpleAuthenticationProvider() { - return new SshShellPasswordAuthenticationProvider(properties.getUser(), properties.getPassword()); - } - - /** - * Primary prompt provider - * - * @return prompt provider - */ - @Bean - @ConditionalOnMissingBean - public PromptProvider sshPromptProvider() { - return () -> new AttributedString(properties.getPrompt().getText(), - AttributedStyle.DEFAULT.foreground(properties.getPrompt().getColor().toJlineAttributedStyle())); - } - - /** - * Creates ssh listener service - * - * @param listeners found listeners in context - * @return listener service - */ - @Bean - public SshShellListenerService sshShellListenerService(@Autowired(required = false) List listeners) { - return new SshShellListenerService(listeners); - } - -} + package com.github.fonimus.ssh.shell; + + import com.fasterxml.jackson.databind.ObjectMapper; + import com.github.fonimus.ssh.shell.auth.SshShellAuthenticationProvider; + import com.github.fonimus.ssh.shell.auth.SshShellPasswordAuthenticationProvider; + import com.github.fonimus.ssh.shell.auth.SshShellSecurityAuthenticationProvider; + import com.github.fonimus.ssh.shell.listeners.SshShellListener; + import com.github.fonimus.ssh.shell.listeners.SshShellListenerService; + import com.github.fonimus.ssh.shell.postprocess.provided.*; + import lombok.AllArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import org.apache.sshd.server.SshServer; + import org.jline.terminal.Terminal; + import org.jline.utils.AttributedString; + import org.jline.utils.AttributedStyle; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.autoconfigure.AutoConfigureAfter; + import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + import org.springframework.boot.context.properties.EnableConfigurationProperties; + import org.springframework.context.ApplicationContext; + import org.springframework.context.ApplicationListener; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.ComponentScan; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.event.ContextRefreshedEvent; + import org.springframework.shell.boot.LineReaderAutoConfiguration; + import org.springframework.shell.boot.SpringShellAutoConfiguration; + import org.springframework.shell.boot.SpringShellProperties; + import org.springframework.shell.context.InteractionMode; + import org.springframework.shell.context.ShellContext; + import org.springframework.shell.jline.PromptProvider; + import org.springframework.shell.standard.ValueProvider; + + import javax.annotation.PostConstruct; + import java.util.List; + + import static com.github.fonimus.ssh.shell.SshShellProperties.SSH_SHELL_ENABLE; + import static com.github.fonimus.ssh.shell.SshShellProperties.SSH_SHELL_PREFIX; + + /** + *

Ssh shell auto configuration

+ *

Can be disabled by property ssh.shell.enable=false

+ */ + @Slf4j + @Configuration + @ConditionalOnClass(SshServer.class) + @ConditionalOnProperty(name = SSH_SHELL_ENABLE, havingValue = "true", matchIfMissing = true) + @EnableConfigurationProperties({SshShellProperties.class}) + @AutoConfigureAfter(value = { + SpringShellAutoConfiguration.class, LineReaderAutoConfiguration.class + }, name = { + "org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.flyway.FlywayEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.jolokia.JolokiaEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.session.SessionsEndpointAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration" + }) + @ComponentScan(basePackages = {"com.github.fonimus.ssh.shell"}) + @AllArgsConstructor + public class SshShellAutoConfiguration { + + private final ApplicationContext context; + private final SshShellProperties properties; + private final SpringShellProperties springShellProperties; + private final ShellContext shellContext; + + /** + * Initialize ssh shell auto config + */ + @PostConstruct + public void init() { + // override some spring shell properties + springShellProperties.getHistory().setName(properties.getHistoryFile().getAbsolutePath()); + // set interactive mode so that ThrowableResultHandler.showShortError() returns true + shellContext.setInteractionMode(InteractionMode.INTERACTIVE); + } + + @Bean + @ConditionalOnProperty(value = "spring.main.lazy-initialization", havingValue = "true") + public ApplicationListener lazyInitApplicationListener() { + return event -> { + String initMessage = "Lazy initialization enabled"; + String initDetails = "Calling configuration beans explicitly to start SSH server and initialize shell correctly"; + LOGGER.info("{}: {}", initMessage, initDetails); + + // Explicitly call beans to initialize + context.getBean(SshShellConfiguration.SshServerLifecycle.class); + context.getBeansOfType(Terminal.class); + context.getBeansOfType(ValueProvider.class); + }; + } + + // Post processors + + @Bean + @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") + public JsonPointerPostProcessor jsonPointerPostProcessor(ObjectMapper mapper) { + return new JsonPointerPostProcessor(mapper); + } + + @Bean + @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") + public PrettyJsonPostProcessor prettyJsonPostProcessor(ObjectMapper mapper) { + return new PrettyJsonPostProcessor(mapper); + } + + @Bean + public SavePostProcessor savePostProcessor() { + return new SavePostProcessor(); + } + + @Bean + public GrepPostProcessor grepPostProcessor() { + return new GrepPostProcessor(); + } + + @Bean + public HighlightPostProcessor highlightPostProcessor() { + return new HighlightPostProcessor(); + } + + @Bean + public SshShellHelper sshShellHelper() { + return new SshShellHelper(properties.getConfirmationWords()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "org.springframework.security.authentication.AuthenticationManager") + @ConditionalOnProperty(value = SSH_SHELL_PREFIX + ".authentication", havingValue = "security") + public SshShellAuthenticationProvider sshShellSecurityAuthenticationProvider() { + return new SshShellSecurityAuthenticationProvider(context, properties.getAuthProviderBeanName()); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(value = SSH_SHELL_PREFIX + ".authentication", havingValue = "simple", matchIfMissing = true) + public SshShellAuthenticationProvider sshShellSimpleAuthenticationProvider() { + return new SshShellPasswordAuthenticationProvider(properties.getUser(), properties.getPassword()); + } + + /** + * Primary prompt provider + * + * @return prompt provider + */ + @Bean + @ConditionalOnMissingBean + public PromptProvider sshPromptProvider() { + return () -> new AttributedString(properties.getPrompt().getText(), + AttributedStyle.DEFAULT.foreground(properties.getPrompt().getColor().toJlineAttributedStyle())); + } + + /** + * Creates ssh listener service + * + * @param listeners found listeners in context + * @return listener service + */ + @Bean + public SshShellListenerService sshShellListenerService(@Autowired(required = false) List listeners) { + return new SshShellListenerService(listeners); + } + + } + diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellAuthenticationProvider.java b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellAuthenticationProvider.java index 798889af..1240c2d1 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellAuthenticationProvider.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellAuthenticationProvider.java @@ -14,17 +14,25 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell.auth; + package com.github.fonimus.ssh.shell.auth; -import org.apache.sshd.server.auth.password.PasswordAuthenticator; - -/** - * Interface to implements custom authentication provider - */ -@FunctionalInterface -public interface SshShellAuthenticationProvider - extends PasswordAuthenticator { - - String AUTHENTICATION_ATTRIBUTE = "authentication"; - -} + import org.apache.sshd.server.auth.password.PasswordAuthenticator; + + /** + * Interface to implement custom authentication providers. + */ + @FunctionalInterface + public interface SshShellAuthenticationProvider extends PasswordAuthenticator { + + String AUTHENTICATION_ATTRIBUTE = "authentication"; + + /** + * Returns the type of authentication provided by this implementation. + * + * @return the authentication type + */ + default String getAuthenticationType() { + return "Generic"; + } + } + \ No newline at end of file diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellPasswordAuthenticationProvider.java b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellPasswordAuthenticationProvider.java index c5e0c0dd..7b53827d 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellPasswordAuthenticationProvider.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/auth/SshShellPasswordAuthenticationProvider.java @@ -14,41 +14,44 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell.auth; - -import lombok.extern.slf4j.Slf4j; -import org.apache.sshd.server.auth.password.PasswordChangeRequiredException; -import org.apache.sshd.server.session.ServerSession; - -import java.util.UUID; - -/** - * Password implementation - */ -@Slf4j -public class SshShellPasswordAuthenticationProvider - implements SshShellAuthenticationProvider { - - private final String user; - - private final String password; - - public SshShellPasswordAuthenticationProvider(String user, String password) { - this.user = user; - String pass = password; - if (pass == null) { - pass = UUID.randomUUID().toString(); - LOGGER.info(" --- Generating password for ssh connection: {}", pass); - } - this.password = pass; - } - - @Override - public boolean authenticate(String username, String pass, - ServerSession serverSession) throws PasswordChangeRequiredException { - - serverSession.getIoSession().setAttribute(AUTHENTICATION_ATTRIBUTE, new SshAuthentication(username, username)); - - return username.equals(this.user) && pass.equals(this.password); - } -} + package com.github.fonimus.ssh.shell.auth; + + import lombok.extern.slf4j.Slf4j; + import org.apache.sshd.server.auth.password.PasswordChangeRequiredException; + import org.apache.sshd.server.session.ServerSession; + + import java.util.UUID; + + /** + * Password implementation + */ + @Slf4j + public class SshShellPasswordAuthenticationProvider implements SshShellAuthenticationProvider { + + private final String user; + + private final String password; + + public SshShellPasswordAuthenticationProvider(String user, String password) { + this.user = user; + String pass = password; + if (pass == null) { + pass = UUID.randomUUID().toString(); + LOGGER.info(" --- Generating password for ssh connection: {}", pass); + } + this.password = pass; + } + + @Override + public boolean authenticate(String username, String pass, ServerSession serverSession) + throws PasswordChangeRequiredException { + + serverSession.getIoSession().setAttribute(AUTHENTICATION_ATTRIBUTE, new SshAuthentication(username, username)); + return username.equals(this.user) && pass.equals(this.password); + } + + @Override + public String getAuthenticationType() { + return "Password"; + } + } \ No newline at end of file diff --git a/starter/src/main/java/com/github/fonimus/ssh/shell/postprocess/provided/GrepPostProcessor.java b/starter/src/main/java/com/github/fonimus/ssh/shell/postprocess/provided/GrepPostProcessor.java index 73c711d9..d8a92774 100644 --- a/starter/src/main/java/com/github/fonimus/ssh/shell/postprocess/provided/GrepPostProcessor.java +++ b/starter/src/main/java/com/github/fonimus/ssh/shell/postprocess/provided/GrepPostProcessor.java @@ -14,53 +14,55 @@ * limitations under the License. */ -package com.github.fonimus.ssh.shell.postprocess.provided; + package com.github.fonimus.ssh.shell.postprocess.provided; -import com.github.fonimus.ssh.shell.postprocess.PostProcessor; -import lombok.extern.slf4j.Slf4j; - -import java.util.List; - -/** - * Grep post processor - */ -@Slf4j -public class GrepPostProcessor - implements PostProcessor { - - @Override - public String getName() { - return "grep"; - } - - @Override - public String getDescription() { - return "Find pattern in result lines"; - } - - @Override - public String process(String result, List parameters) { - if (parameters == null || parameters.isEmpty()) { - LOGGER.debug("Cannot use [{}] post processor without any parameters", getName()); - return result; - } else { - StringBuilder sb = new StringBuilder(); - for (String line : result.split("\n")) { - if (contains(line, parameters)) { - sb.append(line).append("\n"); - } - } - return sb.toString().isEmpty() ? sb.toString() : sb.substring(0, sb.toString().length() - 1); - } - } - - private boolean contains(String line, List parameters) { - for (String parameter : parameters) { - if (parameter == null || parameter.isEmpty() || line.contains(parameter)) { - return true; - } - } - return false; - } - -} + import com.github.fonimus.ssh.shell.postprocess.PostProcessor; + import lombok.extern.slf4j.Slf4j; + + import java.util.List; + + /** + * Grep post processor + */ + @Slf4j + public class GrepPostProcessor implements PostProcessor { + + @Override + public String getName() { + return "grep"; + } + + @Override + public String getDescription() { + return "Find pattern in result lines"; + } + + @Override + public String process(String result, List parameters) { + if (parameters == null || parameters.isEmpty()) { + LOGGER.debug("Cannot use [{}] post processor without any parameters", getName()); + return result; + } else { + StringBuilder sb = new StringBuilder(); + for (String line : result.split("\n")) { + if (isLineMatching(line, parameters)) { + sb.append(line).append("\n"); + } + } + return sb.toString().isEmpty() ? sb.toString() : sb.substring(0, sb.toString().length() - 1); + } + } + + private boolean isLineMatching(String line, List parameters) { + for (String parameter : parameters) { + if (isParameterInvalid(parameter) || line.contains(parameter)) { + return true; + } + } + return false; + } + + private boolean isParameterInvalid(String parameter) { + return parameter == null || parameter.isEmpty(); + } + } \ No newline at end of file