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<StateWrapper> {
public static class StateWrapper {
public List<NetRule> customRules;
public List<Integer> selectedPresets;
@Override
public boolean equals(Object obj) {
if (!(obj instanceof StateWrapper))
return false;
StateWrapper o = (StateWrapper) obj;
if (selectedPresets != o.selectedPresets
&& (selectedPresets == null || !selectedPresets.equals(o.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<ChangeListener> listeners;
private List<PresetNetRule> 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<ButtonModel, Integer> 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<List<Integer>> selectedRules = new AtomicReference<>();
btnOk.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
List<Integer> selected = new ArrayList<>();
for (Entry<ButtonModel, Integer> 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();
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) {
currentState.customRules = lecture.networkExceptions;
currentState.selectedPresets = lecture.presetNetworkExceptionIds;
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<NetRule> netRulesList) {
if (netRulesList == null || netRulesList.isEmpty())
return "";
String decodedRules = "";
Iterator<NetRule> 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<NetRule> parseNetRules(boolean silent) {
String rawNetRules = tpNetworkRules.getText().trim();
List<NetRule> rulesList = new ArrayList<NetRule>();
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"
+ "<host> <port> <IN|OUT>\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<host> <port> <in|out>.\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);
}
}