package org.openslx.dozmod.gui.control; import java.awt.Color; import java.awt.Insets; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.EventListener; import java.util.EventObject; import java.util.Iterator; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextPane; import javax.swing.event.EventListenerList; import javax.swing.text.BadLocationException; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; import org.apache.log4j.Logger; import org.openslx.bwlp.thrift.iface.NetDirection; import org.openslx.bwlp.thrift.iface.NetRule; import org.openslx.dozmod.gui.Gui; import org.openslx.dozmod.gui.helper.GridManager; import org.openslx.dozmod.gui.helper.MessageType; import org.openslx.dozmod.gui.helper.TextChangeListener; /** * Widget for advanced configuration options for lectures. This handles * following options - Network rules - Runscript - USB */ public class AdvancedConfigurator extends AdvancedConfiguratorLayout { private static final long serialVersionUID = -3497629601818983994L; private final static Logger LOGGER = Logger .getLogger(AdvancedConfigurator.class); private String originalRawRuleText = null; private String originalRunScript = null; /** * Character defining how the rules are parsed, e.g. for whitespace \\s * Example: "8.8.8.8 80 in" would be split in -hostname "8.8.8.8" -port "80" * -direction "in" */ private static final String FIELD_DELIMITER = "\\s"; public AdvancedConfigurator() { super(); final TextChangeListener docListener = new TextChangeListener() { @Override public void changed() { fireAdvancedConfigurationChangeEvent(new AdvancedConfigurationChangeEvent( new Object())); } }; tpNetworkRules.getDocument().addDocumentListener(docListener); taRunScript.getDocument().addDocumentListener(docListener); } public boolean hasChanged() { return !originalRawRuleText.equalsIgnoreCase(tpNetworkRules.getText()) || !originalRunScript.equalsIgnoreCase(taRunScript.getText()); } /** * Gets the state of the widget. This will first try to parse the * tpNetworkRules and taRunScript and build the corresponding * AdvancedConfiguration Object returned. * * @return advanced configuration object composed of the parsed network * rules as List and the raw runscript text as String * @see org.openslx.dozmod.gui.control.AdvancedConfigurator.AdvancedConfiguration */ public AdvancedConfiguration getState() { // cleanup the TextPane for network rules if needed String input = tpNetworkRules.getText().trim(); List rules = parseNetRules(input); if (rules != null) { return new AdvancedConfiguration(rules, taRunScript.getText()); } return null; } /** * Sets the state of this widget to the given AdvancedConfiguration. Basicly * this sets the content of the text areas to the corresponding network * rules/runscript as given by the AdvancedConfiguration object * * @param config * AdvancedConfiguration to set the state to */ public void setState(final AdvancedConfiguration config) { // setText() blanks the text area if null is given, so no null checks originalRawRuleText = decodeNetRulesToText(config.netRulesList); originalRunScript = config.runScriptText != null ? config.runScriptText : ""; this.tpNetworkRules.setText(originalRawRuleText); this.taRunScript.setText(originalRunScript); } /** * "Decodes" the given list of NetRule to a single String. This should be * used to set the text in the TextPane for the network rules * * @param netRulesList * list of NetRule to decode * @return String representation of the list of rules */ public static String decodeNetRulesToText(final List netRulesList) { if (netRulesList == null || netRulesList.isEmpty()) return ""; String decodedRules = ""; Iterator it = netRulesList.iterator(); while (it.hasNext()) { String currentLine = ""; NetRule currentRule = it.next(); // simple test for validity (since this comes from the server it // should be correct anyways) if (currentRule.host.isEmpty() || currentRule.port > 65535) { LOGGER.error("Invalid rule! Ignoring: " + currentRule.host + ":" + currentRule.port); continue; } currentLine += currentRule.host + " "; currentLine += currentRule.port + " "; currentLine += currentRule.direction.name(); decodedRules += currentLine + (it.hasNext() ? System.lineSeparator() : ""); } return decodedRules; } /** * Parsed the given rawNetRules String to a list of NetRule * * @param rawNetRules * the raw text to be parsed * @return list of valid net rules parsed from the given rawNetRules, * invalid ones are not included * @throws Exception * when parsing fails */ public List parseNetRules(final String rawNetRules) { if (rawNetRules == null) return null; List rulesList = new ArrayList(); if (rawNetRules.isEmpty()) { return rulesList; } // prune the text first String prunedRawNetRules = rawNetRules.replaceAll("(?m)^\\s*", ""); prunedRawNetRules = prunedRawNetRules.replaceAll("(?m)\\s*$", ""); // split it line by line List netRules = Arrays.asList(prunedRawNetRules.split("[" + System.lineSeparator() + "]")); for (int i = 0; i < netRules.size(); i++) { final String ruleLine = netRules.get(i); if (ruleLine == null || ruleLine.isEmpty()) { netRules.remove(i); continue; } LOGGER.debug("Parsing rule: " + ruleLine); // split the fields and check if we have 3 as expected. String[] fields = ruleLine.split(FIELD_DELIMITER); if (fields.length != 3) { markText(ruleLine, Color.RED); // log numbers for fields independently... LOGGER.debug("Invalid number of fields! Expected 3, got: " + fields.length); Gui.showMessageBox( "Ungültige Syntax: Bitte definieren Sie Ihre Regel im Format: [in|out]", MessageType.ERROR, LOGGER, null); break; } // start to check fields one by one from the last to the first .... // check net direction: accept either 'in' or 'out' (case // insensitive) // TODO support combined 'in/out' rules String ruleDirection = fields[2].toLowerCase(); if (!ruleDirection.matches("^(\\bin\\b|\\bout\\b)$")) { markText(ruleLine, Color.RED); LOGGER.debug("Invalid net direction! Expected 'in' or out'. Got: " + ruleDirection); Gui.showMessageBox( "Ungültige Richtung: Bitte nutzen Sie 'in' bzw. 'out'.", MessageType.ERROR, LOGGER, null); break; } // check port: accept if > -2 and < 65535 int port = -2; try { port = Integer.parseInt(fields[1]); } catch (NumberFormatException e) { LOGGER.error("Could not parse '" + fields[1] + "' to an int."); } // TODO: port = -1 for all ports? if (port <= -2 || port > 65535) { markText(ruleLine, Color.RED); LOGGER.debug("Invalid port! Got: " + port); Gui.showMessageBox( "Ungültiges Port! Bitte nutzen Sie einen Port aus dem Bereich [0-65536].", MessageType.ERROR, LOGGER, null); break; } // check hostname: bit more to do here // for IPs and/or resolvable hostnames we make use of java.net's // InetAddress.getByName() method which checks the validity of // an IP-Address represented by a string or if the given hostname // is resolvable. If any of these happen, we have a valid hostname. // Non-resolvable hostnames are handled differently, see after the // try/catch-block InetAddress ruleHost = null; try { ruleHost = InetAddress.getByName(fields[0]); } catch (UnknownHostException e) { // might be good to see this exception in the log file // LOGGER.debug("Invalid hostname (java.net): ", e); } if (ruleHost == null) { // either invalid IP-Address or an invalid resolvable hostname // however it might also be a non-resolvable hostname that would // be // valid in the actual pool-rooms, so lets check its syntax // according to: http://tools.ietf.org/html/rfc1034#section-3.1 LOGGER.debug("Invalid host/IP! Got: " + fields[0]); if (checkHostnameSimple(fields[0])) { markText(ruleLine, Color.ORANGE); if (!Gui.showMessageBox( "Konnte '" + fields[0] + "' nicht auflösen. Wollen Sie diesen Hostnamen trotzdem verwenden?", MessageType.WARNING_RETRY, LOGGER, null)) { break; } } else { markText(ruleLine, Color.RED); continue; } } else { markText(ruleLine, Color.GREEN); } // valid, put it in the list rulesList.add(new NetRule(NetDirection.valueOf(fields[2] .toUpperCase()), fields[0], port)); } if (netRules.size() == rulesList.size()) { // pruned rules were successfully parsed so they are valid: set the // textpane to it tpNetworkRules.setText(prunedRawNetRules); LOGGER.debug("Success"); return rulesList; } return null; } /** * Very simple hostname check for the given String. This will only check for * some requirements of valid hostnames as stated in * http://tools.ietf.org/html/rfc1034#section-3.1 To recap: max length of * the whole hostname must be < 254 ASCII characters, all domain labels * (between two dots) must be between 1 and 63 chars long and domain labels * can only contain digits, letters and hyphen. (Note: we also accept * forward slash to accept subnets!) * * @param hostname * as String to check the syntax of * @return */ private boolean checkHostnameSimple(final String hostname) { if (hostname.length() > 254) { Gui.showMessageBox("Hostname ist zu lang!", MessageType.ERROR, LOGGER, null); return false; } // split by '.' to get domain levels String[] domainLabels = hostname.split("\\."); for (String domainLabel : domainLabels) { if (domainLabel.length() > 63) { // fail since domain level should be max 63 chars Gui.showMessageBox("Domain-Ebene '" + domainLabel + "' länger als 63 Zeichen!", MessageType.ERROR, LOGGER, null); return false; } // length is ok, check for invalid characters for (int i = 0; i < domainLabel.length(); i++) { Character c = Character.valueOf(domainLabel.charAt(i)); // only accepts numbers, letters, hyphen // forward slash as a special case to accept subnets... if (!(Character.isDigit(c) || Character.isLetter(c) || c.equals('-') || c.equals('/'))) { Gui.showMessageBox("Ungültiges Zeichen '" + c + "' in hostname!", MessageType.ERROR, LOGGER, null); return false; } } } return true; } /** * Searches the given txt within tpNetworkRules and changes its color to the * given color * * @param txt * text to search within the tpNetworkRules * @param color * to set the given txt to */ // TODO still buggy with text colors: the marking works fine // but when trying to input new text, funny things happen private void markText(final String txt, final Color color) { SimpleAttributeSet set = new SimpleAttributeSet(); StyleConstants.setForeground(set, color); StyleConstants.setBold(set, color == Color.red); StyledDocument doc = tpNetworkRules.getStyledDocument(); try { for (int pos = 0; pos < doc.getLength() - txt.length() + 1; ++pos) { String current = doc.getText(pos, txt.length()); if (current.endsWith(System.lineSeparator())) { current = current.substring(0, current.length() - 1); } if (current.equals(txt)) { doc.setCharacterAttributes(pos, txt.length(), set, true); break; } } } catch (BadLocationException e) { LOGGER.error( "Failed to set '" + txt + "' to color " + color.toString(), e); } // resetting the char attr to what they were before (buggy) tpNetworkRules.setStyledDocument(doc); StyleConstants.setForeground(set, Color.WHITE); StyleConstants.setBold(set, false); tpNetworkRules.setCharacterAttributes(set, true); } /** * Wrapper class for the advanced configuration information needed since we * need to return a single object from the runAndReturn routine. This class * has two members: the list of NetRule(s) (as List) and the * runScriptText (as String) */ public static class AdvancedConfiguration { public List netRulesList; public String runScriptText; public AdvancedConfiguration(List netRulesList, String runScriptText) { this.netRulesList = netRulesList; this.runScriptText = runScriptText; } } /** * Custom event mechanism to detect changes to the user list (Mostly needed * for the reactToChange() stuff in LectureDetailsWindow) */ protected EventListenerList listenerList = new EventListenerList(); public class AdvancedConfigurationChangeEvent extends EventObject { private static final long serialVersionUID = -8779550754760035845L; public AdvancedConfigurationChangeEvent(Object source) { super(source); } } public interface AdvancedConfigurationChangeEventListener extends EventListener { public void stateChanged(AdvancedConfigurationChangeEvent event); } public void addAdvancedConfigurationChangeEventListener( AdvancedConfigurationChangeEventListener listener) { listenerList.add(AdvancedConfigurationChangeEventListener.class, listener); } public void removeAdvancedConfigurationChangeEventListener( AdvancedConfigurationChangeEventListener listener) { listenerList.remove(AdvancedConfigurationChangeEventListener.class, listener); } void fireAdvancedConfigurationChangeEvent( AdvancedConfigurationChangeEvent evt) { Object[] listeners = listenerList.getListenerList(); for (int i = 0; i < listeners.length; i++) { if (listeners[i] == AdvancedConfigurationChangeEventListener.class) { ((AdvancedConfigurationChangeEventListener) listeners[i + 1]) .stateChanged(evt); } } } } /** * Internal layout class for the advanced configurator (to keep it clean even * for widgets) */ class AdvancedConfiguratorLayout extends JPanel { private static final long serialVersionUID = 648729071828404053L; private final static String txtNetworkOptionsTitle = "Netzwerk Einstellungen"; private final static String txtNetworkOptionsDesc = "Wenn Sie den Internetzugriff deaktiviert haben, können Sie hier Ausnahmen definieren (Whitelist). Bitte definieren Sie Ihre Regeln im Format ."; private final static String txtNetworkRulesTitle = "Netzwerk-Regeln"; private final static String txtRunScriptTitle = "Run-Skript"; private final static String txtRunScriptDesc = "Ein hier abgelegtes Skript wird beim Start einer VM automatisch ausgeführt."; private final JPanel pnlNetworkOptions; private final JPanel pnlRunScript; protected final JTextPane tpNetworkRules; protected final JTextArea taRunScript; public AdvancedConfiguratorLayout() { GridManager grid = new GridManager(this, 1, true, new Insets(5, 5, 5, 5)); // middle panel for network rules pnlNetworkOptions = new JPanel(); GridManager gridNetworkOptions = new GridManager(pnlNetworkOptions, 1, true, new Insets(2, 2, 2, 2)); pnlNetworkOptions.setBorder(BorderFactory .createTitledBorder(txtNetworkOptionsTitle)); tpNetworkRules = new JTextPane(); JScrollPane scpNetworkRules = new JScrollPane(tpNetworkRules, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); pnlNetworkOptions.setBorder(BorderFactory .createTitledBorder(txtNetworkRulesTitle)); gridNetworkOptions .add(new WordWrapLabel(txtNetworkOptionsDesc, false, true)) .fill(true, false).expand(true, false); gridNetworkOptions.nextRow(); gridNetworkOptions.add(scpNetworkRules).fill(true, true) .expand(true, true); gridNetworkOptions.finish(false); // second middle panel for the run script textpane pnlRunScript = new JPanel(); GridManager gridRunScript = new GridManager(pnlRunScript, 1, true, new Insets(2, 2, 2, 2)); taRunScript = new JTextArea("", 5, 20); taRunScript.setLineWrap(true); taRunScript.setWrapStyleWord(true); JScrollPane scpRunScript = new JScrollPane(taRunScript, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); pnlRunScript.setBorder(BorderFactory .createTitledBorder(txtRunScriptTitle)); gridRunScript .add(new WordWrapLabel(txtRunScriptDesc, false, true)) .fill(true, false).expand(true, false); gridRunScript.nextRow(); gridRunScript.add(scpRunScript).fill(true, true).expand(true, true); gridRunScript.finish(false); // build the final grid grid.add(pnlNetworkOptions).fill(true, true).expand(true, true); grid.nextRow(); grid.add(pnlRunScript).fill(true, true).expand(true, true); grid.finish(false); } }