001: /*
002: * $Id: AccessRuleFile.java,v 1.39 2007/09/18 08:45:06 agoubard Exp $
003: *
004: * Copyright 2003-2007 Orange Nederland Breedband B.V.
005: * See the COPYRIGHT file for redistribution and use restrictions.
006: */
007: package org.xins.server;
008:
009: import java.io.BufferedReader;
010: import java.io.FileNotFoundException;
011: import java.io.FileReader;
012: import java.io.IOException;
013: import java.util.ArrayList;
014: import java.util.List;
015: import java.util.StringTokenizer;
016:
017: import org.xins.common.MandatoryArgumentChecker;
018: import org.xins.common.Utils;
019: import org.xins.common.io.FileWatcher;
020: import org.xins.common.text.TextUtils;
021: import org.xins.common.text.ParseException;
022:
023: /**
024: * Collection of access rules that are read from a separate file.
025: *
026: * <p>An <code>AccessRuleFile</code> instance is constructed using a
027: * descriptor and a file watch interval. The descriptor is a character string
028: * that is parsed to determine which file should be parsed and monitored for
029: * changes. Such a descriptor must match the following pattern:
030: *
031: * <blockquote><code>file <em>filename</em></code></blockquote>
032: *
033: * where <em>filename</em> is the name of the file to parse and watch.
034: *
035: * <p>The file watch interval is specified in seconds. At the specified
036: * interval, the file will be checked for modifications. If there are any
037: * modifications, then the file is reloaded and the access rules are
038: * re-applied.
039: *
040: * <p>If the file watch interval is set to <code>0</code>, then the watching
041: * is disabled, and no automatic reloading will be performed.
042: *
043: * @version $Revision: 1.39 $ $Date: 2007/09/18 08:45:06 $
044: * @author <a href="mailto:ernst@ernstdehaan.com">Ernst de Haan</a>
045: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
046: *
047: * @since XINS 1.1.0
048: */
049: public class AccessRuleFile implements AccessRuleContainer {
050:
051: /**
052: * The ACL file.
053: */
054: private String _file;
055:
056: /**
057: * The interval used to check the ACL file for modification.
058: */
059: private int _interval;
060:
061: /**
062: * Watcher for the ACL file.
063: */
064: private FileWatcher _fileWatcher;
065:
066: /**
067: * The list of rules. Cannot be <code>null</code>.
068: */
069: private AccessRuleContainer[] _rules;
070:
071: /**
072: * String representation of this object. Cannot be <code>null</code>.
073: */
074: private final String _asString;
075:
076: /**
077: * Flag that indicates whether this object is disposed.
078: */
079: private boolean _disposed;
080:
081: /**
082: * Constructs a new <code>AccessRuleFile</code> based on a descriptor and
083: * a file watch interval.
084: *
085: * <p>If the specified interval is <code>0</code>, then no watching will be
086: * performed.
087: *
088: * @param descriptor
089: * the access rule file descriptor, the character string to parse,
090: * cannot be <code>null</code>.
091: *
092: * @param interval
093: * the interval to check the ACL file for modifications, in seconds,
094: * must be >= 0.
095: *
096: * @throws ParseException
097: * If the token is incorrectly formatted.
098: *
099: * @throws IllegalArgumentException
100: * if <code>descriptor == null || interval < 0</code>.
101: */
102: public AccessRuleFile(String descriptor, int interval)
103: throws IllegalArgumentException, ParseException {
104:
105: // Check preconditions
106: MandatoryArgumentChecker.check("descriptor", descriptor);
107: if (interval < 0) {
108: throw new IllegalArgumentException("interval (" + interval
109: + ") < 0");
110: }
111:
112: // First token must be 'file'
113: StringTokenizer tokenizer = new StringTokenizer(descriptor,
114: " \t\n\r");
115: String token = nextToken(descriptor, tokenizer);
116: if (!"file".equals(token)) {
117: throw new ParseException("First token of descriptor is \""
118: + token + "\", instead of \"file\".");
119: }
120:
121: // First try parsing the file as it is
122: _file = nextToken(descriptor, tokenizer);
123: try {
124: parseAndApply(_file, interval);
125:
126: // File not found
127: } catch (FileNotFoundException fnfe) {
128: String message = "File \"" + _file
129: + "\" cannot be opened for reading.";
130: ParseException pe = new ParseException(message, fnfe, null);
131: throw pe;
132:
133: // I/O error reading from the file not found
134: } catch (IOException ioe) {
135: String message = "Cannot parse the file \"" + _file
136: + "\" due to an I/O error.";
137: ParseException pe = new ParseException(message, ioe, null);
138: throw pe;
139: }
140:
141: // Store the interval
142: _interval = interval;
143:
144: // Create and start a file watch thread, if the interval is not zero
145: if (interval > 0) {
146: FileListener fileListener = new FileListener();
147: _fileWatcher = new FileWatcher(_file, interval,
148: fileListener);
149: _fileWatcher.start();
150: }
151:
152: // Generate the string representation
153: _asString = "file " + _file;
154: }
155:
156: /**
157: * Returns the next token in the descriptor.
158: *
159: * @param descriptor
160: * the original descriptor, useful when constructing the message for a
161: * {@link ParseException}, when appropriate, should not be
162: * <code>null</code>.
163: *
164: * @param tokenizer
165: * the {@link StringTokenizer} to retrieve the next token from, cannot be
166: * <code>null</code>.
167: *
168: * @return
169: * the next token, never <code>null</code>.
170: *
171: * @throws ParseException
172: * if <code>tokenizer.{@link StringTokenizer#hasMoreTokens()
173: * hasMoreTokens}() == false</code>.
174: */
175: private static String nextToken(String descriptor,
176: StringTokenizer tokenizer) throws ParseException {
177:
178: if (!tokenizer.hasMoreTokens()) {
179: String message = "The string \""
180: + descriptor
181: + "\" is invalid as an access rule file descriptor. "
182: + "More tokens expected.";
183: throw new ParseException(message);
184: } else {
185: return tokenizer.nextToken();
186: }
187: }
188:
189: /**
190: * Determines if the specified IP address is allowed to access the
191: * specified function, returning a <code>Boolean</code> object or
192: * <code>null</code>.
193: *
194: * <p>This method finds the first matching rule and then returns the
195: * <em>allow</em> property of that rule (see
196: * {@link AccessRule#isAllowRule()}). If there is no matching rule, then
197: * <code>null</code> is returned.
198: *
199: * @param ip
200: * the IP address, cannot be <code>null</code>.
201: *
202: * @param functionName
203: * the name of the function, cannot be <code>null</code>.
204: *
205: * @param conventionName
206: * the name of the calling convention to match, can be <code>null</code>.
207: *
208: * @return
209: * {@link Boolean#TRUE} if the specified IP address is allowed to access
210: * the specified function, {@link Boolean#FALSE} if it is disallowed
211: * access or <code>null</code> if there is no match.
212: *
213: * @throws IllegalArgumentException
214: * if <code>ip == null || functionName == null</code>.
215: *
216: * @throws ParseException
217: * if the specified IP address is malformed.
218: *
219: * @since XINS 2.1.
220: */
221: public Boolean isAllowed(String ip, String functionName,
222: String conventionName) throws IllegalArgumentException,
223: ParseException {
224:
225: // Check state
226: if (_disposed) {
227: String detail = "This AccessRuleFile is disposed.";
228: Utils.logProgrammingError(detail);
229: throw new IllegalStateException(detail);
230: }
231:
232: // Check arguments
233: MandatoryArgumentChecker.check("ip", ip, "functionName",
234: functionName);
235:
236: // Find a matching rule and see if the call is allowed
237: int count = _rules == null ? 0 : _rules.length;
238: Boolean allowed = null;
239: for (int i = 0; i < count && allowed == null; i++) {
240: allowed = _rules[i].isAllowed(ip, functionName,
241: conventionName);
242: }
243:
244: return allowed;
245: }
246:
247: /**
248: * Disposes this access rule. All claimed resources are freed as much as
249: * possible.
250: *
251: * <p>Once disposed, the {@link #isAllowed} method should no longer be
252: * called.
253: *
254: * @throws IllegalStateException
255: * if {@link #dispose()} has been called previously
256: * (<em>since XINS 1.3.0</em>).
257: */
258: public void dispose() throws IllegalStateException {
259:
260: // Check state
261: if (_disposed) {
262: String detail = "This AccessRuleFile is already disposed.";
263: Utils.logProgrammingError(detail);
264: throw new IllegalStateException(detail);
265: }
266:
267: // Dispose all children
268: int count = _rules == null ? 0 : _rules.length;
269: for (int i = 0; i < count; i++) {
270: AccessRuleContainer rule = _rules[i];
271: if (rule != null) {
272: try {
273: rule.dispose();
274: } catch (Throwable exception) {
275: Utils.logIgnoredException(exception);
276: }
277: }
278: }
279: _rules = null;
280:
281: // Stop the file watcher
282: if (_fileWatcher != null) {
283: try {
284: _fileWatcher.end();
285: } catch (Throwable exception) {
286: Utils.logIgnoredException(exception);
287: }
288: _fileWatcher = null;
289: }
290:
291: // Mark this object as disposed
292: _disposed = true;
293: }
294:
295: /**
296: * Reads and parses the specified ACL file and then applies it to this
297: * <code>AccessRuleFile</code> instance.
298: *
299: * @param file
300: * the file to open, read and parse, cannot be <code>null</code>.
301: *
302: * @param interval
303: * the interval for checking the ACL file for modifications, in
304: * milliseconds.
305: *
306: * @throws IllegalArgumentException
307: * if <code>file == null || interval < 0</code>.
308: *
309: * @throws ParseException
310: * if the file could not be parsed successfully.
311: *
312: * @throws IOException
313: * if there was an I/O error while reading from the file.
314: */
315: private void parseAndApply(String file, int interval)
316: throws IllegalArgumentException, ParseException,
317: IOException {
318:
319: // Check preconditions
320: MandatoryArgumentChecker.check("file", file);
321: if (interval < 0) {
322: throw new IllegalArgumentException("interval < 0");
323: }
324:
325: // Buffer the input from the file
326: FileReader fileReader = new FileReader(file);
327: BufferedReader buffReader = null;
328: try {
329: buffReader = new BufferedReader(fileReader);
330:
331: // Delegate
332: parseAndApply(file, buffReader, interval);
333:
334: // Always close the streams
335: } finally {
336: try {
337: fileReader.close();
338: } catch (Throwable exception) {
339: Utils.logIgnoredException(exception);
340: }
341: if (buffReader != null) {
342: try {
343: buffReader.close();
344: } catch (Throwable exception) {
345: Utils.logIgnoredException(exception);
346: }
347: }
348: }
349: }
350:
351: /**
352: * Parses the specified ACL file (already opened) and then applies it to
353: * this <code>AccessRuleFile</code> instance.
354: *
355: * @param file
356: * the name of the opened file, should not be <code>null</code>.
357: *
358: * @param reader
359: * input stream for the file, should not be <code>null</code>.
360: *
361: * @param interval
362: * the interval for checking the ACL file for modifications, in
363: * milliseconds.
364: *
365: * @throws NullPointerException
366: * if <code>file == null || reader == null</code>.
367: *
368: * @throws ParseException
369: * if the file could not be parsed successfully.
370: *
371: * @throws IOException
372: * if there was an I/O error while reading from the file.
373: */
374: private void parseAndApply(String file, BufferedReader reader,
375: int interval) throws NullPointerException, ParseException,
376: IOException {
377:
378: // Loop through the file, line by line
379: List rules = new ArrayList(25);
380: int lineNumber = 0;
381: String nextLine = "";
382: while (reader.ready() && nextLine != null) {
383:
384: // Read the next line and remove leading/trailing whitespace
385: nextLine = TextUtils.trim(reader.readLine(), null);
386:
387: // Increase the line number (so it's 1-based)
388: lineNumber++;
389:
390: // Skip comments and empty lines
391: if (nextLine == null || nextLine.startsWith("#")) {
392: // ignore
393:
394: // Plain access rule
395: } else if (nextLine.startsWith("allow")
396: || nextLine.startsWith("deny")) {
397: rules.add(AccessRule.parseAccessRule(nextLine));
398:
399: // File reference
400: } else if (nextLine.startsWith("file")) {
401:
402: // Make sure the file does not include itself
403: if (nextLine.substring(5).equals(file)) {
404: String detail = "The access rule file \"" + file
405: + "\" includes itself.";
406: throw new ParseException(detail);
407: }
408: rules.add(new AccessRuleFile(nextLine, interval));
409:
410: // Otherwise: Incorrect line
411: } else {
412: String detail = "Failed to parse \"" + file
413: + "\", line #" + lineNumber + ": \"" + nextLine
414: + "\". Expected line to start with \"#\", "
415: + "\"allow\", \"deny\" or \"file\".";
416: throw new ParseException(detail);
417: // XXX: Log parsing problem?
418: }
419: }
420:
421: // Copy to the instance field
422: _rules = (AccessRuleContainer[]) rules
423: .toArray(new AccessRuleContainer[rules.size()]);
424: }
425:
426: /**
427: * Re-initializes the ACL rules for this file.
428: */
429: private void reinit() {
430:
431: // Dispose the current rules
432: int count = _rules == null ? 0 : _rules.length;
433: for (int i = 0; i < count; i++) {
434: _rules[i].dispose();
435: }
436: _rules = null;
437:
438: // Parse the file and apply the rules
439: try {
440: parseAndApply(_file, _interval);
441:
442: // If the parsing fails, then log the exception
443: } catch (Throwable exception) {
444: Utils.logIgnoredException(exception);
445: _rules = new AccessRuleContainer[0];
446: // TODO: The framework re-initialization should fail
447: }
448: }
449:
450: public String toString() {
451: return _asString;
452: }
453:
454: /**
455: * Listener that reloads the ACL file if it changes.
456: *
457: * @version $Revision: 1.39 $ $Date: 2007/09/18 08:45:06 $
458: * @author <a href="mailto:anthony.goubard@japplis.com">Anthony Goubard</a>
459: *
460: * @since XINS 1.1.0
461: */
462: private final class FileListener implements FileWatcher.Listener {
463:
464: /**
465: * Constructs a new <code>FileListener</code> object.
466: */
467: FileListener() {
468: // empty
469: }
470:
471: /**
472: * Callback method called when the configuration file is found while it
473: * was previously not found.
474: *
475: * <p>This will trigger re-initialization.
476: */
477: public void fileFound() {
478: reinit();
479: }
480:
481: /**
482: * Callback method called when the configuration file is (still) not
483: * found.
484: *
485: * <p>The implementation of this method does not perform any actions.
486: */
487: public void fileNotFound() {
488: Log.log_3400(_file);
489: }
490:
491: /**
492: * Callback method called when the configuration file is (still) not
493: * modified.
494: *
495: * <p>The implementation of this method does not perform any actions.
496: */
497: public void fileNotModified() {
498: // empty
499: }
500:
501: /**
502: * Callback method called when the configuration file could not be
503: * examined due to a <code>SecurityException</code>.
504: * modified.
505: *
506: * <p>The implementation of this method does not perform any actions.
507: *
508: * @param exception
509: * the caught security exception, should not be <code>null</code>
510: * (although this is not checked).
511: */
512: public void securityException(SecurityException exception) {
513: Log.log_3401(exception, _file);
514: }
515:
516: /**
517: * Callback method called when the configuration file is modified since
518: * the last time it was checked.
519: *
520: * <p>This will trigger re-initialization.
521: */
522: public void fileModified() {
523: reinit();
524: }
525: }
526: }
|