/* Copyright (c) 2001-2019, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.server;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;

import org.hsqldb.map.BitMap;

A list of ACL permit and deny entries with a permitAccess method which tells whether candidate addresses are permitted or denied by this ACL list.

The ACL file is reloaded whenever a modification to it is detected. If you copy in a file with an older file date, you will need to touch it.

The public runtime method is permitAccess(). The public setup method is the constructor.

Each non-comment line in the ACL file must be a rule of the format:


    {allow|deny} <ip_address>[/significant-bits]
For example

    allow ahostname
    deny ahost.domain.com
    allow 127.0.0.1
    allow 2001:db8::/32

In order to detect bit specification mistakes, we require that non-significant bits be zero in the values. An undesirable consequence of this is, you can't use a specification like the following to mean "all of the hosts on the same network as x.admc.com":


    allow x.admc.com/24
Author:Blaine Simpson (blaine dot simpson at admc dot com)
See Also:
Version:2.3.0
Since:2.0.0
/** * A list of ACL permit and deny entries with a permitAccess method * which tells whether candidate addresses are permitted or denied * by this ACL list. * <P> * The ACL file is reloaded whenever a modification to it is detected. * If you copy in a file with an older file date, you will need to touch it. * <P> * The public runtime method is permitAccess(). * The public setup method is the constructor. * <P> * Each non-comment line in the ACL file must be a rule of the format: * <PRE><CODE> * {allow|deny} &lt;ip_address&gt;[/significant-bits] * </CODE></PRE> * For example * <PRE><CODE> * allow ahostname * deny ahost.domain.com * allow 127.0.0.1 * allow 2001:db8::/32 * </CODE></PRE> * * <P> * In order to detect bit specification mistakes, we require that * non-significant bits be zero in the values. * An undesirable consequence of this is, you can't use a specification like * the following to mean "all of the hosts on the same network as x.admc.com": * <PRE><CODE> * allow x.admc.com/24 * </CODE></PRE> * * * * @see #ServerAcl(File) * @see #permitAccess * * @author Blaine Simpson (blaine dot simpson at admc dot com) * @version 2.3.0 * @since 2.0.0 **/
public final class ServerAcl { public static final class AclFormatException extends Exception { public AclFormatException(String s) { super(s); } } protected static final byte[] ALL_SET_4BYTES = new byte[] { -1, -1, -1, -1 }; protected static final byte[] ALL_SET_16BYTES = new byte[] { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; // -1 is all-bits-on in 2's-complement for signed values. // Must do it this way since Java has no support for unsigned integral // constants. private static final class AclEntry { private byte[] value; private byte[] mask; // These are the bits in candidate which must match private int bitBlockSize; public boolean allow; public AclEntry(byte[] value, int bitBlockSize, boolean allow) throws AclFormatException { byte[] allOn = null; switch (value.length) { case 4 : allOn = ALL_SET_4BYTES; break; case 16 : allOn = ALL_SET_16BYTES; break; default : throw new IllegalArgumentException( "Only 4 and 16 bytes supported, not " + value.length); } if (bitBlockSize > value.length * 8) { throw new IllegalArgumentException( "Specified " + bitBlockSize + " significant bits, but value only has " + (value.length * 8) + " bits"); } this.bitBlockSize = bitBlockSize; this.value = value; mask = BitMap.leftShift(allOn, value.length * 8 - bitBlockSize); if (mask.length != value.length) { throw new RuntimeException( "Basic program assertion failed. " + "Generated mask length " + mask.length + " (bytes) does not match given value length " + value.length + " (bytes)."); } this.allow = allow; validateMask(); } public String toString() { StringBuilder sb = new StringBuilder("Addrs "); sb.append((value.length == 16) ? ("[" + ServerAcl.colonNotation(value) + ']') : ServerAcl.dottedNotation(value)); sb.append("/" + bitBlockSize + ' ' + (allow ? "ALLOW" : "DENY")); return sb.toString(); } public boolean matches(byte[] candidate) { if (value.length != candidate.length) { return false; } return !BitMap.hasAnyBitSet(BitMap.xor(value, BitMap.and(candidate, mask))); } public void validateMask() throws AclFormatException { if (BitMap.hasAnyBitSet(BitMap.and(value, BitMap.not(mask)))) { throw new AclFormatException( "The base address '" + ServerAcl.dottedNotation(value) + "' is too specific for block-size-spec /" + bitBlockSize); } } }
Params:
  • uba – Unsigned byte array
Returns:String
/** * * @param uba Unsigned byte array * @return String */
public static String dottedNotation(byte[] uba) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < uba.length; i++) { if (i > 0) { sb.append('.'); } sb.append((int) uba[i] & 0xff); } return sb.toString(); }
Params:
  • uba – Unsigned byte array
Returns:String
/** * * @param uba Unsigned byte array * @return String */
public static String colonNotation(byte[] uba) { // TODO: handle odd byte lengths. if ((uba.length / 2) * 2 != uba.length) { throw new RuntimeException( "At this time .colonNotation only handles even byte quantities"); } StringBuilder sb = new StringBuilder(); for (int i = 0; i < uba.length; i += 2) { if (i > 0) { sb.append(':'); } sb.append(Integer.toHexString((uba[i] & 0xff) * 256 + (uba[i + 1] & 0xff))); } return sb.toString(); } private PrintWriter pw = null; public void setPrintWriter(PrintWriter pw) { this.pw = pw; } public String toString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < aclEntries.size(); i++) { if (i > 0) { sb.append('\n'); } sb.append("Entry " + (i + 1) + ": " + aclEntries.get(i)); } return sb.toString(); } private List aclEntries; static private AclEntry PROHIBIT_ALL_IPV4; static private AclEntry PROHIBIT_ALL_IPV6; static { try { PROHIBIT_ALL_IPV4 = new AclEntry(InetAddress.getByName("0.0.0.0").getAddress(), 0, false); PROHIBIT_ALL_IPV6 = new AclEntry(InetAddress.getByName("::").getAddress(), 0, false); } catch (UnknownHostException uke) { // Should never reach here, since no name service is needed to // look up either address. throw new RuntimeException( "Unexpected problem in static initializer", uke); } catch (AclFormatException afe) { throw new RuntimeException( "Unexpected problem in static initializer", afe); } }
Uses system network libraries to resolve the given String to an IP addr, then determine whether this address is permitted or denied. Specified name may be a numerical-based String like "1.2.3.4", a constant known to the networking libraries, or a host name to be resolved by the systems name resolution system. If the given String can't be resolved to an IP addr, false is returned.
Params:
  • s – String
See Also:
  • permitAccess(byte[])
Returns:boolean
/** * Uses system network libraries to resolve the given String to an IP addr, * then determine whether this address is permitted or denied. Specified * name may be a numerical-based String like "1.2.3.4", a constant known to * the networking libraries, or a host name to be resolved by the systems * name resolution system. If the given String can't be resolved to an IP * addr, false is returned. * * @see #permitAccess(byte[]) * @param s String * @return boolean */
public boolean permitAccess(String s) { try { return permitAccess(InetAddress.getByName(s).getAddress()); } catch (UnknownHostException uke) { println("'" + s + "' denied because failed to resolve to an addr"); return false; // Resolution of candidate failed } }
Params:
  • addr – byte[]
Returns:true if access for the candidate address should be permitted, false if access should be denied.
/** * * @return true if access for the candidate address should be permitted, * false if access should be denied. * @param addr byte[] */
public boolean permitAccess(byte[] addr) { ensureAclsUptodate(); for (int i = 0; i < aclEntries.size(); i++) { if (((AclEntry) aclEntries.get(i)).matches(addr)) { AclEntry hit = (AclEntry) aclEntries.get(i); println("Addr '" + ServerAcl.dottedNotation(addr) + "' matched rule #" + (i + 1) + ": " + hit); return hit.allow; } } throw new RuntimeException("No rule matches address '" + ServerAcl.dottedNotation(addr) + "'"); } private void println(String s) { if (pw == null) { return; } pw.println(s); pw.flush(); } private File aclFile; private long lastLoadTime = 0; private static final class InternalException extends Exception {} public ServerAcl(File aclFile) throws IOException, AclFormatException { this.aclFile = aclFile; aclEntries = load(); } synchronized protected void ensureAclsUptodate() { if (lastLoadTime > aclFile.lastModified()) { return; } try { aclEntries = load(); println("ACLs reloaded from file"); return; } catch (Exception e) { println("Failed to reload ACL file. Retaining old ACLs. " + e); } } protected List load() throws IOException, AclFormatException { if (!aclFile.exists()) { throw new IOException("File '" + aclFile.getAbsolutePath() + "' is not present"); } if (!aclFile.isFile()) { throw new IOException("'" + aclFile.getAbsolutePath() + "' is not a regular file"); } if (!aclFile.canRead()) { throw new IOException("'" + aclFile.getAbsolutePath() + "' is not accessible"); } String line; String ruleTypeString; StringTokenizer toker; String addrString, bitString = null; int slashIndex; int linenum = 0; byte[] addr; boolean allow; int bits; BufferedReader br = new BufferedReader(new FileReader(aclFile)); List<AclEntry> newAcls = new ArrayList<AclEntry>(); try { while ((line = br.readLine()) != null) { linenum++; line = line.trim(); if (line.length() < 1) { continue; } if (line.charAt(0) == '#') { continue; } toker = new StringTokenizer(line); try { if (toker.countTokens() != 2) { throw new InternalException(); } ruleTypeString = toker.nextToken(); addrString = toker.nextToken(); slashIndex = addrString.indexOf('/'); if (slashIndex > -1) { bitString = addrString.substring(slashIndex + 1); addrString = addrString.substring(0, slashIndex); } addr = InetAddress.getByName(addrString).getAddress(); bits = (bitString == null) ? (addr.length * 8) : Integer.parseInt(bitString); if (ruleTypeString.equalsIgnoreCase("allow")) { allow = true; } else if (ruleTypeString.equalsIgnoreCase("permit")) { allow = true; } else if (ruleTypeString.equalsIgnoreCase("accept")) { allow = true; } else if (ruleTypeString.equalsIgnoreCase("prohibit")) { allow = false; } else if (ruleTypeString.equalsIgnoreCase("deny")) { allow = false; } else if (ruleTypeString.equalsIgnoreCase("reject")) { allow = false; } else { throw new InternalException(); } } catch (NumberFormatException nfe) { throw new AclFormatException("Syntax error at ACL file '" + aclFile.getAbsolutePath() + "', line " + linenum); } catch (InternalException ie) { throw new AclFormatException("Syntax error at ACL file '" + aclFile.getAbsolutePath() + "', line " + linenum); } try { newAcls.add(new AclEntry(addr, bits, allow)); } catch (AclFormatException afe) { throw new AclFormatException("Syntax error at ACL file '" + aclFile.getAbsolutePath() + "', line " + linenum + ": " + afe.toString()); } } } finally { br.close(); } newAcls.add(PROHIBIT_ALL_IPV4); newAcls.add(PROHIBIT_ALL_IPV6); lastLoadTime = new java.util.Date().getTime(); return newAcls; }
Utility method that allows interactive testing of individual ACL records, as well as the net effect of the ACL record list. Run "java -cp path/to/hsqldb.jar org.hsqldb.server.ServerAcl --help" for Syntax help.
Params:
  • sa – String[]
Throws:
/** * Utility method that allows interactive testing of individual ACL records, * as well as the net effect of the ACL record list. Run "java -cp * path/to/hsqldb.jar org.hsqldb.server.ServerAcl --help" for Syntax help. * * @param sa String[] * @throws AclFormatException when badly formatted * @throws IOException when io error */
public static void main(String[] sa) throws AclFormatException, IOException { if (sa.length > 1) { throw new RuntimeException("Try: java -cp path/to/hsqldb.jar " + ServerAcl.class.getName() + " --help"); } if (sa.length > 0 && sa[0].equals("--help")) { System.err.println("SYNTAX: java -cp path/to/hsqldb.jar " + ServerAcl.class.getName() + " [filepath.txt]"); System.err.println("ACL file path defaults to 'acl.txt' in the " + "current directory."); System.exit(0); } ServerAcl serverAcl = new ServerAcl(new File((sa.length == 0) ? "acl.txt" : sa[0])); serverAcl.setPrintWriter(new PrintWriter(System.out)); System.out.println(serverAcl.toString()); BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); System.out.println("Enter hostnames or IP addresses to be tested " + "(one per line)."); String s; while ((s = br.readLine()) != null) { s = s.trim(); if (s.length() < 1) { continue; } System.out.println(Boolean.toString(serverAcl.permitAccess(s))); } } }