Skip to content

Display security auto-configuration with fancy unicode (#82740) #83015

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions distribution/tools/ansi-console/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ dependencies {
api "org.fusesource.jansi:jansi:2.3.4"
}

// the code and tests in this project cover console initialization
// which happens before the SecurityManager is installed
tasks.named("test").configure {
systemProperty 'tests.security.manager', 'false'
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,72 @@
*/
package org.elasticsearch.io.ansi;

import org.apache.logging.log4j.Logger;
import org.elasticsearch.bootstrap.ConsoleLoader;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.core.SuppressForbidden;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiConsole;
import org.fusesource.jansi.AnsiPrintStream;
import org.fusesource.jansi.AnsiType;
import org.fusesource.jansi.io.AnsiOutputStream;

import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.function.Supplier;

import static org.apache.logging.log4j.LogManager.getLogger;

/**
* Loads the({@link PrintStream} print stream) from {@link AnsiConsole} and checks whether it meets our requirements for a "Console".
* Loads the {@link AnsiConsole} and checks whether it meets our requirements for a "Console".
* @see org.elasticsearch.bootstrap.ConsoleLoader
*/
public class AnsiConsoleLoader implements Supplier<PrintStream> {
public class AnsiConsoleLoader implements Supplier<ConsoleLoader.Console> {

private static final Logger logger = getLogger(AnsiConsoleLoader.class);

public PrintStream get() {
public ConsoleLoader.Console get() {
final AnsiPrintStream out = AnsiConsole.out();
return newConsole(out);
}

// package-private for tests
static @Nullable ConsoleLoader.Console newConsole(AnsiPrintStream out) {
if (isValidConsole(out)) {
return out;
return new ConsoleLoader.Console(out, () -> out.getTerminalWidth(), Ansi.isEnabled(), tryExtractPrintCharset(out));
} else {
return null;
}
}

static boolean isValidConsole(AnsiPrintStream out) {
private static boolean isValidConsole(AnsiPrintStream out) {
return out != null // cannot load stdout
&& out.getType() != AnsiType.Redirected // output is a pipe (etc)
&& out.getType() != AnsiType.Unsupported // could not determine terminal type
&& out.getTerminalWidth() > 0 // docker, non-terminal logs
;
}

/**
* Uses reflection on the JANSI lib in order to expose the {@code Charset} used to encode the console's print stream.
* The {@code Charset} is not otherwise exposed by the library, and this avoids replicating the charset selection logic in out code.
*/
@SuppressForbidden(reason = "Best effort exposing print stream's charset with reflection")
@Nullable
private static Charset tryExtractPrintCharset(AnsiPrintStream ansiPrintStream) {
try {
Method getOutMethod = ansiPrintStream.getClass().getDeclaredMethod("getOut");
getOutMethod.setAccessible(true);
AnsiOutputStream ansiOutputStream = (AnsiOutputStream) getOutMethod.invoke(ansiPrintStream);
Field charsetField = ansiOutputStream.getClass().getDeclaredField("cs");
charsetField.setAccessible(true);
return (Charset) charsetField.get(ansiOutputStream);
} catch (Throwable t) {
// has the library been upgraded and it now doesn't expose the same fields with the same names?
// is the Security Manager installed preventing the access
logger.info("Failed to detect JANSI's print stream encoding", t);
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.elasticsearch.io.ansi.AnsiConsoleLoader;
import org.elasticsearch.test.ESTestCase;

import java.io.PrintStream;
import java.util.function.Supplier;

import static org.hamcrest.Matchers.instanceOf;
Expand All @@ -20,7 +19,7 @@
public class ConsoleLoaderTests extends ESTestCase {

public void testBuildSupplier() {
final Supplier<PrintStream> supplier = ConsoleLoader.buildConsoleLoader(AnsiConsoleLoader.class.getClassLoader());
final Supplier<ConsoleLoader.Console> supplier = ConsoleLoader.buildConsoleLoader(AnsiConsoleLoader.class.getClassLoader());
assertThat(supplier, notNullValue());
assertThat(supplier, instanceOf(AnsiConsoleLoader.class));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

package org.elasticsearch.io.ansi;

import org.elasticsearch.bootstrap.ConsoleLoader;
import org.elasticsearch.test.ESTestCase;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.AnsiColors;
import org.fusesource.jansi.AnsiMode;
import org.fusesource.jansi.AnsiPrintStream;
Expand All @@ -17,9 +19,12 @@
import org.fusesource.jansi.io.AnsiProcessor;

import java.io.ByteArrayOutputStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

public class AnsiConsoleLoaderTests extends ESTestCase {

Expand All @@ -30,35 +35,68 @@ public class AnsiConsoleLoaderTests extends ESTestCase {

private static final AnsiOutputStream.IoRunnable NO_OP_RUNNABLE = () -> {};

private static final Charset[] charsets = new Charset[] {
StandardCharsets.US_ASCII,
StandardCharsets.ISO_8859_1,
StandardCharsets.UTF_8,
StandardCharsets.UTF_16,
StandardCharsets.UTF_16LE,
StandardCharsets.UTF_16BE };

public void testNullOutputIsNotConsole() {
assertThat(AnsiConsoleLoader.isValidConsole(null), is(false));
assertThat(AnsiConsoleLoader.newConsole(null), nullValue());
}

public void testRedirectedOutputIsNotConsole() {
try (AnsiPrintStream ansiPrintStream = buildStream(AnsiType.Redirected, randomIntBetween(80, 120))) {
assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false));
assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue());
}
}

public void testUnsupportedTerminalIsNotConsole() {
try (AnsiPrintStream ansiPrintStream = buildStream(AnsiType.Unsupported, randomIntBetween(80, 120))) {
assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false));
assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue());
}
}

public void testZeroWidthTerminalIsNotConsole() {
try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), 0)) {
assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(false));
assertThat(AnsiConsoleLoader.newConsole(ansiPrintStream), nullValue());
}
}

public void testStandardTerminalIsConsole() {
int width = randomIntBetween(40, 260);
try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), width)) {
ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream);
assertThat(console, notNullValue());
assertThat(console.width().get(), is(width));
}
}

public void testConsoleCharset() {
Charset charset = randomFrom(charsets);
try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), randomIntBetween(40, 260), charset)) {
ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream);
assertThat(console, notNullValue());
assertThat(console.charset(), is(charset));
}
}

public void testDisableANSI() {
Ansi.setEnabled(false);
try (AnsiPrintStream ansiPrintStream = buildStream(randomFrom(SUPPORTED_TERMINAL_TYPES), randomIntBetween(40, 260))) {
assertThat(AnsiConsoleLoader.isValidConsole(ansiPrintStream), is(true));
ConsoleLoader.Console console = AnsiConsoleLoader.newConsole(ansiPrintStream);
assertThat(console, notNullValue());
assertThat(console.ansiEnabled(), is(false));
}
}

private AnsiPrintStream buildStream(AnsiType type, int width) {
return buildStream(type, width, randomFrom(charsets));
}

private AnsiPrintStream buildStream(AnsiType type, int width, Charset cs) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final AnsiOutputStream ansiOutputStream = new AnsiOutputStream(
baos,
Expand All @@ -67,12 +105,11 @@ private AnsiPrintStream buildStream(AnsiType type, int width) {
new AnsiProcessor(baos),
type,
randomFrom(AnsiColors.values()),
randomFrom(StandardCharsets.UTF_8, StandardCharsets.US_ASCII, StandardCharsets.UTF_16, StandardCharsets.ISO_8859_1),
cs,
NO_OP_RUNNABLE,
NO_OP_RUNNABLE,
randomBoolean()
);
return new AnsiPrintStream(ansiOutputStream, randomBoolean());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ static void init(final boolean foreground, final Path pidFile, final boolean qui
final SecureSettings keystore = BootstrapUtil.loadSecureSettings(initialEnv);
final Environment environment = createEnvironment(pidFile, keystore, initialEnv.settings(), initialEnv.configFile());

BootstrapInfo.setConsoleOutput(getConsole(environment));
BootstrapInfo.setConsole(getConsole(environment));

// the LogConfigurator will replace System.out and System.err with redirects to our logfile, so we need to capture
// the stream objects before calling LogConfigurator to be able to close them when appropriate
Expand Down Expand Up @@ -417,7 +417,7 @@ static void init(final boolean foreground, final Path pidFile, final boolean qui
}
}

private static PrintStream getConsole(Environment environment) {
private static ConsoleLoader.Console getConsole(Environment environment) {
return ConsoleLoader.loadConsole(environment);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.apache.lucene.util.SetOnce;
import org.elasticsearch.core.SuppressForbidden;

import java.io.PrintStream;
import java.util.Dictionary;
import java.util.Enumeration;

Expand All @@ -21,7 +20,7 @@
@SuppressForbidden(reason = "exposes read-only view of system properties")
public final class BootstrapInfo {

private static final SetOnce<PrintStream> consoleOutput = new SetOnce<>();
private static final SetOnce<ConsoleLoader.Console> console = new SetOnce<>();

/** no instantiation */
private BootstrapInfo() {}
Expand Down Expand Up @@ -53,8 +52,8 @@ public static boolean isSystemCallFilterInstalled() {
/**
* Returns a reference to a stream attached to Standard Output, iff we have determined that stdout is a console (tty)
*/
public static PrintStream getConsoleOutput() {
return consoleOutput.get();
public static ConsoleLoader.Console getConsole() {
return console.get();
}

/**
Expand Down Expand Up @@ -123,8 +122,8 @@ public static Dictionary<Object, Object> getSystemProperties() {

public static void init() {}

static void setConsoleOutput(PrintStream output) {
consoleOutput.set(output);
static void setConsole(ConsoleLoader.Console console) {
BootstrapInfo.console.set(console);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

package org.elasticsearch.bootstrap;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.env.Environment;

import java.io.IOException;
Expand All @@ -16,6 +17,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Supplier;
Expand All @@ -28,20 +30,20 @@ public class ConsoleLoader {

private static final String CONSOLE_LOADER_CLASS = "org.elasticsearch.io.ansi.AnsiConsoleLoader";

public static PrintStream loadConsole(Environment env) {
public static Console loadConsole(Environment env) {
final ClassLoader classLoader = buildClassLoader(env);
final Supplier<PrintStream> supplier = buildConsoleLoader(classLoader);
final Supplier<Console> supplier = buildConsoleLoader(classLoader);
return supplier.get();
}

public record Console(PrintStream printStream, Supplier<Integer> width, Boolean ansiEnabled, @Nullable Charset charset) {}

@SuppressWarnings("unchecked")
static Supplier<PrintStream> buildConsoleLoader(ClassLoader classLoader) {
static Supplier<Console> buildConsoleLoader(ClassLoader classLoader) {
try {
final Class<? extends Supplier<PrintStream>> cls = (Class<? extends Supplier<PrintStream>>) classLoader.loadClass(
CONSOLE_LOADER_CLASS
);
final Constructor<? extends Supplier<PrintStream>> constructor = cls.getConstructor();
final Supplier<PrintStream> supplier = constructor.newInstance();
final Class<? extends Supplier<Console>> cls = (Class<? extends Supplier<Console>>) classLoader.loadClass(CONSOLE_LOADER_CLASS);
final Constructor<? extends Supplier<Console>> constructor = cls.getConstructor();
final Supplier<Console> supplier = constructor.newInstance();
return supplier;
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to load ANSI console", e);
Expand Down
Loading