You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by jb...@apache.org on 2022/10/12 09:57:37 UTC

[karaf] branch karaf-4.3.x updated: [KARAF-6321] Add configuration options to protect against CTRl-D/shell:logout exits

This is an automated email from the ASF dual-hosted git repository.

jbonofre pushed a commit to branch karaf-4.3.x
in repository https://gitbox.apache.org/repos/asf/karaf.git


The following commit(s) were added to refs/heads/karaf-4.3.x by this push:
     new 966138ca10 [KARAF-6321] Add configuration options to protect against CTRl-D/shell:logout exits
966138ca10 is described below

commit 966138ca10ab125f61a588c2bfeab53a494c254d
Author: Aleksy Wróblewski <al...@bbbit.io>
AuthorDate: Sat Sep 24 23:20:22 2022 +0200

    [KARAF-6321] Add configuration options to protect against CTRl-D/shell:logout exits
    
    (cherry picked from commit 7cb44e4e5447440ec99d94a9e281d59dd74dbeec)
---
 .../features/standard/src/main/feature/feature.xml |  7 +++
 .../apache/karaf/itests/DisabledLogoutTest.java    | 54 +++++++++++++++++
 manual/src/main/asciidoc/user-guide/console.adoc   |  6 +-
 manual/src/main/asciidoc/user-guide/remote.adoc    | 10 ++++
 .../karaf/shell/commands/impl/LogoutAction.java    | 12 +++-
 .../apache/karaf/shell/api/console/Session.java    |  2 +
 .../shell/impl/console/ConsoleSessionImpl.java     | 70 ++++++++++------------
 .../org/apache/karaf/shell/support/ShellUtil.java  | 23 +++++++
 8 files changed, 141 insertions(+), 43 deletions(-)

diff --git a/assemblies/features/standard/src/main/feature/feature.xml b/assemblies/features/standard/src/main/feature/feature.xml
index 29903a9570..243becf8b0 100644
--- a/assemblies/features/standard/src/main/feature/feature.xml
+++ b/assemblies/features/standard/src/main/feature/feature.xml
@@ -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
diff --git a/itests/test/src/test/java/org/apache/karaf/itests/DisabledLogoutTest.java b/itests/test/src/test/java/org/apache/karaf/itests/DisabledLogoutTest.java
new file mode 100644
index 0000000000..b6b9241292
--- /dev/null
+++ b/itests/test/src/test/java/org/apache/karaf/itests/DisabledLogoutTest.java
@@ -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"));
+    }
+
+}
diff --git a/manual/src/main/asciidoc/user-guide/console.adoc b/manual/src/main/asciidoc/user-guide/console.adoc
index 08fe392ced..25fac30700 100644
--- a/manual/src/main/asciidoc/user-guide/console.adoc
+++ b/manual/src/main/asciidoc/user-guide/console.adoc
@@ -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
 
@@ -316,7 +317,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
diff --git a/manual/src/main/asciidoc/user-guide/remote.adoc b/manual/src/main/asciidoc/user-guide/remote.adoc
index 3fb047519a..9e75e8eee8 100644
--- a/manual/src/main/asciidoc/user-guide/remote.adoc
+++ b/manual/src/main/asciidoc/user-guide/remote.adoc
@@ -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:
@@ -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
diff --git a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LogoutAction.java b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LogoutAction.java
index 365422563d..f4e0260a65 100644
--- a/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LogoutAction.java
+++ b/shell/commands/src/main/java/org/apache/karaf/shell/commands/impl/LogoutAction.java
@@ -21,6 +21,7 @@ import org.apache.karaf.shell.api.action.Command;
 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;
 
@@ -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;
     }
-
 }
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/console/Session.java b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Session.java
index 05930e0faf..ea24aa862f 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/api/console/Session.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/console/Session.java
@@ -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";
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
index ba7b941864..53af4d2cfe 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
@@ -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;
@@ -77,7 +76,6 @@ import static org.apache.felix.gogo.jline.Shell.VAR_SCOPE;
 
 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";
@@ -85,21 +83,13 @@ public class ConsoleSessionImpl implements Session {
     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;
@@ -107,7 +97,10 @@ public class ConsoleSessionImpl implements Session {
     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;
 
@@ -164,7 +157,7 @@ public class ConsoleSessionImpl implements Session {
         // 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);
@@ -175,13 +168,13 @@ public class ConsoleSessionImpl implements Session {
 
         // 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();
@@ -224,6 +217,7 @@ public class ConsoleSessionImpl implements Session {
         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"));
@@ -254,12 +248,14 @@ public class ConsoleSessionImpl implements Session {
             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;
@@ -279,11 +275,13 @@ public class ConsoleSessionImpl implements Session {
                 }
                 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);
@@ -424,7 +422,7 @@ public class ConsoleSessionImpl implements Session {
 
     /**
      * 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() {
@@ -438,7 +436,7 @@ public class ConsoleSessionImpl implements Session {
     }
 
     private boolean isLocal() {
-        Boolean isLocal = (Boolean)session.get(Session.IS_LOCAL);
+        Boolean isLocal = (Boolean) session.get(Session.IS_LOCAL);
         return isLocal != null && isLocal;
     }
 
@@ -456,7 +454,12 @@ public class ConsoleSessionImpl implements Session {
                 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) {
@@ -553,21 +556,12 @@ public class ConsoleSessionImpl implements Session {
     }
 
     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) {
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/support/ShellUtil.java b/shell/core/src/main/java/org/apache/karaf/shell/support/ShellUtil.java
index 5f5ef154c4..de871baf02 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/support/ShellUtil.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/support/ShellUtil.java
@@ -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;
@@ -243,4 +248,22 @@ public class ShellUtil {
                         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;
+    }
 }