package org.openslx.dozmod.gui.configurator; import java.awt.Color; import java.awt.Insets; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.swing.event.ChangeListener; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.TabSet; import javax.swing.text.TabStop; 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.changemonitor.GenericControlWindow; import org.openslx.dozmod.gui.control.WordWrapLabel; import org.openslx.dozmod.gui.helper.GridManager; import org.openslx.dozmod.gui.helper.MessageType; import org.openslx.dozmod.gui.helper.TextChangeListener; import org.openslx.util.Util; /** * Widget for netrules configuration of lectures */ public class NetrulesConfigurator extends NetrulesConfiguratorLayout implements GenericControlWindow> { private static final long serialVersionUID = -3497629601818983994L; private final static Logger LOGGER = Logger.getLogger(NetrulesConfigurator.class); private boolean checkChange = false; private List currentState; private List listeners; /** * 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+"; private static final Color FOREGROUND_TEXT_COLOR; static { Color fgOrigColor = UIManager.getDefaults().getColor("ColorChooser.foreground"); if (fgOrigColor == null) { // use black as fallback fgOrigColor = Color.BLACK; } FOREGROUND_TEXT_COLOR = fgOrigColor; } public NetrulesConfigurator() { super(); final TextChangeListener docListener = new TextChangeListener() { @Override public void changed() { checkChange = true; fireChangeEvent(); } }; final SimpleAttributeSet as = new SimpleAttributeSet(); StyleConstants.setForeground(as, FOREGROUND_TEXT_COLOR); tpNetworkRules.getDocument().addDocumentListener(docListener); tpNetworkRules.addKeyListener(new KeyAdapter() { @Override public void keyTyped(KeyEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { int pos = tpNetworkRules.getCaretPosition(); if (pos < 0) { return; } // Odd: On windows, getText() returns the text with CRLF line breaks, // regardless of what you put into the text box. // The offsets passed to setCharAttrs() below and the caret position // you get from getCaretPosition() however have to adhere to the // text version with just LF, exactly how we created the document. String text = tpNetworkRules.getText().replace("\r", ""); if (pos >= text.length()) { return; } int start = text.lastIndexOf('\n', pos == 0 ? 0 : pos - 1); int end = text.indexOf('\n', pos); if (start == -1) { start = 0; } if (end == -1) { end = text.length() - 1; } if (end <= start) { return; } tpNetworkRules.getStyledDocument().setCharacterAttributes(start, end - start, as, true); } }); } }); btnCheckRules.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { getState(false); } }); } /** * Gets the state of the widget as a list of netrules. Internally it first * transforms the input text in a list of netrules. * * @return the list of rules as parsed by parseNetRules() */ @Override public List getState() { return getState(true); } public List getState(boolean silent) { if (checkChange || !silent) { currentState = parseNetRules(silent); } return currentState; } @Override public void addChangeListener(ChangeListener l) { if (listeners == null) { listeners = new ArrayList<>(); } listeners.add(l); } private void fireChangeEvent() { if (listeners == null) return; for (ChangeListener cl : listeners) { cl.stateChanged(null); } } /** * Sets the state of this widget to the given list of netrules. This will * internally transform the list to its string representation * * @param netrules * as a list of NetRule to set the state to */ public void setState(final List netrules) { checkChange = true; currentState = netrules; this.tpNetworkRules.setText(decodeNetRulesToText(netrules)); fireChangeEvent(); } /** * "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 + " \t "; currentLine += currentRule.port + " \t "; currentLine += currentRule.direction.name(); decodedRules += currentLine + (it.hasNext() ? "\n" : ""); } return decodedRules; } /** * Parses the given rawNetRules String to a list of NetRule * * @param rawNetRules * the raw text to be parsed * @return list of netrules if successful. If any errors occured while * parsing, null is returned. */ public List parseNetRules(boolean silent) { String rawNetRules = tpNetworkRules.getText().trim(); List rulesList = new ArrayList(); if (rawNetRules.isEmpty()) { return rulesList; } // split it line by line boolean invalid = false; // True if the rules are invalid and null should be returned DefaultStyledDocument newdoc = null; if (!silent) { newdoc = new DefaultStyledDocument(); // Used to build new document with colors } StringBuilder errors = new StringBuilder(); // Error messages to show (if not silent) int lineNo = 0; // Show line numbers in error messages for (String ruleLine : rawNetRules.split("[\r\n]+")) { if (silent && invalid) return null; Color lineColor = null; LOGGER.debug("Parsing rule: " + ruleLine); // split the fields and check if we have 3 as expected. String[] fields = ruleLine.trim().split(FIELD_DELIMITER); if (fields.length != 3) { lineNo += addLine(newdoc, ruleLine, Color.RED, true); // log numbers for fields independently... LOGGER.debug("Invalid number of fields! Expected 3, got: " + fields.length); if (fields.length > 3) { errors.append("Zeile " + lineNo + ": Zu viele Felder.\n"); } else { errors.append("Zeile " + lineNo + ": Zu wenig Felder.\n"); } invalid = true; continue; } // Have 3 fields, pretty up String ruleDirection = fields[2].toUpperCase(); ruleLine = fields[0] + " \t " + fields[1] + " \t " + ruleDirection; // 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 if (!ruleDirection.equals("IN") && !ruleDirection.equals("OUT")) { lineNo += addLine(newdoc, ruleLine, Color.RED, true); LOGGER.debug("Invalid net direction! Expected 'in' or out'. Got: " + ruleDirection); errors.append("Zeile " + lineNo + ": Ungültige Richtung. Bitte nutzen Sie 'IN' bzw. 'OUT'.\n"); invalid = true; continue; } // check port: accept if >= 0 and <= 65535 int port = Util.parseInt(fields[1], -1); // port = 0 means match only host (all protocols and ports) if (port < 0 || port > 65535) { lineNo += addLine(newdoc, ruleLine, Color.RED, true); LOGGER.debug("Invalid port! Got: " + port); errors.append("Zeile " + lineNo + ": Ungültiger Port. Gültiger Bereich ist 0-65535.\n"); invalid = true; continue; } // 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 String checkRes = checkHostnameSimple(fields[0]); if (checkRes != null) { lineNo += addLine(newdoc, ruleLine, Color.RED, true); errors.append("Zeile " + lineNo + ": " + checkRes + "\n"); invalid = true; continue; } // Made it to here - line is valid lineNo += addLine(newdoc, ruleLine, lineColor, false); rulesList.add(new NetRule(NetDirection.valueOf(ruleDirection), fields[0], port)); } if (newdoc != null) { tpNetworkRules.setDocument(newdoc); resetTabStops(); } if (!silent && errors.length() != 0) { Gui.showMessageBox("Fehler beim Auswerten der angegebenen Netzwerkregeln.\n\n" + errors.toString() + "\nBitte geben Sie die Regeln zeilenweise im Format\n" + " \n" + "an.", MessageType.ERROR, null, null); } if (invalid) { return null; } // Success return rulesList; } private int addLine(DefaultStyledDocument doc, String line, Color color, boolean bold) { if (doc == null) return 0; if (color == null) { color = FOREGROUND_TEXT_COLOR; } SimpleAttributeSet attrs = new SimpleAttributeSet(); StyleConstants.setForeground(attrs, color); StyleConstants.setBold(attrs, bold); try { doc.insertString(doc.getLength(), line + "\n", attrs); } catch (BadLocationException e) { LOGGER.warn("Cannot append to new textbox document", e); } return 1; } /** * 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 the hostname to check for syntactical validity * @return null if valid, error string otherwise */ private String checkHostnameSimple(String hostname) { if (hostname.length() > 254) { return "Hostname ist zu lang."; } boolean allNumeric = true; int netmask = -1; String[] domainLabels = null; int ls = hostname.lastIndexOf('/'); if (ls != -1) { netmask = Util.parseInt(hostname.substring(ls + 1), -1); if (netmask == -1) { return "Ungültige Netzmaske."; } hostname = hostname.substring(0, ls); } if (hostname.matches("^\\[.*\\]$")) { hostname = hostname.substring(1, hostname.length() - 2); } else { // split by '.' to get domain levels domainLabels = hostname.split("\\."); } if (domainLabels == null || (domainLabels.length <= 1 && hostname.indexOf(':') != -1)) { // v6 if ((hostname.startsWith(":") && !hostname.startsWith("::")) || (hostname.endsWith(":") && ! hostname.endsWith("::"))) { return "IPv6-Adresse darf nicht mit einem Doppelpunkt beginnen oder enden."; } int numCompressed = (hostname.length() - hostname.replace("::", "").length()) / 2; if (numCompressed > 1) { return "IPv6-Adresse darf nicht mehr als einen komprimierten Teil enthalten."; } if (netmask > 128) { return "IPv6 Netzmaske kann nicht größer 128 Bit sein."; } domainLabels = hostname.split(":"); if (domainLabels.length > 8) { return "IPv6-Adresse enthält zu viele Hextets."; // Yes it's called that apparently } for (String domainLabel : domainLabels) { if (domainLabel.isEmpty()) continue; try { int test = Integer.parseInt(domainLabel, 16); if (test < 0 || test > 65535) { return "IPv6-Adresse enthält ungültiges Hextet."; } } catch (Exception e) { return "IPv6-Adresse enthält nicht-hexadezimale Zeichen."; } } if (!allNumeric || ((domainLabels.length == 8 || numCompressed > 0) && (netmask < -1 || netmask > 128)) || (domainLabels.length < 8 && numCompressed == 0 && (netmask < 0 || netmask > 128))) { return "Fehlerhafte IPv6-Adresse/Netzmaske."; } } else { // v4 or hostname if (netmask > 32) { return "IPv4 Netzmaske kann nicht größer 32 Bit sein."; } for (String domainLabel : domainLabels) { if (domainLabel.length() > 63) { // fail since domain level should be max 63 chars return "Domain-Ebene '" + domainLabel + "' länger als 63 Zeichen."; } int i = Util.parseInt(domainLabel, -1); if (i < 0 || i > 255) { allNumeric = false; } // checking for valid chars is pointless with punycode } if (allNumeric) { if ((domainLabels.length == 4 && (netmask < -1 || netmask > 32)) || domainLabels.length > 4 || (domainLabels.length < 4 && (netmask < 0 || netmask > 32))) { return "Fehlerhafte IPv4-Adresse/Netzmaske."; } } } return null; } } /** * Internal layout class for this widget */ class NetrulesConfiguratorLayout extends JPanel { private static final long serialVersionUID = 5266120380443817325L; private final static String STR_RULES_DESCRIPTION = "Wenn Sie den Internetzugriff deaktiviert haben," + " können Sie hier Ausnahmen definieren (Whitelist)." + " Bitte definieren Sie Ihre Regeln im Format\n .\n" + "Sie können Port 0 angeben, was sämtlichen TCP und UDP Ports eines Hosts entspricht."; private final static String STR_RULES_ADD = "Wenn Sie Internetzugriff aktivieren," + " hat diese Liste den gegenteiligen Effekt (Blacklist)."; private final static String STR_TITLE = "Netzwerkregeln"; protected final JTextPane tpNetworkRules; protected final JButton btnCheckRules; public NetrulesConfiguratorLayout() { GridManager grid = new GridManager(this, 2, true, new Insets(5, 5, 5, 5)); // middle panel for network rules this.setBorder(BorderFactory.createTitledBorder(STR_TITLE)); tpNetworkRules = new JTextPane(); resetTabStops(); grid .add(new WordWrapLabel(STR_RULES_DESCRIPTION), 2) .fill(true, false).expand(true, false); grid.nextRow(); grid .add(new WordWrapLabel(STR_RULES_ADD)) .fill(true, false).expand(true, false); btnCheckRules = new JButton("Regeln überprüfen"); grid.add(btnCheckRules); grid.nextRow(); grid.add(new JScrollPane(tpNetworkRules, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), 2) .fill(true, true).expand(true, true); grid.nextRow(); grid.finish(false); } protected void resetTabStops() { Style tabs = tpNetworkRules.getLogicalStyle(); StyleConstants.setTabSet(tabs, new TabSet(new TabStop[] { new TabStop(300, TabStop.ALIGN_RIGHT, TabStop.LEAD_NONE), new TabStop(310, TabStop.ALIGN_LEFT, TabStop.LEAD_NONE), })); tpNetworkRules.setLogicalStyle(tabs); } }