From 5514a9a5a1fbf0204927300a5af73c251cdf3799 Mon Sep 17 00:00:00 2001 From: Alexander Sulfrian Date: Sat, 9 Mar 2019 07:00:04 +0100 Subject: Add first version of the applet launcher --- .gitignore | 8 ++ build.gradle | 24 ++++ settings.gradle | 5 + src/main/java/de/spline/kvm/Launcher.java | 88 +++++++++++++++ src/main/java/de/spline/kvm/StandaloneApplet.java | 121 +++++++++++++++++++++ .../de/spline/kvm/events/EventMulticaster.java | 29 +++++ .../java/de/spline/kvm/events/LifetimeAdapter.java | 9 ++ .../de/spline/kvm/events/LifetimeListener.java | 8 ++ src/main/java/de/spline/kvm/utils/Kvm.java | 61 +++++++++++ .../java/de/spline/kvm/utils/ReflectionUtils.java | 84 ++++++++++++++ src/main/java/de/spline/kvm/utils/SSLUtils.java | 47 ++++++++ src/main/java/nn/pp/rc/JSSE14_SSLConnector.java | 26 +++++ src/main/java/nn/pp/rc/u.java | 55 ++++++++++ src/main/resources/dfn-g2.jks | Bin 0 -> 1526 bytes 14 files changed, 565 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 settings.gradle create mode 100644 src/main/java/de/spline/kvm/Launcher.java create mode 100644 src/main/java/de/spline/kvm/StandaloneApplet.java create mode 100644 src/main/java/de/spline/kvm/events/EventMulticaster.java create mode 100644 src/main/java/de/spline/kvm/events/LifetimeAdapter.java create mode 100644 src/main/java/de/spline/kvm/events/LifetimeListener.java create mode 100644 src/main/java/de/spline/kvm/utils/Kvm.java create mode 100644 src/main/java/de/spline/kvm/utils/ReflectionUtils.java create mode 100644 src/main/java/de/spline/kvm/utils/SSLUtils.java create mode 100644 src/main/java/nn/pp/rc/JSSE14_SSLConnector.java create mode 100644 src/main/java/nn/pp/rc/u.java create mode 100644 src/main/resources/dfn-g2.jks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8e8291 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# JetBrains IntelliJ IDEA +.idea/ +out/ +*.iml + +# Gradle +.gradle/ +build/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9719996 --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +repositories { + mavenCentral() +} + +dependencies { + compile 'com.gistlabs:mechanize:0.13.1' + compile fileTree('lib') { include '*.jar' } +} + +jar { + manifest { + attributes 'Main-Class': 'de.spline.kvm.Launcher' + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..7614b58 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'kvm' diff --git a/src/main/java/de/spline/kvm/Launcher.java b/src/main/java/de/spline/kvm/Launcher.java new file mode 100644 index 0000000..d27ed95 --- /dev/null +++ b/src/main/java/de/spline/kvm/Launcher.java @@ -0,0 +1,88 @@ +package de.spline.kvm; + +import javax.net.ssl.HttpsURLConnection; +import java.io.Console; +import java.util.Map; + +import de.spline.kvm.events.LifetimeAdapter; +import de.spline.kvm.utils.Kvm; +import de.spline.kvm.utils.SSLUtils; + +public class Launcher +{ + private static final String DEFAULT_USERNAME = "super"; + + private String host; + + public Launcher(String host) + { + this.host = host; + } + + public void run() + { + HttpsURLConnection.setDefaultSSLSocketFactory(SSLUtils.getSocketFactory()); + Kvm kvm = new Kvm("https://" + host); + + Console console = System.console(); + if (console == null) { + System.err.println("Unable to access the console to query login data."); + System.exit(1); + } + + String username = readUsername(console); + String password = readPassword(console, username); + kvm.login(username, password); + + Map params = kvm.getAppletParameters(); + StandaloneApplet applet = new StandaloneApplet(host, params); + + applet.addLifetimeListener(new LifetimeAdapter() + { + @Override + public void appletStopped() + { + kvm.logout(); + } + }); + + applet.init(); + applet.start(); + } + + public static void main(String[] args) + { + String host = "kvm.spline.inf.fu-berlin.de"; + if (args.length > 0) { + host = args[0]; + } + + Launcher launcher = new Launcher(host); + launcher.run(); + } + + private String readUsername(Console console) + { + String input = console.readLine("Username for %s [%s]: ", host, DEFAULT_USERNAME); + + if (input == null) { + return null; + } + + if (input.isEmpty()) { + return DEFAULT_USERNAME; + } + + return input; + } + + private String readPassword(Console console, String username) + { + char[] password = console.readPassword("Password for %s@%s: ", username, host); + if (password == null) { + return null; + } + + return String.valueOf(password); + } +} \ No newline at end of file diff --git a/src/main/java/de/spline/kvm/StandaloneApplet.java b/src/main/java/de/spline/kvm/StandaloneApplet.java new file mode 100644 index 0000000..cf8a5d5 --- /dev/null +++ b/src/main/java/de/spline/kvm/StandaloneApplet.java @@ -0,0 +1,121 @@ +package de.spline.kvm; + +import java.awt.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Locale; +import java.util.Map; + +import de.spline.kvm.events.EventMulticaster; +import de.spline.kvm.events.LifetimeListener; +import de.spline.kvm.utils.ReflectionUtils; +import nn.pp.rc.RemoteConsoleApplet; +import nn.pp.rc.ServerConsolePanelBase; + +/** + * This is a class, to run an applet without an browser. It just overrides the + * necessary methods to run the applet. The applet "window" displayed in tbe + * browser is not drawn, but the interesting stuff (aka. the remote console) + * is in a separate Frame. + */ +public class StandaloneApplet extends RemoteConsoleApplet +{ + protected String host; + protected Map parameters; + + protected LifetimeListener lifetimeListener = null; + + public StandaloneApplet(String host, Map parameters) + { + this.host = host; + this.parameters = parameters; + } + + /** + * Add a listener for the lifetime events of the applet. + * + * @param listener Listener instance to add + */ + public void addLifetimeListener(LifetimeListener listener) + { + lifetimeListener = EventMulticaster.add(listener, lifetimeListener); + } + + /** + * Remove a listener for the lifetime events of the applet. + * + * @param listener Listener instance to remove + */ + public void removeLifetimeListener(LifetimeListener listener) + { + lifetimeListener = EventMulticaster.remove(listener, lifetimeListener); + } + + @Override + public Image getImage(URL url) + { + return getToolkit().getImage(url); + } + + @Override + public URL getCodeBase() + { + URL url = null; + + try { + url = new URL("https", host, 443, ""); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + + return url; + } + + @Override + public URL getDocumentBase() + { + return getCodeBase(); + } + + @Override + public Locale getLocale() + { + return Locale.getDefault(); + } + + /** + * This are the parameters of the applet. The applet usually get the values + * from html tags inside the applet tag. We have parsed this tags into a + * map and simply return these values here. + * + * @param name Name of the parameter to get + * @return Value of the parameter + */ + @Override + public String getParameter(String name) + { + return parameters.get(name.toUpperCase()); + } + + @Override + public void init() + { + super.init(); + + // The initial label is removed during init and this is causing + // NullPointerExceptions when the popup should be displayed, so we + // simply add the label again. + ServerConsolePanelBase consolePanel = ReflectionUtils.getConsolePanel(this); + PopupMenu menu = ReflectionUtils.getOptionMenu(consolePanel); + menu.setLabel("Options"); + } + + @Override + public void stop() + { + super.stop(); + destroy(); + + lifetimeListener.appletStopped(); + } +} diff --git a/src/main/java/de/spline/kvm/events/EventMulticaster.java b/src/main/java/de/spline/kvm/events/EventMulticaster.java new file mode 100644 index 0000000..7f216bd --- /dev/null +++ b/src/main/java/de/spline/kvm/events/EventMulticaster.java @@ -0,0 +1,29 @@ +package de.spline.kvm.events; + +import java.awt.AWTEventMulticaster; +import java.util.EventListener; + +public class EventMulticaster extends AWTEventMulticaster implements LifetimeListener +{ + public EventMulticaster(EventListener a, EventListener b) + { + super(a, b); + } + + @Override + public void appletStopped() + { + ((LifetimeListener) a).appletStopped(); + ((LifetimeListener) b).appletStopped(); + } + + public static LifetimeListener add(LifetimeListener a, LifetimeListener b) + { + return (LifetimeListener) AWTEventMulticaster.addInternal(a, b); + } + + public static LifetimeListener remove(LifetimeListener a, LifetimeListener b) + { + return (LifetimeListener) AWTEventMulticaster.removeInternal(a, b); + } +} diff --git a/src/main/java/de/spline/kvm/events/LifetimeAdapter.java b/src/main/java/de/spline/kvm/events/LifetimeAdapter.java new file mode 100644 index 0000000..2fc2618 --- /dev/null +++ b/src/main/java/de/spline/kvm/events/LifetimeAdapter.java @@ -0,0 +1,9 @@ +package de.spline.kvm.events; + +public abstract class LifetimeAdapter implements LifetimeListener +{ + @Override + public void appletStopped() + { + } +} diff --git a/src/main/java/de/spline/kvm/events/LifetimeListener.java b/src/main/java/de/spline/kvm/events/LifetimeListener.java new file mode 100644 index 0000000..77c89d8 --- /dev/null +++ b/src/main/java/de/spline/kvm/events/LifetimeListener.java @@ -0,0 +1,8 @@ +package de.spline.kvm.events; + +import java.util.EventListener; + +public interface LifetimeListener extends EventListener +{ + void appletStopped(); +} diff --git a/src/main/java/de/spline/kvm/utils/Kvm.java b/src/main/java/de/spline/kvm/utils/Kvm.java new file mode 100644 index 0000000..896150a --- /dev/null +++ b/src/main/java/de/spline/kvm/utils/Kvm.java @@ -0,0 +1,61 @@ +package de.spline.kvm.utils; + +import java.util.HashMap; +import java.util.Map; + +import com.gistlabs.mechanize.MechanizeAgent; +import com.gistlabs.mechanize.Resource; +import com.gistlabs.mechanize.document.Document; +import com.gistlabs.mechanize.document.node.Node; +import com.gistlabs.mechanize.parameters.Parameters; + +public class Kvm +{ + protected String host; + protected MechanizeAgent agent; + + public Kvm(String host) + { + this.host = host; + this.agent = new MechanizeAgent(); + } + + public void login(String username, String password) + { + Parameters param = new Parameters(); + param.add("login", username); + param.add("password", password); + param.add("action_login", "Login"); + + Resource resource = agent.post(host + "/auth.asp", param); + if (resource.asString().contains("Authentication failed.")) { + System.err.println("Invalid login!"); + System.exit(1); + } + } + + public Map getAppletParameters() + { + Document doc = agent.get(host + "/title_app.asp"); + if (!doc.asString().contains(" params = new HashMap<>(); + for (Node node : doc.getRoot().findAll("html body applet param")) { + String name = node.getAttribute("name"); + String value = node.getAttribute("value"); + params.put(name.toUpperCase(), value); + } + + return params; + } + + public void logout() + { + agent.get(host + "/logout"); + } +} diff --git a/src/main/java/de/spline/kvm/utils/ReflectionUtils.java b/src/main/java/de/spline/kvm/utils/ReflectionUtils.java new file mode 100644 index 0000000..03f3a9f --- /dev/null +++ b/src/main/java/de/spline/kvm/utils/ReflectionUtils.java @@ -0,0 +1,84 @@ +package de.spline.kvm.utils; + +import java.awt.PopupMenu; +import java.lang.reflect.Field; + +import nn.pp.rc.RemoteConsoleApplet; +import nn.pp.rc.ServerConsolePanelBase; + +public class ReflectionUtils +{ + /** + * Set a field of an object with reflection. This ignores all access + * permissions (f.e. private) and accesses the field by name (so it works + * for strange field names). + * + * @param cls + * @param instance + * @param field + * @param value + */ + public static void setField(Class cls, Object instance, String field, Object value) + { + try { + Field f = cls.getDeclaredField(field); + f.setAccessible(true); + f.set(instance, value); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + /** + * Get a field of an object with reflection. This ignores all access + * permissions (f.e. private) and accesses the field by name (so it works + * for strange field names). + * + * @param cls + * @param instance + * @param field + * @return + */ + public static Object getField(Class cls, Object instance, String field) + { + try { + Field f = cls.getDeclaredField(field); + f.setAccessible(true); + return f.get(instance); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + + return null; + } + + /** + * Get the ServerConsolePanelBase field from the applet instance. We need + * to use reflection here, because the obfuscation has produced strange + * field names and we cannot access it using normal methods. + * + * @param applet RemoteConsoleApplet instance + * @return ServerConsolePanelBase instance of this applet + */ + public static ServerConsolePanelBase getConsolePanel(RemoteConsoleApplet applet) + { + return (ServerConsolePanelBase) ReflectionUtils.getField(RemoteConsoleApplet.class, applet, "else"); + } + + /** + * Get the popup option menu from a ServerConsolePanelBase instance. The + * field name is okay-ish here, but we also need to use reflection here + * because this field in private. + * + * (We use this option menu to fix a silly bug: The menu is not displayed, + * because the "label" is removed after initialization and this is causing + * NullPointerExceptions.) + * + * @param consolePanel ServerConsolePanelBase instance + * @return options menu + */ + public static PopupMenu getOptionMenu(ServerConsolePanelBase consolePanel) + { + return (PopupMenu)ReflectionUtils.getField(ServerConsolePanelBase.class, consolePanel, "G"); + } +} diff --git a/src/main/java/de/spline/kvm/utils/SSLUtils.java b/src/main/java/de/spline/kvm/utils/SSLUtils.java new file mode 100644 index 0000000..75bb1dd --- /dev/null +++ b/src/main/java/de/spline/kvm/utils/SSLUtils.java @@ -0,0 +1,47 @@ +package de.spline.kvm.utils; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +public class SSLUtils +{ + public static SSLSocketFactory getSocketFactory() + { + SSLContext ctx = null; + + try { + ctx = SSLContext.getInstance("TLS"); + ctx.init(null, getTrustManagers(), null); + } + catch (GeneralSecurityException | IOException e) { + e.printStackTrace(); + System.exit(1); + } + + return ctx.getSocketFactory(); + } + + private static TrustManager[] getTrustManagers() + throws GeneralSecurityException, IOException + { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(loadKeyStore()); + return trustManagerFactory.getTrustManagers(); + } + + private static KeyStore loadKeyStore() + throws GeneralSecurityException, IOException + { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream in = ClassLoader.getSystemResourceAsStream("dfn-g2.jks"); + keyStore.load(in, "changeit".toCharArray()); + return keyStore; + } +} diff --git a/src/main/java/nn/pp/rc/JSSE14_SSLConnector.java b/src/main/java/nn/pp/rc/JSSE14_SSLConnector.java new file mode 100644 index 0000000..487b1b9 --- /dev/null +++ b/src/main/java/nn/pp/rc/JSSE14_SSLConnector.java @@ -0,0 +1,26 @@ +package nn.pp.rc; + +import javax.net.ssl.SSLSocketFactory; + +import de.spline.kvm.utils.ReflectionUtils; +import de.spline.kvm.utils.SSLUtils; + +/** + * We replace the JSSE14_SSLConnector with our own version. + * + * The default class from the applet uses a broken (outdated?) + * X509PluginTrustManager. So we simply use your own SSLSocketFactory here, + * that already has a correct setup for TLS connections of the remote console. + */ +public class JSSE14_SSLConnector extends aq +{ + public JSSE14_SSLConnector() { + // Just visual stuff to beautify the display name of this connector + ReflectionUtils.setField(aq.class, this, "do", "TLS"); + } + + public SSLSocketFactory a() + { + return SSLUtils.getSocketFactory(); + } +} diff --git a/src/main/java/nn/pp/rc/u.java b/src/main/java/nn/pp/rc/u.java new file mode 100644 index 0000000..c5c08d4 --- /dev/null +++ b/src/main/java/nn/pp/rc/u.java @@ -0,0 +1,55 @@ +package nn.pp.rc; + +import java.applet.Applet; +import java.awt.Frame; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +/** + * This is a simple class to override the nn.pp.rc.u class from the applet. + * + * We need to do this, because the original class is doing some nasty things in + * the processWindowEvent method. This class has the same name (and the same + * package) and is somewhere before the applet in the classpath. So we cannot + * import the original class here, but the class is so simple, that we can + * implement it from scratch. + */ +public class u extends Frame +{ + ServerConsolePanelBase consolePanel; + + public u(Applet applet, String title, ServerConsolePanelBase consolePanel) + { + super(title + " Remote Console"); + this.consolePanel = consolePanel; + this.add(consolePanel, "Center"); + + this.addWindowListener(new WindowAdapter() + { + @Override + public void windowClosing(WindowEvent windowEvent) + { + applet.stop(); + } + }); + } + + /** + * Close this frame. + */ + public void a() + { + this.setVisible(false); + this.setState(0); + this.dispose(); + + // "Close" the console panel + consolePanel.r(); + } + + public void a(String path, String target) + { + // Nothing here, because we do not need the redirection to another page + // when the applet exists. + } +} diff --git a/src/main/resources/dfn-g2.jks b/src/main/resources/dfn-g2.jks new file mode 100644 index 0000000..032a560 Binary files /dev/null and b/src/main/resources/dfn-g2.jks differ -- cgit v1.2.3-1-g7c22