package org.openslx.dozmod.gui.configurator; import java.awt.Color; import java.awt.Dialog.ModalityType; import java.awt.GridBagConstraints; 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.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicReference; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.ButtonModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; 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.LectureRead; import org.openslx.bwlp.thrift.iface.NetDirection; import org.openslx.bwlp.thrift.iface.NetRule; import org.openslx.bwlp.thrift.iface.PresetNetRule; import org.openslx.dozmod.gui.Gui; import org.openslx.dozmod.gui.changemonitor.GenericControlWindow; import org.openslx.dozmod.gui.configurator.NetrulesConfigurator.StateWrapper; 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.dozmod.thrift.cache.MetaDataCache; import org.openslx.util.QuickTimer; import org.openslx.util.QuickTimer.Task; import org.openslx.util.Util; /** * Widget for netrules configuration of lectures */ public class NetrulesConfigurator extends NetrulesConfiguratorLayout implements GenericControlWindow { public static class StateWrapper { public List customRules; public List selectedPresets; @Override public boolean equals(Object obj) { if (!(obj instanceof StateWrapper)) return false; StateWrapper o = (StateWrapper) obj; if (selectedPresets != null && !selectedPresets.equals(o.selectedPresets)) return false; if (o.selectedPresets != null && !o.selectedPresets.equals(selectedPresets)) return false; if (customRules == o.customRules) return true; return customRules != null && customRules.equals(o.customRules); } @Override protected StateWrapper clone() { StateWrapper r = new StateWrapper(); if (this.customRules != null) { r.customRules = new ArrayList<>(this.customRules); } if (this.selectedPresets != null) { r.selectedPresets = new ArrayList<>(this.selectedPresets); } return r; } } private static final long serialVersionUID = -3497629601818983994L; private final static Logger LOGGER = Logger.getLogger(NetrulesConfigurator.class); private boolean checkChange = false; private StateWrapper currentState = new StateWrapper(); private LectureRead lecture; private List listeners; private List predefinedRules; /** * 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); } }); QuickTimer.scheduleOnce(new Task() { @Override public void fire() { predefinedRules = MetaDataCache.getPredefinedNetRules(); if (predefinedRules.isEmpty()) { Gui.asyncExec(new Runnable() { @Override public void run() { btnShowPresets.setVisible(false); } }); } } }); btnShowPresets.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { showPresetSelector(); } }); } private void showPresetSelector() { if (predefinedRules == null) { Gui.showMessageBox("Wah wah wah! Null preset list", MessageType.ERROR, null, null); return; } final JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor(this), "Regelsets auswählen", ModalityType.APPLICATION_MODAL); JPanel pane = new JPanel(); dialog.setContentPane(pane); dialog.setMinimumSize(Gui.getScaledDimension(200, 300)); GridManager grid = new GridManager(pane, 2, true, new Insets(2, 2, 2, 2)); final Map mapper = new HashMap<>(); for (PresetNetRule ruleSet : predefinedRules) { JCheckBox button = new JCheckBox(ruleSet.displayName); grid.add(button, 2); grid.nextRow(); mapper.put(button.getModel(), ruleSet.ruleId); if (lecture != null && lecture.presetNetworkExceptionIds != null && lecture.presetNetworkExceptionIds.contains(ruleSet.ruleId)) { button.setSelected(true); } } grid.add(Box.createVerticalGlue(), 2); grid.nextRow(); JButton btnCancel = new JButton("Abbrechen"); JButton btnOk = new JButton("SPASCHAN"); grid.add(btnCancel).anchor(GridBagConstraints.LINE_START); grid.add(btnOk).anchor(GridBagConstraints.LINE_END); grid.finish(false); btnCancel.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dialog.dispose(); } }); final AtomicReference> selectedRules = new AtomicReference<>(); btnOk.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { List selected = new ArrayList<>(); for (Entry button : mapper.entrySet()) { if (button.getKey().isSelected()) { selected.add(button.getValue()); } } selectedRules.set(selected); dialog.dispose(); } }); dialog.pack(); Gui.centerShellOverShell(SwingUtilities.getWindowAncestor(this), dialog); dialog.setVisible(true); // Call blocks as it's a modal window if (selectedRules.get() != null) { // User clicked OK lecture.presetNetworkExceptionIds = currentState.selectedPresets = selectedRules.get(); // TODO: Make it work checkChange = true; fireChangeEvent(); } } /** * 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 StateWrapper getState() { return getState(true); } public StateWrapper getState(boolean silent) { if (checkChange || !silent) { currentState.customRules = parseNetRules(silent); } return currentState.clone(); } @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 lecture * as a list of NetRule to set the state to */ public void setState(final LectureRead lecture) { checkChange = true; this.lecture = lecture; this.tpNetworkRules.setText(decodeNetRulesToText(lecture.networkExceptions)); 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; protected final JButton btnShowPresets; 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(); btnShowPresets = new JButton("Vordefinierte Regelsets..."); grid.add(btnShowPresets, 2).anchor(GridBagConstraints.LINE_END); 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); } }