org.xins.server.AccessRuleFile Maven / Gradle / Ivy
/*
* $Id: AccessRuleFile.java,v 1.41 2010/09/29 17:21:48 agoubard Exp $
*
* See the COPYRIGHT file for redistribution and use restrictions.
*/
package org.xins.server;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import org.xins.common.MandatoryArgumentChecker;
import org.xins.common.Utils;
import org.xins.common.io.FileWatcher;
import org.xins.common.text.TextUtils;
import org.xins.common.text.ParseException;
/**
* Collection of access rules that are read from a separate file.
*
* An AccessRuleFile
instance is constructed using a
* descriptor and a file watch interval. The descriptor is a character string
* that is parsed to determine which file should be parsed and monitored for
* changes. Such a descriptor must match the following pattern:
*
*
file filename
*
* where filename is the name of the file to parse and watch.
*
* The file watch interval is specified in seconds. At the specified
* interval, the file will be checked for modifications. If there are any
* modifications, then the file is reloaded and the access rules are
* re-applied.
*
*
If the file watch interval is set to 0
, then the watching
* is disabled, and no automatic reloading will be performed.
*
* @version $Revision: 1.41 $ $Date: 2010/09/29 17:21:48 $
* @author Ernst de Haan
* @author Anthony Goubard
*
* @since XINS 1.1.0
*/
public class AccessRuleFile implements AccessRuleContainer {
/**
* The ACL file.
*/
private String _file;
/**
* The interval used to check the ACL file for modification.
*/
private int _interval;
/**
* Watcher for the ACL file.
*/
private FileWatcher _fileWatcher;
/**
* The list of rules. Cannot be null
.
*/
private AccessRuleContainer[] _rules;
/**
* String representation of this object. Cannot be null
.
*/
private final String _asString;
/**
* Flag that indicates whether this object is disposed.
*/
private boolean _disposed;
/**
* Constructs a new AccessRuleFile
based on a descriptor and
* a file watch interval.
*
*
If the specified interval is 0
, then no watching will be
* performed.
*
* @param descriptor
* the access rule file descriptor, the character string to parse,
* cannot be null
.
*
* @param interval
* the interval to check the ACL file for modifications, in seconds,
* must be >= 0.
*
* @throws ParseException
* If the token is incorrectly formatted.
*
* @throws IllegalArgumentException
* if descriptor == null || interval < 0
.
*/
public AccessRuleFile(String descriptor, int interval)
throws IllegalArgumentException, ParseException {
// Check preconditions
MandatoryArgumentChecker.check("descriptor", descriptor);
if (interval < 0) {
throw new IllegalArgumentException("interval ("
+ interval
+ ") < 0");
}
// First token must be 'file'
StringTokenizer tokenizer = new StringTokenizer(descriptor, " \t\n\r");
String token = nextToken(descriptor, tokenizer);
if (! "file".equals(token)) {
throw new ParseException("First token of descriptor is \""
+ token
+ "\", instead of \"file\".");
}
// First try parsing the file as it is
_file = nextToken(descriptor, tokenizer);
try {
parseAndApply(_file, interval);
// File not found
} catch (FileNotFoundException fnfe) {
String message = "File \""
+ _file
+ "\" cannot be opened for reading.";
ParseException pe = new ParseException(message, fnfe, null);
throw pe;
// I/O error reading from the file not found
} catch (IOException ioe) {
String message = "Cannot parse the file \""
+ _file
+ "\" due to an I/O error.";
ParseException pe = new ParseException(message, ioe, null);
throw pe;
}
// Store the interval
_interval = interval;
// Create and start a file watch thread, if the interval is not zero
if (interval > 0) {
FileListener fileListener = new FileListener();
_fileWatcher = new FileWatcher(_file, interval, fileListener);
_fileWatcher.start();
}
// Generate the string representation
_asString = "file " + _file;
}
/**
* Returns the next token in the descriptor.
*
* @param descriptor
* the original descriptor, useful when constructing the message for a
* {@link ParseException}, when appropriate, should not be
* null
.
*
* @param tokenizer
* the {@link StringTokenizer} to retrieve the next token from, cannot be
* null
.
*
* @return
* the next token, never null
.
*
* @throws ParseException
* if tokenizer.{@link StringTokenizer#hasMoreTokens()
* hasMoreTokens}() == false
.
*/
private static String nextToken(String descriptor,
StringTokenizer tokenizer)
throws ParseException {
if (! tokenizer.hasMoreTokens()) {
String message = "The string \""
+ descriptor
+ "\" is invalid as an access rule file descriptor. "
+ "More tokens expected.";
throw new ParseException(message);
} else {
return tokenizer.nextToken();
}
}
/**
* Determines if the specified IP address is allowed to access the
* specified function, returning a Boolean
object or
* null
.
*
*
This method finds the first matching rule and then returns the
* allow property of that rule (see
* {@link AccessRule#isAllowRule()}). If there is no matching rule, then
* null
is returned.
*
* @param ip
* the IP address, cannot be null
.
*
* @param functionName
* the name of the function, cannot be null
.
*
* @param conventionName
* the name of the calling convention to match, can be null
.
*
* @return
* {@link Boolean#TRUE} if the specified IP address is allowed to access
* the specified function, {@link Boolean#FALSE} if it is disallowed
* access or null
if there is no match.
*
* @throws IllegalArgumentException
* if ip == null || functionName == null
.
*
* @throws ParseException
* if the specified IP address is malformed.
*
* @since XINS 2.1.
*/
public Boolean isAllowed(String ip, String functionName, String conventionName)
throws IllegalArgumentException, ParseException {
// Check state
if (_disposed) {
String detail = "This AccessRuleFile is disposed.";
Utils.logProgrammingError(detail);
throw new IllegalStateException(detail);
}
// Check arguments
MandatoryArgumentChecker.check("ip", ip,
"functionName", functionName);
// Find a matching rule and see if the call is allowed
int count = _rules == null ? 0 : _rules.length;
Boolean allowed = null;
for (int i = 0; i < count && allowed == null; i++) {
allowed = _rules[i].isAllowed(ip, functionName, conventionName);
}
return allowed;
}
/**
* Disposes this access rule. All claimed resources are freed as much as
* possible.
*
*
Once disposed, the {@link #isAllowed} method should no longer be
* called.
*
* @throws IllegalStateException
* if {@link #dispose()} has been called previously
* (since XINS 1.3.0).
*/
public void dispose() throws IllegalStateException {
// Check state
if (_disposed) {
String detail = "This AccessRuleFile is already disposed.";
Utils.logProgrammingError(detail);
throw new IllegalStateException(detail);
}
// Dispose all children
int count = _rules == null ? 0 : _rules.length;
for (int i = 0; i < count; i++) {
AccessRuleContainer rule = _rules[i];
if (rule != null) {
try {
rule.dispose();
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
}
}
_rules = null;
// Stop the file watcher
if (_fileWatcher != null) {
try {
_fileWatcher.end();
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
_fileWatcher = null;
}
// Mark this object as disposed
_disposed = true;
}
/**
* Reads and parses the specified ACL file and then applies it to this
* AccessRuleFile
instance.
*
* @param file
* the file to open, read and parse, cannot be null
.
*
* @param interval
* the interval for checking the ACL file for modifications, in
* milliseconds.
*
* @throws IllegalArgumentException
* if file == null || interval < 0
.
*
* @throws ParseException
* if the file could not be parsed successfully.
*
* @throws IOException
* if there was an I/O error while reading from the file.
*/
private void parseAndApply(String file, int interval)
throws IllegalArgumentException, ParseException, IOException {
// Check preconditions
MandatoryArgumentChecker.check("file", file);
if (interval < 0) {
throw new IllegalArgumentException("interval < 0");
}
// Buffer the input from the file
FileReader fileReader = new FileReader(file);
BufferedReader buffReader = null;
try {
buffReader = new BufferedReader(fileReader);
// Delegate
parseAndApply(file, buffReader, interval);
// Always close the streams
} finally {
try {
fileReader.close();
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
if (buffReader != null) {
try {
buffReader.close();
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
}
}
}
}
/**
* Parses the specified ACL file (already opened) and then applies it to
* this AccessRuleFile
instance.
*
* @param file
* the name of the opened file, should not be null
.
*
* @param reader
* input stream for the file, should not be null
.
*
* @param interval
* the interval for checking the ACL file for modifications, in
* milliseconds.
*
* @throws NullPointerException
* if file == null || reader == null
.
*
* @throws ParseException
* if the file could not be parsed successfully.
*
* @throws IOException
* if there was an I/O error while reading from the file.
*/
private void parseAndApply(String file,
BufferedReader reader,
int interval)
throws NullPointerException, ParseException, IOException {
// Loop through the file, line by line
List rules = new ArrayList(25);
int lineNumber = 0;
String nextLine = "";
while (reader.ready() && nextLine != null) {
// Read the next line and remove leading/trailing whitespace
nextLine = TextUtils.trim(reader.readLine(), null);
// Increase the line number (so it's 1-based)
lineNumber++;
// Skip comments and empty lines
if (nextLine == null || nextLine.startsWith("#")) {
// ignore
// Plain access rule
} else if (nextLine.startsWith("allow") || nextLine.startsWith("deny")) {
rules.add(AccessRule.parseAccessRule(nextLine));
// File reference
} else if (nextLine.startsWith("file")) {
// Make sure the file does not include itself
if (nextLine.substring(5).equals(file)) {
String detail = "The access rule file \""
+ file
+ "\" includes itself.";
throw new ParseException(detail);
}
rules.add(new AccessRuleFile(nextLine, interval));
// Otherwise: Incorrect line
} else {
String detail = "Failed to parse \""
+ file
+ "\", line #"
+ lineNumber
+ ": \""
+ nextLine
+ "\". Expected line to start with \"#\", "
+ "\"allow\", \"deny\" or \"file\".";
throw new ParseException(detail);
// XXX: Log parsing problem?
}
}
// Copy to the instance field
_rules = rules.toArray(new AccessRuleContainer[rules.size()]);
}
/**
* Re-initializes the ACL rules for this file.
*/
private void reinit() {
// Dispose the current rules
int count = _rules == null ? 0 : _rules.length;
for (int i = 0; i < count; i++) {
_rules[i].dispose();
}
_rules = null;
// Parse the file and apply the rules
try {
parseAndApply(_file, _interval);
// If the parsing fails, then log the exception
} catch (Throwable exception) {
Utils.logIgnoredException(exception);
_rules = new AccessRuleContainer[0];
// TODO: The framework re-initialization should fail
}
}
public String toString() {
return _asString;
}
/**
* Listener that reloads the ACL file if it changes.
*
* @version $Revision: 1.41 $ $Date: 2010/09/29 17:21:48 $
* @author Anthony Goubard
*
* @since XINS 1.1.0
*/
private final class FileListener implements FileWatcher.Listener {
/**
* Constructs a new FileListener
object.
*/
FileListener() {
// empty
}
/**
* Callback method called when the configuration file is found while it
* was previously not found.
*
* This will trigger re-initialization.
*/
public void fileFound() {
reinit();
}
/**
* Callback method called when the configuration file is (still) not
* found.
*
*
The implementation of this method does not perform any actions.
*/
public void fileNotFound() {
Log.log_3400(_file);
}
/**
* Callback method called when the configuration file is (still) not
* modified.
*
*
The implementation of this method does not perform any actions.
*/
public void fileNotModified() {
// empty
}
/**
* Callback method called when the configuration file could not be
* examined due to a SecurityException
.
* modified.
*
*
The implementation of this method does not perform any actions.
*
* @param exception
* the caught security exception, should not be null
* (although this is not checked).
*/
public void securityException(SecurityException exception) {
Log.log_3401(exception, _file);
}
/**
* Callback method called when the configuration file is modified since
* the last time it was checked.
*
*
This will trigger re-initialization.
*/
public void fileModified() {
reinit();
}
}
}