All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.hsqldb.server.ServerAcl Maven / Gradle / Ivy

There is a newer version: 2.7.4
Show newest version
/* 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))); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy