package examples.security.net;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StreamCorruptedException;
import java.net.InetAddress;
import java.util.StringTokenizer;
import java.util.Vector;
import weblogic.security.net.ConnectionEvent;
import weblogic.security.net.ConnectionFilter;
import weblogic.security.net.FilterException;
/**
* Simple rule-based connection filter example. This example reads in
* a set of rules from a file and bases its filtering decisions on
* these rules. <p>
*
* Syntax of the rule file is as follows: each rule is written on a
* single line. Tokens in a rule are separated by whitespace. "#" is
* the comment character; everything after it on a line is ignored.
* Whitespace before or after a rule is ignored. Lines consisting
* solely of whitespace or comments are skipped. <p>
*
* All rules follow this form:
*
* <pre>
target action protocols
</pre>
*
* where <tt>target</tt> is a specification of one or more hosts to
* filter, <tt>action</tt> is the action to perform (and must be
* either <tt>allow</tt> or <tt>deny</tt>), and <tt>protocols</tt> is
* the list of protocol names to match (must be one of <tt>http</tt>,
* <tt>https</tt>, <tt>t3</tt>, <tt>t3s</tt>, or <tt>giop</tt>; if no
* protocols are listed, all protocols will match a rule). <p>
*
* This example recognizes two kinds of rule:
*
* <ul>
*
* <li>A "fast" rule applies to a hostname or IP address, with an
* optional netmask. If a hostname corresponds to multiple IP
* addresses, multiple rules are generated (in no particular order).
* Netmasks can be specified either in numeric or dotted-quad form.
* Examples:
*
* <pre>
dialup-650-555-1212.pa.example.net deny t3 t3s # http and https OK
192.168.81.0/255.255.254.0 allow # 23-bit netmask
192.168.0.0/16 deny # like /255.255.0.0
</pre>
*
* Hostnames for fast rules are looked up once, at server startup
* time. While this greatly reduces connect-time overhead, it can
* result in the filter having an out-of-date idea of what addresses
* correspond to a hostname. For maximal comfort of mind, use numeric
* IP addresses instead.
*
* <li>A "slow" rule applies to part of a domain name. Since it
* requires a connect-time DNS lookup on a client in order to perform
* a match, it may be much slower than the "fast" rule, and it may
* also be subject to DNS spoofing. Slow rules are specified as follows:
*
* <pre>
*.script-kiddiez.org deny
</pre>
*
* The "*" <i>only</i> matches at the head of a pattern. If you
* specify one anywhere else, it will be treated as part of the
* pattern (and so that pattern will never match anything, since "*"
* is not a legal part of a domain name).
*
* </ul>
*
* When a client connects, these rules are evaluated in the order in
* which they were written, and the first rule to match determines how
* the connection is treated. If no rules match, the connection is
* permitted. <p>
*
* If you want to "lock down" your server and only allow connections
* from certain addresses, you can specify <tt>0.0.0.0/0 deny</tt> as
* your last rule. <p>
*
* Note that this example does not take full advantage of the
* information provided by the connection filter. Further expansion
* is left as an exercise for the reader. Note further that this
* example assumes IPv4 addresses, but it should be easy to convert it
* to use IPv6 addresses, if necessary.
*
* @author Copyright (c) 1999-2000 by BEA Systems, Inc. All Rights Reserved.
*/
public class SimpleConnectionFilter
implements ConnectionFilter
{
/**
* The name of the filter rule file.
*/
public static final String FILTER_FILE = "filter";
/**
* This is our set of filter rules.
*/
private FilterEntry[] rules;
/**
* Construct a new connection filter. This constructor attempts to
* find the rule file in either the current directory or as a
* resource in the server's classpath.
*
* @see #FILTER_FILE
* @exception IOException a problem occurred while reading the rule
* file
*/
public SimpleConnectionFilter()
throws IOException
{
InputStream in = null;
try
{
in = new FileInputStream(FILTER_FILE);
}
catch (FileNotFoundException e)
{
// ignore
}
if (in == null)
{
in = SimpleConnectionFilter.class.getResourceAsStream(FILTER_FILE);
}
if (in == null)
{
throw new FileNotFoundException("cannot find \"" + FILTER_FILE +
"\" in current directory or classpath");
}
setup(in);
}
/**
* Construct a new connection filter. Rules are read from the given
* stream.
*
* @param is stream to read from
* @exception IOException a problem occurred while reading the rule
* file
*/
public SimpleConnectionFilter(InputStream is)
throws IOException
{
setup(is);
}
/**
* Read rules from the given stream.
*
* @param is stream to read from
* @exception IOException a problem occurred while reading the rule
* file
*/
private void setup(InputStream is)
throws IOException
{
LineNumberReader r = new LineNumberReader(new InputStreamReader(is));
Vector entries = new Vector();
String line;
while ((line = r.readLine()) != null)
{
// Ignore comments, surrounding whitespace, and empty lines.
int hash = line.indexOf('#');
if (hash != -1)
{
line = line.substring(0, hash).trim();
}
if (line.length() == 0)
{
continue;
}
try
{
parseLine(line, entries);
}
catch (StreamCorruptedException e)
{
throw new IOException("line " + r.getLineNumber() + ": " + e.getMessage());
}
catch (IllegalArgumentException e)
{
throw new IOException("line " + r.getLineNumber() + ": " + e.getMessage());
}
}
rules = new FilterEntry[entries.size()];
entries.copyInto(rules);
}
/**
* Parse an individual line of the rule file. Any resulting rules
* are added to the given entries vector.
*
* @param line the line to parse (guaranteed not to contain
* comments, surrounding whitespace, or be empty)
* @param entries the running list of rules
*/
protected void parseLine(String line, Vector entries)
throws IOException, IllegalArgumentException
{
StringTokenizer toks = new StringTokenizer(line);
String addr = toks.nextToken();
boolean allow = parseAction(toks.nextToken());
// If it starts with a star, it's a slow rule.
if (addr.startsWith("*"))
{
SlowFilterEntry sent = new SlowFilterEntry(allow,
parseProtocols(toks),
addr);
entries.addElement(sent);
} else {
// It doesn't start with a star; it's a fast rule. Check for a
// netmask.
int slash = addr.indexOf('/');
int[] addrs = null;
int netmask = 0xffffffff; // if no explicit netmask, require exact match
if (slash != -1)
{
addrs = parseAddresses(addr.substring(0, slash));
netmask = parseNetmask(addr.substring(slash + 1));
} else {
addrs = parseAddresses(addr);
}
int protomask = parseProtocols(toks);
// We may have obtained multiple addresses from a DNS lookup.
// Add a rule for each. Note that most DNS servers will return
// multiple addresses in different orders on every lookup.
for (int i = 0; i < addrs.length; i++)
{
FastFilterEntry fent = new FastFilterEntry(allow,
protomask,
addrs[i],
netmask);
entries.addElement(fent);
}
}
}
/**
* Filter a client connection event. If the connection should be
* allowed, this method returns normally.
*
* @param evt the connection event
* @exception FilterException the connection should be rejected by
* the server
*/
public void accept(ConnectionEvent evt)
throws FilterException
{
InetAddress remoteAddress = evt.getRemoteAddress();
String protocol = evt.getProtocol().toLowerCase();
int bit = protocolToMaskBit(protocol);
if (bit == 0xdeadbeef)
{
bit = 0;
}
// Check rules in the order in which they were written.
for (int i = 0; i < rules.length; i++)
{
switch (rules[i].check(remoteAddress, bit))
{
case FilterEntry.ALLOW:
return;
case FilterEntry.DENY:
throw new FilterException("rule " + (i + 1));
case FilterEntry.IGNORE:
break;
default:
throw new RuntimeException("connection filter internal error!");
}
}
// If no rule matched, we allow the connection to succeed.
return;
}
/**
* This array is used for quick matching of protocols to bits. It
* should only contain protocols that the server supports.
*/
private static final String[] PROTOCOLS = new String[]
{
"http", "t3", "https", "t3s", "giop"
};
/**
* Parse a list of protocols and return a bitmask that will let us
* match a protocol quickly at connect time.
*/
protected static final int parseProtocols(StringTokenizer toks)
throws FilterException
{
int protomask = 0;
while (toks.hasMoreTokens())
{
String tok = toks.nextToken();
int bit = protocolToMaskBit(tok);
if (bit == 0xdeadbeef)
{
throw new IllegalArgumentException("unknown protocol \"" + tok + "\"");
}
protomask |= bit;
}
return protomask;
}
/**
* Parse a single protocol description into a bit in a field.
*/
private static final int protocolToMaskBit(String proto)
throws FilterException
{
if (proto == null)
{
return 0;
}
proto = proto.toLowerCase();
for (int i = 1; i < PROTOCOLS.length; i++)
{
if (proto.equals(PROTOCOLS[i]))
{
return 1 << i;
}
}
// Magic return indicating no match.
return 0xdeadbeef;
}
/**
* Given a string, return an array of IPv4 addresses corresponding
* to that string as a host.
*
* @param str hostname or IPv4 address in string form
*/
protected static final int[] parseAddresses(String str)
throws IOException
{
InetAddress[] addrs = InetAddress.getAllByName(str);
int[] raw = new int[addrs.length];
for (int i = 0; i < addrs.length; i++)
{
raw[i] = addressToInt(addrs[i]);
}
return raw;
}
/**
* Turn an address object into a single IPv4 address.
*/
static final int addressToInt(InetAddress addr)
{
byte[] roh = addr.getAddress();
int raw = 0;
for (int j = 0; j < roh.length; j++)
{
raw |= (0xff & roh[j]) << (8 * (roh.length - j - 1));
}
return raw;
}
/**
* Return an IPv4 netmask, as derived from a spec string. The
* string can either be a number, for a mask length, or a
* dotted-quad mask.
*
* @param maskStr mask spec string
*/
protected static final int parseNetmask(String maskStr)
throws IOException
{
StringTokenizer toks = new StringTokenizer(maskStr, ".");
int ntoks = toks.countTokens();
try
{
if (ntoks == 1)
{
int bits = Integer.parseInt(toks.nextToken());
if (bits > 32 || bits < 0)
{
throw new StreamCorruptedException("bad netmask: \"" + maskStr + "\"");
}
return ~((1 << (32 - bits)) - 1);
}
int mask = 0;
if (ntoks != 4)
{
throw new StreamCorruptedException("bad netmask: \"" + maskStr + "\"");
} else {
for (int i = 24; toks.hasMoreTokens(); i -= 8)
{
int num = Integer.parseInt(toks.nextToken());
if (num < 0 || num > 255)
{
throw new StreamCorruptedException("bad netmask: \"" + maskStr + "\"");
}
mask |= num << i;
}
}
return mask;
}
catch (NumberFormatException e)
{
throw new StreamCorruptedException("bad netmask: \"" + maskStr + "\"");
}
}
/**
* Parse an action and return its meaning. True to allow, false to
* deny.
*
* @param whatever the action string
*/
protected static final boolean parseAction(String whatever)
throws IOException
{
String action = whatever.toLowerCase();
if (action.equals("allow"))
{
return true;
}
else if (action.equals("deny"))
{
return false;
} else {
throw new StreamCorruptedException("bad action \"" + action + "\"");
}
}
/**
* Simple test harness. You can use this to write rules by hand,
* and then check them.
*/
public static void main(String[] args)
throws Exception
{
System.out.println("Enter rules on separate lines:");
ConnectionFilter cf = new SimpleConnectionFilter(System.in);
LineNumberReader r =
new LineNumberReader(new InputStreamReader(System.in));
String line;
System.out.println("Enter addresses on separate lines:");
while ((line = r.readLine()) != null)
{
try
{
StringTokenizer toks = new StringTokenizer(line.trim());
String addr = toks.nextToken();
String proto = toks.nextToken();
InetAddress[] addrs = InetAddress.getAllByName(addr);
for (int i = 0; i < addrs.length; i++)
{
try
{
cf.accept(new ConnectionEvent(addrs[i], 0, 0, proto));
System.out.println("ALLOW");
}
catch (FilterException e)
{
System.out.println("DENY: " + e.getMessage());
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
}