package org.cdlib.xtf.dynaXML; /** * Copyright (c) 2004, Regents of the University of California * 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 University of California 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 THE COPYRIGHT OWNER 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. */ import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.Reader; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; /** * Loads and provides quick access to a map of IP addresses. Reads a file * containing IP addresses and ranges, as well as excluded addresses, and * provides a way to check if a given IP address matches. */ class IpList { /** * Data class to keep track of the four numeric components of an IP * address. */ private class IpAddr { public int[] components = new int[4]; /** Constructs a blank IP address structure */ IpAddr() { } /** Constructs an IP address from the four numeric components */ IpAddr(int c0, int c1, int c2, int c3) { components[0] = c0; components[1] = c1; components[2] = c2; components[3] = c3; } // IpAddr() /** * Checks if the first 'nComps' components of this IP address are * equal to the specified one. * * @param other The IP address to compare to * @param nComps How many components to compare (1-4) */ boolean componentsEqual(IpAddr other, int nComps) { for (int i = 0; i < nComps; i++) { if (components[i] != other.components[i]) return false; } return true; } // componentsEqual() /** * Parses a string of the form "aaa.bbb.ccc.ddd", replacing the * contents of this IP address structure with the results. The * components don't need all three digits to be valid. * * @param str String to parse * * @return The remainder that wasn't part of the IP address, * or null if no IP address could be parsed. */ String parse(String str) { // Parse each component. int pos = 0; for (int compNum = 0; compNum < 4; compNum++) { // Get the next character. If none, that's an error. if (pos >= str.length()) return null; char c = str.charAt(pos++); // If it's a '*', that means 'any' to us. if (c == '*') components[compNum] = 999; // If it's a digit, process the number. else if (Character.isDigit(c)) { // Form the whole number. StringBuffer numStr = new StringBuffer(); numStr.append(c); while (pos < str.length()) { c = str.charAt(pos); if (!Character.isDigit(c)) break; numStr.append(c); pos++; } // Make sure it's 0-255. int num = Integer.parseInt(numStr.toString()); if (num > 255) return null; // Record it. components[compNum] = num; } // else if // Any other character is an error. else return null; // If this isn't the last component, we expect a '.' // separator next. // if (compNum < 3) { if (pos >= str.length()) return null; c = str.charAt(pos++); if (c != '.') return null; } } // for compNum // Return the unparsed remainder of the string. return str.substring(pos); } // parse() } // class IpAddr /** * Data structure to keep track of a range of IP addresses and whether * they are "positive" or "negative". Provides a way to check if an IP * address falls within the range. */ private class IpRange { /** Start of the range */ public IpAddr startAddr; /** End of the range (can be equal to startAddr) */ public IpAddr endAddr; /** * true if the range is specified IP address to include, false if * it specifies addresses to exclude. */ public boolean isPositive; /** * Construct an IP range. * * @param _start Starting address * @param _end Ending address (can equal _start) * @param _isPos true if range specified IPs to include, false to * specify IPs to exclude. */ IpRange(IpAddr _start, IpAddr _end, boolean _isPos) { startAddr = _start; endAddr = _end; isPositive = _isPos; } /** Checks if a specified IP address falls within the range. */ boolean matches(IpAddr addr) { // First, make sure the addr is >= the range start. for (int i = 0; i < 4; i++) { int ic = addr.components[i]; int sc = startAddr.components[i]; if (sc == 999) // handle wildcard sc = 0; if (ic < sc) return false; if (ic > sc) break; } // Then make sure the addr is <= the range end for (int i = 0; i < 4; i++) { int ic = addr.components[i]; int ec = endAddr.components[i]; if (ec == 999) // handle wildcard ec = 255; if (ic > ec) return false; if (ic < ec) break; } return true; } // matches() } // class IpRange /** * Constructs and loads an IP map from the specified file. * * @param path Path to the file to load the IP map from. * * @exception IOException If the IP map file couldn't be opened. */ public IpList(String path) throws IOException { ranges = new ArrayList<IpRange>(); readRanges(path); } // IpList() /** * Parses the given IP address and checks whether it falls within one * of the positive ranges of the map, and doesn't fall in one of the * excluded ranges. * * @param ipAddrStr A string of the form "a.b.c.d" where each component * is a decimal number from 0-255. * * @return true if and only if the address matches. */ public boolean isApproved(String ipAddrStr) { // First, parse the address we got. If we can't, it's definitely not // approved. // IpAddr parsedAddr = new IpAddr(); if (parsedAddr.parse(ipAddrStr) == null) return false; // Scan each entry. We used to try to be clever and use a sorted map to // limit the number we had to scan, but this has been shown to fail in // the case of overlapping entries. Consider for instance if the first // entry in the map is "0.0.0.0 - 255.255.255.255". No sorting algorithm // is going to help you -- that entry *always* has to be checked. By // induction, *every* entry *always* has to be checked. // boolean positive = false; boolean negative = false; for (IpRange range : ranges) { // See if the parsed address matches the one in the map. If not, // skip it. // if (!range.matches(parsedAddr)) continue; // Found a matching entry. Set the appropriate flag. if (range.isPositive) positive = true; else negative = true; } // while // If any matching negative entry was found, that takes precedence. if (negative) return false; // If a positive entry was found, the address is approved. if (positive) return true; // If there was no match, the address is not approved. return false; } // isApproved() /** * Reads the contents the given file into the IP map. * * @param path Path to the file to load * * @exception IOException If the file couldn't be read from. */ private void readRanges(String path) throws IOException { Reader rawReader; if (path.startsWith("http://")) { URLConnection conn = new URL(path).openConnection(); conn.setAllowUserInteraction(false); conn.setDoInput(true); conn.setDoOutput(false); conn.setUseCaches(false); conn.connect(); rawReader = new InputStreamReader(conn.getInputStream()); } else rawReader = new FileReader(path); // Open the file for reading line-by-line. LineNumberReader reader = new LineNumberReader(rawReader); // Process each line in turn. while (true) { String line = reader.readLine(); if (line == null) break; // Strip leading and trailing whitespace. line = line.trim(); // Skip blank lines. if (line.equals("")) continue; // Lines beginning with numbers specify positive IP addresses if (Character.isDigit(line.charAt(0)) || (line.charAt(0) == '*')) { processEntry(line, true); } // Lines beginning with "exclude" specify negative IP addresses else if (line.startsWith("exclude")) { line = line.substring(7).trim(); processEntry(line, false); } // Ignore other kinds of lines. else continue; } } // readMap() /** * Used by readMap to parse a single entry in the IP map file. * * @param line The line of text to parse * @param isPositive true if this is an "exclude" line */ private void processEntry(String line, boolean isPositive) { IpAddr startIp = new IpAddr(); IpAddr endIp = new IpAddr(); // Parse the first IP address. If invalid, skip this line. String str = line; str = startIp.parse(str); if (str == null) return; // If the next char is a "-", then it's a range. str = str.trim(); if (str.startsWith("-")) { // Strip the "-" and any spaces after it. str = str.substring(1).trim(); // Now try to parse the end IP addr. If invalid, skip the line. str = endIp.parse(str); if (str == null) return; } // Otherwise, it's a single address. else endIp = startIp; // Add the new entry IpRange range = new IpRange(startIp, endIp, isPositive); ranges.add(range); } /** List of IpRanges. */ private ArrayList<IpRange> ranges; } // class IpList