org.hsqldb.server.ServerAcl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hsqldb Show documentation
Show all versions of hsqldb Show documentation
HSQLDB - Lightweight 100% Java SQL Database Engine
/* Copyright (c) 2001-2024, 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:
*
{@code
* {allow|deny} <ip_address>[/significant-bits]
* }
* For example
* {@code
* 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":
*
{@code
* allow x.admc.com/24
* }
*
*
*
* @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);
}
}
static final byte[] ALL_SET_4BYTES = new byte[]{ -1, -1, -1, -1 };
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();
sb.append("Addrs ")
.append((value.length == 16)
? ("[" + ServerAcl.colonNotation(value) + ']')
: ServerAcl.dottedNotation(value))
.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);
}
}
}
/**
*
* @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();
}
/**
*
* @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.
*
* @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
}
}
/**
*
* @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 ((aclEntries.get(i)).matches(addr)) {
AclEntry hit = 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 void ensureAclsUptodate() {
if (lastLoadTime > aclFile.lastModified()) {
return;
}
try {
aclEntries = load();
println("ACLs reloaded from file");
} catch (Exception e) {
println("Failed to reload ACL file. Retaining old ACLs. " + e);
}
}
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 newAcls = new ArrayList<>();
try {
while ((line = br.readLine()) != null) {
linenum++;
line = line.trim();
if (line.isEmpty()) {
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.
*
* @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.isEmpty()) {
continue;
}
System.out.println(Boolean.toString(serverAcl.permitAccess(s)));
}
}
}