Skip to content

Commit

Permalink
[KARAF-6321] Add configuration options to protect against CTRl-D/shel…
Browse files Browse the repository at this point in the history
…l:logout exits
  • Loading branch information
awrb authored and jbonofre committed Oct 12, 2022
1 parent 0727c4f commit 7cb44e4
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 43 deletions.
7 changes: 7 additions & 0 deletions assemblies/features/standard/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ hostKey = ${karaf.etc}/host.key
#
completionMode = GLOBAL

# If set to true, shell:logout command will not exit Karaf. This can be usfeul to avoid accidental exits.
# You will be able to exit via 'shutdown' or 'halt' instead.
disableLogout = false
# If set to true, it will stop CTRL-D from exiting Karaf. This can be usfeul to avoid accidental exits.
# You will be able to exit via 'shutdown' or 'halt' instead.
disableEofExit = false

#
# Override allowed SSH cipher algorithms.
# Default: aes256-ctr,aes192-ctr,aes128-ctr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.karaf.itests;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.junit.PaxExam;
import org.ops4j.pax.exam.karaf.options.KarafDistributionOption;
import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
import org.ops4j.pax.exam.spi.reactors.PerClass;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;

@RunWith(PaxExam.class)
@ExamReactorStrategy(PerClass.class)
public class DisabledLogoutTest extends BaseTest {

@Configuration
public Option[] config() {
List<Option> options = new ArrayList<>(Arrays.asList(super.config()));
options.add(KarafDistributionOption.editConfigurationFilePut("etc/org.apache.karaf.shell.cfg",
"disableLogout", "true"));
return options.toArray(new Option[options.size()]);
}

@Test
public void testShellLogoutDisabled() {
executeCommand("shell:logout");
// Execute anything at all to verify that we didn't exit from Karaf. If we did, we'd get a runtime exception
assertNotNull(executeAlias("ld"));
}

}
6 changes: 4 additions & 2 deletions manual/src/main/asciidoc/user-guide/console.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ You can create your own aliases in the `etc/shell.init.script` file.
Like on most Unix environments, the Karaf console supports some key bindings:
* the arrows key to navigate in the commands history
* CTRL-D to logout/shutdown Karaf
* CTRL-D to logout/shutdown Karaf. Note: to avoid accidental logouts,
this can be disabled by setting `disableEofExit = true` in `etc/org.apache.karaf.shell.cfg`.
* CTRL-R to search previously executed command
* CTRL-U to remove the current line
Expand Down Expand Up @@ -314,7 +315,8 @@ Karaf console provides some core commands similar to a Unix environment:
* `shell:info` prints various information about the current Karaf instance
* `shell:java` executes a Java application
* `shell:less` file pager
* `shell:logout` disconnects shell from current session
* `shell:logout` disconnects shell from current session. Note: to avoid accidental logouts,
this can be disabled by setting `disableLogout = true` in `etc/org.apache.karaf.shell.cfg`.
* `shell:more` is a file pager
* `shell:new` creates a new Java object
* `shell:printf` formats and prints arguments
Expand Down
10 changes: 10 additions & 0 deletions manual/src/main/asciidoc/user-guide/remote.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ sftpEnabled=true
# You can change the completion mode directly in the shell console, using shell:completion command.
#
completionMode = GLOBAL
# If set to true, shell:logout command will not exit Karaf. This can be usfeul to avoid accidental exits.
# You will be able to exit via 'shutdown' or 'halt' instead.
disableLogout = false
# If set to true, it will stop CTRL-D from exiting Karaf. This can be usfeul to avoid accidental exits.
# You will be able to exit via 'shutdown' or 'halt' instead.
disableEofExit = false
----

The `etc/org.apache.karaf.shell.cfg` configuration file contains different properties to configure the SSHd server:
Expand Down Expand Up @@ -376,6 +383,9 @@ When you are connected to a remote Apache Karaf console, you can logout using:
the Apache Karaf instance (as CTRL-D does when used on a local console).
* using `shell:logout` command (or simply `logout`)

To avoid accidental logouts, one or both of these can be disabled in `etc/org.apache.karaf.shell.cfg`, by setting `disableEofExit = true`
and `disableLogout = true` respectively.

===== Filesystem clients

Apache Karaf SSHd server also provides complete fileystem access via SSH. For security reasons, the available filesystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.apache.karaf.shell.api.action.lifecycle.Reference;
import org.apache.karaf.shell.api.action.lifecycle.Service;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.support.ShellUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -35,9 +36,14 @@ public class LogoutAction implements Action {

@Override
public Object execute() throws Exception {
log.info("Disconnecting from current session...");
session.close();
boolean disableLogout = ShellUtil.loadPropertyFromShellCfg("disableLogout", Boolean::parseBoolean, false);
if (disableLogout) {
log.info("shell:logout disabled in org.apache.karaf.shell.cfg.");
} else {
log.info("Disconnecting from current session...");
session.close();
}

return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public interface Session extends Runnable, Closeable {
String IGNORE_INTERRUPTS = "karaf.ignoreInterrupts";
String IS_LOCAL = "karaf.shell.local";
String COMPLETION_MODE = "karaf.completionMode";
String DISABLE_EOF_EXIT = "karaf.disableEofExit";
String DISABLE_LOGOUT = "karaf.disableLogout";

String COMPLETION_MODE_GLOBAL = "global";
String COMPLETION_MODE_SUBSHELL = "subshell";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package org.apache.karaf.shell.impl.console;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
Expand Down Expand Up @@ -77,37 +76,31 @@

public class ConsoleSessionImpl implements Session {

private static final String SUPPRESS_WELCOME = "karaf.shell.suppress.welcome";
public static final String SHELL_INIT_SCRIPT = "karaf.shell.init.script";
public static final String SHELL_HISTORY_MAXSIZE = "karaf.shell.history.maxSize";
public static final String SHELL_HISTORY_FILE_MAXSIZE = "karaf.shell.history.file.maxSize";
public static final String PROMPT = "PROMPT";
public static final String DEFAULT_PROMPT = "\u001B[1m${USER}\u001B[0m@${APPLICATION}(${SUBSHELL})> ";
public static final String RPROMPT = "RPROMPT";
public static final String DEFAULT_RPROMPT = null;

private static final String SUPPRESS_WELCOME = "karaf.shell.suppress.welcome";
private static final Logger LOGGER = LoggerFactory.getLogger(ConsoleSessionImpl.class);

// Input stream
volatile boolean running;

private AtomicBoolean closed = new AtomicBoolean(false);

final SessionFactory factory;
final ThreadIO threadIO;
final InputStream in;
final PrintStream out;
final PrintStream err;
private Runnable closeCallback;

final CommandSession session;
final Registry registry;
final Terminal terminal;
final org.jline.terminal.Terminal jlineTerminal;
final History history;
final LineReader reader;
final AggregateMaskingCallback maskingCallback;

// Input stream
volatile boolean running;
private AtomicBoolean closed = new AtomicBoolean(false);
private Runnable closeCallback;
private Thread thread;
private Properties brandingProps;

Expand Down Expand Up @@ -164,7 +157,7 @@ public ConsoleSessionImpl(SessionFactory factory,
// Completers
Completer builtinCompleter = createBuiltinCompleter();
CommandsCompleter commandsCompleter = new CommandsCompleter(factory, this);
Completer completer = (rdr, line, candidates) -> {
Completer completer = (rdr, line, candidates) -> {
builtinCompleter.complete(rdr, line, candidates);
commandsCompleter.complete(rdr, line, candidates);
merge(candidates);
Expand All @@ -175,13 +168,13 @@ public ConsoleSessionImpl(SessionFactory factory,

// Console reader
reader = LineReaderBuilder.builder()
.terminal(jlineTerminal)
.appName("karaf")
.variables(((CommandSessionImpl) session).getVariables())
.highlighter(new org.apache.felix.gogo.jline.Highlighter(session))
.parser(new KarafParser(this))
.completer(completer)
.build();
.terminal(jlineTerminal)
.appName("karaf")
.variables(((CommandSessionImpl) session).getVariables())
.highlighter(new org.apache.felix.gogo.jline.Highlighter(session))
.parser(new KarafParser(this))
.completer(completer)
.build();

// History
final Path file = getHistoryFile();
Expand Down Expand Up @@ -224,6 +217,7 @@ public ConsoleSessionImpl(SessionFactory factory,
session.put(Session.SCOPE, "shell:bundle:*");
session.put(Session.SUBSHELL, "");
session.put(Session.COMPLETION_MODE, loadCompletionMode());
session.put(Session.DISABLE_EOF_EXIT, loadDisableEofExit());
session.put("USER", ShellUtil.getCurrentUserName());
session.put("TERM", terminal.getType());
session.put("APPLICATION", System.getProperty("karaf.name", "root"));
Expand Down Expand Up @@ -254,12 +248,14 @@ private Completer createBuiltinCompleter() {
public Map<String, List<Completers.CompletionData>> getCompletions() {
return Shell.getCompletions(session);
}

@Override
public Set<String> getCommands() {
return factory.getRegistry().getCommands().stream()
.map(c -> c.getScope() + ":" + c.getName())
.collect(Collectors.toSet());
}

@Override
public String resolveCommand(String command) {
String resolved = command;
Expand All @@ -279,11 +275,13 @@ public String resolveCommand(String command) {
}
return resolved;
}

@Override
public String commandName(String command) {
int idx = command.indexOf(':');
return idx >= 0 ? command.substring(idx + 1) : command;
}

@Override
public Object evaluate(LineReader reader, ParsedLine line, String func) throws Exception {
session.put(Shell.VAR_COMMAND_LINE, line);
Expand Down Expand Up @@ -424,7 +422,7 @@ public void run() {

/**
* On the local console we only show the welcome banner once. This allows to suppress the banner
* on refreshs of the shell core bundle.
* on refreshs of the shell core bundle.
* On ssh we show it every time.
*/
private void welcomeBanner() {
Expand All @@ -438,7 +436,7 @@ private void welcomeBanner() {
}

private boolean isLocal() {
Boolean isLocal = (Boolean)session.get(Session.IS_LOCAL);
Boolean isLocal = (Boolean) session.get(Session.IS_LOCAL);
return isLocal != null && isLocal;
}

Expand All @@ -456,7 +454,12 @@ private CharSequence readCommand(AtomicBoolean reading) throws UserInterruptExce
command = reader.getBuffer().toString();
}
} catch (EndOfFileException e) {
command = null;
boolean disableEofExit = (boolean) session.get(Session.DISABLE_EOF_EXIT);
if (disableEofExit) {
command = "";
} else {
command = null;
}
} catch (UserInterruptException e) {
command = ""; // Do nothing
} catch (Throwable t) {
Expand Down Expand Up @@ -553,21 +556,12 @@ public String readLine(String prompt, Character mask) throws IOException {
}

private String loadCompletionMode() {
String mode;
try {
File shellCfg = new File(System.getProperty("karaf.etc"), "/org.apache.karaf.shell.cfg");
Properties properties = new Properties();
properties.load(new FileInputStream(shellCfg));
mode = (String) properties.get("completionMode");
if (mode == null) {
LOGGER.debug("completionMode property is not defined in etc/org.apache.karaf.shell.cfg file. Using default completion mode.");
mode = Session.COMPLETION_MODE_GLOBAL;
}
} catch (Exception e) {
LOGGER.warn("Can't read {}/org.apache.karaf.shell.cfg file. The completion is set to default.", System.getProperty("karaf.etc"));
mode = Session.COMPLETION_MODE_GLOBAL;
}
return mode;
return ShellUtil.loadPropertyFromShellCfg("completionMode", java.util.function.Function.identity(),
Session.COMPLETION_MODE_GLOBAL);
}

private boolean loadDisableEofExit() {
return ShellUtil.loadPropertyFromShellCfg("disableEofExit", Boolean::parseBoolean, false);
}

private void executeScript(String names) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,18 @@
*/
package org.apache.karaf.shell.support;

import java.io.File;
import java.io.FileInputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Paths;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.security.auth.Subject;
Expand Down Expand Up @@ -243,4 +248,22 @@ public static Map<String, String> getColorMap(Session session, String name, Stri
s -> s.substring(s.indexOf('=') + 1)));
}

public static <T> T loadPropertyFromShellCfg(String key, Function<String, T> parser, T defaultValue) {
File shellCfg = Paths.get(System.getProperty("karaf.etc"), "org.apache.karaf.shell.cfg").toFile();
try (FileInputStream fis = new FileInputStream(shellCfg)) {
Properties properties = new Properties();
properties.load(fis);

String value = (String) properties.get(key);
if (value != null) {
return parser.apply(value);
} else {
LOGGER.debug("{} property is not defined in etc/org.apache.karaf.shell.cfg file. Using default value {}.", key, defaultValue);
}
} catch (Exception e) {
LOGGER.warn("Can't read {}/org.apache.karaf.shell.cfg file. The {} is set to default.", key, System.getProperty("karaf.etc"));
}

return defaultValue;
}
}

0 comments on commit 7cb44e4

Please sign in to comment.