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();
      }
    }
  }
}
