diff options
Diffstat (limited to 'src/org/xbill')
131 files changed, 22488 insertions, 0 deletions
diff --git a/src/org/xbill/DNS/A6Record.java b/src/org/xbill/DNS/A6Record.java new file mode 100644 index 0000000..a1c613a --- /dev/null +++ b/src/org/xbill/DNS/A6Record.java @@ -0,0 +1,127 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; + +/** + * A6 Record - maps a domain name to an IPv6 address (experimental) + * + * @author Brian Wellington + */ + +public class A6Record extends Record { + +private static final long serialVersionUID = -8815026887337346789L; + +private int prefixBits; +private InetAddress suffix; +private Name prefix; + +A6Record() {} + +Record +getObject() { + return new A6Record(); +} + +/** + * Creates an A6 Record from the given data + * @param prefixBits The number of bits in the address prefix + * @param suffix The address suffix + * @param prefix The name of the prefix + */ +public +A6Record(Name name, int dclass, long ttl, int prefixBits, + InetAddress suffix, Name prefix) +{ + super(name, Type.A6, dclass, ttl); + this.prefixBits = checkU8("prefixBits", prefixBits); + if (suffix != null && Address.familyOf(suffix) != Address.IPv6) + throw new IllegalArgumentException("invalid IPv6 address"); + this.suffix = suffix; + if (prefix != null) + this.prefix = checkName("prefix", prefix); +} + +void +rrFromWire(DNSInput in) throws IOException { + prefixBits = in.readU8(); + int suffixbits = 128 - prefixBits; + int suffixbytes = (suffixbits + 7) / 8; + if (prefixBits < 128) { + byte [] bytes = new byte[16]; + in.readByteArray(bytes, 16 - suffixbytes, suffixbytes); + suffix = InetAddress.getByAddress(bytes); + } + if (prefixBits > 0) + prefix = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + prefixBits = st.getUInt8(); + if (prefixBits > 128) { + throw st.exception("prefix bits must be [0..128]"); + } else if (prefixBits < 128) { + String s = st.getString(); + try { + suffix = Address.getByAddress(s, Address.IPv6); + } + catch (UnknownHostException e) { + throw st.exception("invalid IPv6 address: " + s); + } + } + if (prefixBits > 0) + prefix = st.getName(origin); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(prefixBits); + if (suffix != null) { + sb.append(" "); + sb.append(suffix.getHostAddress()); + } + if (prefix != null) { + sb.append(" "); + sb.append(prefix); + } + return sb.toString(); +} + +/** Returns the number of bits in the prefix */ +public int +getPrefixBits() { + return prefixBits; +} + +/** Returns the address suffix */ +public InetAddress +getSuffix() { + return suffix; +} + +/** Returns the address prefix */ +public Name +getPrefix() { + return prefix; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(prefixBits); + if (suffix != null) { + int suffixbits = 128 - prefixBits; + int suffixbytes = (suffixbits + 7) / 8; + byte [] data = suffix.getAddress(); + out.writeByteArray(data, 16 - suffixbytes, suffixbytes); + } + if (prefix != null) + prefix.toWire(out, null, canonical); +} + +} diff --git a/src/org/xbill/DNS/AAAARecord.java b/src/org/xbill/DNS/AAAARecord.java new file mode 100644 index 0000000..4b637aa --- /dev/null +++ b/src/org/xbill/DNS/AAAARecord.java @@ -0,0 +1,67 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; + +/** + * IPv6 Address Record - maps a domain name to an IPv6 address + * + * @author Brian Wellington + */ + +public class AAAARecord extends Record { + +private static final long serialVersionUID = -4588601512069748050L; + +private InetAddress address; + +AAAARecord() {} + +Record +getObject() { + return new AAAARecord(); +} + +/** + * Creates an AAAA Record from the given data + * @param address The address suffix + */ +public +AAAARecord(Name name, int dclass, long ttl, InetAddress address) { + super(name, Type.AAAA, dclass, ttl); + if (Address.familyOf(address) != Address.IPv6) + throw new IllegalArgumentException("invalid IPv6 address"); + this.address = address; +} + +void +rrFromWire(DNSInput in) throws IOException { + address = InetAddress.getByAddress(name.toString(), + in.readByteArray(16)); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + address = st.getAddress(Address.IPv6); +} + +/** Converts rdata to a String */ +String +rrToString() { + return address.getHostAddress(); +} + +/** Returns the address */ +public InetAddress +getAddress() { + return address; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(address.getAddress()); +} + +} diff --git a/src/org/xbill/DNS/AFSDBRecord.java b/src/org/xbill/DNS/AFSDBRecord.java new file mode 100644 index 0000000..4814faa --- /dev/null +++ b/src/org/xbill/DNS/AFSDBRecord.java @@ -0,0 +1,46 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * AFS Data Base Record - maps a domain name to the name of an AFS cell + * database server. + * + * + * @author Brian Wellington + */ + +public class AFSDBRecord extends U16NameBase { + +private static final long serialVersionUID = 3034379930729102437L; + +AFSDBRecord() {} + +Record +getObject() { + return new AFSDBRecord(); +} + +/** + * Creates an AFSDB Record from the given data. + * @param subtype Indicates the type of service provided by the host. + * @param host The host providing the service. + */ +public +AFSDBRecord(Name name, int dclass, long ttl, int subtype, Name host) { + super(name, Type.AFSDB, dclass, ttl, subtype, "subtype", host, "host"); +} + +/** Gets the subtype indicating the service provided by the host. */ +public int +getSubtype() { + return getU16Field(); +} + +/** Gets the host providing service for the domain. */ +public Name +getHost() { + return getNameField(); +} + +} diff --git a/src/org/xbill/DNS/APLRecord.java b/src/org/xbill/DNS/APLRecord.java new file mode 100644 index 0000000..5940da2 --- /dev/null +++ b/src/org/xbill/DNS/APLRecord.java @@ -0,0 +1,287 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * APL - Address Prefix List. See RFC 3123. + * + * @author Brian Wellington + */ + +/* + * Note: this currently uses the same constants as the Address class; + * this could change if more constants are defined for APL records. + */ + +public class APLRecord extends Record { + +public static class Element { + public final int family; + public final boolean negative; + public final int prefixLength; + public final Object address; + + private + Element(int family, boolean negative, Object address, int prefixLength) + { + this.family = family; + this.negative = negative; + this.address = address; + this.prefixLength = prefixLength; + if (!validatePrefixLength(family, prefixLength)) { + throw new IllegalArgumentException("invalid prefix " + + "length"); + } + } + + /** + * Creates an APL element corresponding to an IPv4 or IPv6 prefix. + * @param negative Indicates if this prefix is a negation. + * @param address The IPv4 or IPv6 address. + * @param prefixLength The length of this prefix, in bits. + * @throws IllegalArgumentException The prefix length is invalid. + */ + public + Element(boolean negative, InetAddress address, int prefixLength) { + this(Address.familyOf(address), negative, address, + prefixLength); + } + + public String + toString() { + StringBuffer sb = new StringBuffer(); + if (negative) + sb.append("!"); + sb.append(family); + sb.append(":"); + if (family == Address.IPv4 || family == Address.IPv6) + sb.append(((InetAddress) address).getHostAddress()); + else + sb.append(base16.toString((byte []) address)); + sb.append("/"); + sb.append(prefixLength); + return sb.toString(); + } + + public boolean + equals(Object arg) { + if (arg == null || !(arg instanceof Element)) + return false; + Element elt = (Element) arg; + return (family == elt.family && + negative == elt.negative && + prefixLength == elt.prefixLength && + address.equals(elt.address)); + } + + public int + hashCode() { + return address.hashCode() + prefixLength + (negative ? 1 : 0); + } +} + +private static final long serialVersionUID = -1348173791712935864L; + +private List elements; + +APLRecord() {} + +Record +getObject() { + return new APLRecord(); +} + +private static boolean +validatePrefixLength(int family, int prefixLength) { + if (prefixLength < 0 || prefixLength >= 256) + return false; + if ((family == Address.IPv4 && prefixLength > 32) || + (family == Address.IPv6 && prefixLength > 128)) + return false; + return true; +} + +/** + * Creates an APL Record from the given data. + * @param elements The list of APL elements. + */ +public +APLRecord(Name name, int dclass, long ttl, List elements) { + super(name, Type.APL, dclass, ttl); + this.elements = new ArrayList(elements.size()); + for (Iterator it = elements.iterator(); it.hasNext(); ) { + Object o = it.next(); + if (!(o instanceof Element)) { + throw new IllegalArgumentException("illegal element"); + } + Element element = (Element) o; + if (element.family != Address.IPv4 && + element.family != Address.IPv6) + { + throw new IllegalArgumentException("unknown family"); + } + this.elements.add(element); + + } +} + +private static byte [] +parseAddress(byte [] in, int length) throws WireParseException { + if (in.length > length) + throw new WireParseException("invalid address length"); + if (in.length == length) + return in; + byte [] out = new byte[length]; + System.arraycopy(in, 0, out, 0, in.length); + return out; +} + +void +rrFromWire(DNSInput in) throws IOException { + elements = new ArrayList(1); + while (in.remaining() != 0) { + int family = in.readU16(); + int prefix = in.readU8(); + int length = in.readU8(); + boolean negative = (length & 0x80) != 0; + length &= ~0x80; + + byte [] data = in.readByteArray(length); + Element element; + if (!validatePrefixLength(family, prefix)) { + throw new WireParseException("invalid prefix length"); + } + + if (family == Address.IPv4 || family == Address.IPv6) { + data = parseAddress(data, + Address.addressLength(family)); + InetAddress addr = InetAddress.getByAddress(data); + element = new Element(negative, addr, prefix); + } else { + element = new Element(family, negative, data, prefix); + } + elements.add(element); + + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + elements = new ArrayList(1); + while (true) { + Tokenizer.Token t = st.get(); + if (!t.isString()) + break; + + boolean negative = false; + int family = 0; + int prefix = 0; + + String s = t.value; + int start = 0; + if (s.startsWith("!")) { + negative = true; + start = 1; + } + int colon = s.indexOf(':', start); + if (colon < 0) + throw st.exception("invalid address prefix element"); + int slash = s.indexOf('/', colon); + if (slash < 0) + throw st.exception("invalid address prefix element"); + + String familyString = s.substring(start, colon); + String addressString = s.substring(colon + 1, slash); + String prefixString = s.substring(slash + 1); + + try { + family = Integer.parseInt(familyString); + } + catch (NumberFormatException e) { + throw st.exception("invalid family"); + } + if (family != Address.IPv4 && family != Address.IPv6) + throw st.exception("unknown family"); + + try { + prefix = Integer.parseInt(prefixString); + } + catch (NumberFormatException e) { + throw st.exception("invalid prefix length"); + } + + if (!validatePrefixLength(family, prefix)) { + throw st.exception("invalid prefix length"); + } + + byte [] bytes = Address.toByteArray(addressString, family); + if (bytes == null) + throw st.exception("invalid IP address " + + addressString); + + InetAddress address = InetAddress.getByAddress(bytes); + elements.add(new Element(negative, address, prefix)); + } + st.unget(); +} + +String +rrToString() { + StringBuffer sb = new StringBuffer(); + for (Iterator it = elements.iterator(); it.hasNext(); ) { + Element element = (Element) it.next(); + sb.append(element); + if (it.hasNext()) + sb.append(" "); + } + return sb.toString(); +} + +/** Returns the list of APL elements. */ +public List +getElements() { + return elements; +} + +private static int +addressLength(byte [] addr) { + for (int i = addr.length - 1; i >= 0; i--) { + if (addr[i] != 0) + return i + 1; + } + return 0; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + for (Iterator it = elements.iterator(); it.hasNext(); ) { + Element element = (Element) it.next(); + int length = 0; + byte [] data; + if (element.family == Address.IPv4 || + element.family == Address.IPv6) + { + InetAddress addr = (InetAddress) element.address; + data = addr.getAddress(); + length = addressLength(data); + } else { + data = (byte []) element.address; + length = data.length; + } + int wlength = length; + if (element.negative) { + wlength |= 0x80; + } + out.writeU16(element.family); + out.writeU8(element.prefixLength); + out.writeU8(wlength); + out.writeByteArray(data, 0, length); + } +} + +} diff --git a/src/org/xbill/DNS/ARecord.java b/src/org/xbill/DNS/ARecord.java new file mode 100644 index 0000000..4e13aa7 --- /dev/null +++ b/src/org/xbill/DNS/ARecord.java @@ -0,0 +1,90 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.net.*; +import java.io.*; + +/** + * Address Record - maps a domain name to an Internet address + * + * @author Brian Wellington + */ + +public class ARecord extends Record { + +private static final long serialVersionUID = -2172609200849142323L; + +private int addr; + +ARecord() {} + +Record +getObject() { + return new ARecord(); +} + +private static final int +fromArray(byte [] array) { + return (((array[0] & 0xFF) << 24) | + ((array[1] & 0xFF) << 16) | + ((array[2] & 0xFF) << 8) | + (array[3] & 0xFF)); +} + +private static final byte [] +toArray(int addr) { + byte [] bytes = new byte[4]; + bytes[0] = (byte) ((addr >>> 24) & 0xFF); + bytes[1] = (byte) ((addr >>> 16) & 0xFF); + bytes[2] = (byte) ((addr >>> 8) & 0xFF); + bytes[3] = (byte) (addr & 0xFF); + return bytes; +} + +/** + * Creates an A Record from the given data + * @param address The address that the name refers to + */ +public +ARecord(Name name, int dclass, long ttl, InetAddress address) { + super(name, Type.A, dclass, ttl); + if (Address.familyOf(address) != Address.IPv4) + throw new IllegalArgumentException("invalid IPv4 address"); + addr = fromArray(address.getAddress()); +} + +void +rrFromWire(DNSInput in) throws IOException { + addr = fromArray(in.readByteArray(4)); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + InetAddress address = st.getAddress(Address.IPv4); + addr = fromArray(address.getAddress()); +} + +/** Converts rdata to a String */ +String +rrToString() { + return (Address.toDottedQuad(toArray(addr))); +} + +/** Returns the Internet address */ +public InetAddress +getAddress() { + try { + return InetAddress.getByAddress(name.toString(), + toArray(addr)); + } catch (UnknownHostException e) { + return null; + } +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU32(((long)addr) & 0xFFFFFFFFL); +} + +} diff --git a/src/org/xbill/DNS/Address.java b/src/org/xbill/DNS/Address.java new file mode 100644 index 0000000..799185b --- /dev/null +++ b/src/org/xbill/DNS/Address.java @@ -0,0 +1,402 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.net.*; +import java.net.Inet6Address; + +/** + * Routines dealing with IP addresses. Includes functions similar to + * those in the java.net.InetAddress class. + * + * @author Brian Wellington + */ + +public final class Address { + +public static final int IPv4 = 1; +public static final int IPv6 = 2; + +private +Address() {} + +private static byte [] +parseV4(String s) { + int numDigits; + int currentOctet; + byte [] values = new byte[4]; + int currentValue; + int length = s.length(); + + currentOctet = 0; + currentValue = 0; + numDigits = 0; + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c >= '0' && c <= '9') { + /* Can't have more than 3 digits per octet. */ + if (numDigits == 3) + return null; + /* Octets shouldn't start with 0, unless they are 0. */ + if (numDigits > 0 && currentValue == 0) + return null; + numDigits++; + currentValue *= 10; + currentValue += (c - '0'); + /* 255 is the maximum value for an octet. */ + if (currentValue > 255) + return null; + } else if (c == '.') { + /* Can't have more than 3 dots. */ + if (currentOctet == 3) + return null; + /* Two consecutive dots are bad. */ + if (numDigits == 0) + return null; + values[currentOctet++] = (byte) currentValue; + currentValue = 0; + numDigits = 0; + } else + return null; + } + /* Must have 4 octets. */ + if (currentOctet != 3) + return null; + /* The fourth octet can't be empty. */ + if (numDigits == 0) + return null; + values[currentOctet] = (byte) currentValue; + return values; +} + +private static byte [] +parseV6(String s) { + int range = -1; + byte [] data = new byte[16]; + + String [] tokens = s.split(":", -1); + + int first = 0; + int last = tokens.length - 1; + + if (tokens[0].length() == 0) { + // If the first two tokens are empty, it means the string + // started with ::, which is fine. If only the first is + // empty, the string started with :, which is bad. + if (last - first > 0 && tokens[1].length() == 0) + first++; + else + return null; + } + + if (tokens[last].length() == 0) { + // If the last two tokens are empty, it means the string + // ended with ::, which is fine. If only the last is + // empty, the string ended with :, which is bad. + if (last - first > 0 && tokens[last - 1].length() == 0) + last--; + else + return null; + } + + if (last - first + 1 > 8) + return null; + + int i, j; + for (i = first, j = 0; i <= last; i++) { + if (tokens[i].length() == 0) { + if (range >= 0) + return null; + range = j; + continue; + } + + if (tokens[i].indexOf('.') >= 0) { + // An IPv4 address must be the last component + if (i < last) + return null; + // There can't have been more than 6 components. + if (i > 6) + return null; + byte [] v4addr = Address.toByteArray(tokens[i], IPv4); + if (v4addr == null) + return null; + for (int k = 0; k < 4; k++) + data[j++] = v4addr[k]; + break; + } + + try { + for (int k = 0; k < tokens[i].length(); k++) { + char c = tokens[i].charAt(k); + if (Character.digit(c, 16) < 0) + return null; + } + int x = Integer.parseInt(tokens[i], 16); + if (x > 0xFFFF || x < 0) + return null; + data[j++] = (byte)(x >>> 8); + data[j++] = (byte)(x & 0xFF); + } + catch (NumberFormatException e) { + return null; + } + } + + if (j < 16 && range < 0) + return null; + + if (range >= 0) { + int empty = 16 - j; + System.arraycopy(data, range, data, range + empty, j - range); + for (i = range; i < range + empty; i++) + data[i] = 0; + } + + return data; +} + +/** + * Convert a string containing an IP address to an array of 4 or 16 integers. + * @param s The address, in text format. + * @param family The address family. + * @return The address + */ +public static int [] +toArray(String s, int family) { + byte [] byteArray = toByteArray(s, family); + if (byteArray == null) + return null; + int [] intArray = new int[byteArray.length]; + for (int i = 0; i < byteArray.length; i++) + intArray[i] = byteArray[i] & 0xFF; + return intArray; +} + +/** + * Convert a string containing an IPv4 address to an array of 4 integers. + * @param s The address, in text format. + * @return The address + */ +public static int [] +toArray(String s) { + return toArray(s, IPv4); +} + +/** + * Convert a string containing an IP address to an array of 4 or 16 bytes. + * @param s The address, in text format. + * @param family The address family. + * @return The address + */ +public static byte [] +toByteArray(String s, int family) { + if (family == IPv4) + return parseV4(s); + else if (family == IPv6) + return parseV6(s); + else + throw new IllegalArgumentException("unknown address family"); +} + +/** + * Determines if a string contains a valid IP address. + * @param s The string + * @return Whether the string contains a valid IP address + */ +public static boolean +isDottedQuad(String s) { + byte [] address = Address.toByteArray(s, IPv4); + return (address != null); +} + +/** + * Converts a byte array containing an IPv4 address into a dotted quad string. + * @param addr The array + * @return The string representation + */ +public static String +toDottedQuad(byte [] addr) { + return ((addr[0] & 0xFF) + "." + (addr[1] & 0xFF) + "." + + (addr[2] & 0xFF) + "." + (addr[3] & 0xFF)); +} + +/** + * Converts an int array containing an IPv4 address into a dotted quad string. + * @param addr The array + * @return The string representation + */ +public static String +toDottedQuad(int [] addr) { + return (addr[0] + "." + addr[1] + "." + addr[2] + "." + addr[3]); +} + +private static Record [] +lookupHostName(String name) throws UnknownHostException { + try { + Record [] records = new Lookup(name).run(); + if (records == null) + throw new UnknownHostException("unknown host"); + return records; + } + catch (TextParseException e) { + throw new UnknownHostException("invalid name"); + } +} + +private static InetAddress +addrFromRecord(String name, Record r) throws UnknownHostException { + ARecord a = (ARecord) r; + return InetAddress.getByAddress(name, a.getAddress().getAddress()); +} + +/** + * Determines the IP address of a host + * @param name The hostname to look up + * @return The first matching IP address + * @exception UnknownHostException The hostname does not have any addresses + */ +public static InetAddress +getByName(String name) throws UnknownHostException { + try { + return getByAddress(name); + } catch (UnknownHostException e) { + Record [] records = lookupHostName(name); + return addrFromRecord(name, records[0]); + } +} + +/** + * Determines all IP address of a host + * @param name The hostname to look up + * @return All matching IP addresses + * @exception UnknownHostException The hostname does not have any addresses + */ +public static InetAddress [] +getAllByName(String name) throws UnknownHostException { + try { + InetAddress addr = getByAddress(name); + return new InetAddress[] {addr}; + } catch (UnknownHostException e) { + Record [] records = lookupHostName(name); + InetAddress [] addrs = new InetAddress[records.length]; + for (int i = 0; i < records.length; i++) + addrs[i] = addrFromRecord(name, records[i]); + return addrs; + } +} + +/** + * Converts an address from its string representation to an IP address. + * The address can be either IPv4 or IPv6. + * @param addr The address, in string form + * @return The IP addresses + * @exception UnknownHostException The address is not a valid IP address. + */ +public static InetAddress +getByAddress(String addr) throws UnknownHostException { + byte [] bytes; + bytes = toByteArray(addr, IPv4); + if (bytes != null) + return InetAddress.getByAddress(addr, bytes); + bytes = toByteArray(addr, IPv6); + if (bytes != null) + return InetAddress.getByAddress(addr, bytes); + throw new UnknownHostException("Invalid address: " + addr); +} + +/** + * Converts an address from its string representation to an IP address in + * a particular family. + * @param addr The address, in string form + * @param family The address family, either IPv4 or IPv6. + * @return The IP addresses + * @exception UnknownHostException The address is not a valid IP address in + * the specified address family. + */ +public static InetAddress +getByAddress(String addr, int family) throws UnknownHostException { + if (family != IPv4 && family != IPv6) + throw new IllegalArgumentException("unknown address family"); + byte [] bytes; + bytes = toByteArray(addr, family); + if (bytes != null) + return InetAddress.getByAddress(addr, bytes); + throw new UnknownHostException("Invalid address: " + addr); +} + +/** + * Determines the hostname for an address + * @param addr The address to look up + * @return The associated host name + * @exception UnknownHostException There is no hostname for the address + */ +public static String +getHostName(InetAddress addr) throws UnknownHostException { + Name name = ReverseMap.fromAddress(addr); + Record [] records = new Lookup(name, Type.PTR).run(); + if (records == null) + throw new UnknownHostException("unknown address"); + PTRRecord ptr = (PTRRecord) records[0]; + return ptr.getTarget().toString(); +} + +/** + * Returns the family of an InetAddress. + * @param address The supplied address. + * @return The family, either IPv4 or IPv6. + */ +public static int +familyOf(InetAddress address) { + if (address instanceof Inet4Address) + return IPv4; + if (address instanceof Inet6Address) + return IPv6; + throw new IllegalArgumentException("unknown address family"); +} + +/** + * Returns the length of an address in a particular family. + * @param family The address family, either IPv4 or IPv6. + * @return The length of addresses in that family. + */ +public static int +addressLength(int family) { + if (family == IPv4) + return 4; + if (family == IPv6) + return 16; + throw new IllegalArgumentException("unknown address family"); +} + +/** + * Truncates an address to the specified number of bits. For example, + * truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0. + * @param address The source address + * @param maskLength The number of bits to truncate the address to. + */ +public static InetAddress +truncate(InetAddress address, int maskLength) +{ + int family = familyOf(address); + int maxMaskLength = addressLength(family) * 8; + if (maskLength < 0 || maskLength > maxMaskLength) + throw new IllegalArgumentException("invalid mask length"); + if (maskLength == maxMaskLength) + return address; + byte [] bytes = address.getAddress(); + for (int i = maskLength / 8 + 1; i < bytes.length; i++) + bytes[i] = 0; + int maskBits = maskLength % 8; + int bitmask = 0; + for (int i = 0; i < maskBits; i++) + bitmask |= (1 << (7 - i)); + bytes[maskLength / 8] &= bitmask; + try { + return InetAddress.getByAddress(bytes); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("invalid address"); + } +} + +} diff --git a/src/org/xbill/DNS/CERTRecord.java b/src/org/xbill/DNS/CERTRecord.java new file mode 100644 index 0000000..39bcef3 --- /dev/null +++ b/src/org/xbill/DNS/CERTRecord.java @@ -0,0 +1,224 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * Certificate Record - Stores a certificate associated with a name. The + * certificate might also be associated with a KEYRecord. + * @see KEYRecord + * + * @author Brian Wellington + */ + +public class CERTRecord extends Record { + +public static class CertificateType { + /** Certificate type identifiers. See RFC 4398 for more detail. */ + + private CertificateType() {} + + /** PKIX (X.509v3) */ + public static final int PKIX = 1; + + /** Simple Public Key Infrastructure */ + public static final int SPKI = 2; + + /** Pretty Good Privacy */ + public static final int PGP = 3; + + /** URL of an X.509 data object */ + public static final int IPKIX = 4; + + /** URL of an SPKI certificate */ + public static final int ISPKI = 5; + + /** Fingerprint and URL of an OpenPGP packet */ + public static final int IPGP = 6; + + /** Attribute Certificate */ + public static final int ACPKIX = 7; + + /** URL of an Attribute Certificate */ + public static final int IACPKIX = 8; + + /** Certificate format defined by URI */ + public static final int URI = 253; + + /** Certificate format defined by OID */ + public static final int OID = 254; + + private static Mnemonic types = new Mnemonic("Certificate type", + Mnemonic.CASE_UPPER); + + static { + types.setMaximum(0xFFFF); + types.setNumericAllowed(true); + + types.add(PKIX, "PKIX"); + types.add(SPKI, "SPKI"); + types.add(PGP, "PGP"); + types.add(PKIX, "IPKIX"); + types.add(SPKI, "ISPKI"); + types.add(PGP, "IPGP"); + types.add(PGP, "ACPKIX"); + types.add(PGP, "IACPKIX"); + types.add(URI, "URI"); + types.add(OID, "OID"); + } + + /** + * Converts a certificate type into its textual representation + */ + public static String + string(int type) { + return types.getText(type); + } + + /** + * Converts a textual representation of an certificate type into its + * numeric code. Integers in the range 0..65535 are also accepted. + * @param s The textual representation of the algorithm + * @return The algorithm code, or -1 on error. + */ + public static int + value(String s) { + return types.getValue(s); + } +} + +/** PKIX (X.509v3) */ +public static final int PKIX = CertificateType.PKIX; + +/** Simple Public Key Infrastructure */ +public static final int SPKI = CertificateType.SPKI; + +/** Pretty Good Privacy */ +public static final int PGP = CertificateType.PGP; + +/** Certificate format defined by URI */ +public static final int URI = CertificateType.URI; + +/** Certificate format defined by IOD */ +public static final int OID = CertificateType.OID; + +private static final long serialVersionUID = 4763014646517016835L; + +private int certType, keyTag; +private int alg; +private byte [] cert; + +CERTRecord() {} + +Record +getObject() { + return new CERTRecord(); +} + +/** + * Creates a CERT Record from the given data + * @param certType The type of certificate (see constants) + * @param keyTag The ID of the associated KEYRecord, if present + * @param alg The algorithm of the associated KEYRecord, if present + * @param cert Binary data representing the certificate + */ +public +CERTRecord(Name name, int dclass, long ttl, int certType, int keyTag, + int alg, byte [] cert) +{ + super(name, Type.CERT, dclass, ttl); + this.certType = checkU16("certType", certType); + this.keyTag = checkU16("keyTag", keyTag); + this.alg = checkU8("alg", alg); + this.cert = cert; +} + +void +rrFromWire(DNSInput in) throws IOException { + certType = in.readU16(); + keyTag = in.readU16(); + alg = in.readU8(); + cert = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String certTypeString = st.getString(); + certType = CertificateType.value(certTypeString); + if (certType < 0) + throw st.exception("Invalid certificate type: " + + certTypeString); + keyTag = st.getUInt16(); + String algString = st.getString(); + alg = DNSSEC.Algorithm.value(algString); + if (alg < 0) + throw st.exception("Invalid algorithm: " + algString); + cert = st.getBase64(); +} + +/** + * Converts rdata to a String + */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append (certType); + sb.append (" "); + sb.append (keyTag); + sb.append (" "); + sb.append (alg); + if (cert != null) { + if (Options.check("multiline")) { + sb.append(" (\n"); + sb.append(base64.formatString(cert, 64, "\t", true)); + } else { + sb.append(" "); + sb.append(base64.toString(cert)); + } + } + return sb.toString(); +} + +/** + * Returns the type of certificate + */ +public int +getCertType() { + return certType; +} + +/** + * Returns the ID of the associated KEYRecord, if present + */ +public int +getKeyTag() { + return keyTag; +} + +/** + * Returns the algorithm of the associated KEYRecord, if present + */ +public int +getAlgorithm() { + return alg; +} + +/** + * Returns the binary representation of the certificate + */ +public byte [] +getCert() { + return cert; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(certType); + out.writeU16(keyTag); + out.writeU8(alg); + out.writeByteArray(cert); +} + +} diff --git a/src/org/xbill/DNS/CNAMERecord.java b/src/org/xbill/DNS/CNAMERecord.java new file mode 100644 index 0000000..8db9453 --- /dev/null +++ b/src/org/xbill/DNS/CNAMERecord.java @@ -0,0 +1,45 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * CNAME Record - maps an alias to its real name + * + * @author Brian Wellington + */ + +public class CNAMERecord extends SingleCompressedNameBase { + +private static final long serialVersionUID = -4020373886892538580L; + +CNAMERecord() {} + +Record +getObject() { + return new CNAMERecord(); +} + +/** + * Creates a new CNAMERecord with the given data + * @param alias The name to which the CNAME alias points + */ +public +CNAMERecord(Name name, int dclass, long ttl, Name alias) { + super(name, Type.CNAME, dclass, ttl, alias, "alias"); +} + +/** + * Gets the target of the CNAME Record + */ +public Name +getTarget() { + return getSingleName(); +} + +/** Gets the alias specified by the CNAME Record */ +public Name +getAlias() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/Cache.java b/src/org/xbill/DNS/Cache.java new file mode 100644 index 0000000..5497f45 --- /dev/null +++ b/src/org/xbill/DNS/Cache.java @@ -0,0 +1,846 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A cache of DNS records. The cache obeys TTLs, so items are purged after + * their validity period is complete. Negative answers are cached, to + * avoid repeated failed DNS queries. The credibility of each RRset is + * maintained, so that more credible records replace less credible records, + * and lookups can specify the minimum credibility of data they are requesting. + * @see RRset + * @see Credibility + * + * @author Brian Wellington + */ + +public class Cache { + +private interface Element { + public boolean expired(); + public int compareCredibility(int cred); + public int getType(); +} + +private static int +limitExpire(long ttl, long maxttl) { + if (maxttl >= 0 && maxttl < ttl) + ttl = maxttl; + long expire = (System.currentTimeMillis() / 1000) + ttl; + if (expire < 0 || expire > Integer.MAX_VALUE) + return Integer.MAX_VALUE; + return (int)expire; +} + +private static class CacheRRset extends RRset implements Element { + private static final long serialVersionUID = 5971755205903597024L; + + int credibility; + int expire; + + public + CacheRRset(Record rec, int cred, long maxttl) { + super(); + this.credibility = cred; + this.expire = limitExpire(rec.getTTL(), maxttl); + addRR(rec); + } + + public + CacheRRset(RRset rrset, int cred, long maxttl) { + super(rrset); + this.credibility = cred; + this.expire = limitExpire(rrset.getTTL(), maxttl); + } + + public final boolean + expired() { + int now = (int)(System.currentTimeMillis() / 1000); + return (now >= expire); + } + + public final int + compareCredibility(int cred) { + return credibility - cred; + } + + public String + toString() { + StringBuffer sb = new StringBuffer(); + sb.append(super.toString()); + sb.append(" cl = "); + sb.append(credibility); + return sb.toString(); + } +} + +private static class NegativeElement implements Element { + int type; + Name name; + int credibility; + int expire; + + public + NegativeElement(Name name, int type, SOARecord soa, int cred, + long maxttl) + { + this.name = name; + this.type = type; + long cttl = 0; + if (soa != null) + cttl = soa.getMinimum(); + this.credibility = cred; + this.expire = limitExpire(cttl, maxttl); + } + + public int + getType() { + return type; + } + + public final boolean + expired() { + int now = (int)(System.currentTimeMillis() / 1000); + return (now >= expire); + } + + public final int + compareCredibility(int cred) { + return credibility - cred; + } + + public String + toString() { + StringBuffer sb = new StringBuffer(); + if (type == 0) + sb.append("NXDOMAIN " + name); + else + sb.append("NXRRSET " + name + " " + Type.string(type)); + sb.append(" cl = "); + sb.append(credibility); + return sb.toString(); + } +} + +private static class CacheMap extends LinkedHashMap { + private int maxsize = -1; + + CacheMap(int maxsize) { + super(16, (float) 0.75, true); + this.maxsize = maxsize; + } + + int + getMaxSize() { + return maxsize; + } + + void + setMaxSize(int maxsize) { + /* + * Note that this doesn't shrink the size of the map if + * the maximum size is lowered, but it should shrink as + * entries expire. + */ + this.maxsize = maxsize; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return maxsize >= 0 && size() > maxsize; + } +} + +private CacheMap data; +private int maxncache = -1; +private int maxcache = -1; +private int dclass; + +private static final int defaultMaxEntries = 50000; + +/** + * Creates an empty Cache + * + * @param dclass The DNS class of this cache + * @see DClass + */ +public +Cache(int dclass) { + this.dclass = dclass; + data = new CacheMap(defaultMaxEntries); +} + +/** + * Creates an empty Cache for class IN. + * @see DClass + */ +public +Cache() { + this(DClass.IN); +} + +/** + * Creates a Cache which initially contains all records in the specified file. + */ +public +Cache(String file) throws IOException { + data = new CacheMap(defaultMaxEntries); + Master m = new Master(file); + Record record; + while ((record = m.nextRecord()) != null) + addRecord(record, Credibility.HINT, m); +} + +private synchronized Object +exactName(Name name) { + return data.get(name); +} + +private synchronized void +removeName(Name name) { + data.remove(name); +} + +private synchronized Element [] +allElements(Object types) { + if (types instanceof List) { + List typelist = (List) types; + int size = typelist.size(); + return (Element []) typelist.toArray(new Element[size]); + } else { + Element set = (Element) types; + return new Element[] {set}; + } +} + +private synchronized Element +oneElement(Name name, Object types, int type, int minCred) { + Element found = null; + + if (type == Type.ANY) + throw new IllegalArgumentException("oneElement(ANY)"); + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + Element set = (Element) list.get(i); + if (set.getType() == type) { + found = set; + break; + } + } + } else { + Element set = (Element) types; + if (set.getType() == type) + found = set; + } + if (found == null) + return null; + if (found.expired()) { + removeElement(name, type); + return null; + } + if (found.compareCredibility(minCred) < 0) + return null; + return found; +} + +private synchronized Element +findElement(Name name, int type, int minCred) { + Object types = exactName(name); + if (types == null) + return null; + return oneElement(name, types, type, minCred); +} + +private synchronized void +addElement(Name name, Element element) { + Object types = data.get(name); + if (types == null) { + data.put(name, element); + return; + } + int type = element.getType(); + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + Element elt = (Element) list.get(i); + if (elt.getType() == type) { + list.set(i, element); + return; + } + } + list.add(element); + } else { + Element elt = (Element) types; + if (elt.getType() == type) + data.put(name, element); + else { + LinkedList list = new LinkedList(); + list.add(elt); + list.add(element); + data.put(name, list); + } + } +} + +private synchronized void +removeElement(Name name, int type) { + Object types = data.get(name); + if (types == null) { + return; + } + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + Element elt = (Element) list.get(i); + if (elt.getType() == type) { + list.remove(i); + if (list.size() == 0) + data.remove(name); + return; + } + } + } else { + Element elt = (Element) types; + if (elt.getType() != type) + return; + data.remove(name); + } +} + +/** Empties the Cache. */ +public synchronized void +clearCache() { + data.clear(); +} + +/** + * Adds a record to the Cache. + * @param r The record to be added + * @param cred The credibility of the record + * @param o The source of the record (this could be a Message, for example) + * @see Record + */ +public synchronized void +addRecord(Record r, int cred, Object o) { + Name name = r.getName(); + int type = r.getRRsetType(); + if (!Type.isRR(type)) + return; + Element element = findElement(name, type, cred); + if (element == null) { + CacheRRset crrset = new CacheRRset(r, cred, maxcache); + addRRset(crrset, cred); + } else if (element.compareCredibility(cred) == 0) { + if (element instanceof CacheRRset) { + CacheRRset crrset = (CacheRRset) element; + crrset.addRR(r); + } + } +} + +/** + * Adds an RRset to the Cache. + * @param rrset The RRset to be added + * @param cred The credibility of these records + * @see RRset + */ +public synchronized void +addRRset(RRset rrset, int cred) { + long ttl = rrset.getTTL(); + Name name = rrset.getName(); + int type = rrset.getType(); + Element element = findElement(name, type, 0); + if (ttl == 0) { + if (element != null && element.compareCredibility(cred) <= 0) + removeElement(name, type); + } else { + if (element != null && element.compareCredibility(cred) <= 0) + element = null; + if (element == null) { + CacheRRset crrset; + if (rrset instanceof CacheRRset) + crrset = (CacheRRset) rrset; + else + crrset = new CacheRRset(rrset, cred, maxcache); + addElement(name, crrset); + } + } +} + +/** + * Adds a negative entry to the Cache. + * @param name The name of the negative entry + * @param type The type of the negative entry + * @param soa The SOA record to add to the negative cache entry, or null. + * The negative cache ttl is derived from the SOA. + * @param cred The credibility of the negative entry + */ +public synchronized void +addNegative(Name name, int type, SOARecord soa, int cred) { + long ttl = 0; + if (soa != null) + ttl = soa.getTTL(); + Element element = findElement(name, type, 0); + if (ttl == 0) { + if (element != null && element.compareCredibility(cred) <= 0) + removeElement(name, type); + } else { + if (element != null && element.compareCredibility(cred) <= 0) + element = null; + if (element == null) + addElement(name, new NegativeElement(name, type, + soa, cred, + maxncache)); + } +} + +/** + * Finds all matching sets or something that causes the lookup to stop. + */ +protected synchronized SetResponse +lookup(Name name, int type, int minCred) { + int labels; + int tlabels; + Element element; + Name tname; + Object types; + SetResponse sr; + + labels = name.labels(); + + for (tlabels = labels; tlabels >= 1; tlabels--) { + boolean isRoot = (tlabels == 1); + boolean isExact = (tlabels == labels); + + if (isRoot) + tname = Name.root; + else if (isExact) + tname = name; + else + tname = new Name(name, labels - tlabels); + + types = data.get(tname); + if (types == null) + continue; + + /* + * If this is the name, look for the actual type or a CNAME + * (unless it's an ANY query, where we return everything). + * Otherwise, look for a DNAME. + */ + if (isExact && type == Type.ANY) { + sr = new SetResponse(SetResponse.SUCCESSFUL); + Element [] elements = allElements(types); + int added = 0; + for (int i = 0; i < elements.length; i++) { + element = elements[i]; + if (element.expired()) { + removeElement(tname, element.getType()); + continue; + } + if (!(element instanceof CacheRRset)) + continue; + if (element.compareCredibility(minCred) < 0) + continue; + sr.addRRset((CacheRRset)element); + added++; + } + /* There were positive entries */ + if (added > 0) + return sr; + } else if (isExact) { + element = oneElement(tname, types, type, minCred); + if (element != null && + element instanceof CacheRRset) + { + sr = new SetResponse(SetResponse.SUCCESSFUL); + sr.addRRset((CacheRRset) element); + return sr; + } else if (element != null) { + sr = new SetResponse(SetResponse.NXRRSET); + return sr; + } + + element = oneElement(tname, types, Type.CNAME, minCred); + if (element != null && + element instanceof CacheRRset) + { + return new SetResponse(SetResponse.CNAME, + (CacheRRset) element); + } + } else { + element = oneElement(tname, types, Type.DNAME, minCred); + if (element != null && + element instanceof CacheRRset) + { + return new SetResponse(SetResponse.DNAME, + (CacheRRset) element); + } + } + + /* Look for an NS */ + element = oneElement(tname, types, Type.NS, minCred); + if (element != null && element instanceof CacheRRset) + return new SetResponse(SetResponse.DELEGATION, + (CacheRRset) element); + + /* Check for the special NXDOMAIN element. */ + if (isExact) { + element = oneElement(tname, types, 0, minCred); + if (element != null) + return SetResponse.ofType(SetResponse.NXDOMAIN); + } + + } + return SetResponse.ofType(SetResponse.UNKNOWN); +} + +/** + * Looks up Records in the Cache. This follows CNAMEs and handles negatively + * cached data. + * @param name The name to look up + * @param type The type to look up + * @param minCred The minimum acceptable credibility + * @return A SetResponse object + * @see SetResponse + * @see Credibility + */ +public SetResponse +lookupRecords(Name name, int type, int minCred) { + return lookup(name, type, minCred); +} + +private RRset [] +findRecords(Name name, int type, int minCred) { + SetResponse cr = lookupRecords(name, type, minCred); + if (cr.isSuccessful()) + return cr.answers(); + else + return null; +} + +/** + * Looks up credible Records in the Cache (a wrapper around lookupRecords). + * Unlike lookupRecords, this given no indication of why failure occurred. + * @param name The name to look up + * @param type The type to look up + * @return An array of RRsets, or null + * @see Credibility + */ +public RRset [] +findRecords(Name name, int type) { + return findRecords(name, type, Credibility.NORMAL); +} + +/** + * Looks up Records in the Cache (a wrapper around lookupRecords). Unlike + * lookupRecords, this given no indication of why failure occurred. + * @param name The name to look up + * @param type The type to look up + * @return An array of RRsets, or null + * @see Credibility + */ +public RRset [] +findAnyRecords(Name name, int type) { + return findRecords(name, type, Credibility.GLUE); +} + +private final int +getCred(int section, boolean isAuth) { + if (section == Section.ANSWER) { + if (isAuth) + return Credibility.AUTH_ANSWER; + else + return Credibility.NONAUTH_ANSWER; + } else if (section == Section.AUTHORITY) { + if (isAuth) + return Credibility.AUTH_AUTHORITY; + else + return Credibility.NONAUTH_AUTHORITY; + } else if (section == Section.ADDITIONAL) { + return Credibility.ADDITIONAL; + } else + throw new IllegalArgumentException("getCred: invalid section"); +} + +private static void +markAdditional(RRset rrset, Set names) { + Record first = rrset.first(); + if (first.getAdditionalName() == null) + return; + + Iterator it = rrset.rrs(); + while (it.hasNext()) { + Record r = (Record) it.next(); + Name name = r.getAdditionalName(); + if (name != null) + names.add(name); + } +} + +/** + * Adds all data from a Message into the Cache. Each record is added with + * the appropriate credibility, and negative answers are cached as such. + * @param in The Message to be added + * @return A SetResponse that reflects what would be returned from a cache + * lookup, or null if nothing useful could be cached from the message. + * @see Message + */ +public SetResponse +addMessage(Message in) { + boolean isAuth = in.getHeader().getFlag(Flags.AA); + Record question = in.getQuestion(); + Name qname; + Name curname; + int qtype; + int qclass; + int cred; + int rcode = in.getHeader().getRcode(); + boolean completed = false; + RRset [] answers, auth, addl; + SetResponse response = null; + boolean verbose = Options.check("verbosecache"); + HashSet additionalNames; + + if ((rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) || + question == null) + return null; + + qname = question.getName(); + qtype = question.getType(); + qclass = question.getDClass(); + + curname = qname; + + additionalNames = new HashSet(); + + answers = in.getSectionRRsets(Section.ANSWER); + for (int i = 0; i < answers.length; i++) { + if (answers[i].getDClass() != qclass) + continue; + int type = answers[i].getType(); + Name name = answers[i].getName(); + cred = getCred(Section.ANSWER, isAuth); + if ((type == qtype || qtype == Type.ANY) && + name.equals(curname)) + { + addRRset(answers[i], cred); + completed = true; + if (curname == qname) { + if (response == null) + response = new SetResponse( + SetResponse.SUCCESSFUL); + response.addRRset(answers[i]); + } + markAdditional(answers[i], additionalNames); + } else if (type == Type.CNAME && name.equals(curname)) { + CNAMERecord cname; + addRRset(answers[i], cred); + if (curname == qname) + response = new SetResponse(SetResponse.CNAME, + answers[i]); + cname = (CNAMERecord) answers[i].first(); + curname = cname.getTarget(); + } else if (type == Type.DNAME && curname.subdomain(name)) { + DNAMERecord dname; + addRRset(answers[i], cred); + if (curname == qname) + response = new SetResponse(SetResponse.DNAME, + answers[i]); + dname = (DNAMERecord) answers[i].first(); + try { + curname = curname.fromDNAME(dname); + } + catch (NameTooLongException e) { + break; + } + } + } + + auth = in.getSectionRRsets(Section.AUTHORITY); + RRset soa = null, ns = null; + for (int i = 0; i < auth.length; i++) { + if (auth[i].getType() == Type.SOA && + curname.subdomain(auth[i].getName())) + soa = auth[i]; + else if (auth[i].getType() == Type.NS && + curname.subdomain(auth[i].getName())) + ns = auth[i]; + } + if (!completed) { + /* This is a negative response or a referral. */ + int cachetype = (rcode == Rcode.NXDOMAIN) ? 0 : qtype; + if (rcode == Rcode.NXDOMAIN || soa != null || ns == null) { + /* Negative response */ + cred = getCred(Section.AUTHORITY, isAuth); + SOARecord soarec = null; + if (soa != null) + soarec = (SOARecord) soa.first(); + addNegative(curname, cachetype, soarec, cred); + if (response == null) { + int responseType; + if (rcode == Rcode.NXDOMAIN) + responseType = SetResponse.NXDOMAIN; + else + responseType = SetResponse.NXRRSET; + response = SetResponse.ofType(responseType); + } + /* DNSSEC records are not cached. */ + } else { + /* Referral response */ + cred = getCred(Section.AUTHORITY, isAuth); + addRRset(ns, cred); + markAdditional(ns, additionalNames); + if (response == null) + response = new SetResponse( + SetResponse.DELEGATION, + ns); + } + } else if (rcode == Rcode.NOERROR && ns != null) { + /* Cache the NS set from a positive response. */ + cred = getCred(Section.AUTHORITY, isAuth); + addRRset(ns, cred); + markAdditional(ns, additionalNames); + } + + addl = in.getSectionRRsets(Section.ADDITIONAL); + for (int i = 0; i < addl.length; i++) { + int type = addl[i].getType(); + if (type != Type.A && type != Type.AAAA && type != Type.A6) + continue; + Name name = addl[i].getName(); + if (!additionalNames.contains(name)) + continue; + cred = getCred(Section.ADDITIONAL, isAuth); + addRRset(addl[i], cred); + } + if (verbose) + System.out.println("addMessage: " + response); + return (response); +} + +/** + * Flushes an RRset from the cache + * @param name The name of the records to be flushed + * @param type The type of the records to be flushed + * @see RRset + */ +public void +flushSet(Name name, int type) { + removeElement(name, type); +} + +/** + * Flushes all RRsets with a given name from the cache + * @param name The name of the records to be flushed + * @see RRset + */ +public void +flushName(Name name) { + removeName(name); +} + +/** + * Sets the maximum length of time that a negative response will be stored + * in this Cache. A negative value disables this feature (that is, sets + * no limit). + */ +public void +setMaxNCache(int seconds) { + maxncache = seconds; +} + +/** + * Gets the maximum length of time that a negative response will be stored + * in this Cache. A negative value indicates no limit. + */ +public int +getMaxNCache() { + return maxncache; +} + +/** + * Sets the maximum length of time that records will be stored in this + * Cache. A negative value disables this feature (that is, sets no limit). + */ +public void +setMaxCache(int seconds) { + maxcache = seconds; +} + +/** + * Gets the maximum length of time that records will be stored + * in this Cache. A negative value indicates no limit. + */ +public int +getMaxCache() { + return maxcache; +} + +/** + * Gets the current number of entries in the Cache, where an entry consists + * of all records with a specific Name. + */ +public int +getSize() { + return data.size(); +} + +/** + * Gets the maximum number of entries in the Cache, where an entry consists + * of all records with a specific Name. A negative value is treated as an + * infinite limit. + */ +public int +getMaxEntries() { + return data.getMaxSize(); +} + +/** + * Sets the maximum number of entries in the Cache, where an entry consists + * of all records with a specific Name. A negative value is treated as an + * infinite limit. + * + * Note that setting this to a value lower than the current number + * of entries will not cause the Cache to shrink immediately. + * + * The default maximum number of entries is 50000. + * + * @param entries The maximum number of entries in the Cache. + */ +public void +setMaxEntries(int entries) { + data.setMaxSize(entries); +} + +/** + * Returns the DNS class of this cache. + */ +public int +getDClass() { + return dclass; +} + +/** + * Returns the contents of the Cache as a string. + */ +public String +toString() { + StringBuffer sb = new StringBuffer(); + synchronized (this) { + Iterator it = data.values().iterator(); + while (it.hasNext()) { + Element [] elements = allElements(it.next()); + for (int i = 0; i < elements.length; i++) { + sb.append(elements[i]); + sb.append("\n"); + } + } + } + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/Client.java b/src/org/xbill/DNS/Client.java new file mode 100644 index 0000000..2eef44f --- /dev/null +++ b/src/org/xbill/DNS/Client.java @@ -0,0 +1,58 @@ +// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import java.nio.channels.*; +import org.xbill.DNS.utils.hexdump; + +class Client { + +protected long endTime; +protected SelectionKey key; + +protected +Client(SelectableChannel channel, long endTime) throws IOException { + boolean done = false; + Selector selector = null; + this.endTime = endTime; + try { + selector = Selector.open(); + channel.configureBlocking(false); + key = channel.register(selector, SelectionKey.OP_READ); + done = true; + } + finally { + if (!done && selector != null) + selector.close(); + if (!done) + channel.close(); + } +} + +static protected void +blockUntil(SelectionKey key, long endTime) throws IOException { + long timeout = endTime - System.currentTimeMillis(); + int nkeys = 0; + if (timeout > 0) + nkeys = key.selector().select(timeout); + else if (timeout == 0) + nkeys = key.selector().selectNow(); + if (nkeys == 0) + throw new SocketTimeoutException(); +} + +static protected void +verboseLog(String prefix, byte [] data) { + if (Options.check("verbosemsg")) + System.err.println(hexdump.dump(prefix, data)); +} + +void +cleanup() throws IOException { + key.selector().close(); + key.channel().close(); +} + +} diff --git a/src/org/xbill/DNS/ClientSubnetOption.java b/src/org/xbill/DNS/ClientSubnetOption.java new file mode 100644 index 0000000..4a98a12 --- /dev/null +++ b/src/org/xbill/DNS/ClientSubnetOption.java @@ -0,0 +1,175 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.net.*; +import java.util.regex.*; + +/** + * The Client Subnet EDNS Option, defined in + * http://tools.ietf.org/html/draft-vandergaast-edns-client-subnet-00 + * ("Client subnet in DNS requests"). + * + * The option is used to convey information about the IP address of the + * originating client, so that an authoritative server can make decisions + * based on this address, rather than the address of the intermediate + * caching name server. + * + * The option is transmitted as part of an OPTRecord in the additional section + * of a DNS message, as defined by RFC 2671 (EDNS0). + * + * An option code has not been assigned by IANA; the value 20730 (used here) is + * also used by several other implementations. + * + * The wire format of the option contains a 2-byte length field (1 for IPv4, 2 + * for IPv6), a 1-byte source netmask, a 1-byte scope netmask, and an address + * truncated to the source netmask length (where the final octet is padded with + * bits set to 0) + * + * + * @see OPTRecord + * + * @author Brian Wellington + * @author Ming Zhou <mizhou@bnivideo.com>, Beaumaris Networks + */ +public class ClientSubnetOption extends EDNSOption { + +private static final long serialVersionUID = -3868158449890266347L; + +private int family; +private int sourceNetmask; +private int scopeNetmask; +private InetAddress address; + +ClientSubnetOption() { + super(EDNSOption.Code.CLIENT_SUBNET); +} + +private static int +checkMaskLength(String field, int family, int val) { + int max = Address.addressLength(family) * 8; + if (val < 0 || val > max) + throw new IllegalArgumentException("\"" + field + "\" " + val + + " must be in the range " + + "[0.." + max + "]"); + return val; +} + +/** + * Construct a Client Subnet option. Note that the number of significant bits in + * the address must not be greater than the supplied source netmask. + * XXX something about Java's mapped addresses + * @param sourceNetmask The length of the netmask pertaining to the query. + * In replies, it mirrors the same value as in the requests. + * @param scopeNetmask The length of the netmask pertaining to the reply. + * In requests, it MUST be set to 0. In responses, this may or may not match + * the source netmask. + * @param address The address of the client. + */ +public +ClientSubnetOption(int sourceNetmask, int scopeNetmask, InetAddress address) { + super(EDNSOption.Code.CLIENT_SUBNET); + + this.family = Address.familyOf(address); + this.sourceNetmask = checkMaskLength("source netmask", this.family, + sourceNetmask); + this.scopeNetmask = checkMaskLength("scope netmask", this.family, + scopeNetmask); + this.address = Address.truncate(address, sourceNetmask); + + if (!address.equals(this.address)) + throw new IllegalArgumentException("source netmask is not " + + "valid for address"); +} + +/** + * Construct a Client Subnet option with scope netmask set to 0. + * @param sourceNetmask The length of the netmask pertaining to the query. + * In replies, it mirrors the same value as in the requests. + * @param address The address of the client. + * @see ClientSubnetOption + */ +public +ClientSubnetOption(int sourceNetmask, InetAddress address) { + this(sourceNetmask, 0, address); +} + +/** + * Returns the family of the network address. This will be either IPv4 (1) + * or IPv6 (2). + */ +public int +getFamily() { + return family; +} + +/** Returns the source netmask. */ +public int +getSourceNetmask() { + return sourceNetmask; +} + +/** Returns the scope netmask. */ +public int +getScopeNetmask() { + return scopeNetmask; +} + +/** Returns the IP address of the client. */ +public InetAddress +getAddress() { + return address; +} + +void +optionFromWire(DNSInput in) throws WireParseException { + family = in.readU16(); + if (family != Address.IPv4 && family != Address.IPv6) + throw new WireParseException("unknown address family"); + sourceNetmask = in.readU8(); + if (sourceNetmask > Address.addressLength(family) * 8) + throw new WireParseException("invalid source netmask"); + scopeNetmask = in.readU8(); + if (scopeNetmask > Address.addressLength(family) * 8) + throw new WireParseException("invalid scope netmask"); + + // Read the truncated address + byte [] addr = in.readByteArray(); + if (addr.length != (sourceNetmask + 7) / 8) + throw new WireParseException("invalid address"); + + // Convert it to a full length address. + byte [] fulladdr = new byte[Address.addressLength(family)]; + System.arraycopy(addr, 0, fulladdr, 0, addr.length); + + try { + address = InetAddress.getByAddress(fulladdr); + } catch (UnknownHostException e) { + throw new WireParseException("invalid address", e); + } + + InetAddress tmp = Address.truncate(address, sourceNetmask); + if (!tmp.equals(address)) + throw new WireParseException("invalid padding"); +} + +void +optionToWire(DNSOutput out) { + out.writeU16(family); + out.writeU8(sourceNetmask); + out.writeU8(scopeNetmask); + out.writeByteArray(address.getAddress(), 0, (sourceNetmask + 7) / 8); +} + +String +optionToString() { + StringBuffer sb = new StringBuffer(); + sb.append(address.getHostAddress()); + sb.append("/"); + sb.append(sourceNetmask); + sb.append(", scope netmask "); + sb.append(scopeNetmask); + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/Compression.java b/src/org/xbill/DNS/Compression.java new file mode 100644 index 0000000..e3e81c0 --- /dev/null +++ b/src/org/xbill/DNS/Compression.java @@ -0,0 +1,72 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * DNS Name Compression object. + * @see Message + * @see Name + * + * @author Brian Wellington + */ + +public class Compression { + +private static class Entry { + Name name; + int pos; + Entry next; +} + +private static final int TABLE_SIZE = 17; +private static final int MAX_POINTER = 0x3FFF; +private Entry [] table; +private boolean verbose = Options.check("verbosecompression"); + +/** + * Creates a new Compression object. + */ +public +Compression() { + table = new Entry[TABLE_SIZE]; +} + +/** + * Adds a compression entry mapping a name to a position in a message. + * @param pos The position at which the name is added. + * @param name The name being added to the message. + */ +public void +add(int pos, Name name) { + if (pos > MAX_POINTER) + return; + int row = (name.hashCode() & 0x7FFFFFFF) % TABLE_SIZE; + Entry entry = new Entry(); + entry.name = name; + entry.pos = pos; + entry.next = table[row]; + table[row] = entry; + if (verbose) + System.err.println("Adding " + name + " at " + pos); +} + +/** + * Retrieves the position of the given name, if it has been previously + * included in the message. + * @param name The name to find in the compression table. + * @return The position of the name, or -1 if not found. + */ +public int +get(Name name) { + int row = (name.hashCode() & 0x7FFFFFFF) % TABLE_SIZE; + int pos = -1; + for (Entry entry = table[row]; entry != null; entry = entry.next) { + if (entry.name.equals(name)) + pos = entry.pos; + } + if (verbose) + System.err.println("Looking for " + name + ", found " + pos); + return pos; +} + +} diff --git a/src/org/xbill/DNS/Credibility.java b/src/org/xbill/DNS/Credibility.java new file mode 100644 index 0000000..fa10686 --- /dev/null +++ b/src/org/xbill/DNS/Credibility.java @@ -0,0 +1,50 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants relating to the credibility of cached data, which is based on + * the data's source. The constants NORMAL and ANY should be used by most + * callers. + * @see Cache + * @see Section + * + * @author Brian Wellington + */ + +public final class Credibility { + +private +Credibility() {} + +/** A hint or cache file on disk. */ +public static final int HINT = 0; + +/** The additional section of a response. */ +public static final int ADDITIONAL = 1; + +/** The additional section of a response. */ +public static final int GLUE = 2; + +/** The authority section of a nonauthoritative response. */ +public static final int NONAUTH_AUTHORITY = 3; + +/** The answer section of a nonauthoritative response. */ +public static final int NONAUTH_ANSWER = 3; + +/** The authority section of an authoritative response. */ +public static final int AUTH_AUTHORITY = 4; + +/** The answer section of a authoritative response. */ +public static final int AUTH_ANSWER = 4; + +/** A zone. */ +public static final int ZONE = 5; + +/** Credible data. */ +public static final int NORMAL = 3; + +/** Data not required to be credible. */ +public static final int ANY = 1; + +} diff --git a/src/org/xbill/DNS/DClass.java b/src/org/xbill/DNS/DClass.java new file mode 100644 index 0000000..22180cf --- /dev/null +++ b/src/org/xbill/DNS/DClass.java @@ -0,0 +1,92 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to DNS classes. This is called DClass + * to avoid confusion with Class. + * + * @author Brian Wellington + */ + +public final class DClass { + +/** Internet */ +public static final int IN = 1; + +/** Chaos network (MIT) */ +public static final int CH = 3; + +/** Chaos network (MIT, alternate name) */ +public static final int CHAOS = 3; + +/** Hesiod name server (MIT) */ +public static final int HS = 4; + +/** Hesiod name server (MIT, alternate name) */ +public static final int HESIOD = 4; + +/** Special value used in dynamic update messages */ +public static final int NONE = 254; + +/** Matches any class */ +public static final int ANY = 255; + +private static class DClassMnemonic extends Mnemonic { + public + DClassMnemonic() { + super("DClass", CASE_UPPER); + setPrefix("CLASS"); + } + + public void + check(int val) { + DClass.check(val); + } +} + +private static Mnemonic classes = new DClassMnemonic(); + +static { + classes.add(IN, "IN"); + classes.add(CH, "CH"); + classes.addAlias(CH, "CHAOS"); + classes.add(HS, "HS"); + classes.addAlias(HS, "HESIOD"); + classes.add(NONE, "NONE"); + classes.add(ANY, "ANY"); +} + +private +DClass() {} + +/** + * Checks that a numeric DClass is valid. + * @throws InvalidDClassException The class is out of range. + */ +public static void +check(int i) { + if (i < 0 || i > 0xFFFF) + throw new InvalidDClassException(i); +} + +/** + * Converts a numeric DClass into a String + * @return The canonical string representation of the class + * @throws InvalidDClassException The class is out of range. + */ +public static String +string(int i) { + return classes.getText(i); +} + +/** + * Converts a String representation of a DClass into its numeric value + * @return The class code, or -1 on error. + */ +public static int +value(String s) { + return classes.getValue(s); +} + +} diff --git a/src/org/xbill/DNS/DHCIDRecord.java b/src/org/xbill/DNS/DHCIDRecord.java new file mode 100644 index 0000000..e160a8c --- /dev/null +++ b/src/org/xbill/DNS/DHCIDRecord.java @@ -0,0 +1,65 @@ +// Copyright (c) 2008 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.base64; + +/** + * DHCID - Dynamic Host Configuration Protocol (DHCP) ID (RFC 4701) + * + * @author Brian Wellington + */ + +public class DHCIDRecord extends Record { + +private static final long serialVersionUID = -8214820200808997707L; + +private byte [] data; + +DHCIDRecord() {} + +Record +getObject() { + return new DHCIDRecord(); +} + +/** + * Creates an DHCID Record from the given data + * @param data The binary data, which is opaque to DNS. + */ +public +DHCIDRecord(Name name, int dclass, long ttl, byte [] data) { + super(name, Type.DHCID, dclass, ttl); + this.data = data; +} + +void +rrFromWire(DNSInput in) throws IOException { + data = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + data = st.getBase64(); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(data); +} + +String +rrToString() { + return base64.toString(data); +} + +/** + * Returns the binary data. + */ +public byte [] +getData() { + return data; +} + +} diff --git a/src/org/xbill/DNS/DLVRecord.java b/src/org/xbill/DNS/DLVRecord.java new file mode 100644 index 0000000..8acc90f --- /dev/null +++ b/src/org/xbill/DNS/DLVRecord.java @@ -0,0 +1,132 @@ +// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * DLV - contains a Delegation Lookaside Validation record, which acts + * as the equivalent of a DS record in a lookaside zone. + * @see DNSSEC + * @see DSRecord + * + * @author David Blacka + * @author Brian Wellington + */ + +public class DLVRecord extends Record { + +public static final int SHA1_DIGEST_ID = DSRecord.Digest.SHA1; +public static final int SHA256_DIGEST_ID = DSRecord.Digest.SHA1; + +private static final long serialVersionUID = 1960742375677534148L; + +private int footprint; +private int alg; +private int digestid; +private byte [] digest; + +DLVRecord() {} + +Record +getObject() { + return new DLVRecord(); +} + +/** + * Creates a DLV Record from the given data + * @param footprint The original KEY record's footprint (keyid). + * @param alg The original key algorithm. + * @param digestid The digest id code. + * @param digest A hash of the original key. + */ +public +DLVRecord(Name name, int dclass, long ttl, int footprint, int alg, + int digestid, byte [] digest) +{ + super(name, Type.DLV, dclass, ttl); + this.footprint = checkU16("footprint", footprint); + this.alg = checkU8("alg", alg); + this.digestid = checkU8("digestid", digestid); + this.digest = digest; +} + +void +rrFromWire(DNSInput in) throws IOException { + footprint = in.readU16(); + alg = in.readU8(); + digestid = in.readU8(); + digest = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + footprint = st.getUInt16(); + alg = st.getUInt8(); + digestid = st.getUInt8(); + digest = st.getHex(); +} + +/** + * Converts rdata to a String + */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(footprint); + sb.append(" "); + sb.append(alg); + sb.append(" "); + sb.append(digestid); + if (digest != null) { + sb.append(" "); + sb.append(base16.toString(digest)); + } + + return sb.toString(); +} + +/** + * Returns the key's algorithm. + */ +public int +getAlgorithm() { + return alg; +} + +/** + * Returns the key's Digest ID. + */ +public int +getDigestID() +{ + return digestid; +} + +/** + * Returns the binary hash of the key. + */ +public byte [] +getDigest() { + return digest; +} + +/** + * Returns the key's footprint. + */ +public int +getFootprint() { + return footprint; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(footprint); + out.writeU8(alg); + out.writeU8(digestid); + if (digest != null) + out.writeByteArray(digest); +} + +} diff --git a/src/org/xbill/DNS/DNAMERecord.java b/src/org/xbill/DNS/DNAMERecord.java new file mode 100644 index 0000000..cbb322f --- /dev/null +++ b/src/org/xbill/DNS/DNAMERecord.java @@ -0,0 +1,45 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * DNAME Record - maps a nonterminal alias (subtree) to a different domain + * + * @author Brian Wellington + */ + +public class DNAMERecord extends SingleNameBase { + +private static final long serialVersionUID = 2670767677200844154L; + +DNAMERecord() {} + +Record +getObject() { + return new DNAMERecord(); +} + +/** + * Creates a new DNAMERecord with the given data + * @param alias The name to which the DNAME alias points + */ +public +DNAMERecord(Name name, int dclass, long ttl, Name alias) { + super(name, Type.DNAME, dclass, ttl, alias, "alias"); +} + +/** + * Gets the target of the DNAME Record + */ +public Name +getTarget() { + return getSingleName(); +} + +/** Gets the alias specified by the DNAME Record */ +public Name +getAlias() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/DNSInput.java b/src/org/xbill/DNS/DNSInput.java new file mode 100644 index 0000000..d3134ed --- /dev/null +++ b/src/org/xbill/DNS/DNSInput.java @@ -0,0 +1,239 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An class for parsing DNS messages. + * + * @author Brian Wellington + */ + +public class DNSInput { + +private byte [] array; +private int pos; +private int end; +private int saved_pos; +private int saved_end; + +/** + * Creates a new DNSInput + * @param input The byte array to read from + */ +public +DNSInput(byte [] input) { + array = input; + pos = 0; + end = array.length; + saved_pos = -1; + saved_end = -1; +} + +/** + * Returns the current position. + */ +public int +current() { + return pos; +} + +/** + * Returns the number of bytes that can be read from this stream before + * reaching the end. + */ +public int +remaining() { + return end - pos; +} + +private void +require(int n) throws WireParseException{ + if (n > remaining()) { + throw new WireParseException("end of input"); + } +} + +/** + * Marks the following bytes in the stream as active. + * @param len The number of bytes in the active region. + * @throws IllegalArgumentException The number of bytes in the active region + * is longer than the remainder of the input. + */ +public void +setActive(int len) { + if (len > array.length - pos) { + throw new IllegalArgumentException("cannot set active " + + "region past end of input"); + } + end = pos + len; +} + +/** + * Clears the active region of the string. Further operations are not + * restricted to part of the input. + */ +public void +clearActive() { + end = array.length; +} + +/** + * Returns the position of the end of the current active region. + */ +public int +saveActive() { + return end; +} + +/** + * Restores the previously set active region. This differs from setActive() in + * that restoreActive() takes an absolute position, and setActive takes an + * offset from the current location. + * @param pos The end of the active region. + */ +public void +restoreActive(int pos) { + if (pos > array.length) { + throw new IllegalArgumentException("cannot set active " + + "region past end of input"); + } + end = pos; +} + +/** + * Resets the current position of the input stream to the specified index, + * and clears the active region. + * @param index The position to continue parsing at. + * @throws IllegalArgumentException The index is not within the input. + */ +public void +jump(int index) { + if (index >= array.length) { + throw new IllegalArgumentException("cannot jump past " + + "end of input"); + } + pos = index; + end = array.length; +} + +/** + * Saves the current state of the input stream. Both the current position and + * the end of the active region are saved. + * @throws IllegalArgumentException The index is not within the input. + */ +public void +save() { + saved_pos = pos; + saved_end = end; +} + +/** + * Restores the input stream to its state before the call to {@link #save}. + */ +public void +restore() { + if (saved_pos < 0) { + throw new IllegalStateException("no previous state"); + } + pos = saved_pos; + end = saved_end; + saved_pos = -1; + saved_end = -1; +} + +/** + * Reads an unsigned 8 bit value from the stream, as an int. + * @return An unsigned 8 bit value. + * @throws WireParseException The end of the stream was reached. + */ +public int +readU8() throws WireParseException { + require(1); + return (array[pos++] & 0xFF); +} + +/** + * Reads an unsigned 16 bit value from the stream, as an int. + * @return An unsigned 16 bit value. + * @throws WireParseException The end of the stream was reached. + */ +public int +readU16() throws WireParseException { + require(2); + int b1 = array[pos++] & 0xFF; + int b2 = array[pos++] & 0xFF; + return ((b1 << 8) + b2); +} + +/** + * Reads an unsigned 32 bit value from the stream, as a long. + * @return An unsigned 32 bit value. + * @throws WireParseException The end of the stream was reached. + */ +public long +readU32() throws WireParseException { + require(4); + int b1 = array[pos++] & 0xFF; + int b2 = array[pos++] & 0xFF; + int b3 = array[pos++] & 0xFF; + int b4 = array[pos++] & 0xFF; + return (((long)b1 << 24) + (b2 << 16) + (b3 << 8) + b4); +} + +/** + * Reads a byte array of a specified length from the stream into an existing + * array. + * @param b The array to read into. + * @param off The offset of the array to start copying data into. + * @param len The number of bytes to copy. + * @throws WireParseException The end of the stream was reached. + */ +public void +readByteArray(byte [] b, int off, int len) throws WireParseException { + require(len); + System.arraycopy(array, pos, b, off, len); + pos += len; +} + +/** + * Reads a byte array of a specified length from the stream. + * @return The byte array. + * @throws WireParseException The end of the stream was reached. + */ +public byte [] +readByteArray(int len) throws WireParseException { + require(len); + byte [] out = new byte[len]; + System.arraycopy(array, pos, out, 0, len); + pos += len; + return out; +} + +/** + * Reads a byte array consisting of the remainder of the stream (or the + * active region, if one is set. + * @return The byte array. + */ +public byte [] +readByteArray() { + int len = remaining(); + byte [] out = new byte[len]; + System.arraycopy(array, pos, out, 0, len); + pos += len; + return out; +} + +/** + * Reads a counted string from the stream. A counted string is a one byte + * value indicating string length, followed by bytes of data. + * @return A byte array containing the string. + * @throws WireParseException The end of the stream was reached. + */ +public byte [] +readCountedString() throws WireParseException { + require(1); + int len = array[pos++] & 0xFF; + return readByteArray(len); +} + +} diff --git a/src/org/xbill/DNS/DNSKEYRecord.java b/src/org/xbill/DNS/DNSKEYRecord.java new file mode 100644 index 0000000..6e9bafd --- /dev/null +++ b/src/org/xbill/DNS/DNSKEYRecord.java @@ -0,0 +1,91 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.security.PublicKey; + +/** + * Key - contains a cryptographic public key for use by DNS. + * The data can be converted to objects implementing + * java.security.interfaces.PublicKey + * @see DNSSEC + * + * @author Brian Wellington + */ + +public class DNSKEYRecord extends KEYBase { + +public static class Protocol { + private Protocol() {} + + /** Key will be used for DNSSEC */ + public static final int DNSSEC = 3; +} + +public static class Flags { + private Flags() {} + + /** Key is a zone key */ + public static final int ZONE_KEY = 0x100; + + /** Key is a secure entry point key */ + public static final int SEP_KEY = 0x1; + + /** Key has been revoked */ + public static final int REVOKE = 0x80; +} + +private static final long serialVersionUID = -8679800040426675002L; + +DNSKEYRecord() {} + +Record +getObject() { + return new DNSKEYRecord(); +} + +/** + * Creates a DNSKEY Record from the given data + * @param flags Flags describing the key's properties + * @param proto The protocol that the key was created for + * @param alg The key's algorithm + * @param key Binary representation of the key + */ +public +DNSKEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg, + byte [] key) +{ + super(name, Type.DNSKEY, dclass, ttl, flags, proto, alg, key); +} + +/** + * Creates a DNSKEY Record from the given data + * @param flags Flags describing the key's properties + * @param proto The protocol that the key was created for + * @param alg The key's algorithm + * @param key The key as a PublicKey + * @throws DNSSEC.DNSSECException The PublicKey could not be converted into DNS + * format. + */ +public +DNSKEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg, + PublicKey key) throws DNSSEC.DNSSECException +{ + super(name, Type.DNSKEY, dclass, ttl, flags, proto, alg, + DNSSEC.fromPublicKey(key, alg)); + publicKey = key; +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + flags = st.getUInt16(); + proto = st.getUInt8(); + String algString = st.getString(); + alg = DNSSEC.Algorithm.value(algString); + if (alg < 0) + throw st.exception("Invalid algorithm: " + algString); + key = st.getBase64(); +} + +} diff --git a/src/org/xbill/DNS/DNSOutput.java b/src/org/xbill/DNS/DNSOutput.java new file mode 100644 index 0000000..29a8f68 --- /dev/null +++ b/src/org/xbill/DNS/DNSOutput.java @@ -0,0 +1,203 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * A class for rendering DNS messages. + * + * @author Brian Wellington + */ + + +public class DNSOutput { + +private byte [] array; +private int pos; +private int saved_pos; + +/** + * Create a new DNSOutput with a specified size. + * @param size The initial size + */ +public +DNSOutput(int size) { + array = new byte[size]; + pos = 0; + saved_pos = -1; +} + +/** + * Create a new DNSOutput + */ +public +DNSOutput() { + this(32); +} + +/** + * Returns the current position. + */ +public int +current() { + return pos; +} + +private void +check(long val, int bits) { + long max = 1; + max <<= bits; + if (val < 0 || val > max) { + throw new IllegalArgumentException(val + " out of range for " + + bits + " bit value"); + } +} + +private void +need(int n) { + if (array.length - pos >= n) { + return; + } + int newsize = array.length * 2; + if (newsize < pos + n) { + newsize = pos + n; + } + byte [] newarray = new byte[newsize]; + System.arraycopy(array, 0, newarray, 0, pos); + array = newarray; +} + +/** + * Resets the current position of the output stream to the specified index. + * @param index The new current position. + * @throws IllegalArgumentException The index is not within the output. + */ +public void +jump(int index) { + if (index > pos) { + throw new IllegalArgumentException("cannot jump past " + + "end of data"); + } + pos = index; +} + +/** + * Saves the current state of the output stream. + * @throws IllegalArgumentException The index is not within the output. + */ +public void +save() { + saved_pos = pos; +} + +/** + * Restores the input stream to its state before the call to {@link #save}. + */ +public void +restore() { + if (saved_pos < 0) { + throw new IllegalStateException("no previous state"); + } + pos = saved_pos; + saved_pos = -1; +} + +/** + * Writes an unsigned 8 bit value to the stream. + * @param val The value to be written + */ +public void +writeU8(int val) { + check(val, 8); + need(1); + array[pos++] = (byte)(val & 0xFF); +} + +/** + * Writes an unsigned 16 bit value to the stream. + * @param val The value to be written + */ +public void +writeU16(int val) { + check(val, 16); + need(2); + array[pos++] = (byte)((val >>> 8) & 0xFF); + array[pos++] = (byte)(val & 0xFF); +} + +/** + * Writes an unsigned 16 bit value to the specified position in the stream. + * @param val The value to be written + * @param where The position to write the value. + */ +public void +writeU16At(int val, int where) { + check(val, 16); + if (where > pos - 2) + throw new IllegalArgumentException("cannot write past " + + "end of data"); + array[where++] = (byte)((val >>> 8) & 0xFF); + array[where++] = (byte)(val & 0xFF); +} + +/** + * Writes an unsigned 32 bit value to the stream. + * @param val The value to be written + */ +public void +writeU32(long val) { + check(val, 32); + need(4); + array[pos++] = (byte)((val >>> 24) & 0xFF); + array[pos++] = (byte)((val >>> 16) & 0xFF); + array[pos++] = (byte)((val >>> 8) & 0xFF); + array[pos++] = (byte)(val & 0xFF); +} + +/** + * Writes a byte array to the stream. + * @param b The array to write. + * @param off The offset of the array to start copying data from. + * @param len The number of bytes to write. + */ +public void +writeByteArray(byte [] b, int off, int len) { + need(len); + System.arraycopy(b, off, array, pos, len); + pos += len; +} + +/** + * Writes a byte array to the stream. + * @param b The array to write. + */ +public void +writeByteArray(byte [] b) { + writeByteArray(b, 0, b.length); +} + +/** + * Writes a counted string from the stream. A counted string is a one byte + * value indicating string length, followed by bytes of data. + * @param s The string to write. + */ +public void +writeCountedString(byte [] s) { + if (s.length > 0xFF) { + throw new IllegalArgumentException("Invalid counted string"); + } + need(1 + s.length); + array[pos++] = (byte)(s.length & 0xFF); + writeByteArray(s, 0, s.length); +} + +/** + * Returns a byte array containing the current contents of the stream. + */ +public byte [] +toByteArray() { + byte [] out = new byte[pos]; + System.arraycopy(array, 0, out, 0, pos); + return out; +} + +} diff --git a/src/org/xbill/DNS/DNSSEC.java b/src/org/xbill/DNS/DNSSEC.java new file mode 100644 index 0000000..2707947 --- /dev/null +++ b/src/org/xbill/DNS/DNSSEC.java @@ -0,0 +1,1026 @@ +// Copyright (c) 1999-2010 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.math.*; +import java.security.*; +import java.security.interfaces.*; +import java.security.spec.*; +import java.util.*; + +/** + * Constants and methods relating to DNSSEC. + * + * DNSSEC provides authentication for DNS information. + * @see RRSIGRecord + * @see DNSKEYRecord + * @see RRset + * + * @author Brian Wellington + */ + +public class DNSSEC { + +public static class Algorithm { + private Algorithm() {} + + /** RSA/MD5 public key (deprecated) */ + public static final int RSAMD5 = 1; + + /** Diffie Hellman key */ + public static final int DH = 2; + + /** DSA public key */ + public static final int DSA = 3; + + /** RSA/SHA1 public key */ + public static final int RSASHA1 = 5; + + /** DSA/SHA1, NSEC3-aware public key */ + public static final int DSA_NSEC3_SHA1 = 6; + + /** RSA/SHA1, NSEC3-aware public key */ + public static final int RSA_NSEC3_SHA1 = 7; + + /** RSA/SHA256 public key */ + public static final int RSASHA256 = 8; + + /** RSA/SHA512 public key */ + public static final int RSASHA512 = 10; + + /** ECDSA Curve P-256 with SHA-256 public key **/ + public static final int ECDSAP256SHA256 = 13; + + /** ECDSA Curve P-384 with SHA-384 public key **/ + public static final int ECDSAP384SHA384 = 14; + + /** Indirect keys; the actual key is elsewhere. */ + public static final int INDIRECT = 252; + + /** Private algorithm, specified by domain name */ + public static final int PRIVATEDNS = 253; + + /** Private algorithm, specified by OID */ + public static final int PRIVATEOID = 254; + + private static Mnemonic algs = new Mnemonic("DNSSEC algorithm", + Mnemonic.CASE_UPPER); + + static { + algs.setMaximum(0xFF); + algs.setNumericAllowed(true); + + algs.add(RSAMD5, "RSAMD5"); + algs.add(DH, "DH"); + algs.add(DSA, "DSA"); + algs.add(RSASHA1, "RSASHA1"); + algs.add(DSA_NSEC3_SHA1, "DSA-NSEC3-SHA1"); + algs.add(RSA_NSEC3_SHA1, "RSA-NSEC3-SHA1"); + algs.add(RSASHA256, "RSASHA256"); + algs.add(RSASHA512, "RSASHA512"); + algs.add(ECDSAP256SHA256, "ECDSAP256SHA256"); + algs.add(ECDSAP384SHA384, "ECDSAP384SHA384"); + algs.add(INDIRECT, "INDIRECT"); + algs.add(PRIVATEDNS, "PRIVATEDNS"); + algs.add(PRIVATEOID, "PRIVATEOID"); + } + + /** + * Converts an algorithm into its textual representation + */ + public static String + string(int alg) { + return algs.getText(alg); + } + + /** + * Converts a textual representation of an algorithm into its numeric + * code. Integers in the range 0..255 are also accepted. + * @param s The textual representation of the algorithm + * @return The algorithm code, or -1 on error. + */ + public static int + value(String s) { + return algs.getValue(s); + } +} + +private +DNSSEC() { } + +private static void +digestSIG(DNSOutput out, SIGBase sig) { + out.writeU16(sig.getTypeCovered()); + out.writeU8(sig.getAlgorithm()); + out.writeU8(sig.getLabels()); + out.writeU32(sig.getOrigTTL()); + out.writeU32(sig.getExpire().getTime() / 1000); + out.writeU32(sig.getTimeSigned().getTime() / 1000); + out.writeU16(sig.getFootprint()); + sig.getSigner().toWireCanonical(out); +} + +/** + * Creates a byte array containing the concatenation of the fields of the + * SIG record and the RRsets to be signed/verified. This does not perform + * a cryptographic digest. + * @param rrsig The RRSIG record used to sign/verify the rrset. + * @param rrset The data to be signed/verified. + * @return The data to be cryptographically signed or verified. + */ +public static byte [] +digestRRset(RRSIGRecord rrsig, RRset rrset) { + DNSOutput out = new DNSOutput(); + digestSIG(out, rrsig); + + int size = rrset.size(); + Record [] records = new Record[size]; + + Iterator it = rrset.rrs(); + Name name = rrset.getName(); + Name wild = null; + int sigLabels = rrsig.getLabels() + 1; // Add the root label back. + if (name.labels() > sigLabels) + wild = name.wild(name.labels() - sigLabels); + while (it.hasNext()) + records[--size] = (Record) it.next(); + Arrays.sort(records); + + DNSOutput header = new DNSOutput(); + if (wild != null) + wild.toWireCanonical(header); + else + name.toWireCanonical(header); + header.writeU16(rrset.getType()); + header.writeU16(rrset.getDClass()); + header.writeU32(rrsig.getOrigTTL()); + for (int i = 0; i < records.length; i++) { + out.writeByteArray(header.toByteArray()); + int lengthPosition = out.current(); + out.writeU16(0); + out.writeByteArray(records[i].rdataToWireCanonical()); + int rrlength = out.current() - lengthPosition - 2; + out.save(); + out.jump(lengthPosition); + out.writeU16(rrlength); + out.restore(); + } + return out.toByteArray(); +} + +/** + * Creates a byte array containing the concatenation of the fields of the + * SIG(0) record and the message to be signed. This does not perform + * a cryptographic digest. + * @param sig The SIG record used to sign the rrset. + * @param msg The message to be signed. + * @param previous If this is a response, the signature from the query. + * @return The data to be cryptographically signed. + */ +public static byte [] +digestMessage(SIGRecord sig, Message msg, byte [] previous) { + DNSOutput out = new DNSOutput(); + digestSIG(out, sig); + + if (previous != null) + out.writeByteArray(previous); + + msg.toWire(out); + return out.toByteArray(); +} + +/** + * A DNSSEC exception. + */ +public static class DNSSECException extends Exception { + DNSSECException(String s) { + super(s); + } +} + +/** + * An algorithm is unsupported by this DNSSEC implementation. + */ +public static class UnsupportedAlgorithmException extends DNSSECException { + UnsupportedAlgorithmException(int alg) { + super("Unsupported algorithm: " + alg); + } +} + +/** + * The cryptographic data in a DNSSEC key is malformed. + */ +public static class MalformedKeyException extends DNSSECException { + MalformedKeyException(KEYBase rec) { + super("Invalid key data: " + rec.rdataToString()); + } +} + +/** + * A DNSSEC verification failed because fields in the DNSKEY and RRSIG records + * do not match. + */ +public static class KeyMismatchException extends DNSSECException { + private KEYBase key; + private SIGBase sig; + + KeyMismatchException(KEYBase key, SIGBase sig) { + super("key " + + key.getName() + "/" + + DNSSEC.Algorithm.string(key.getAlgorithm()) + "/" + + key.getFootprint() + " " + + "does not match signature " + + sig.getSigner() + "/" + + DNSSEC.Algorithm.string(sig.getAlgorithm()) + "/" + + sig.getFootprint()); + } +} + +/** + * A DNSSEC verification failed because the signature has expired. + */ +public static class SignatureExpiredException extends DNSSECException { + private Date when, now; + + SignatureExpiredException(Date when, Date now) { + super("signature expired"); + this.when = when; + this.now = now; + } + + /** + * @return When the signature expired + */ + public Date + getExpiration() { + return when; + } + + /** + * @return When the verification was attempted + */ + public Date + getVerifyTime() { + return now; + } +} + +/** + * A DNSSEC verification failed because the signature has not yet become valid. + */ +public static class SignatureNotYetValidException extends DNSSECException { + private Date when, now; + + SignatureNotYetValidException(Date when, Date now) { + super("signature is not yet valid"); + this.when = when; + this.now = now; + } + + /** + * @return When the signature will become valid + */ + public Date + getExpiration() { + return when; + } + + /** + * @return When the verification was attempted + */ + public Date + getVerifyTime() { + return now; + } +} + +/** + * A DNSSEC verification failed because the cryptographic signature + * verification failed. + */ +public static class SignatureVerificationException extends DNSSECException { + SignatureVerificationException() { + super("signature verification failed"); + } +} + +/** + * The key data provided is inconsistent. + */ +public static class IncompatibleKeyException extends IllegalArgumentException { + IncompatibleKeyException() { + super("incompatible keys"); + } +} + +private static int +BigIntegerLength(BigInteger i) { + return (i.bitLength() + 7) / 8; +} + +private static BigInteger +readBigInteger(DNSInput in, int len) throws IOException { + byte [] b = in.readByteArray(len); + return new BigInteger(1, b); +} + +private static BigInteger +readBigInteger(DNSInput in) { + byte [] b = in.readByteArray(); + return new BigInteger(1, b); +} + +private static void +writeBigInteger(DNSOutput out, BigInteger val) { + byte [] b = val.toByteArray(); + if (b[0] == 0) + out.writeByteArray(b, 1, b.length - 1); + else + out.writeByteArray(b); +} + +private static PublicKey +toRSAPublicKey(KEYBase r) throws IOException, GeneralSecurityException { + DNSInput in = new DNSInput(r.getKey()); + int exponentLength = in.readU8(); + if (exponentLength == 0) + exponentLength = in.readU16(); + BigInteger exponent = readBigInteger(in, exponentLength); + BigInteger modulus = readBigInteger(in); + + KeyFactory factory = KeyFactory.getInstance("RSA"); + return factory.generatePublic(new RSAPublicKeySpec(modulus, exponent)); +} + +private static PublicKey +toDSAPublicKey(KEYBase r) throws IOException, GeneralSecurityException, + MalformedKeyException +{ + DNSInput in = new DNSInput(r.getKey()); + + int t = in.readU8(); + if (t > 8) + throw new MalformedKeyException(r); + + BigInteger q = readBigInteger(in, 20); + BigInteger p = readBigInteger(in, 64 + t*8); + BigInteger g = readBigInteger(in, 64 + t*8); + BigInteger y = readBigInteger(in, 64 + t*8); + + KeyFactory factory = KeyFactory.getInstance("DSA"); + return factory.generatePublic(new DSAPublicKeySpec(y, p, q, g)); +} + +private static class ECKeyInfo { + int length; + public BigInteger p, a, b, gx, gy, n; + EllipticCurve curve; + ECParameterSpec spec; + + ECKeyInfo(int length, String p_str, String a_str, String b_str, + String gx_str, String gy_str, String n_str) + { + this.length = length; + p = new BigInteger(p_str, 16); + a = new BigInteger(a_str, 16); + b = new BigInteger(b_str, 16); + gx = new BigInteger(gx_str, 16); + gy = new BigInteger(gy_str, 16); + n = new BigInteger(n_str, 16); + curve = new EllipticCurve(new ECFieldFp(p), a, b); + spec = new ECParameterSpec(curve, new ECPoint(gx, gy), n, 1); + } +} + +// RFC 5114 Section 2.6 +private static final ECKeyInfo ECDSA_P256 = new ECKeyInfo(32, + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", + "FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", + "5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", + "6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", + "4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", + "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551"); + +// RFC 5114 Section 2.7 +private static final ECKeyInfo ECDSA_P384 = new ECKeyInfo(48, + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", + "B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", + "AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", + "3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973"); + +private static PublicKey +toECDSAPublicKey(KEYBase r, ECKeyInfo keyinfo) throws IOException, + GeneralSecurityException, MalformedKeyException +{ + DNSInput in = new DNSInput(r.getKey()); + + // RFC 6605 Section 4 + BigInteger x = readBigInteger(in, keyinfo.length); + BigInteger y = readBigInteger(in, keyinfo.length); + ECPoint q = new ECPoint(x, y); + + KeyFactory factory = KeyFactory.getInstance("EC"); + return factory.generatePublic(new ECPublicKeySpec(q, keyinfo.spec)); +} + +/** Converts a KEY/DNSKEY record into a PublicKey */ +static PublicKey +toPublicKey(KEYBase r) throws DNSSECException { + int alg = r.getAlgorithm(); + try { + switch (alg) { + case Algorithm.RSAMD5: + case Algorithm.RSASHA1: + case Algorithm.RSA_NSEC3_SHA1: + case Algorithm.RSASHA256: + case Algorithm.RSASHA512: + return toRSAPublicKey(r); + case Algorithm.DSA: + case Algorithm.DSA_NSEC3_SHA1: + return toDSAPublicKey(r); + case Algorithm.ECDSAP256SHA256: + return toECDSAPublicKey(r, ECDSA_P256); + case Algorithm.ECDSAP384SHA384: + return toECDSAPublicKey(r, ECDSA_P384); + default: + throw new UnsupportedAlgorithmException(alg); + } + } + catch (IOException e) { + throw new MalformedKeyException(r); + } + catch (GeneralSecurityException e) { + throw new DNSSECException(e.toString()); + } +} + +private static byte [] +fromRSAPublicKey(RSAPublicKey key) { + DNSOutput out = new DNSOutput(); + BigInteger exponent = key.getPublicExponent(); + BigInteger modulus = key.getModulus(); + int exponentLength = BigIntegerLength(exponent); + + if (exponentLength < 256) + out.writeU8(exponentLength); + else { + out.writeU8(0); + out.writeU16(exponentLength); + } + writeBigInteger(out, exponent); + writeBigInteger(out, modulus); + + return out.toByteArray(); +} + +private static byte [] +fromDSAPublicKey(DSAPublicKey key) { + DNSOutput out = new DNSOutput(); + BigInteger q = key.getParams().getQ(); + BigInteger p = key.getParams().getP(); + BigInteger g = key.getParams().getG(); + BigInteger y = key.getY(); + int t = (p.toByteArray().length - 64) / 8; + + out.writeU8(t); + writeBigInteger(out, q); + writeBigInteger(out, p); + writeBigInteger(out, g); + writeBigInteger(out, y); + + return out.toByteArray(); +} + +private static byte [] +fromECDSAPublicKey(ECPublicKey key) { + DNSOutput out = new DNSOutput(); + + BigInteger x = key.getW().getAffineX(); + BigInteger y = key.getW().getAffineY(); + + writeBigInteger(out, x); + writeBigInteger(out, y); + + return out.toByteArray(); +} + +/** Builds a DNSKEY record from a PublicKey */ +static byte [] +fromPublicKey(PublicKey key, int alg) throws DNSSECException +{ + + switch (alg) { + case Algorithm.RSAMD5: + case Algorithm.RSASHA1: + case Algorithm.RSA_NSEC3_SHA1: + case Algorithm.RSASHA256: + case Algorithm.RSASHA512: + if (! (key instanceof RSAPublicKey)) + throw new IncompatibleKeyException(); + return fromRSAPublicKey((RSAPublicKey) key); + case Algorithm.DSA: + case Algorithm.DSA_NSEC3_SHA1: + if (! (key instanceof DSAPublicKey)) + throw new IncompatibleKeyException(); + return fromDSAPublicKey((DSAPublicKey) key); + case Algorithm.ECDSAP256SHA256: + case Algorithm.ECDSAP384SHA384: + if (! (key instanceof ECPublicKey)) + throw new IncompatibleKeyException(); + return fromECDSAPublicKey((ECPublicKey) key); + default: + throw new UnsupportedAlgorithmException(alg); + } +} + +/** + * Convert an algorithm number to the corresponding JCA string. + * @param alg The algorithm number. + * @throws UnsupportedAlgorithmException The algorithm is unknown. + */ +public static String +algString(int alg) throws UnsupportedAlgorithmException { + switch (alg) { + case Algorithm.RSAMD5: + return "MD5withRSA"; + case Algorithm.DSA: + case Algorithm.DSA_NSEC3_SHA1: + return "SHA1withDSA"; + case Algorithm.RSASHA1: + case Algorithm.RSA_NSEC3_SHA1: + return "SHA1withRSA"; + case Algorithm.RSASHA256: + return "SHA256withRSA"; + case Algorithm.RSASHA512: + return "SHA512withRSA"; + case Algorithm.ECDSAP256SHA256: + return "SHA256withECDSA"; + case Algorithm.ECDSAP384SHA384: + return "SHA384withECDSA"; + default: + throw new UnsupportedAlgorithmException(alg); + } +} + +private static final int ASN1_SEQ = 0x30; +private static final int ASN1_INT = 0x2; + +private static final int DSA_LEN = 20; + +private static byte [] +DSASignaturefromDNS(byte [] dns) throws DNSSECException, IOException { + if (dns.length != 1 + DSA_LEN * 2) + throw new SignatureVerificationException(); + + DNSInput in = new DNSInput(dns); + DNSOutput out = new DNSOutput(); + + int t = in.readU8(); + + byte [] r = in.readByteArray(DSA_LEN); + int rlen = DSA_LEN; + if (r[0] < 0) + rlen++; + + byte [] s = in.readByteArray(DSA_LEN); + int slen = DSA_LEN; + if (s[0] < 0) + slen++; + + out.writeU8(ASN1_SEQ); + out.writeU8(rlen + slen + 4); + + out.writeU8(ASN1_INT); + out.writeU8(rlen); + if (rlen > DSA_LEN) + out.writeU8(0); + out.writeByteArray(r); + + out.writeU8(ASN1_INT); + out.writeU8(slen); + if (slen > DSA_LEN) + out.writeU8(0); + out.writeByteArray(s); + + return out.toByteArray(); +} + +private static byte [] +DSASignaturetoDNS(byte [] signature, int t) throws IOException { + DNSInput in = new DNSInput(signature); + DNSOutput out = new DNSOutput(); + + out.writeU8(t); + + int tmp = in.readU8(); + if (tmp != ASN1_SEQ) + throw new IOException(); + int seqlen = in.readU8(); + + tmp = in.readU8(); + if (tmp != ASN1_INT) + throw new IOException(); + int rlen = in.readU8(); + if (rlen == DSA_LEN + 1) { + if (in.readU8() != 0) + throw new IOException(); + } else if (rlen != DSA_LEN) + throw new IOException(); + byte [] bytes = in.readByteArray(DSA_LEN); + out.writeByteArray(bytes); + + tmp = in.readU8(); + if (tmp != ASN1_INT) + throw new IOException(); + int slen = in.readU8(); + if (slen == DSA_LEN + 1) { + if (in.readU8() != 0) + throw new IOException(); + } else if (slen != DSA_LEN) + throw new IOException(); + bytes = in.readByteArray(DSA_LEN); + out.writeByteArray(bytes); + + return out.toByteArray(); +} + +private static byte [] +ECDSASignaturefromDNS(byte [] signature, ECKeyInfo keyinfo) + throws DNSSECException, IOException +{ + if (signature.length != keyinfo.length * 2) + throw new SignatureVerificationException(); + + DNSInput in = new DNSInput(signature); + DNSOutput out = new DNSOutput(); + + byte [] r = in.readByteArray(keyinfo.length); + int rlen = keyinfo.length; + if (r[0] < 0) + rlen++; + + byte [] s = in.readByteArray(keyinfo.length); + int slen = keyinfo.length; + if (s[0] < 0) + slen++; + + out.writeU8(ASN1_SEQ); + out.writeU8(rlen + slen + 4); + + out.writeU8(ASN1_INT); + out.writeU8(rlen); + if (rlen > keyinfo.length) + out.writeU8(0); + out.writeByteArray(r); + + out.writeU8(ASN1_INT); + out.writeU8(slen); + if (slen > keyinfo.length) + out.writeU8(0); + out.writeByteArray(s); + + return out.toByteArray(); +} + +private static byte [] +ECDSASignaturetoDNS(byte [] signature, ECKeyInfo keyinfo) throws IOException { + DNSInput in = new DNSInput(signature); + DNSOutput out = new DNSOutput(); + + int tmp = in.readU8(); + if (tmp != ASN1_SEQ) + throw new IOException(); + int seqlen = in.readU8(); + + tmp = in.readU8(); + if (tmp != ASN1_INT) + throw new IOException(); + int rlen = in.readU8(); + if (rlen == keyinfo.length + 1) { + if (in.readU8() != 0) + throw new IOException(); + } else if (rlen != keyinfo.length) + throw new IOException(); + byte[] bytes = in.readByteArray(keyinfo.length); + out.writeByteArray(bytes); + + tmp = in.readU8(); + if (tmp != ASN1_INT) + throw new IOException(); + int slen = in.readU8(); + if (slen == keyinfo.length + 1) { + if (in.readU8() != 0) + throw new IOException(); + } else if (slen != keyinfo.length) + throw new IOException(); + bytes = in.readByteArray(keyinfo.length); + out.writeByteArray(bytes); + + return out.toByteArray(); +} + +private static void +verify(PublicKey key, int alg, byte [] data, byte [] signature) +throws DNSSECException +{ + if (key instanceof DSAPublicKey) { + try { + signature = DSASignaturefromDNS(signature); + } + catch (IOException e) { + throw new IllegalStateException(); + } + } else if (key instanceof ECPublicKey) { + try { + switch (alg) { + case Algorithm.ECDSAP256SHA256: + signature = ECDSASignaturefromDNS(signature, + ECDSA_P256); + break; + case Algorithm.ECDSAP384SHA384: + signature = ECDSASignaturefromDNS(signature, + ECDSA_P384); + break; + default: + throw new UnsupportedAlgorithmException(alg); + } + } + catch (IOException e) { + throw new IllegalStateException(); + } + } + + try { + Signature s = Signature.getInstance(algString(alg)); + s.initVerify(key); + s.update(data); + if (!s.verify(signature)) + throw new SignatureVerificationException(); + } + catch (GeneralSecurityException e) { + throw new DNSSECException(e.toString()); + } +} + +private static boolean +matches(SIGBase sig, KEYBase key) +{ + return (key.getAlgorithm() == sig.getAlgorithm() && + key.getFootprint() == sig.getFootprint() && + key.getName().equals(sig.getSigner())); +} + +/** + * Verify a DNSSEC signature. + * @param rrset The data to be verified. + * @param rrsig The RRSIG record containing the signature. + * @param key The DNSKEY record to verify the signature with. + * @throws UnsupportedAlgorithmException The algorithm is unknown + * @throws MalformedKeyException The key is malformed + * @throws KeyMismatchException The key and signature do not match + * @throws SignatureExpiredException The signature has expired + * @throws SignatureNotYetValidException The signature is not yet valid + * @throws SignatureVerificationException The signature does not verify. + * @throws DNSSECException Some other error occurred. + */ +public static void +verify(RRset rrset, RRSIGRecord rrsig, DNSKEYRecord key) throws DNSSECException +{ + if (!matches(rrsig, key)) + throw new KeyMismatchException(key, rrsig); + + Date now = new Date(); + if (now.compareTo(rrsig.getExpire()) > 0) + throw new SignatureExpiredException(rrsig.getExpire(), now); + if (now.compareTo(rrsig.getTimeSigned()) < 0) + throw new SignatureNotYetValidException(rrsig.getTimeSigned(), + now); + + verify(key.getPublicKey(), rrsig.getAlgorithm(), + digestRRset(rrsig, rrset), rrsig.getSignature()); +} + +private static byte [] +sign(PrivateKey privkey, PublicKey pubkey, int alg, byte [] data, + String provider) throws DNSSECException +{ + byte [] signature; + try { + Signature s; + if (provider != null) + s = Signature.getInstance(algString(alg), provider); + else + s = Signature.getInstance(algString(alg)); + s.initSign(privkey); + s.update(data); + signature = s.sign(); + } + catch (GeneralSecurityException e) { + throw new DNSSECException(e.toString()); + } + + if (pubkey instanceof DSAPublicKey) { + try { + DSAPublicKey dsa = (DSAPublicKey) pubkey; + BigInteger P = dsa.getParams().getP(); + int t = (BigIntegerLength(P) - 64) / 8; + signature = DSASignaturetoDNS(signature, t); + } + catch (IOException e) { + throw new IllegalStateException(); + } + } else if (pubkey instanceof ECPublicKey) { + try { + switch (alg) { + case Algorithm.ECDSAP256SHA256: + signature = ECDSASignaturetoDNS(signature, + ECDSA_P256); + break; + case Algorithm.ECDSAP384SHA384: + signature = ECDSASignaturetoDNS(signature, + ECDSA_P384); + break; + default: + throw new UnsupportedAlgorithmException(alg); + } + } + catch (IOException e) { + throw new IllegalStateException(); + } + } + + return signature; +} +static void +checkAlgorithm(PrivateKey key, int alg) throws UnsupportedAlgorithmException +{ + switch (alg) { + case Algorithm.RSAMD5: + case Algorithm.RSASHA1: + case Algorithm.RSA_NSEC3_SHA1: + case Algorithm.RSASHA256: + case Algorithm.RSASHA512: + if (! (key instanceof RSAPrivateKey)) + throw new IncompatibleKeyException(); + break; + case Algorithm.DSA: + case Algorithm.DSA_NSEC3_SHA1: + if (! (key instanceof DSAPrivateKey)) + throw new IncompatibleKeyException(); + break; + case Algorithm.ECDSAP256SHA256: + case Algorithm.ECDSAP384SHA384: + if (! (key instanceof ECPrivateKey)) + throw new IncompatibleKeyException(); + break; + default: + throw new UnsupportedAlgorithmException(alg); + } +} + +/** + * Generate a DNSSEC signature. key and privateKey must refer to the + * same underlying cryptographic key. + * @param rrset The data to be signed + * @param key The DNSKEY record to use as part of signing + * @param privkey The PrivateKey to use when signing + * @param inception The time at which the signatures should become valid + * @param expiration The time at which the signatures should expire + * @throws UnsupportedAlgorithmException The algorithm is unknown + * @throws MalformedKeyException The key is malformed + * @throws DNSSECException Some other error occurred. + * @return The generated signature + */ +public static RRSIGRecord +sign(RRset rrset, DNSKEYRecord key, PrivateKey privkey, + Date inception, Date expiration) throws DNSSECException +{ + return sign(rrset, key, privkey, inception, expiration, null); +} + +/** + * Generate a DNSSEC signature. key and privateKey must refer to the + * same underlying cryptographic key. + * @param rrset The data to be signed + * @param key The DNSKEY record to use as part of signing + * @param privkey The PrivateKey to use when signing + * @param inception The time at which the signatures should become valid + * @param expiration The time at which the signatures should expire + * @param provider The name of the JCA provider. If non-null, it will be + * passed to JCA getInstance() methods. + * @throws UnsupportedAlgorithmException The algorithm is unknown + * @throws MalformedKeyException The key is malformed + * @throws DNSSECException Some other error occurred. + * @return The generated signature + */ +public static RRSIGRecord +sign(RRset rrset, DNSKEYRecord key, PrivateKey privkey, + Date inception, Date expiration, String provider) throws DNSSECException +{ + int alg = key.getAlgorithm(); + checkAlgorithm(privkey, alg); + + RRSIGRecord rrsig = new RRSIGRecord(rrset.getName(), rrset.getDClass(), + rrset.getTTL(), rrset.getType(), + alg, rrset.getTTL(), + expiration, inception, + key.getFootprint(), + key.getName(), null); + + rrsig.setSignature(sign(privkey, key.getPublicKey(), alg, + digestRRset(rrsig, rrset), provider)); + return rrsig; +} + +static SIGRecord +signMessage(Message message, SIGRecord previous, KEYRecord key, + PrivateKey privkey, Date inception, Date expiration) + throws DNSSECException +{ + int alg = key.getAlgorithm(); + checkAlgorithm(privkey, alg); + + SIGRecord sig = new SIGRecord(Name.root, DClass.ANY, 0, 0, + alg, 0, expiration, inception, + key.getFootprint(), + key.getName(), null); + DNSOutput out = new DNSOutput(); + digestSIG(out, sig); + if (previous != null) + out.writeByteArray(previous.getSignature()); + message.toWire(out); + + sig.setSignature(sign(privkey, key.getPublicKey(), + alg, out.toByteArray(), null)); + return sig; +} + +static void +verifyMessage(Message message, byte [] bytes, SIGRecord sig, SIGRecord previous, + KEYRecord key) throws DNSSECException +{ + if (!matches(sig, key)) + throw new KeyMismatchException(key, sig); + + Date now = new Date(); + + if (now.compareTo(sig.getExpire()) > 0) + throw new SignatureExpiredException(sig.getExpire(), now); + if (now.compareTo(sig.getTimeSigned()) < 0) + throw new SignatureNotYetValidException(sig.getTimeSigned(), + now); + + DNSOutput out = new DNSOutput(); + digestSIG(out, sig); + if (previous != null) + out.writeByteArray(previous.getSignature()); + + Header header = (Header) message.getHeader().clone(); + header.decCount(Section.ADDITIONAL); + out.writeByteArray(header.toWire()); + + out.writeByteArray(bytes, Header.LENGTH, + message.sig0start - Header.LENGTH); + + verify(key.getPublicKey(), sig.getAlgorithm(), + out.toByteArray(), sig.getSignature()); +} + +/** + * Generate the digest value for a DS key + * @param key Which is covered by the DS record + * @param digestid The type of digest + * @return The digest value as an array of bytes + */ +static byte [] +generateDSDigest(DNSKEYRecord key, int digestid) +{ + MessageDigest digest; + try { + switch (digestid) { + case DSRecord.Digest.SHA1: + digest = MessageDigest.getInstance("sha-1"); + break; + case DSRecord.Digest.SHA256: + digest = MessageDigest.getInstance("sha-256"); + break; + case DSRecord.Digest.SHA384: + digest = MessageDigest.getInstance("sha-384"); + break; + default: + throw new IllegalArgumentException( + "unknown DS digest type " + digestid); + } + } + catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("no message digest support"); + } + digest.update(key.getName().toWire()); + digest.update(key.rdataToWireCanonical()); + return digest.digest(); +} + +} diff --git a/src/org/xbill/DNS/DSRecord.java b/src/org/xbill/DNS/DSRecord.java new file mode 100644 index 0000000..4c1ebce --- /dev/null +++ b/src/org/xbill/DNS/DSRecord.java @@ -0,0 +1,157 @@ +// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * DS - contains a Delegation Signer record, which acts as a + * placeholder for KEY records in the parent zone. + * @see DNSSEC + * + * @author David Blacka + * @author Brian Wellington + */ + +public class DSRecord extends Record { + +public static class Digest { + private Digest() {} + + /** SHA-1 */ + public static final int SHA1 = 1; + + /** SHA-256 */ + public static final int SHA256 = 2; + + /** SHA-384 */ + public static final int SHA384 = 4; +} + +public static final int SHA1_DIGEST_ID = Digest.SHA1; +public static final int SHA256_DIGEST_ID = Digest.SHA256; +public static final int SHA384_DIGEST_ID = Digest.SHA384; + +private static final long serialVersionUID = -9001819329700081493L; + +private int footprint; +private int alg; +private int digestid; +private byte [] digest; + +DSRecord() {} + +Record +getObject() { + return new DSRecord(); +} + +/** + * Creates a DS Record from the given data + * @param footprint The original KEY record's footprint (keyid). + * @param alg The original key algorithm. + * @param digestid The digest id code. + * @param digest A hash of the original key. + */ +public +DSRecord(Name name, int dclass, long ttl, int footprint, int alg, + int digestid, byte [] digest) +{ + super(name, Type.DS, dclass, ttl); + this.footprint = checkU16("footprint", footprint); + this.alg = checkU8("alg", alg); + this.digestid = checkU8("digestid", digestid); + this.digest = digest; +} + +/** + * Creates a DS Record from the given data + * @param digestid The digest id code. + * @param key The key to digest + */ +public +DSRecord(Name name, int dclass, long ttl, int digestid, DNSKEYRecord key) +{ + this(name, dclass, ttl, key.getFootprint(), key.getAlgorithm(), + digestid, DNSSEC.generateDSDigest(key, digestid)); +} + +void +rrFromWire(DNSInput in) throws IOException { + footprint = in.readU16(); + alg = in.readU8(); + digestid = in.readU8(); + digest = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + footprint = st.getUInt16(); + alg = st.getUInt8(); + digestid = st.getUInt8(); + digest = st.getHex(); +} + +/** + * Converts rdata to a String + */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(footprint); + sb.append(" "); + sb.append(alg); + sb.append(" "); + sb.append(digestid); + if (digest != null) { + sb.append(" "); + sb.append(base16.toString(digest)); + } + + return sb.toString(); +} + +/** + * Returns the key's algorithm. + */ +public int +getAlgorithm() { + return alg; +} + +/** + * Returns the key's Digest ID. + */ +public int +getDigestID() +{ + return digestid; +} + +/** + * Returns the binary hash of the key. + */ +public byte [] +getDigest() { + return digest; +} + +/** + * Returns the key's footprint. + */ +public int +getFootprint() { + return footprint; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(footprint); + out.writeU8(alg); + out.writeU8(digestid); + if (digest != null) + out.writeByteArray(digest); +} + +} diff --git a/src/org/xbill/DNS/EDNSOption.java b/src/org/xbill/DNS/EDNSOption.java new file mode 100644 index 0000000..a65bd19 --- /dev/null +++ b/src/org/xbill/DNS/EDNSOption.java @@ -0,0 +1,215 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) +package org.xbill.DNS; + +import java.io.*; +import java.util.Arrays; + +/** + * DNS extension options, as described in RFC 2671. The rdata of an OPT record + * is defined as a list of options; this represents a single option. + * + * @author Brian Wellington + * @author Ming Zhou <mizhou@bnivideo.com>, Beaumaris Networks + */ +public abstract class EDNSOption { + +public static class Code { + private Code() {} + + /** Name Server Identifier, RFC 5001 */ + public final static int NSID = 3; + + /** Client Subnet, defined in draft-vandergaast-edns-client-subnet-00 */ + public final static int CLIENT_SUBNET = 20730; + + private static Mnemonic codes = new Mnemonic("EDNS Option Codes", + Mnemonic.CASE_UPPER); + + static { + codes.setMaximum(0xFFFF); + codes.setPrefix("CODE"); + codes.setNumericAllowed(true); + + codes.add(NSID, "NSID"); + codes.add(CLIENT_SUBNET, "CLIENT_SUBNET"); + } + + /** + * Converts an EDNS Option Code into its textual representation + */ + public static String + string(int code) { + return codes.getText(code); + } + + /** + * Converts a textual representation of an EDNS Option Code into its + * numeric value. + * @param s The textual representation of the option code + * @return The option code, or -1 on error. + */ + public static int + value(String s) { + return codes.getValue(s); + } +} + +private final int code; + +/** + * + * Creates an option with the given option code and data. + */ +public +EDNSOption(int code) { + this.code = Record.checkU16("code", code); +} + +public String +toString() { + StringBuffer sb = new StringBuffer(); + + sb.append("{"); + sb.append(EDNSOption.Code.string(code)); + sb.append(": "); + sb.append(optionToString()); + sb.append("}"); + + return sb.toString(); +} + +/** + * Returns the EDNS Option's code. + * + * @return the option code + */ +public int +getCode() { + return code; +} + +/** + * Returns the EDNS Option's data, as a byte array. + * + * @return the option data + */ +byte [] +getData() { + DNSOutput out = new DNSOutput(); + optionToWire(out); + return out.toByteArray(); +} + +/** + * Converts the wire format of an EDNS Option (the option data only) into the + * type-specific format. + * @param in The input Stream. + */ +abstract void +optionFromWire(DNSInput in) throws IOException; + +/** + * Converts the wire format of an EDNS Option (including code and length) into + * the type-specific format. + * @param out The input stream. + */ +static EDNSOption +fromWire(DNSInput in) throws IOException { + int code, length; + + code = in.readU16(); + length = in.readU16(); + if (in.remaining() < length) + throw new WireParseException("truncated option"); + int save = in.saveActive(); + in.setActive(length); + EDNSOption option; + switch (code) { + case Code.NSID: + option = new NSIDOption(); + break; + case Code.CLIENT_SUBNET: + option = new ClientSubnetOption(); + break; + default: + option = new GenericEDNSOption(code); + break; + } + option.optionFromWire(in); + in.restoreActive(save); + + return option; +} + +/** + * Converts the wire format of an EDNS Option (including code and length) into + * the type-specific format. + * @return The option, in wire format. + */ +public static EDNSOption +fromWire(byte [] b) throws IOException { + return fromWire(new DNSInput(b)); +} + +/** + * Converts an EDNS Option (the type-specific option data only) into wire format. + * @param out The output stream. + */ +abstract void +optionToWire(DNSOutput out); + +/** + * Converts an EDNS Option (including code and length) into wire format. + * @param out The output stream. + */ +void +toWire(DNSOutput out) { + out.writeU16(code); + int lengthPosition = out.current(); + out.writeU16(0); /* until we know better */ + optionToWire(out); + int length = out.current() - lengthPosition - 2; + out.writeU16At(length, lengthPosition); +} + +/** + * Converts an EDNS Option (including code and length) into wire format. + * @return The option, in wire format. + */ +public byte [] +toWire() throws IOException { + DNSOutput out = new DNSOutput(); + toWire(out); + return out.toByteArray(); +} + +/** + * Determines if two EDNS Options are identical. + * @param arg The option to compare to + * @return true if the options are equal, false otherwise. + */ +public boolean +equals(Object arg) { + if (arg == null || !(arg instanceof EDNSOption)) + return false; + EDNSOption opt = (EDNSOption) arg; + if (code != opt.code) + return false; + return Arrays.equals(getData(), opt.getData()); +} + +/** + * Generates a hash code based on the EDNS Option's data. + */ +public int +hashCode() { + byte [] array = getData(); + int hashval = 0; + for (int i = 0; i < array.length; i++) + hashval += ((hashval << 3) + (array[i] & 0xFF)); + return hashval; +} + +abstract String optionToString(); + +} diff --git a/src/org/xbill/DNS/EmptyRecord.java b/src/org/xbill/DNS/EmptyRecord.java new file mode 100644 index 0000000..f5e61e8 --- /dev/null +++ b/src/org/xbill/DNS/EmptyRecord.java @@ -0,0 +1,42 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * A class implementing Records with no data; that is, records used in + * the question section of messages and meta-records in dynamic update. + * + * @author Brian Wellington + */ + +class EmptyRecord extends Record { + +private static final long serialVersionUID = 3601852050646429582L; + +EmptyRecord() {} + +Record +getObject() { + return new EmptyRecord(); +} + +void +rrFromWire(DNSInput in) throws IOException { +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { +} + +String +rrToString() { + return ""; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { +} + +} diff --git a/src/org/xbill/DNS/ExtendedFlags.java b/src/org/xbill/DNS/ExtendedFlags.java new file mode 100644 index 0000000..8f3bbab --- /dev/null +++ b/src/org/xbill/DNS/ExtendedFlags.java @@ -0,0 +1,45 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to EDNS flags. + * + * @author Brian Wellington + */ + +public final class ExtendedFlags { + +private static Mnemonic extflags = new Mnemonic("EDNS Flag", + Mnemonic.CASE_LOWER); + +/** dnssec ok */ +public static final int DO = 0x8000; + +static { + extflags.setMaximum(0xFFFF); + extflags.setPrefix("FLAG"); + extflags.setNumericAllowed(true); + + extflags.add(DO, "do"); +} + +private +ExtendedFlags() {} + +/** Converts a numeric extended flag into a String */ +public static String +string(int i) { + return extflags.getText(i); +} + +/** + * Converts a textual representation of an extended flag into its numeric + * value + */ +public static int +value(String s) { + return extflags.getValue(s); +} + +} diff --git a/src/org/xbill/DNS/ExtendedResolver.java b/src/org/xbill/DNS/ExtendedResolver.java new file mode 100644 index 0000000..f762b84 --- /dev/null +++ b/src/org/xbill/DNS/ExtendedResolver.java @@ -0,0 +1,419 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; +import java.io.*; +import java.net.*; + +/** + * An implementation of Resolver that can send queries to multiple servers, + * sending the queries multiple times if necessary. + * @see Resolver + * + * @author Brian Wellington + */ + +public class ExtendedResolver implements Resolver { + +private static class Resolution implements ResolverListener { + Resolver [] resolvers; + int [] sent; + Object [] inprogress; + int retries; + int outstanding; + boolean done; + Message query; + Message response; + Throwable thrown; + ResolverListener listener; + + public + Resolution(ExtendedResolver eres, Message query) { + List l = eres.resolvers; + resolvers = (Resolver []) l.toArray (new Resolver[l.size()]); + if (eres.loadBalance) { + int nresolvers = resolvers.length; + /* + * Note: this is not synchronized, since the + * worst thing that can happen is a random + * ordering, which is ok. + */ + int start = eres.lbStart++ % nresolvers; + if (eres.lbStart > nresolvers) + eres.lbStart %= nresolvers; + if (start > 0) { + Resolver [] shuffle = new Resolver[nresolvers]; + for (int i = 0; i < nresolvers; i++) { + int pos = (i + start) % nresolvers; + shuffle[i] = resolvers[pos]; + } + resolvers = shuffle; + } + } + sent = new int[resolvers.length]; + inprogress = new Object[resolvers.length]; + retries = eres.retries; + this.query = query; + } + + /* Asynchronously sends a message. */ + public void + send(int n) { + sent[n]++; + outstanding++; + try { + inprogress[n] = resolvers[n].sendAsync(query, this); + } + catch (Throwable t) { + synchronized (this) { + thrown = t; + done = true; + if (listener == null) { + notifyAll(); + return; + } + } + } + } + + /* Start a synchronous resolution */ + public Message + start() throws IOException { + try { + /* + * First, try sending synchronously. If this works, + * we're done. Otherwise, we'll get an exception + * and continue. It would be easier to call send(0), + * but this avoids a thread creation. If and when + * SimpleResolver.sendAsync() can be made to not + * create a thread, this could be changed. + */ + sent[0]++; + outstanding++; + inprogress[0] = new Object(); + return resolvers[0].send(query); + } + catch (Exception e) { + /* + * This will either cause more queries to be sent + * asynchronously or will set the 'done' flag. + */ + handleException(inprogress[0], e); + } + /* + * Wait for a successful response or for each + * subresolver to fail. + */ + synchronized (this) { + while (!done) { + try { + wait(); + } + catch (InterruptedException e) { + } + } + } + /* Return the response or throw an exception */ + if (response != null) + return response; + else if (thrown instanceof IOException) + throw (IOException) thrown; + else if (thrown instanceof RuntimeException) + throw (RuntimeException) thrown; + else if (thrown instanceof Error) + throw (Error) thrown; + else + throw new IllegalStateException + ("ExtendedResolver failure"); + } + + /* Start an asynchronous resolution */ + public void + startAsync(ResolverListener listener) { + this.listener = listener; + send(0); + } + + /* + * Receive a response. If the resolution hasn't been completed, + * either wake up the blocking thread or call the callback. + */ + public void + receiveMessage(Object id, Message m) { + if (Options.check("verbose")) + System.err.println("ExtendedResolver: " + + "received message"); + synchronized (this) { + if (done) + return; + response = m; + done = true; + if (listener == null) { + notifyAll(); + return; + } + } + listener.receiveMessage(this, response); + } + + /* + * Receive an exception. If the resolution has been completed, + * do nothing. Otherwise make progress. + */ + public void + handleException(Object id, Exception e) { + if (Options.check("verbose")) + System.err.println("ExtendedResolver: got " + e); + synchronized (this) { + outstanding--; + if (done) + return; + int n; + for (n = 0; n < inprogress.length; n++) + if (inprogress[n] == id) + break; + /* If we don't know what this is, do nothing. */ + if (n == inprogress.length) + return; + boolean startnext = false; + /* + * If this is the first response from server n, + * we should start sending queries to server n + 1. + */ + if (sent[n] == 1 && n < resolvers.length - 1) + startnext = true; + if (e instanceof InterruptedIOException) { + /* Got a timeout; resend */ + if (sent[n] < retries) + send(n); + if (thrown == null) + thrown = e; + } else if (e instanceof SocketException) { + /* + * Problem with the socket; don't resend + * on it + */ + if (thrown == null || + thrown instanceof InterruptedIOException) + thrown = e; + } else { + /* + * Problem with the response; don't resend + * on the same socket. + */ + thrown = e; + } + if (done) + return; + if (startnext) + send(n + 1); + if (done) + return; + if (outstanding == 0) { + /* + * If we're done and this is synchronous, + * wake up the blocking thread. + */ + done = true; + if (listener == null) { + notifyAll(); + return; + } + } + if (!done) + return; + } + /* If we're done and this is asynchronous, call the callback. */ + if (!(thrown instanceof Exception)) + thrown = new RuntimeException(thrown.getMessage()); + listener.handleException(this, (Exception) thrown); + } +} + +private static final int quantum = 5; + +private List resolvers; +private boolean loadBalance = false; +private int lbStart = 0; +private int retries = 3; + +private void +init() { + resolvers = new ArrayList(); +} + +/** + * Creates a new Extended Resolver. The default ResolverConfig is used to + * determine the servers for which SimpleResolver contexts should be + * initialized. + * @see SimpleResolver + * @see ResolverConfig + * @exception UnknownHostException Failure occured initializing SimpleResolvers + */ +public +ExtendedResolver() throws UnknownHostException { + init(); + String [] servers = ResolverConfig.getCurrentConfig().servers(); + if (servers != null) { + for (int i = 0; i < servers.length; i++) { + Resolver r = new SimpleResolver(servers[i]); + r.setTimeout(quantum); + resolvers.add(r); + } + } + else + resolvers.add(new SimpleResolver()); +} + +/** + * Creates a new Extended Resolver + * @param servers An array of server names for which SimpleResolver + * contexts should be initialized. + * @see SimpleResolver + * @exception UnknownHostException Failure occured initializing SimpleResolvers + */ +public +ExtendedResolver(String [] servers) throws UnknownHostException { + init(); + for (int i = 0; i < servers.length; i++) { + Resolver r = new SimpleResolver(servers[i]); + r.setTimeout(quantum); + resolvers.add(r); + } +} + +/** + * Creates a new Extended Resolver + * @param res An array of pre-initialized Resolvers is provided. + * @see SimpleResolver + * @exception UnknownHostException Failure occured initializing SimpleResolvers + */ +public +ExtendedResolver(Resolver [] res) throws UnknownHostException { + init(); + for (int i = 0; i < res.length; i++) + resolvers.add(res[i]); +} + +public void +setPort(int port) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setPort(port); +} + +public void +setTCP(boolean flag) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setTCP(flag); +} + +public void +setIgnoreTruncation(boolean flag) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setIgnoreTruncation(flag); +} + +public void +setEDNS(int level) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setEDNS(level); +} + +public void +setEDNS(int level, int payloadSize, int flags, List options) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setEDNS(level, payloadSize, + flags, options); +} + +public void +setTSIGKey(TSIG key) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setTSIGKey(key); +} + +public void +setTimeout(int secs, int msecs) { + for (int i = 0; i < resolvers.size(); i++) + ((Resolver)resolvers.get(i)).setTimeout(secs, msecs); +} + +public void +setTimeout(int secs) { + setTimeout(secs, 0); +} + +/** + * Sends a message and waits for a response. Multiple servers are queried, + * and queries are sent multiple times until either a successful response + * is received, or it is clear that there is no successful response. + * @param query The query to send. + * @return The response. + * @throws IOException An error occurred while sending or receiving. + */ +public Message +send(Message query) throws IOException { + Resolution res = new Resolution(this, query); + return res.start(); +} + +/** + * Asynchronously sends a message to multiple servers, potentially multiple + * times, registering a listener to receive a callback on success or exception. + * Multiple asynchronous lookups can be performed in parallel. Since the + * callback may be invoked before the function returns, external + * synchronization is necessary. + * @param query The query to send + * @param listener The object containing the callbacks. + * @return An identifier, which is also a parameter in the callback + */ +public Object +sendAsync(final Message query, final ResolverListener listener) { + Resolution res = new Resolution(this, query); + res.startAsync(listener); + return res; +} + +/** Returns the nth resolver used by this ExtendedResolver */ +public Resolver +getResolver(int n) { + if (n < resolvers.size()) + return (Resolver)resolvers.get(n); + return null; +} + +/** Returns all resolvers used by this ExtendedResolver */ +public Resolver [] +getResolvers() { + return (Resolver []) resolvers.toArray(new Resolver[resolvers.size()]); +} + +/** Adds a new resolver to be used by this ExtendedResolver */ +public void +addResolver(Resolver r) { + resolvers.add(r); +} + +/** Deletes a resolver used by this ExtendedResolver */ +public void +deleteResolver(Resolver r) { + resolvers.remove(r); +} + +/** Sets whether the servers should be load balanced. + * @param flag If true, servers will be tried in round-robin order. If false, + * servers will always be queried in the same order. + */ +public void +setLoadBalance(boolean flag) { + loadBalance = flag; +} + +/** Sets the number of retries sent to each server per query */ +public void +setRetries(int retries) { + this.retries = retries; +} + +} diff --git a/src/org/xbill/DNS/Flags.java b/src/org/xbill/DNS/Flags.java new file mode 100644 index 0000000..964ce23 --- /dev/null +++ b/src/org/xbill/DNS/Flags.java @@ -0,0 +1,81 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to flags in the DNS header. + * + * @author Brian Wellington + */ + +public final class Flags { + +private static Mnemonic flags = new Mnemonic("DNS Header Flag", + Mnemonic.CASE_LOWER); + +/** query/response */ +public static final byte QR = 0; + +/** authoritative answer */ +public static final byte AA = 5; + +/** truncated */ +public static final byte TC = 6; + +/** recursion desired */ +public static final byte RD = 7; + +/** recursion available */ +public static final byte RA = 8; + +/** authenticated data */ +public static final byte AD = 10; + +/** (security) checking disabled */ +public static final byte CD = 11; + +/** dnssec ok (extended) */ +public static final int DO = ExtendedFlags.DO; + +static { + flags.setMaximum(0xF); + flags.setPrefix("FLAG"); + flags.setNumericAllowed(true); + + flags.add(QR, "qr"); + flags.add(AA, "aa"); + flags.add(TC, "tc"); + flags.add(RD, "rd"); + flags.add(RA, "ra"); + flags.add(AD, "ad"); + flags.add(CD, "cd"); +} + +private +Flags() {} + +/** Converts a numeric Flag into a String */ +public static String +string(int i) { + return flags.getText(i); +} + +/** Converts a String representation of an Flag into its numeric value */ +public static int +value(String s) { + return flags.getValue(s); +} + +/** + * Indicates if a bit in the flags field is a flag or not. If it's part of + * the rcode or opcode, it's not. + */ +public static boolean +isFlag(int index) { + flags.check(index); + if ((index >= 1 && index <= 4) || (index >= 12)) + return false; + return true; +} + +} diff --git a/src/org/xbill/DNS/FormattedTime.java b/src/org/xbill/DNS/FormattedTime.java new file mode 100644 index 0000000..c76a846 --- /dev/null +++ b/src/org/xbill/DNS/FormattedTime.java @@ -0,0 +1,79 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Routines for converting time values to and from YYYYMMDDHHMMSS format. + * + * @author Brian Wellington + */ + +import java.util.*; +import java.text.*; + +final class FormattedTime { + +private static NumberFormat w2, w4; + +static { + w2 = new DecimalFormat(); + w2.setMinimumIntegerDigits(2); + + w4 = new DecimalFormat(); + w4.setMinimumIntegerDigits(4); + w4.setGroupingUsed(false); +} + +private +FormattedTime() {} + +/** + * Converts a Date into a formatted string. + * @param date The Date to convert. + * @return The formatted string. + */ +public static String +format(Date date) { + Calendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + StringBuffer sb = new StringBuffer(); + + c.setTime(date); + sb.append(w4.format(c.get(Calendar.YEAR))); + sb.append(w2.format(c.get(Calendar.MONTH)+1)); + sb.append(w2.format(c.get(Calendar.DAY_OF_MONTH))); + sb.append(w2.format(c.get(Calendar.HOUR_OF_DAY))); + sb.append(w2.format(c.get(Calendar.MINUTE))); + sb.append(w2.format(c.get(Calendar.SECOND))); + return sb.toString(); +} + +/** + * Parses a formatted time string into a Date. + * @param s The string, in the form YYYYMMDDHHMMSS. + * @return The Date object. + * @throws TextParseExcetption The string was invalid. + */ +public static Date +parse(String s) throws TextParseException { + if (s.length() != 14) { + throw new TextParseException("Invalid time encoding: " + s); + } + + Calendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC")); + c.clear(); + try { + int year = Integer.parseInt(s.substring(0, 4)); + int month = Integer.parseInt(s.substring(4, 6)) - 1; + int date = Integer.parseInt(s.substring(6, 8)); + int hour = Integer.parseInt(s.substring(8, 10)); + int minute = Integer.parseInt(s.substring(10, 12)); + int second = Integer.parseInt(s.substring(12, 14)); + c.set(year, month, date, hour, minute, second); + } + catch (NumberFormatException e) { + throw new TextParseException("Invalid time encoding: " + s); + } + return c.getTime(); +} + +} diff --git a/src/org/xbill/DNS/GPOSRecord.java b/src/org/xbill/DNS/GPOSRecord.java new file mode 100644 index 0000000..688d567 --- /dev/null +++ b/src/org/xbill/DNS/GPOSRecord.java @@ -0,0 +1,178 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Geographical Location - describes the physical location of a host. + * + * @author Brian Wellington + */ + +public class GPOSRecord extends Record { + +private static final long serialVersionUID = -6349714958085750705L; + +private byte [] latitude, longitude, altitude; + +GPOSRecord() {} + +Record +getObject() { + return new GPOSRecord(); +} + +private void +validate(double longitude, double latitude) throws IllegalArgumentException +{ + if (longitude < -90.0 || longitude > 90.0) { + throw new IllegalArgumentException("illegal longitude " + + longitude); + } + if (latitude < -180.0 || latitude > 180.0) { + throw new IllegalArgumentException("illegal latitude " + + latitude); + } +} + +/** + * Creates an GPOS Record from the given data + * @param longitude The longitude component of the location. + * @param latitude The latitude component of the location. + * @param altitude The altitude component of the location (in meters above sea + * level). +*/ +public +GPOSRecord(Name name, int dclass, long ttl, double longitude, double latitude, + double altitude) +{ + super(name, Type.GPOS, dclass, ttl); + validate(longitude, latitude); + this.longitude = Double.toString(longitude).getBytes(); + this.latitude = Double.toString(latitude).getBytes(); + this.altitude = Double.toString(altitude).getBytes(); +} + +/** + * Creates an GPOS Record from the given data + * @param longitude The longitude component of the location. + * @param latitude The latitude component of the location. + * @param altitude The altitude component of the location (in meters above sea + * level). +*/ +public +GPOSRecord(Name name, int dclass, long ttl, String longitude, String latitude, + String altitude) +{ + super(name, Type.GPOS, dclass, ttl); + try { + this.longitude = byteArrayFromString(longitude); + this.latitude = byteArrayFromString(latitude); + validate(getLongitude(), getLatitude()); + this.altitude = byteArrayFromString(altitude); + } + catch (TextParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } +} + +void +rrFromWire(DNSInput in) throws IOException { + longitude = in.readCountedString(); + latitude = in.readCountedString(); + altitude = in.readCountedString(); + try { + validate(getLongitude(), getLatitude()); + } + catch(IllegalArgumentException e) { + throw new WireParseException(e.getMessage()); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + try { + longitude = byteArrayFromString(st.getString()); + latitude = byteArrayFromString(st.getString()); + altitude = byteArrayFromString(st.getString()); + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } + try { + validate(getLongitude(), getLatitude()); + } + catch(IllegalArgumentException e) { + throw new WireParseException(e.getMessage()); + } +} + +/** Convert to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(byteArrayToString(longitude, true)); + sb.append(" "); + sb.append(byteArrayToString(latitude, true)); + sb.append(" "); + sb.append(byteArrayToString(altitude, true)); + return sb.toString(); +} + +/** Returns the longitude as a string */ +public String +getLongitudeString() { + return byteArrayToString(longitude, false); +} + +/** + * Returns the longitude as a double + * @throws NumberFormatException The string does not contain a valid numeric + * value. + */ +public double +getLongitude() { + return Double.parseDouble(getLongitudeString()); +} + +/** Returns the latitude as a string */ +public String +getLatitudeString() { + return byteArrayToString(latitude, false); +} + +/** + * Returns the latitude as a double + * @throws NumberFormatException The string does not contain a valid numeric + * value. + */ +public double +getLatitude() { + return Double.parseDouble(getLatitudeString()); +} + +/** Returns the altitude as a string */ +public String +getAltitudeString() { + return byteArrayToString(altitude, false); +} + +/** + * Returns the altitude as a double + * @throws NumberFormatException The string does not contain a valid numeric + * value. + */ +public double +getAltitude() { + return Double.parseDouble(getAltitudeString()); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeCountedString(longitude); + out.writeCountedString(latitude); + out.writeCountedString(altitude); +} + +} diff --git a/src/org/xbill/DNS/Generator.java b/src/org/xbill/DNS/Generator.java new file mode 100644 index 0000000..a08d343 --- /dev/null +++ b/src/org/xbill/DNS/Generator.java @@ -0,0 +1,264 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A representation of a $GENERATE statement in a master file. + * + * @author Brian Wellington + */ + +public class Generator { + +/** The start of the range. */ +public long start; + +/** The end of the range. */ +public long end; + +/** The step value of the range. */ +public long step; + +/** The pattern to use for generating record names. */ +public final String namePattern; + +/** The type of the generated records. */ +public final int type; + +/** The class of the generated records. */ +public final int dclass; + +/** The ttl of the generated records. */ +public final long ttl; + +/** The pattern to use for generating record data. */ +public final String rdataPattern; + +/** The origin to append to relative names. */ +public final Name origin; + +private long current; + +/** + * Indicates whether generation is supported for this type. + * @throws InvalidTypeException The type is out of range. + */ +public static boolean +supportedType(int type) { + Type.check(type); + return (type == Type.PTR || type == Type.CNAME || type == Type.DNAME || + type == Type.A || type == Type.AAAA || type == Type.NS); +} + +/** + * Creates a specification for generating records, as a $GENERATE + * statement in a master file. + * @param start The start of the range. + * @param end The end of the range. + * @param step The step value of the range. + * @param namePattern The pattern to use for generating record names. + * @param type The type of the generated records. The supported types are + * PTR, CNAME, DNAME, A, AAAA, and NS. + * @param dclass The class of the generated records. + * @param ttl The ttl of the generated records. + * @param rdataPattern The pattern to use for generating record data. + * @param origin The origin to append to relative names. + * @throws IllegalArgumentException The range is invalid. + * @throws IllegalArgumentException The type does not support generation. + * @throws IllegalArgumentException The dclass is not a valid class. + */ +public +Generator(long start, long end, long step, String namePattern, + int type, int dclass, long ttl, String rdataPattern, Name origin) +{ + if (start < 0 || end < 0 || start > end || step <= 0) + throw new IllegalArgumentException + ("invalid range specification"); + if (!supportedType(type)) + throw new IllegalArgumentException("unsupported type"); + DClass.check(dclass); + + this.start = start; + this.end = end; + this.step = step; + this.namePattern = namePattern; + this.type = type; + this.dclass = dclass; + this.ttl = ttl; + this.rdataPattern = rdataPattern; + this.origin = origin; + this.current = start; +} + +private String +substitute(String spec, long n) throws IOException { + boolean escaped = false; + byte [] str = spec.getBytes(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < str.length; i++) { + char c = (char)(str[i] & 0xFF); + if (escaped) { + sb.append(c); + escaped = false; + } else if (c == '\\') { + if (i + 1 == str.length) + throw new TextParseException + ("invalid escape character"); + escaped = true; + } else if (c == '$') { + boolean negative = false; + long offset = 0; + long width = 0; + long base = 10; + boolean wantUpperCase = false; + if (i + 1 < str.length && str[i + 1] == '$') { + // '$$' == literal '$' for backwards + // compatibility with old versions of BIND. + c = (char)(str[++i] & 0xFF); + sb.append(c); + continue; + } else if (i + 1 < str.length && str[i + 1] == '{') { + // It's a substitution with modifiers. + i++; + if (i + 1 < str.length && str[i + 1] == '-') { + negative = true; + i++; + } + while (i + 1 < str.length) { + c = (char)(str[++i] & 0xFF); + if (c == ',' || c == '}') + break; + if (c < '0' || c > '9') + throw new TextParseException( + "invalid offset"); + c -= '0'; + offset *= 10; + offset += c; + } + if (negative) + offset = -offset; + + if (c == ',') { + while (i + 1 < str.length) { + c = (char)(str[++i] & 0xFF); + if (c == ',' || c == '}') + break; + if (c < '0' || c > '9') + throw new + TextParseException( + "invalid width"); + c -= '0'; + width *= 10; + width += c; + } + } + + if (c == ',') { + if (i + 1 == str.length) + throw new TextParseException( + "invalid base"); + c = (char)(str[++i] & 0xFF); + if (c == 'o') + base = 8; + else if (c == 'x') + base = 16; + else if (c == 'X') { + base = 16; + wantUpperCase = true; + } + else if (c != 'd') + throw new TextParseException( + "invalid base"); + } + + if (i + 1 == str.length || str[i + 1] != '}') + throw new TextParseException + ("invalid modifiers"); + i++; + } + long v = n + offset; + if (v < 0) + throw new TextParseException + ("invalid offset expansion"); + String number; + if (base == 8) + number = Long.toOctalString(v); + else if (base == 16) + number = Long.toHexString(v); + else + number = Long.toString(v); + if (wantUpperCase) + number = number.toUpperCase(); + if (width != 0 && width > number.length()) { + int zeros = (int)width - number.length(); + while (zeros-- > 0) + sb.append('0'); + } + sb.append(number); + } else { + sb.append(c); + } + } + return sb.toString(); +} + +/** + * Constructs and returns the next record in the expansion. + * @throws IOException The name or rdata was invalid after substitutions were + * performed. + */ +public Record +nextRecord() throws IOException { + if (current > end) + return null; + String namestr = substitute(namePattern, current); + Name name = Name.fromString(namestr, origin); + String rdata = substitute(rdataPattern, current); + current += step; + return Record.fromString(name, type, dclass, ttl, rdata, origin); +} + +/** + * Constructs and returns all records in the expansion. + * @throws IOException The name or rdata of a record was invalid after + * substitutions were performed. + */ +public Record [] +expand() throws IOException { + List list = new ArrayList(); + for (long i = start; i < end; i += step) { + String namestr = substitute(namePattern, current); + Name name = Name.fromString(namestr, origin); + String rdata = substitute(rdataPattern, current); + list.add(Record.fromString(name, type, dclass, ttl, + rdata, origin)); + } + return (Record []) list.toArray(new Record[list.size()]); +} + +/** + * Converts the generate specification to a string containing the corresponding + * $GENERATE statement. + */ +public String +toString() { + StringBuffer sb = new StringBuffer(); + sb.append("$GENERATE "); + sb.append(start + "-" + end); + if (step > 1) + sb.append("/" + step); + sb.append(" "); + sb.append(namePattern + " "); + sb.append(ttl + " "); + if (dclass != DClass.IN || !Options.check("noPrintIN")) + sb.append(DClass.string(dclass) + " "); + sb.append(Type.string(type) + " "); + sb.append(rdataPattern + " "); + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/GenericEDNSOption.java b/src/org/xbill/DNS/GenericEDNSOption.java new file mode 100644 index 0000000..7c8b51b --- /dev/null +++ b/src/org/xbill/DNS/GenericEDNSOption.java @@ -0,0 +1,47 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) +package org.xbill.DNS; + +import java.io.*; + +import org.xbill.DNS.utils.base16; + +/** + * An EDNSOption with no internal structure. + * + * @author Ming Zhou <mizhou@bnivideo.com>, Beaumaris Networks + * @author Brian Wellington + */ +public class GenericEDNSOption extends EDNSOption { + +private byte [] data; + +GenericEDNSOption(int code) { + super(code); +} + +/** + * Construct a generic EDNS option. + * @param data The contents of the option. + */ +public +GenericEDNSOption(int code, byte [] data) { + super(code); + this.data = Record.checkByteArrayLength("option data", data, 0xFFFF); +} + +void +optionFromWire(DNSInput in) throws IOException { + data = in.readByteArray(); +} + +void +optionToWire(DNSOutput out) { + out.writeByteArray(data); +} + +String +optionToString() { + return "<" + base16.toString(data) + ">"; +} + +} diff --git a/src/org/xbill/DNS/HINFORecord.java b/src/org/xbill/DNS/HINFORecord.java new file mode 100644 index 0000000..18fed32 --- /dev/null +++ b/src/org/xbill/DNS/HINFORecord.java @@ -0,0 +1,95 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Host Information - describes the CPU and OS of a host + * + * @author Brian Wellington + */ + +public class HINFORecord extends Record { + +private static final long serialVersionUID = -4732870630947452112L; + +private byte [] cpu, os; + +HINFORecord() {} + +Record +getObject() { + return new HINFORecord(); +} + +/** + * Creates an HINFO Record from the given data + * @param cpu A string describing the host's CPU + * @param os A string describing the host's OS + * @throws IllegalArgumentException One of the strings has invalid escapes + */ +public +HINFORecord(Name name, int dclass, long ttl, String cpu, String os) { + super(name, Type.HINFO, dclass, ttl); + try { + this.cpu = byteArrayFromString(cpu); + this.os = byteArrayFromString(os); + } + catch (TextParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } +} + +void +rrFromWire(DNSInput in) throws IOException { + cpu = in.readCountedString(); + os = in.readCountedString(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + try { + cpu = byteArrayFromString(st.getString()); + os = byteArrayFromString(st.getString()); + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } +} + +/** + * Returns the host's CPU + */ +public String +getCPU() { + return byteArrayToString(cpu, false); +} + +/** + * Returns the host's OS + */ +public String +getOS() { + return byteArrayToString(os, false); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeCountedString(cpu); + out.writeCountedString(os); +} + +/** + * Converts to a string + */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(byteArrayToString(cpu, true)); + sb.append(" "); + sb.append(byteArrayToString(os, true)); + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/Header.java b/src/org/xbill/DNS/Header.java new file mode 100644 index 0000000..2a44d08 --- /dev/null +++ b/src/org/xbill/DNS/Header.java @@ -0,0 +1,286 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A DNS message header + * @see Message + * + * @author Brian Wellington + */ + +public class Header implements Cloneable { + +private int id; +private int flags; +private int [] counts; + +private static Random random = new Random(); + +/** The length of a DNS Header in wire format. */ +public static final int LENGTH = 12; + +private void +init() { + counts = new int[4]; + flags = 0; + id = -1; +} + +/** + * Create a new empty header. + * @param id The message id + */ +public +Header(int id) { + init(); + setID(id); +} + +/** + * Create a new empty header with a random message id + */ +public +Header() { + init(); +} + +/** + * Parses a Header from a stream containing DNS wire format. + */ +Header(DNSInput in) throws IOException { + this(in.readU16()); + flags = in.readU16(); + for (int i = 0; i < counts.length; i++) + counts[i] = in.readU16(); +} + +/** + * Creates a new Header from its DNS wire format representation + * @param b A byte array containing the DNS Header. + */ +public +Header(byte [] b) throws IOException { + this(new DNSInput(b)); +} + +void +toWire(DNSOutput out) { + out.writeU16(getID()); + out.writeU16(flags); + for (int i = 0; i < counts.length; i++) + out.writeU16(counts[i]); +} + +public byte [] +toWire() { + DNSOutput out = new DNSOutput(); + toWire(out); + return out.toByteArray(); +} + +static private boolean +validFlag(int bit) { + return (bit >= 0 && bit <= 0xF && Flags.isFlag(bit)); +} + +static private void +checkFlag(int bit) { + if (!validFlag(bit)) + throw new IllegalArgumentException("invalid flag bit " + bit); +} + +/** + * Sets a flag to the supplied value + * @see Flags + */ +public void +setFlag(int bit) { + checkFlag(bit); + // bits are indexed from left to right + flags |= (1 << (15 - bit)); +} + +/** + * Sets a flag to the supplied value + * @see Flags + */ +public void +unsetFlag(int bit) { + checkFlag(bit); + // bits are indexed from left to right + flags &= ~(1 << (15 - bit)); +} + +/** + * Retrieves a flag + * @see Flags + */ +public boolean +getFlag(int bit) { + checkFlag(bit); + // bits are indexed from left to right + return (flags & (1 << (15 - bit))) != 0; +} + +boolean [] +getFlags() { + boolean [] array = new boolean[16]; + for (int i = 0; i < array.length; i++) + if (validFlag(i)) + array[i] = getFlag(i); + return array; +} + +/** + * Retrieves the message ID + */ +public int +getID() { + if (id >= 0) + return id; + synchronized (this) { + if (id < 0) + id = random.nextInt(0xffff); + return id; + } +} + +/** + * Sets the message ID + */ +public void +setID(int id) { + if (id < 0 || id > 0xffff) + throw new IllegalArgumentException("DNS message ID " + id + + " is out of range"); + this.id = id; +} + +/** + * Sets the message's rcode + * @see Rcode + */ +public void +setRcode(int value) { + if (value < 0 || value > 0xF) + throw new IllegalArgumentException("DNS Rcode " + value + + " is out of range"); + flags &= ~0xF; + flags |= value; +} + +/** + * Retrieves the mesasge's rcode + * @see Rcode + */ +public int +getRcode() { + return flags & 0xF; +} + +/** + * Sets the message's opcode + * @see Opcode + */ +public void +setOpcode(int value) { + if (value < 0 || value > 0xF) + throw new IllegalArgumentException("DNS Opcode " + value + + "is out of range"); + flags &= 0x87FF; + flags |= (value << 11); +} + +/** + * Retrieves the mesasge's opcode + * @see Opcode + */ +public int +getOpcode() { + return (flags >> 11) & 0xF; +} + +void +setCount(int field, int value) { + if (value < 0 || value > 0xFFFF) + throw new IllegalArgumentException("DNS section count " + + value + " is out of range"); + counts[field] = value; +} + +void +incCount(int field) { + if (counts[field] == 0xFFFF) + throw new IllegalStateException("DNS section count cannot " + + "be incremented"); + counts[field]++; +} + +void +decCount(int field) { + if (counts[field] == 0) + throw new IllegalStateException("DNS section count cannot " + + "be decremented"); + counts[field]--; +} + +/** + * Retrieves the record count for the given section + * @see Section + */ +public int +getCount(int field) { + return counts[field]; +} + +/** Converts the header's flags into a String */ +public String +printFlags() { + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < 16; i++) + if (validFlag(i) && getFlag(i)) { + sb.append(Flags.string(i)); + sb.append(" "); + } + return sb.toString(); +} + +String +toStringWithRcode(int newrcode) { + StringBuffer sb = new StringBuffer(); + + sb.append(";; ->>HEADER<<- "); + sb.append("opcode: " + Opcode.string(getOpcode())); + sb.append(", status: " + Rcode.string(newrcode)); + sb.append(", id: " + getID()); + sb.append("\n"); + + sb.append(";; flags: " + printFlags()); + sb.append("; "); + for (int i = 0; i < 4; i++) + sb.append(Section.string(i) + ": " + getCount(i) + " "); + return sb.toString(); +} + +/** Converts the header into a String */ +public String +toString() { + return toStringWithRcode(getRcode()); +} + +/* Creates a new Header identical to the current one */ +public Object +clone() { + Header h = new Header(); + h.id = id; + h.flags = flags; + System.arraycopy(counts, 0, h.counts, 0, counts.length); + return h; +} + +} diff --git a/src/org/xbill/DNS/IPSECKEYRecord.java b/src/org/xbill/DNS/IPSECKEYRecord.java new file mode 100644 index 0000000..7eb2956 --- /dev/null +++ b/src/org/xbill/DNS/IPSECKEYRecord.java @@ -0,0 +1,231 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import org.xbill.DNS.utils.*; + +/** + * IPsec Keying Material (RFC 4025) + * + * @author Brian Wellington + */ + +public class IPSECKEYRecord extends Record { + +private static final long serialVersionUID = 3050449702765909687L; + +public static class Algorithm { + private Algorithm() {} + + public static final int DSA = 1; + public static final int RSA = 2; +} + +public static class Gateway { + private Gateway() {} + + public static final int None = 0; + public static final int IPv4 = 1; + public static final int IPv6 = 2; + public static final int Name = 3; +} + +private int precedence; +private int gatewayType; +private int algorithmType; +private Object gateway; +private byte [] key; + +IPSECKEYRecord() {} + +Record +getObject() { + return new IPSECKEYRecord(); +} + +/** + * Creates an IPSECKEY Record from the given data. + * @param precedence The record's precedence. + * @param gatewayType The record's gateway type. + * @param algorithmType The record's algorithm type. + * @param gateway The record's gateway. + * @param key The record's public key. + */ +public +IPSECKEYRecord(Name name, int dclass, long ttl, int precedence, + int gatewayType, int algorithmType, Object gateway, + byte [] key) +{ + super(name, Type.IPSECKEY, dclass, ttl); + this.precedence = checkU8("precedence", precedence); + this.gatewayType = checkU8("gatewayType", gatewayType); + this.algorithmType = checkU8("algorithmType", algorithmType); + switch (gatewayType) { + case Gateway.None: + this.gateway = null; + break; + case Gateway.IPv4: + if (!(gateway instanceof InetAddress)) + throw new IllegalArgumentException("\"gateway\" " + + "must be an IPv4 " + + "address"); + this.gateway = gateway; + break; + case Gateway.IPv6: + if (!(gateway instanceof Inet6Address)) + throw new IllegalArgumentException("\"gateway\" " + + "must be an IPv6 " + + "address"); + this.gateway = gateway; + break; + case Gateway.Name: + if (!(gateway instanceof Name)) + throw new IllegalArgumentException("\"gateway\" " + + "must be a DNS " + + "name"); + this.gateway = checkName("gateway", (Name) gateway); + break; + default: + throw new IllegalArgumentException("\"gatewayType\" " + + "must be between 0 and 3"); + } + + this.key = key; +} + +void +rrFromWire(DNSInput in) throws IOException { + precedence = in.readU8(); + gatewayType = in.readU8(); + algorithmType = in.readU8(); + switch (gatewayType) { + case Gateway.None: + gateway = null; + break; + case Gateway.IPv4: + gateway = InetAddress.getByAddress(in.readByteArray(4)); + break; + case Gateway.IPv6: + gateway = InetAddress.getByAddress(in.readByteArray(16)); + break; + case Gateway.Name: + gateway = new Name(in); + break; + default: + throw new WireParseException("invalid gateway type"); + } + if (in.remaining() > 0) + key = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + precedence = st.getUInt8(); + gatewayType = st.getUInt8(); + algorithmType = st.getUInt8(); + switch (gatewayType) { + case Gateway.None: + String s = st.getString(); + if (!s.equals(".")) + throw new TextParseException("invalid gateway format"); + gateway = null; + break; + case Gateway.IPv4: + gateway = st.getAddress(Address.IPv4); + break; + case Gateway.IPv6: + gateway = st.getAddress(Address.IPv6); + break; + case Gateway.Name: + gateway = st.getName(origin); + break; + default: + throw new WireParseException("invalid gateway type"); + } + key = st.getBase64(false); +} + +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(precedence); + sb.append(" "); + sb.append(gatewayType); + sb.append(" "); + sb.append(algorithmType); + sb.append(" "); + switch (gatewayType) { + case Gateway.None: + sb.append("."); + break; + case Gateway.IPv4: + case Gateway.IPv6: + InetAddress gatewayAddr = (InetAddress) gateway; + sb.append(gatewayAddr.getHostAddress()); + break; + case Gateway.Name: + sb.append(gateway); + break; + } + if (key != null) { + sb.append(" "); + sb.append(base64.toString(key)); + } + return sb.toString(); +} + +/** Returns the record's precedence. */ +public int +getPrecedence() { + return precedence; +} + +/** Returns the record's gateway type. */ +public int +getGatewayType() { + return gatewayType; +} + +/** Returns the record's algorithm type. */ +public int +getAlgorithmType() { + return algorithmType; +} + +/** Returns the record's gateway. */ +public Object +getGateway() { + return gateway; +} + +/** Returns the record's public key */ +public byte [] +getKey() { + return key; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(precedence); + out.writeU8(gatewayType); + out.writeU8(algorithmType); + switch (gatewayType) { + case Gateway.None: + break; + case Gateway.IPv4: + case Gateway.IPv6: + InetAddress gatewayAddr = (InetAddress) gateway; + out.writeByteArray(gatewayAddr.getAddress()); + break; + case Gateway.Name: + Name gatewayName = (Name) gateway; + gatewayName.toWire(out, null, canonical); + break; + } + if (key != null) + out.writeByteArray(key); +} + +} diff --git a/src/org/xbill/DNS/ISDNRecord.java b/src/org/xbill/DNS/ISDNRecord.java new file mode 100644 index 0000000..8f9b629 --- /dev/null +++ b/src/org/xbill/DNS/ISDNRecord.java @@ -0,0 +1,105 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * ISDN - identifies the ISDN number and subaddress associated with a name. + * + * @author Brian Wellington + */ + +public class ISDNRecord extends Record { + +private static final long serialVersionUID = -8730801385178968798L; + +private byte [] address; +private byte [] subAddress; + +ISDNRecord() {} + +Record +getObject() { + return new ISDNRecord(); +} + +/** + * Creates an ISDN Record from the given data + * @param address The ISDN number associated with the domain. + * @param subAddress The subaddress, if any. + * @throws IllegalArgumentException One of the strings is invalid. + */ +public +ISDNRecord(Name name, int dclass, long ttl, String address, String subAddress) { + super(name, Type.ISDN, dclass, ttl); + try { + this.address = byteArrayFromString(address); + if (subAddress != null) + this.subAddress = byteArrayFromString(subAddress); + } + catch (TextParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } +} + +void +rrFromWire(DNSInput in) throws IOException { + address = in.readCountedString(); + if (in.remaining() > 0) + subAddress = in.readCountedString(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + try { + address = byteArrayFromString(st.getString()); + Tokenizer.Token t = st.get(); + if (t.isString()) { + subAddress = byteArrayFromString(t.value); + } else { + st.unget(); + } + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } +} + +/** + * Returns the ISDN number associated with the domain. + */ +public String +getAddress() { + return byteArrayToString(address, false); +} + +/** + * Returns the ISDN subaddress, or null if there is none. + */ +public String +getSubAddress() { + if (subAddress == null) + return null; + return byteArrayToString(subAddress, false); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeCountedString(address); + if (subAddress != null) + out.writeCountedString(subAddress); +} + +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(byteArrayToString(address, true)); + if (subAddress != null) { + sb.append(" "); + sb.append(byteArrayToString(subAddress, true)); + } + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/InvalidDClassException.java b/src/org/xbill/DNS/InvalidDClassException.java new file mode 100644 index 0000000..6c95cd4 --- /dev/null +++ b/src/org/xbill/DNS/InvalidDClassException.java @@ -0,0 +1,18 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when an invalid dclass code is specified. + * + * @author Brian Wellington + */ + +public class InvalidDClassException extends IllegalArgumentException { + +public +InvalidDClassException(int dclass) { + super("Invalid DNS class: " + dclass); +} + +} diff --git a/src/org/xbill/DNS/InvalidTTLException.java b/src/org/xbill/DNS/InvalidTTLException.java new file mode 100644 index 0000000..95776fe --- /dev/null +++ b/src/org/xbill/DNS/InvalidTTLException.java @@ -0,0 +1,18 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when an invalid TTL is specified. + * + * @author Brian Wellington + */ + +public class InvalidTTLException extends IllegalArgumentException { + +public +InvalidTTLException(long ttl) { + super("Invalid DNS TTL: " + ttl); +} + +} diff --git a/src/org/xbill/DNS/InvalidTypeException.java b/src/org/xbill/DNS/InvalidTypeException.java new file mode 100644 index 0000000..7c61276 --- /dev/null +++ b/src/org/xbill/DNS/InvalidTypeException.java @@ -0,0 +1,18 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when an invalid type code is specified. + * + * @author Brian Wellington + */ + +public class InvalidTypeException extends IllegalArgumentException { + +public +InvalidTypeException(int type) { + super("Invalid DNS type: " + type); +} + +} diff --git a/src/org/xbill/DNS/KEYBase.java b/src/org/xbill/DNS/KEYBase.java new file mode 100644 index 0000000..59a2c6c --- /dev/null +++ b/src/org/xbill/DNS/KEYBase.java @@ -0,0 +1,161 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.security.PublicKey; + +import org.xbill.DNS.utils.*; + +/** + * The base class for KEY/DNSKEY records, which have identical formats + * + * @author Brian Wellington + */ + +abstract class KEYBase extends Record { + +private static final long serialVersionUID = 3469321722693285454L; + +protected int flags, proto, alg; +protected byte [] key; +protected int footprint = -1; +protected PublicKey publicKey = null; + +protected +KEYBase() {} + +public +KEYBase(Name name, int type, int dclass, long ttl, int flags, int proto, + int alg, byte [] key) +{ + super(name, type, dclass, ttl); + this.flags = checkU16("flags", flags); + this.proto = checkU8("proto", proto); + this.alg = checkU8("alg", alg); + this.key = key; +} + +void +rrFromWire(DNSInput in) throws IOException { + flags = in.readU16(); + proto = in.readU8(); + alg = in.readU8(); + if (in.remaining() > 0) + key = in.readByteArray(); +} + +/** Converts the DNSKEY/KEY Record to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(flags); + sb.append(" "); + sb.append(proto); + sb.append(" "); + sb.append(alg); + if (key != null) { + if (Options.check("multiline")) { + sb.append(" (\n"); + sb.append(base64.formatString(key, 64, "\t", true)); + sb.append(" ; key_tag = "); + sb.append(getFootprint()); + } else { + sb.append(" "); + sb.append(base64.toString(key)); + } + } + return sb.toString(); +} + +/** + * Returns the flags describing the key's properties + */ +public int +getFlags() { + return flags; +} + +/** + * Returns the protocol that the key was created for + */ +public int +getProtocol() { + return proto; +} + +/** + * Returns the key's algorithm + */ +public int +getAlgorithm() { + return alg; +} + +/** + * Returns the binary data representing the key + */ +public byte [] +getKey() { + return key; +} + +/** + * Returns the key's footprint (after computing it) + */ +public int +getFootprint() { + if (footprint >= 0) + return footprint; + + int foot = 0; + + DNSOutput out = new DNSOutput(); + rrToWire(out, null, false); + byte [] rdata = out.toByteArray(); + + if (alg == DNSSEC.Algorithm.RSAMD5) { + int d1 = rdata[rdata.length - 3] & 0xFF; + int d2 = rdata[rdata.length - 2] & 0xFF; + foot = (d1 << 8) + d2; + } + else { + int i; + for (i = 0; i < rdata.length - 1; i += 2) { + int d1 = rdata[i] & 0xFF; + int d2 = rdata[i + 1] & 0xFF; + foot += ((d1 << 8) + d2); + } + if (i < rdata.length) { + int d1 = rdata[i] & 0xFF; + foot += (d1 << 8); + } + foot += ((foot >> 16) & 0xFFFF); + } + footprint = (foot & 0xFFFF); + return footprint; +} + +/** + * Returns a PublicKey corresponding to the data in this key. + * @throws DNSSEC.DNSSECException The key could not be converted. + */ +public PublicKey +getPublicKey() throws DNSSEC.DNSSECException { + if (publicKey != null) + return publicKey; + + publicKey = DNSSEC.toPublicKey(this); + return publicKey; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(flags); + out.writeU8(proto); + out.writeU8(alg); + if (key != null) + out.writeByteArray(key); +} + +} diff --git a/src/org/xbill/DNS/KEYRecord.java b/src/org/xbill/DNS/KEYRecord.java new file mode 100644 index 0000000..3d2e01c --- /dev/null +++ b/src/org/xbill/DNS/KEYRecord.java @@ -0,0 +1,352 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.security.PublicKey; +import java.util.*; + +/** + * Key - contains a cryptographic public key. The data can be converted + * to objects implementing java.security.interfaces.PublicKey + * @see DNSSEC + * + * @author Brian Wellington + */ + +public class KEYRecord extends KEYBase { + +private static final long serialVersionUID = 6385613447571488906L; + +public static class Protocol { + /** + * KEY protocol identifiers. + */ + + private Protocol() {} + + /** No defined protocol. */ + public static final int NONE = 0; + + /** Transaction Level Security */ + public static final int TLS = 1; + + /** Email */ + public static final int EMAIL = 2; + + /** DNSSEC */ + public static final int DNSSEC = 3; + + /** IPSEC Control */ + public static final int IPSEC = 4; + + /** Any protocol */ + public static final int ANY = 255; + + private static Mnemonic protocols = new Mnemonic("KEY protocol", + Mnemonic.CASE_UPPER); + + static { + protocols.setMaximum(0xFF); + protocols.setNumericAllowed(true); + + protocols.add(NONE, "NONE"); + protocols.add(TLS, "TLS"); + protocols.add(EMAIL, "EMAIL"); + protocols.add(DNSSEC, "DNSSEC"); + protocols.add(IPSEC, "IPSEC"); + protocols.add(ANY, "ANY"); + } + + /** + * Converts an KEY protocol value into its textual representation + */ + public static String + string(int type) { + return protocols.getText(type); + } + + /** + * Converts a textual representation of a KEY protocol into its + * numeric code. Integers in the range 0..255 are also accepted. + * @param s The textual representation of the protocol + * @return The protocol code, or -1 on error. + */ + public static int + value(String s) { + return protocols.getValue(s); + } +} + +public static class Flags { + /** + * KEY flags identifiers. + */ + + private Flags() {} + + /** KEY cannot be used for confidentiality */ + public static final int NOCONF = 0x4000; + + /** KEY cannot be used for authentication */ + public static final int NOAUTH = 0x8000; + + /** No key present */ + public static final int NOKEY = 0xC000; + + /** Bitmask of the use fields */ + public static final int USE_MASK = 0xC000; + + /** Flag 2 (unused) */ + public static final int FLAG2 = 0x2000; + + /** Flags extension */ + public static final int EXTEND = 0x1000; + + /** Flag 4 (unused) */ + public static final int FLAG4 = 0x0800; + + /** Flag 5 (unused) */ + public static final int FLAG5 = 0x0400; + + /** Key is owned by a user. */ + public static final int USER = 0x0000; + + /** Key is owned by a zone. */ + public static final int ZONE = 0x0100; + + /** Key is owned by a host. */ + public static final int HOST = 0x0200; + + /** Key owner type 3 (reserved). */ + public static final int NTYP3 = 0x0300; + + /** Key owner bitmask. */ + public static final int OWNER_MASK = 0x0300; + + /** Flag 8 (unused) */ + public static final int FLAG8 = 0x0080; + + /** Flag 9 (unused) */ + public static final int FLAG9 = 0x0040; + + /** Flag 10 (unused) */ + public static final int FLAG10 = 0x0020; + + /** Flag 11 (unused) */ + public static final int FLAG11 = 0x0010; + + /** Signatory value 0 */ + public static final int SIG0 = 0; + + /** Signatory value 1 */ + public static final int SIG1 = 1; + + /** Signatory value 2 */ + public static final int SIG2 = 2; + + /** Signatory value 3 */ + public static final int SIG3 = 3; + + /** Signatory value 4 */ + public static final int SIG4 = 4; + + /** Signatory value 5 */ + public static final int SIG5 = 5; + + /** Signatory value 6 */ + public static final int SIG6 = 6; + + /** Signatory value 7 */ + public static final int SIG7 = 7; + + /** Signatory value 8 */ + public static final int SIG8 = 8; + + /** Signatory value 9 */ + public static final int SIG9 = 9; + + /** Signatory value 10 */ + public static final int SIG10 = 10; + + /** Signatory value 11 */ + public static final int SIG11 = 11; + + /** Signatory value 12 */ + public static final int SIG12 = 12; + + /** Signatory value 13 */ + public static final int SIG13 = 13; + + /** Signatory value 14 */ + public static final int SIG14 = 14; + + /** Signatory value 15 */ + public static final int SIG15 = 15; + + private static Mnemonic flags = new Mnemonic("KEY flags", + Mnemonic.CASE_UPPER); + + static { + flags.setMaximum(0xFFFF); + flags.setNumericAllowed(false); + + flags.add(NOCONF, "NOCONF"); + flags.add(NOAUTH, "NOAUTH"); + flags.add(NOKEY, "NOKEY"); + flags.add(FLAG2, "FLAG2"); + flags.add(EXTEND, "EXTEND"); + flags.add(FLAG4, "FLAG4"); + flags.add(FLAG5, "FLAG5"); + flags.add(USER, "USER"); + flags.add(ZONE, "ZONE"); + flags.add(HOST, "HOST"); + flags.add(NTYP3, "NTYP3"); + flags.add(FLAG8, "FLAG8"); + flags.add(FLAG9, "FLAG9"); + flags.add(FLAG10, "FLAG10"); + flags.add(FLAG11, "FLAG11"); + flags.add(SIG0, "SIG0"); + flags.add(SIG1, "SIG1"); + flags.add(SIG2, "SIG2"); + flags.add(SIG3, "SIG3"); + flags.add(SIG4, "SIG4"); + flags.add(SIG5, "SIG5"); + flags.add(SIG6, "SIG6"); + flags.add(SIG7, "SIG7"); + flags.add(SIG8, "SIG8"); + flags.add(SIG9, "SIG9"); + flags.add(SIG10, "SIG10"); + flags.add(SIG11, "SIG11"); + flags.add(SIG12, "SIG12"); + flags.add(SIG13, "SIG13"); + flags.add(SIG14, "SIG14"); + flags.add(SIG15, "SIG15"); + } + + /** + * Converts a textual representation of KEY flags into its + * numeric code. Integers in the range 0..65535 are also accepted. + * @param s The textual representation of the protocol + * @return The protocol code, or -1 on error. + */ + public static int + value(String s) { + int value; + try { + value = Integer.parseInt(s); + if (value >= 0 && value <= 0xFFFF) { + return value; + } + return -1; + } catch (NumberFormatException e) { + } + StringTokenizer st = new StringTokenizer(s, "|"); + value = 0; + while (st.hasMoreTokens()) { + int val = flags.getValue(st.nextToken()); + if (val < 0) { + return -1; + } + value |= val; + } + return value; + } +} + +/* flags */ +/** This key cannot be used for confidentiality (encryption) */ +public static final int FLAG_NOCONF = Flags.NOCONF; + +/** This key cannot be used for authentication */ +public static final int FLAG_NOAUTH = Flags.NOAUTH; + +/** This key cannot be used for authentication or confidentiality */ +public static final int FLAG_NOKEY = Flags.NOKEY; + +/** A zone key */ +public static final int OWNER_ZONE = Flags.ZONE; + +/** A host/end entity key */ +public static final int OWNER_HOST = Flags.HOST; + +/** A user key */ +public static final int OWNER_USER = Flags.USER; + +/* protocols */ +/** Key was created for use with transaction level security */ +public static final int PROTOCOL_TLS = Protocol.TLS; + +/** Key was created for use with email */ +public static final int PROTOCOL_EMAIL = Protocol.EMAIL; + +/** Key was created for use with DNSSEC */ +public static final int PROTOCOL_DNSSEC = Protocol.DNSSEC; + +/** Key was created for use with IPSEC */ +public static final int PROTOCOL_IPSEC = Protocol.IPSEC; + +/** Key was created for use with any protocol */ +public static final int PROTOCOL_ANY = Protocol.ANY; + +KEYRecord() {} + +Record +getObject() { + return new KEYRecord(); +} + +/** + * Creates a KEY Record from the given data + * @param flags Flags describing the key's properties + * @param proto The protocol that the key was created for + * @param alg The key's algorithm + * @param key Binary data representing the key + */ +public +KEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg, + byte [] key) +{ + super(name, Type.KEY, dclass, ttl, flags, proto, alg, key); +} + +/** + * Creates a KEY Record from the given data + * @param flags Flags describing the key's properties + * @param proto The protocol that the key was created for + * @param alg The key's algorithm + * @param key The key as a PublicKey + * @throws DNSSEC.DNSSECException The PublicKey could not be converted into DNS + * format. + */ +public +KEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg, + PublicKey key) throws DNSSEC.DNSSECException +{ + super(name, Type.KEY, dclass, ttl, flags, proto, alg, + DNSSEC.fromPublicKey(key, alg)); + publicKey = key; +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String flagString = st.getIdentifier(); + flags = Flags.value(flagString); + if (flags < 0) + throw st.exception("Invalid flags: " + flagString); + String protoString = st.getIdentifier(); + proto = Protocol.value(protoString); + if (proto < 0) + throw st.exception("Invalid protocol: " + protoString); + String algString = st.getIdentifier(); + alg = DNSSEC.Algorithm.value(algString); + if (alg < 0) + throw st.exception("Invalid algorithm: " + algString); + /* If this is a null KEY, there's no key data */ + if ((flags & Flags.USE_MASK) == Flags.NOKEY) + key = null; + else + key = st.getBase64(); +} + +} diff --git a/src/org/xbill/DNS/KXRecord.java b/src/org/xbill/DNS/KXRecord.java new file mode 100644 index 0000000..481d21b --- /dev/null +++ b/src/org/xbill/DNS/KXRecord.java @@ -0,0 +1,51 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Key Exchange - delegation of authority + * + * @author Brian Wellington + */ + +public class KXRecord extends U16NameBase { + +private static final long serialVersionUID = 7448568832769757809L; + +KXRecord() {} + +Record +getObject() { + return new KXRecord(); +} + +/** + * Creates a KX Record from the given data + * @param preference The preference of this KX. Records with lower priority + * are preferred. + * @param target The host that authority is delegated to + */ +public +KXRecord(Name name, int dclass, long ttl, int preference, Name target) { + super(name, Type.KX, dclass, ttl, preference, "preference", + target, "target"); +} + +/** Returns the target of the KX record */ +public Name +getTarget() { + return getNameField(); +} + +/** Returns the preference of this KX record */ +public int +getPreference() { + return getU16Field(); +} + +public Name +getAdditionalName() { + return getNameField(); +} + +} diff --git a/src/org/xbill/DNS/LOCRecord.java b/src/org/xbill/DNS/LOCRecord.java new file mode 100644 index 0000000..4eddc15 --- /dev/null +++ b/src/org/xbill/DNS/LOCRecord.java @@ -0,0 +1,314 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.text.*; + +/** + * Location - describes the physical location of hosts, networks, subnets. + * + * @author Brian Wellington + */ + +public class LOCRecord extends Record { + +private static final long serialVersionUID = 9058224788126750409L; + +private static NumberFormat w2, w3; + +private long size, hPrecision, vPrecision; +private long latitude, longitude, altitude; + +static { + w2 = new DecimalFormat(); + w2.setMinimumIntegerDigits(2); + + w3 = new DecimalFormat(); + w3.setMinimumIntegerDigits(3); +} + +LOCRecord() {} + +Record +getObject() { + return new LOCRecord(); +} + +/** + * Creates an LOC Record from the given data + * @param latitude The latitude of the center of the sphere + * @param longitude The longitude of the center of the sphere + * @param altitude The altitude of the center of the sphere, in m + * @param size The diameter of a sphere enclosing the described entity, in m. + * @param hPrecision The horizontal precision of the data, in m. + * @param vPrecision The vertical precision of the data, in m. +*/ +public +LOCRecord(Name name, int dclass, long ttl, double latitude, double longitude, + double altitude, double size, double hPrecision, double vPrecision) +{ + super(name, Type.LOC, dclass, ttl); + this.latitude = (long)(latitude * 3600 * 1000 + (1L << 31)); + this.longitude = (long)(longitude * 3600 * 1000 + (1L << 31)); + this.altitude = (long)((altitude + 100000) * 100); + this.size = (long)(size * 100); + this.hPrecision = (long)(hPrecision * 100); + this.vPrecision = (long)(vPrecision * 100); +} + +void +rrFromWire(DNSInput in) throws IOException { + int version; + + version = in.readU8(); + if (version != 0) + throw new WireParseException("Invalid LOC version"); + + size = parseLOCformat(in.readU8()); + hPrecision = parseLOCformat(in.readU8()); + vPrecision = parseLOCformat(in.readU8()); + latitude = in.readU32(); + longitude = in.readU32(); + altitude = in.readU32(); +} + +private double +parseFixedPoint(String s) +{ + if (s.matches("^-?\\d+$")) + return Integer.parseInt(s); + else if (s.matches("^-?\\d+\\.\\d*$")) { + String [] parts = s.split("\\."); + double value = Integer.parseInt(parts[0]); + double fraction = Integer.parseInt(parts[1]); + if (value < 0) + fraction *= -1; + int digits = parts[1].length(); + return value + (fraction / Math.pow(10, digits)); + } else + throw new NumberFormatException(); +} + +private long +parsePosition(Tokenizer st, String type) throws IOException { + boolean isLatitude = type.equals("latitude"); + int deg = 0, min = 0; + double sec = 0; + long value; + String s; + + deg = st.getUInt16(); + if (deg > 180 || (deg > 90 && isLatitude)) + throw st.exception("Invalid LOC " + type + " degrees"); + + s = st.getString(); + try { + min = Integer.parseInt(s); + if (min < 0 || min > 59) + throw st.exception("Invalid LOC " + type + " minutes"); + s = st.getString(); + sec = parseFixedPoint(s); + if (sec < 0 || sec >= 60) + throw st.exception("Invalid LOC " + type + " seconds"); + s = st.getString(); + } catch (NumberFormatException e) { + } + + if (s.length() != 1) + throw st.exception("Invalid LOC " + type); + + value = (long) (1000 * (sec + 60L * (min + 60L * deg))); + + char c = Character.toUpperCase(s.charAt(0)); + if ((isLatitude && c == 'S') || (!isLatitude && c == 'W')) + value = -value; + else if ((isLatitude && c != 'N') || (!isLatitude && c != 'E')) + throw st.exception("Invalid LOC " + type); + + value += (1L << 31); + + return value; +} + +private long +parseDouble(Tokenizer st, String type, boolean required, long min, long max, + long defaultValue) +throws IOException +{ + Tokenizer.Token token = st.get(); + if (token.isEOL()) { + if (required) + throw st.exception("Invalid LOC " + type); + st.unget(); + return defaultValue; + } + String s = token.value; + if (s.length() > 1 && s.charAt(s.length() - 1) == 'm') + s = s.substring(0, s.length() - 1); + try { + long value = (long)(100 * parseFixedPoint(s)); + if (value < min || value > max) + throw st.exception("Invalid LOC " + type); + return value; + } + catch (NumberFormatException e) { + throw st.exception("Invalid LOC " + type); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + latitude = parsePosition(st, "latitude"); + longitude = parsePosition(st, "longitude"); + altitude = parseDouble(st, "altitude", true, + -10000000, 4284967295L, 0) + 10000000; + size = parseDouble(st, "size", false, 0, 9000000000L, 100); + hPrecision = parseDouble(st, "horizontal precision", false, + 0, 9000000000L, 1000000); + vPrecision = parseDouble(st, "vertical precision", false, + 0, 9000000000L, 1000); +} + +private void +renderFixedPoint(StringBuffer sb, NumberFormat formatter, long value, + long divisor) +{ + sb.append(value / divisor); + value %= divisor; + if (value != 0) { + sb.append("."); + sb.append(formatter.format(value)); + } +} + +private String +positionToString(long value, char pos, char neg) { + StringBuffer sb = new StringBuffer(); + char direction; + + long temp = value - (1L << 31); + if (temp < 0) { + temp = -temp; + direction = neg; + } else + direction = pos; + + sb.append(temp / (3600 * 1000)); /* degrees */ + temp = temp % (3600 * 1000); + sb.append(" "); + + sb.append(temp / (60 * 1000)); /* minutes */ + temp = temp % (60 * 1000); + sb.append(" "); + + renderFixedPoint(sb, w3, temp, 1000); /* seconds */ + sb.append(" "); + + sb.append(direction); + + return sb.toString(); +} + + +/** Convert to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + + /* Latitude */ + sb.append(positionToString(latitude, 'N', 'S')); + sb.append(" "); + + /* Latitude */ + sb.append(positionToString(longitude, 'E', 'W')); + sb.append(" "); + + /* Altitude */ + renderFixedPoint(sb, w2, altitude - 10000000, 100); + sb.append("m "); + + /* Size */ + renderFixedPoint(sb, w2, size, 100); + sb.append("m "); + + /* Horizontal precision */ + renderFixedPoint(sb, w2, hPrecision, 100); + sb.append("m "); + + /* Vertical precision */ + renderFixedPoint(sb, w2, vPrecision, 100); + sb.append("m"); + + return sb.toString(); +} + +/** Returns the latitude */ +public double +getLatitude() { + return ((double)(latitude - (1L << 31))) / (3600 * 1000); +} + +/** Returns the longitude */ +public double +getLongitude() { + return ((double)(longitude - (1L << 31))) / (3600 * 1000); +} + +/** Returns the altitude */ +public double +getAltitude() { + return ((double)(altitude - 10000000)) / 100; +} + +/** Returns the diameter of the enclosing sphere */ +public double +getSize() { + return ((double)size) / 100; +} + +/** Returns the horizontal precision */ +public double +getHPrecision() { + return ((double)hPrecision) / 100; +} + +/** Returns the horizontal precision */ +public double +getVPrecision() { + return ((double)vPrecision) / 100; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(0); /* version */ + out.writeU8(toLOCformat(size)); + out.writeU8(toLOCformat(hPrecision)); + out.writeU8(toLOCformat(vPrecision)); + out.writeU32(latitude); + out.writeU32(longitude); + out.writeU32(altitude); +} + +private static long +parseLOCformat(int b) throws WireParseException { + long out = b >> 4; + int exp = b & 0xF; + if (out > 9 || exp > 9) + throw new WireParseException("Invalid LOC Encoding"); + while (exp-- > 0) + out *= 10; + return (out); +} + +private int +toLOCformat(long l) { + byte exp = 0; + while (l > 9) { + exp++; + l /= 10; + } + return (int)((l << 4) + exp); +} + +} diff --git a/src/org/xbill/DNS/Lookup.java b/src/org/xbill/DNS/Lookup.java new file mode 100644 index 0000000..0beba59 --- /dev/null +++ b/src/org/xbill/DNS/Lookup.java @@ -0,0 +1,647 @@ +// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; +import java.io.*; +import java.net.*; + +/** + * The Lookup object issues queries to caching DNS servers. The input consists + * of a name, an optional type, and an optional class. Caching is enabled + * by default and used when possible to reduce the number of DNS requests. + * A Resolver, which defaults to an ExtendedResolver initialized with the + * resolvers located by the ResolverConfig class, performs the queries. A + * search path of domain suffixes is used to resolve relative names, and is + * also determined by the ResolverConfig class. + * + * A Lookup object may be reused, but should not be used by multiple threads. + * + * @see Cache + * @see Resolver + * @see ResolverConfig + * + * @author Brian Wellington + */ + +public final class Lookup { + +private static Resolver defaultResolver; +private static Name [] defaultSearchPath; +private static Map defaultCaches; +private static int defaultNdots; + +private Resolver resolver; +private Name [] searchPath; +private Cache cache; +private boolean temporary_cache; +private int credibility; +private Name name; +private int type; +private int dclass; +private boolean verbose; +private int iterations; +private boolean foundAlias; +private boolean done; +private boolean doneCurrent; +private List aliases; +private Record [] answers; +private int result; +private String error; +private boolean nxdomain; +private boolean badresponse; +private String badresponse_error; +private boolean networkerror; +private boolean timedout; +private boolean nametoolong; +private boolean referral; + +private static final Name [] noAliases = new Name[0]; + +/** The lookup was successful. */ +public static final int SUCCESSFUL = 0; + +/** + * The lookup failed due to a data or server error. Repeating the lookup + * would not be helpful. + */ +public static final int UNRECOVERABLE = 1; + +/** + * The lookup failed due to a network error. Repeating the lookup may be + * helpful. + */ +public static final int TRY_AGAIN = 2; + +/** The host does not exist. */ +public static final int HOST_NOT_FOUND = 3; + +/** The host exists, but has no records associated with the queried type. */ +public static final int TYPE_NOT_FOUND = 4; + +public static synchronized void +refreshDefault() { + + try { + defaultResolver = new ExtendedResolver(); + } + catch (UnknownHostException e) { + throw new RuntimeException("Failed to initialize resolver"); + } + defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath(); + defaultCaches = new HashMap(); + defaultNdots = ResolverConfig.getCurrentConfig().ndots(); +} + +static { + refreshDefault(); +} + +/** + * Gets the Resolver that will be used as the default by future Lookups. + * @return The default resolver. + */ +public static synchronized Resolver +getDefaultResolver() { + return defaultResolver; +} + +/** + * Sets the default Resolver to be used as the default by future Lookups. + * @param resolver The default resolver. + */ +public static synchronized void +setDefaultResolver(Resolver resolver) { + defaultResolver = resolver; +} + +/** + * Gets the Cache that will be used as the default for the specified + * class by future Lookups. + * @param dclass The class whose cache is being retrieved. + * @return The default cache for the specified class. + */ +public static synchronized Cache +getDefaultCache(int dclass) { + DClass.check(dclass); + Cache c = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass)); + if (c == null) { + c = new Cache(dclass); + defaultCaches.put(Mnemonic.toInteger(dclass), c); + } + return c; +} + +/** + * Sets the Cache to be used as the default for the specified class by future + * Lookups. + * @param cache The default cache for the specified class. + * @param dclass The class whose cache is being set. + */ +public static synchronized void +setDefaultCache(Cache cache, int dclass) { + DClass.check(dclass); + defaultCaches.put(Mnemonic.toInteger(dclass), cache); +} + +/** + * Gets the search path that will be used as the default by future Lookups. + * @return The default search path. + */ +public static synchronized Name [] +getDefaultSearchPath() { + return defaultSearchPath; +} + +/** + * Sets the search path to be used as the default by future Lookups. + * @param domains The default search path. + */ +public static synchronized void +setDefaultSearchPath(Name [] domains) { + defaultSearchPath = domains; +} + +/** + * Sets the search path that will be used as the default by future Lookups. + * @param domains The default search path. + * @throws TextParseException A name in the array is not a valid DNS name. + */ +public static synchronized void +setDefaultSearchPath(String [] domains) throws TextParseException { + if (domains == null) { + defaultSearchPath = null; + return; + } + Name [] newdomains = new Name[domains.length]; + for (int i = 0; i < domains.length; i++) + newdomains[i] = Name.fromString(domains[i], Name.root); + defaultSearchPath = newdomains; +} + +private final void +reset() { + iterations = 0; + foundAlias = false; + done = false; + doneCurrent = false; + aliases = null; + answers = null; + result = -1; + error = null; + nxdomain = false; + badresponse = false; + badresponse_error = null; + networkerror = false; + timedout = false; + nametoolong = false; + referral = false; + if (temporary_cache) + cache.clearCache(); +} + +/** + * Create a Lookup object that will find records of the given name, type, + * and class. The lookup will use the default cache, resolver, and search + * path, and look for records that are reasonably credible. + * @param name The name of the desired records + * @param type The type of the desired records + * @param dclass The class of the desired records + * @throws IllegalArgumentException The type is a meta type other than ANY. + * @see Cache + * @see Resolver + * @see Credibility + * @see Name + * @see Type + * @see DClass + */ +public +Lookup(Name name, int type, int dclass) { + Type.check(type); + DClass.check(dclass); + if (!Type.isRR(type) && type != Type.ANY) + throw new IllegalArgumentException("Cannot query for " + + "meta-types other than ANY"); + this.name = name; + this.type = type; + this.dclass = dclass; + synchronized (Lookup.class) { + this.resolver = getDefaultResolver(); + this.searchPath = getDefaultSearchPath(); + this.cache = getDefaultCache(dclass); + } + this.credibility = Credibility.NORMAL; + this.verbose = Options.check("verbose"); + this.result = -1; +} + +/** + * Create a Lookup object that will find records of the given name and type + * in the IN class. + * @param name The name of the desired records + * @param type The type of the desired records + * @throws IllegalArgumentException The type is a meta type other than ANY. + * @see #Lookup(Name,int,int) + */ +public +Lookup(Name name, int type) { + this(name, type, DClass.IN); +} + +/** + * Create a Lookup object that will find records of type A at the given name + * in the IN class. + * @param name The name of the desired records + * @see #Lookup(Name,int,int) + */ +public +Lookup(Name name) { + this(name, Type.A, DClass.IN); +} + +/** + * Create a Lookup object that will find records of the given name, type, + * and class. + * @param name The name of the desired records + * @param type The type of the desired records + * @param dclass The class of the desired records + * @throws TextParseException The name is not a valid DNS name + * @throws IllegalArgumentException The type is a meta type other than ANY. + * @see #Lookup(Name,int,int) + */ +public +Lookup(String name, int type, int dclass) throws TextParseException { + this(Name.fromString(name), type, dclass); +} + +/** + * Create a Lookup object that will find records of the given name and type + * in the IN class. + * @param name The name of the desired records + * @param type The type of the desired records + * @throws TextParseException The name is not a valid DNS name + * @throws IllegalArgumentException The type is a meta type other than ANY. + * @see #Lookup(Name,int,int) + */ +public +Lookup(String name, int type) throws TextParseException { + this(Name.fromString(name), type, DClass.IN); +} + +/** + * Create a Lookup object that will find records of type A at the given name + * in the IN class. + * @param name The name of the desired records + * @throws TextParseException The name is not a valid DNS name + * @see #Lookup(Name,int,int) + */ +public +Lookup(String name) throws TextParseException { + this(Name.fromString(name), Type.A, DClass.IN); +} + +/** + * Sets the resolver to use when performing this lookup. This overrides the + * default value. + * @param resolver The resolver to use. + */ +public void +setResolver(Resolver resolver) { + this.resolver = resolver; +} + +/** + * Sets the search path to use when performing this lookup. This overrides the + * default value. + * @param domains An array of names containing the search path. + */ +public void +setSearchPath(Name [] domains) { + this.searchPath = domains; +} + +/** + * Sets the search path to use when performing this lookup. This overrides the + * default value. + * @param domains An array of names containing the search path. + * @throws TextParseException A name in the array is not a valid DNS name. + */ +public void +setSearchPath(String [] domains) throws TextParseException { + if (domains == null) { + this.searchPath = null; + return; + } + Name [] newdomains = new Name[domains.length]; + for (int i = 0; i < domains.length; i++) + newdomains[i] = Name.fromString(domains[i], Name.root); + this.searchPath = newdomains; +} + +/** + * Sets the cache to use when performing this lookup. This overrides the + * default value. If the results of this lookup should not be permanently + * cached, null can be provided here. + * @param cache The cache to use. + */ +public void +setCache(Cache cache) { + if (cache == null) { + this.cache = new Cache(dclass); + this.temporary_cache = true; + } else { + this.cache = cache; + this.temporary_cache = false; + } +} + +/** + * Sets ndots to use when performing this lookup, overriding the default value. + * Specifically, this refers to the number of "dots" which, if present in a + * name, indicate that a lookup for the absolute name should be attempted + * before appending any search path elements. + * @param ndots The ndots value to use, which must be greater than or equal to + * 0. + */ +public void +setNdots(int ndots) { + if (ndots < 0) + throw new IllegalArgumentException("Illegal ndots value: " + + ndots); + defaultNdots = ndots; +} + +/** + * Sets the minimum credibility level that will be accepted when performing + * the lookup. This defaults to Credibility.NORMAL. + * @param credibility The minimum credibility level. + */ +public void +setCredibility(int credibility) { + this.credibility = credibility; +} + +private void +follow(Name name, Name oldname) { + foundAlias = true; + badresponse = false; + networkerror = false; + timedout = false; + nxdomain = false; + referral = false; + iterations++; + if (iterations >= 6 || name.equals(oldname)) { + result = UNRECOVERABLE; + error = "CNAME loop"; + done = true; + return; + } + if (aliases == null) + aliases = new ArrayList(); + aliases.add(oldname); + lookup(name); +} + +private void +processResponse(Name name, SetResponse response) { + if (response.isSuccessful()) { + RRset [] rrsets = response.answers(); + List l = new ArrayList(); + Iterator it; + int i; + + for (i = 0; i < rrsets.length; i++) { + it = rrsets[i].rrs(); + while (it.hasNext()) + l.add(it.next()); + } + + result = SUCCESSFUL; + answers = (Record []) l.toArray(new Record[l.size()]); + done = true; + } else if (response.isNXDOMAIN()) { + nxdomain = true; + doneCurrent = true; + if (iterations > 0) { + result = HOST_NOT_FOUND; + done = true; + } + } else if (response.isNXRRSET()) { + result = TYPE_NOT_FOUND; + answers = null; + done = true; + } else if (response.isCNAME()) { + CNAMERecord cname = response.getCNAME(); + follow(cname.getTarget(), name); + } else if (response.isDNAME()) { + DNAMERecord dname = response.getDNAME(); + try { + follow(name.fromDNAME(dname), name); + } catch (NameTooLongException e) { + result = UNRECOVERABLE; + error = "Invalid DNAME target"; + done = true; + } + } else if (response.isDelegation()) { + // We shouldn't get a referral. Ignore it. + referral = true; + } +} + +private void +lookup(Name current) { + SetResponse sr = cache.lookupRecords(current, type, credibility); + if (verbose) { + System.err.println("lookup " + current + " " + + Type.string(type)); + System.err.println(sr); + } + processResponse(current, sr); + if (done || doneCurrent) + return; + + Record question = Record.newRecord(current, type, dclass); + Message query = Message.newQuery(question); + Message response = null; + try { + response = resolver.send(query); + } + catch (IOException e) { + // A network error occurred. Press on. + if (e instanceof InterruptedIOException) + timedout = true; + else + networkerror = true; + return; + } + int rcode = response.getHeader().getRcode(); + if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) { + // The server we contacted is broken or otherwise unhelpful. + // Press on. + badresponse = true; + badresponse_error = Rcode.string(rcode); + return; + } + + if (!query.getQuestion().equals(response.getQuestion())) { + // The answer doesn't match the question. That's not good. + badresponse = true; + badresponse_error = "response does not match query"; + return; + } + + sr = cache.addMessage(response); + if (sr == null) + sr = cache.lookupRecords(current, type, credibility); + if (verbose) { + System.err.println("queried " + current + " " + + Type.string(type)); + System.err.println(sr); + } + processResponse(current, sr); +} + +private void +resolve(Name current, Name suffix) { + doneCurrent = false; + Name tname = null; + if (suffix == null) + tname = current; + else { + try { + tname = Name.concatenate(current, suffix); + } + catch (NameTooLongException e) { + nametoolong = true; + return; + } + } + lookup(tname); +} + +/** + * Performs the lookup, using the specified Cache, Resolver, and search path. + * @return The answers, or null if none are found. + */ +public Record [] +run() { + if (done) + reset(); + if (name.isAbsolute()) + resolve(name, null); + else if (searchPath == null) + resolve(name, Name.root); + else { + if (name.labels() > defaultNdots) + resolve(name, Name.root); + if (done) + return answers; + + for (int i = 0; i < searchPath.length; i++) { + resolve(name, searchPath[i]); + if (done) + return answers; + else if (foundAlias) + break; + } + } + if (!done) { + if (badresponse) { + result = TRY_AGAIN; + error = badresponse_error; + done = true; + } else if (timedout) { + result = TRY_AGAIN; + error = "timed out"; + done = true; + } else if (networkerror) { + result = TRY_AGAIN; + error = "network error"; + done = true; + } else if (nxdomain) { + result = HOST_NOT_FOUND; + done = true; + } else if (referral) { + result = UNRECOVERABLE; + error = "referral"; + done = true; + } else if (nametoolong) { + result = UNRECOVERABLE; + error = "name too long"; + done = true; + } + } + return answers; +} + +private void +checkDone() { + if (done && result != -1) + return; + StringBuffer sb = new StringBuffer("Lookup of " + name + " "); + if (dclass != DClass.IN) + sb.append(DClass.string(dclass) + " "); + sb.append(Type.string(type) + " isn't done"); + throw new IllegalStateException(sb.toString()); +} + +/** + * Returns the answers from the lookup. + * @return The answers, or null if none are found. + * @throws IllegalStateException The lookup has not completed. + */ +public Record [] +getAnswers() { + checkDone(); + return answers; +} + +/** + * Returns all known aliases for this name. Whenever a CNAME/DNAME is + * followed, an alias is added to this array. The last element in this + * array will be the owner name for records in the answer, if there are any. + * @return The aliases. + * @throws IllegalStateException The lookup has not completed. + */ +public Name [] +getAliases() { + checkDone(); + if (aliases == null) + return noAliases; + return (Name []) aliases.toArray(new Name[aliases.size()]); +} + +/** + * Returns the result code of the lookup. + * @return The result code, which can be SUCCESSFUL, UNRECOVERABLE, TRY_AGAIN, + * HOST_NOT_FOUND, or TYPE_NOT_FOUND. + * @throws IllegalStateException The lookup has not completed. + */ +public int +getResult() { + checkDone(); + return result; +} + +/** + * Returns an error string describing the result code of this lookup. + * @return A string, which may either directly correspond the result code + * or be more specific. + * @throws IllegalStateException The lookup has not completed. + */ +public String +getErrorString() { + checkDone(); + if (error != null) + return error; + switch (result) { + case SUCCESSFUL: return "successful"; + case UNRECOVERABLE: return "unrecoverable error"; + case TRY_AGAIN: return "try again"; + case HOST_NOT_FOUND: return "host not found"; + case TYPE_NOT_FOUND: return "type not found"; + } + throw new IllegalStateException("unknown result"); +} + +} diff --git a/src/org/xbill/DNS/MBRecord.java b/src/org/xbill/DNS/MBRecord.java new file mode 100644 index 0000000..6b65edf --- /dev/null +++ b/src/org/xbill/DNS/MBRecord.java @@ -0,0 +1,42 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mailbox Record - specifies a host containing a mailbox. + * + * @author Brian Wellington + */ + +public class MBRecord extends SingleNameBase { + +private static final long serialVersionUID = 532349543479150419L; + +MBRecord() {} + +Record +getObject() { + return new MBRecord(); +} + +/** + * Creates a new MB Record with the given data + * @param mailbox The host containing the mailbox for the domain. + */ +public +MBRecord(Name name, int dclass, long ttl, Name mailbox) { + super(name, Type.MB, dclass, ttl, mailbox, "mailbox"); +} + +/** Gets the mailbox for the domain */ +public Name +getMailbox() { + return getSingleName(); +} + +public Name +getAdditionalName() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/MDRecord.java b/src/org/xbill/DNS/MDRecord.java new file mode 100644 index 0000000..dbf51af --- /dev/null +++ b/src/org/xbill/DNS/MDRecord.java @@ -0,0 +1,43 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mail Destination Record - specifies a mail agent which delivers mail + * for a domain (obsolete) + * + * @author Brian Wellington + */ + +public class MDRecord extends SingleNameBase { + +private static final long serialVersionUID = 5268878603762942202L; + +MDRecord() {} + +Record +getObject() { + return new MDRecord(); +} + +/** + * Creates a new MD Record with the given data + * @param mailAgent The mail agent that delivers mail for the domain. + */ +public +MDRecord(Name name, int dclass, long ttl, Name mailAgent) { + super(name, Type.MD, dclass, ttl, mailAgent, "mail agent"); +} + +/** Gets the mail agent for the domain */ +public Name +getMailAgent() { + return getSingleName(); +} + +public Name +getAdditionalName() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/MFRecord.java b/src/org/xbill/DNS/MFRecord.java new file mode 100644 index 0000000..ff293d7 --- /dev/null +++ b/src/org/xbill/DNS/MFRecord.java @@ -0,0 +1,43 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mail Forwarder Record - specifies a mail agent which forwards mail + * for a domain (obsolete) + * + * @author Brian Wellington + */ + +public class MFRecord extends SingleNameBase { + +private static final long serialVersionUID = -6670449036843028169L; + +MFRecord() {} + +Record +getObject() { + return new MFRecord(); +} + +/** + * Creates a new MF Record with the given data + * @param mailAgent The mail agent that forwards mail for the domain. + */ +public +MFRecord(Name name, int dclass, long ttl, Name mailAgent) { + super(name, Type.MF, dclass, ttl, mailAgent, "mail agent"); +} + +/** Gets the mail agent for the domain */ +public Name +getMailAgent() { + return getSingleName(); +} + +public Name +getAdditionalName() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/MGRecord.java b/src/org/xbill/DNS/MGRecord.java new file mode 100644 index 0000000..5752f49 --- /dev/null +++ b/src/org/xbill/DNS/MGRecord.java @@ -0,0 +1,38 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mail Group Record - specifies a mailbox which is a member of a mail group. + * + * @author Brian Wellington + */ + +public class MGRecord extends SingleNameBase { + +private static final long serialVersionUID = -3980055550863644582L; + +MGRecord() {} + +Record +getObject() { + return new MGRecord(); +} + +/** + * Creates a new MG Record with the given data + * @param mailbox The mailbox that is a member of the group specified by the + * domain. + */ +public +MGRecord(Name name, int dclass, long ttl, Name mailbox) { + super(name, Type.MG, dclass, ttl, mailbox, "mailbox"); +} + +/** Gets the mailbox in the mail group specified by the domain */ +public Name +getMailbox() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/MINFORecord.java b/src/org/xbill/DNS/MINFORecord.java new file mode 100644 index 0000000..4324cda --- /dev/null +++ b/src/org/xbill/DNS/MINFORecord.java @@ -0,0 +1,90 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Mailbox information Record - lists the address responsible for a mailing + * list/mailbox and the address to receive error messages relating to the + * mailing list/mailbox. + * + * @author Brian Wellington + */ + +public class MINFORecord extends Record { + +private static final long serialVersionUID = -3962147172340353796L; + +private Name responsibleAddress; +private Name errorAddress; + +MINFORecord() {} + +Record +getObject() { + return new MINFORecord(); +} + +/** + * Creates an MINFO Record from the given data + * @param responsibleAddress The address responsible for the + * mailing list/mailbox. + * @param errorAddress The address to receive error messages relating to the + * mailing list/mailbox. + */ +public +MINFORecord(Name name, int dclass, long ttl, + Name responsibleAddress, Name errorAddress) +{ + super(name, Type.MINFO, dclass, ttl); + + this.responsibleAddress = checkName("responsibleAddress", + responsibleAddress); + this.errorAddress = checkName("errorAddress", errorAddress); +} + +void +rrFromWire(DNSInput in) throws IOException { + responsibleAddress = new Name(in); + errorAddress = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + responsibleAddress = st.getName(origin); + errorAddress = st.getName(origin); +} + +/** Converts the MINFO Record to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(responsibleAddress); + sb.append(" "); + sb.append(errorAddress); + return sb.toString(); +} + +/** Gets the address responsible for the mailing list/mailbox. */ +public Name +getResponsibleAddress() { + return responsibleAddress; +} + +/** + * Gets the address to receive error messages relating to the mailing + * list/mailbox. + */ +public Name +getErrorAddress() { + return errorAddress; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + responsibleAddress.toWire(out, null, canonical); + errorAddress.toWire(out, null, canonical); +} + +} diff --git a/src/org/xbill/DNS/MRRecord.java b/src/org/xbill/DNS/MRRecord.java new file mode 100644 index 0000000..a7ff4fc --- /dev/null +++ b/src/org/xbill/DNS/MRRecord.java @@ -0,0 +1,38 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mailbox Rename Record - specifies a rename of a mailbox. + * + * @author Brian Wellington + */ + +public class MRRecord extends SingleNameBase { + +private static final long serialVersionUID = -5617939094209927533L; + +MRRecord() {} + +Record +getObject() { + return new MRRecord(); +} + +/** + * Creates a new MR Record with the given data + * @param newName The new name of the mailbox specified by the domain. + * domain. + */ +public +MRRecord(Name name, int dclass, long ttl, Name newName) { + super(name, Type.MR, dclass, ttl, newName, "new name"); +} + +/** Gets the new name of the mailbox specified by the domain */ +public Name +getNewName() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/MXRecord.java b/src/org/xbill/DNS/MXRecord.java new file mode 100644 index 0000000..111977d --- /dev/null +++ b/src/org/xbill/DNS/MXRecord.java @@ -0,0 +1,57 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Mail Exchange - specifies where mail to a domain is sent + * + * @author Brian Wellington + */ + +public class MXRecord extends U16NameBase { + +private static final long serialVersionUID = 2914841027584208546L; + +MXRecord() {} + +Record +getObject() { + return new MXRecord(); +} + +/** + * Creates an MX Record from the given data + * @param priority The priority of this MX. Records with lower priority + * are preferred. + * @param target The host that mail is sent to + */ +public +MXRecord(Name name, int dclass, long ttl, int priority, Name target) { + super(name, Type.MX, dclass, ttl, priority, "priority", + target, "target"); +} + +/** Returns the target of the MX record */ +public Name +getTarget() { + return getNameField(); +} + +/** Returns the priority of this MX record */ +public int +getPriority() { + return getU16Field(); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(u16Field); + nameField.toWire(out, c, canonical); +} + +public Name +getAdditionalName() { + return getNameField(); +} + +} diff --git a/src/org/xbill/DNS/Master.java b/src/org/xbill/DNS/Master.java new file mode 100644 index 0000000..c795a9c --- /dev/null +++ b/src/org/xbill/DNS/Master.java @@ -0,0 +1,427 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A DNS master file parser. This incrementally parses the file, returning + * one record at a time. When directives are seen, they are added to the + * state and used when parsing future records. + * + * @author Brian Wellington + */ + +public class Master { + +private Name origin; +private File file; +private Record last = null; +private long defaultTTL; +private Master included = null; +private Tokenizer st; +private int currentType; +private int currentDClass; +private long currentTTL; +private boolean needSOATTL; + +private Generator generator; +private List generators; +private boolean noExpandGenerate; + +Master(File file, Name origin, long initialTTL) throws IOException { + if (origin != null && !origin.isAbsolute()) { + throw new RelativeNameException(origin); + } + this.file = file; + st = new Tokenizer(file); + this.origin = origin; + defaultTTL = initialTTL; +} + +/** + * Initializes the master file reader and opens the specified master file. + * @param filename The master file. + * @param origin The initial origin to append to relative names. + * @param ttl The initial default TTL. + * @throws IOException The master file could not be opened. + */ +public +Master(String filename, Name origin, long ttl) throws IOException { + this(new File(filename), origin, ttl); +} + +/** + * Initializes the master file reader and opens the specified master file. + * @param filename The master file. + * @param origin The initial origin to append to relative names. + * @throws IOException The master file could not be opened. + */ +public +Master(String filename, Name origin) throws IOException { + this(new File(filename), origin, -1); +} + +/** + * Initializes the master file reader and opens the specified master file. + * @param filename The master file. + * @throws IOException The master file could not be opened. + */ +public +Master(String filename) throws IOException { + this(new File(filename), null, -1); +} + +/** + * Initializes the master file reader. + * @param in The input stream containing a master file. + * @param origin The initial origin to append to relative names. + * @param ttl The initial default TTL. + */ +public +Master(InputStream in, Name origin, long ttl) { + if (origin != null && !origin.isAbsolute()) { + throw new RelativeNameException(origin); + } + st = new Tokenizer(in); + this.origin = origin; + defaultTTL = ttl; +} + +/** + * Initializes the master file reader. + * @param in The input stream containing a master file. + * @param origin The initial origin to append to relative names. + */ +public +Master(InputStream in, Name origin) { + this(in, origin, -1); +} + +/** + * Initializes the master file reader. + * @param in The input stream containing a master file. + */ +public +Master(InputStream in) { + this(in, null, -1); +} + +private Name +parseName(String s, Name origin) throws TextParseException { + try { + return Name.fromString(s, origin); + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } +} + +private void +parseTTLClassAndType() throws IOException { + String s; + boolean seen_class = false; + + + // This is a bit messy, since any of the following are legal: + // class ttl type + // ttl class type + // class type + // ttl type + // type + seen_class = false; + s = st.getString(); + if ((currentDClass = DClass.value(s)) >= 0) { + s = st.getString(); + seen_class = true; + } + + currentTTL = -1; + try { + currentTTL = TTL.parseTTL(s); + s = st.getString(); + } + catch (NumberFormatException e) { + if (defaultTTL >= 0) + currentTTL = defaultTTL; + else if (last != null) + currentTTL = last.getTTL(); + } + + if (!seen_class) { + if ((currentDClass = DClass.value(s)) >= 0) { + s = st.getString(); + } else { + currentDClass = DClass.IN; + } + } + + if ((currentType = Type.value(s)) < 0) + throw st.exception("Invalid type '" + s + "'"); + + // BIND allows a missing TTL for the initial SOA record, and uses + // the SOA minimum value. If the SOA is not the first record, + // this is an error. + if (currentTTL < 0) { + if (currentType != Type.SOA) + throw st.exception("missing TTL"); + needSOATTL = true; + currentTTL = 0; + } +} + +private long +parseUInt32(String s) { + if (!Character.isDigit(s.charAt(0))) + return -1; + try { + long l = Long.parseLong(s); + if (l < 0 || l > 0xFFFFFFFFL) + return -1; + return l; + } + catch (NumberFormatException e) { + return -1; + } +} + +private void +startGenerate() throws IOException { + String s; + int n; + + // The first field is of the form start-end[/step] + // Regexes would be useful here. + s = st.getIdentifier(); + n = s.indexOf("-"); + if (n < 0) + throw st.exception("Invalid $GENERATE range specifier: " + s); + String startstr = s.substring(0, n); + String endstr = s.substring(n + 1); + String stepstr = null; + n = endstr.indexOf("/"); + if (n >= 0) { + stepstr = endstr.substring(n + 1); + endstr = endstr.substring(0, n); + } + long start = parseUInt32(startstr); + long end = parseUInt32(endstr); + long step; + if (stepstr != null) + step = parseUInt32(stepstr); + else + step = 1; + if (start < 0 || end < 0 || start > end || step <= 0) + throw st.exception("Invalid $GENERATE range specifier: " + s); + + // The next field is the name specification. + String nameSpec = st.getIdentifier(); + + // Then the ttl/class/type, in the same form as a normal record. + // Only some types are supported. + parseTTLClassAndType(); + if (!Generator.supportedType(currentType)) + throw st.exception("$GENERATE does not support " + + Type.string(currentType) + " records"); + + // Next comes the rdata specification. + String rdataSpec = st.getIdentifier(); + + // That should be the end. However, we don't want to move past the + // line yet, so put back the EOL after reading it. + st.getEOL(); + st.unget(); + + generator = new Generator(start, end, step, nameSpec, + currentType, currentDClass, currentTTL, + rdataSpec, origin); + if (generators == null) + generators = new ArrayList(1); + generators.add(generator); +} + +private void +endGenerate() throws IOException { + // Read the EOL that we put back before. + st.getEOL(); + + generator = null; +} + +private Record +nextGenerated() throws IOException { + try { + return generator.nextRecord(); + } + catch (Tokenizer.TokenizerException e) { + throw st.exception("Parsing $GENERATE: " + e.getBaseMessage()); + } + catch (TextParseException e) { + throw st.exception("Parsing $GENERATE: " + e.getMessage()); + } +} + +/** + * Returns the next record in the master file. This will process any + * directives before the next record. + * @return The next record. + * @throws IOException The master file could not be read, or was syntactically + * invalid. + */ +public Record +_nextRecord() throws IOException { + Tokenizer.Token token; + String s; + + if (included != null) { + Record rec = included.nextRecord(); + if (rec != null) + return rec; + included = null; + } + if (generator != null) { + Record rec = nextGenerated(); + if (rec != null) + return rec; + endGenerate(); + } + while (true) { + Name name; + + token = st.get(true, false); + if (token.type == Tokenizer.WHITESPACE) { + Tokenizer.Token next = st.get(); + if (next.type == Tokenizer.EOL) + continue; + else if (next.type == Tokenizer.EOF) + return null; + else + st.unget(); + if (last == null) + throw st.exception("no owner"); + name = last.getName(); + } + else if (token.type == Tokenizer.EOL) + continue; + else if (token.type == Tokenizer.EOF) + return null; + else if (((String) token.value).charAt(0) == '$') { + s = token.value; + + if (s.equalsIgnoreCase("$ORIGIN")) { + origin = st.getName(Name.root); + st.getEOL(); + continue; + } else if (s.equalsIgnoreCase("$TTL")) { + defaultTTL = st.getTTL(); + st.getEOL(); + continue; + } else if (s.equalsIgnoreCase("$INCLUDE")) { + String filename = st.getString(); + File newfile; + if (file != null) { + String parent = file.getParent(); + newfile = new File(parent, filename); + } else { + newfile = new File(filename); + } + Name incorigin = origin; + token = st.get(); + if (token.isString()) { + incorigin = parseName(token.value, + Name.root); + st.getEOL(); + } + included = new Master(newfile, incorigin, + defaultTTL); + /* + * If we continued, we wouldn't be looking in + * the new file. Recursing works better. + */ + return nextRecord(); + } else if (s.equalsIgnoreCase("$GENERATE")) { + if (generator != null) + throw new IllegalStateException + ("cannot nest $GENERATE"); + startGenerate(); + if (noExpandGenerate) { + endGenerate(); + continue; + } + return nextGenerated(); + } else { + throw st.exception("Invalid directive: " + s); + } + } else { + s = token.value; + name = parseName(s, origin); + if (last != null && name.equals(last.getName())) { + name = last.getName(); + } + } + + parseTTLClassAndType(); + last = Record.fromString(name, currentType, currentDClass, + currentTTL, st, origin); + if (needSOATTL) { + long ttl = ((SOARecord)last).getMinimum(); + last.setTTL(ttl); + defaultTTL = ttl; + needSOATTL = false; + } + return last; + } +} + +/** + * Returns the next record in the master file. This will process any + * directives before the next record. + * @return The next record. + * @throws IOException The master file could not be read, or was syntactically + * invalid. + */ +public Record +nextRecord() throws IOException { + Record rec = null; + try { + rec = _nextRecord(); + } + finally { + if (rec == null) { + st.close(); + } + } + return rec; +} + +/** + * Specifies whether $GENERATE statements should be expanded. Whether + * expanded or not, the specifications for generated records are available + * by calling {@link #generators}. This must be called before a $GENERATE + * statement is seen during iteration to have an effect. + */ +public void +expandGenerate(boolean wantExpand) { + noExpandGenerate = !wantExpand; +} + +/** + * Returns an iterator over the generators specified in the master file; that + * is, the parsed contents of $GENERATE statements. + * @see Generator + */ +public Iterator +generators() { + if (generators != null) + return Collections.unmodifiableList(generators).iterator(); + else + return Collections.EMPTY_LIST.iterator(); +} + +protected void +finalize() { + st.close(); +} + +} diff --git a/src/org/xbill/DNS/Message.java b/src/org/xbill/DNS/Message.java new file mode 100644 index 0000000..fe0c3c9 --- /dev/null +++ b/src/org/xbill/DNS/Message.java @@ -0,0 +1,611 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; +import java.io.*; + +/** + * A DNS Message. A message is the basic unit of communication between + * the client and server of a DNS operation. A message consists of a Header + * and 4 message sections. + * @see Resolver + * @see Header + * @see Section + * + * @author Brian Wellington + */ + +public class Message implements Cloneable { + +/** The maximum length of a message in wire format. */ +public static final int MAXLENGTH = 65535; + +private Header header; +private List [] sections; +private int size; +private TSIG tsigkey; +private TSIGRecord querytsig; +private int tsigerror; + +int tsigstart; +int tsigState; +int sig0start; + +/* The message was not signed */ +static final int TSIG_UNSIGNED = 0; + +/* The message was signed and verification succeeded */ +static final int TSIG_VERIFIED = 1; + +/* The message was an unsigned message in multiple-message response */ +static final int TSIG_INTERMEDIATE = 2; + +/* The message was signed and no verification was attempted. */ +static final int TSIG_SIGNED = 3; + +/* + * The message was signed and verification failed, or was not signed + * when it should have been. + */ +static final int TSIG_FAILED = 4; + +private static Record [] emptyRecordArray = new Record[0]; +private static RRset [] emptyRRsetArray = new RRset[0]; + +private +Message(Header header) { + sections = new List[4]; + this.header = header; +} + +/** Creates a new Message with the specified Message ID */ +public +Message(int id) { + this(new Header(id)); +} + +/** Creates a new Message with a random Message ID */ +public +Message() { + this(new Header()); +} + +/** + * Creates a new Message with a random Message ID suitable for sending as a + * query. + * @param r A record containing the question + */ +public static Message +newQuery(Record r) { + Message m = new Message(); + m.header.setOpcode(Opcode.QUERY); + m.header.setFlag(Flags.RD); + m.addRecord(r, Section.QUESTION); + return m; +} + +/** + * Creates a new Message to contain a dynamic update. A random Message ID + * and the zone are filled in. + * @param zone The zone to be updated + */ +public static Message +newUpdate(Name zone) { + return new Update(zone); +} + +Message(DNSInput in) throws IOException { + this(new Header(in)); + boolean isUpdate = (header.getOpcode() == Opcode.UPDATE); + boolean truncated = header.getFlag(Flags.TC); + try { + for (int i = 0; i < 4; i++) { + int count = header.getCount(i); + if (count > 0) + sections[i] = new ArrayList(count); + for (int j = 0; j < count; j++) { + int pos = in.current(); + Record rec = Record.fromWire(in, i, isUpdate); + sections[i].add(rec); + if (i == Section.ADDITIONAL) { + if (rec.getType() == Type.TSIG) + tsigstart = pos; + if (rec.getType() == Type.SIG) { + SIGRecord sig = (SIGRecord) rec; + if (sig.getTypeCovered() == 0) + sig0start = pos; + } + } + } + } + } catch (WireParseException e) { + if (!truncated) + throw e; + } + size = in.current(); +} + +/** + * Creates a new Message from its DNS wire format representation + * @param b A byte array containing the DNS Message. + */ +public +Message(byte [] b) throws IOException { + this(new DNSInput(b)); +} + +/** + * Replaces the Header with a new one. + * @see Header + */ +public void +setHeader(Header h) { + header = h; +} + +/** + * Retrieves the Header. + * @see Header + */ +public Header +getHeader() { + return header; +} + +/** + * Adds a record to a section of the Message, and adjusts the header. + * @see Record + * @see Section + */ +public void +addRecord(Record r, int section) { + if (sections[section] == null) + sections[section] = new LinkedList(); + header.incCount(section); + sections[section].add(r); +} + +/** + * Removes a record from a section of the Message, and adjusts the header. + * @see Record + * @see Section + */ +public boolean +removeRecord(Record r, int section) { + if (sections[section] != null && sections[section].remove(r)) { + header.decCount(section); + return true; + } + else + return false; +} + +/** + * Removes all records from a section of the Message, and adjusts the header. + * @see Record + * @see Section + */ +public void +removeAllRecords(int section) { + sections[section] = null; + header.setCount(section, 0); +} + +/** + * Determines if the given record is already present in the given section. + * @see Record + * @see Section + */ +public boolean +findRecord(Record r, int section) { + return (sections[section] != null && sections[section].contains(r)); +} + +/** + * Determines if the given record is already present in any section. + * @see Record + * @see Section + */ +public boolean +findRecord(Record r) { + for (int i = Section.ANSWER; i <= Section.ADDITIONAL; i++) + if (sections[i] != null && sections[i].contains(r)) + return true; + return false; +} + +/** + * Determines if an RRset with the given name and type is already + * present in the given section. + * @see RRset + * @see Section + */ +public boolean +findRRset(Name name, int type, int section) { + if (sections[section] == null) + return false; + for (int i = 0; i < sections[section].size(); i++) { + Record r = (Record) sections[section].get(i); + if (r.getType() == type && name.equals(r.getName())) + return true; + } + return false; +} + +/** + * Determines if an RRset with the given name and type is already + * present in any section. + * @see RRset + * @see Section + */ +public boolean +findRRset(Name name, int type) { + return (findRRset(name, type, Section.ANSWER) || + findRRset(name, type, Section.AUTHORITY) || + findRRset(name, type, Section.ADDITIONAL)); +} + +/** + * Returns the first record in the QUESTION section. + * @see Record + * @see Section + */ +public Record +getQuestion() { + List l = sections[Section.QUESTION]; + if (l == null || l.size() == 0) + return null; + return (Record) l.get(0); +} + +/** + * Returns the TSIG record from the ADDITIONAL section, if one is present. + * @see TSIGRecord + * @see TSIG + * @see Section + */ +public TSIGRecord +getTSIG() { + int count = header.getCount(Section.ADDITIONAL); + if (count == 0) + return null; + List l = sections[Section.ADDITIONAL]; + Record rec = (Record) l.get(count - 1); + if (rec.type != Type.TSIG) + return null; + return (TSIGRecord) rec; +} + +/** + * Was this message signed by a TSIG? + * @see TSIG + */ +public boolean +isSigned() { + return (tsigState == TSIG_SIGNED || + tsigState == TSIG_VERIFIED || + tsigState == TSIG_FAILED); +} + +/** + * If this message was signed by a TSIG, was the TSIG verified? + * @see TSIG + */ +public boolean +isVerified() { + return (tsigState == TSIG_VERIFIED); +} + +/** + * Returns the OPT record from the ADDITIONAL section, if one is present. + * @see OPTRecord + * @see Section + */ +public OPTRecord +getOPT() { + Record [] additional = getSectionArray(Section.ADDITIONAL); + for (int i = 0; i < additional.length; i++) + if (additional[i] instanceof OPTRecord) + return (OPTRecord) additional[i]; + return null; +} + +/** + * Returns the message's rcode (error code). This incorporates the EDNS + * extended rcode. + */ +public int +getRcode() { + int rcode = header.getRcode(); + OPTRecord opt = getOPT(); + if (opt != null) + rcode += (opt.getExtendedRcode() << 4); + return rcode; +} + +/** + * Returns an array containing all records in the given section, or an + * empty array if the section is empty. + * @see Record + * @see Section + */ +public Record [] +getSectionArray(int section) { + if (sections[section] == null) + return emptyRecordArray; + List l = sections[section]; + return (Record []) l.toArray(new Record[l.size()]); +} + +private static boolean +sameSet(Record r1, Record r2) { + return (r1.getRRsetType() == r2.getRRsetType() && + r1.getDClass() == r2.getDClass() && + r1.getName().equals(r2.getName())); +} + +/** + * Returns an array containing all records in the given section grouped into + * RRsets. + * @see RRset + * @see Section + */ +public RRset [] +getSectionRRsets(int section) { + if (sections[section] == null) + return emptyRRsetArray; + List sets = new LinkedList(); + Record [] recs = getSectionArray(section); + Set hash = new HashSet(); + for (int i = 0; i < recs.length; i++) { + Name name = recs[i].getName(); + boolean newset = true; + if (hash.contains(name)) { + for (int j = sets.size() - 1; j >= 0; j--) { + RRset set = (RRset) sets.get(j); + if (set.getType() == recs[i].getRRsetType() && + set.getDClass() == recs[i].getDClass() && + set.getName().equals(name)) + { + set.addRR(recs[i]); + newset = false; + break; + } + } + } + if (newset) { + RRset set = new RRset(recs[i]); + sets.add(set); + hash.add(name); + } + } + return (RRset []) sets.toArray(new RRset[sets.size()]); +} + +void +toWire(DNSOutput out) { + header.toWire(out); + Compression c = new Compression(); + for (int i = 0; i < 4; i++) { + if (sections[i] == null) + continue; + for (int j = 0; j < sections[i].size(); j++) { + Record rec = (Record)sections[i].get(j); + rec.toWire(out, i, c); + } + } +} + +/* Returns the number of records not successfully rendered. */ +private int +sectionToWire(DNSOutput out, int section, Compression c, + int maxLength) +{ + int n = sections[section].size(); + int pos = out.current(); + int rendered = 0; + Record lastrec = null; + + for (int i = 0; i < n; i++) { + Record rec = (Record)sections[section].get(i); + if (lastrec != null && !sameSet(rec, lastrec)) { + pos = out.current(); + rendered = i; + } + lastrec = rec; + rec.toWire(out, section, c); + if (out.current() > maxLength) { + out.jump(pos); + return n - rendered; + } + } + return 0; +} + +/* Returns true if the message could be rendered. */ +private boolean +toWire(DNSOutput out, int maxLength) { + if (maxLength < Header.LENGTH) + return false; + + Header newheader = null; + + int tempMaxLength = maxLength; + if (tsigkey != null) + tempMaxLength -= tsigkey.recordLength(); + + int startpos = out.current(); + header.toWire(out); + Compression c = new Compression(); + for (int i = 0; i < 4; i++) { + int skipped; + if (sections[i] == null) + continue; + skipped = sectionToWire(out, i, c, tempMaxLength); + if (skipped != 0) { + if (newheader == null) + newheader = (Header) header.clone(); + if (i != Section.ADDITIONAL) + newheader.setFlag(Flags.TC); + int count = newheader.getCount(i); + newheader.setCount(i, count - skipped); + for (int j = i + 1; j < 4; j++) + newheader.setCount(j, 0); + + out.save(); + out.jump(startpos); + newheader.toWire(out); + out.restore(); + break; + } + } + + if (tsigkey != null) { + TSIGRecord tsigrec = tsigkey.generate(this, out.toByteArray(), + tsigerror, querytsig); + + if (newheader == null) + newheader = (Header) header.clone(); + tsigrec.toWire(out, Section.ADDITIONAL, c); + newheader.incCount(Section.ADDITIONAL); + + out.save(); + out.jump(startpos); + newheader.toWire(out); + out.restore(); + } + + return true; +} + +/** + * Returns an array containing the wire format representation of the Message. + */ +public byte [] +toWire() { + DNSOutput out = new DNSOutput(); + toWire(out); + size = out.current(); + return out.toByteArray(); +} + +/** + * Returns an array containing the wire format representation of the Message + * with the specified maximum length. This will generate a truncated + * message (with the TC bit) if the message doesn't fit, and will also + * sign the message with the TSIG key set by a call to setTSIG(). This + * method may return null if the message could not be rendered at all; this + * could happen if maxLength is smaller than a DNS header, for example. + * @param maxLength The maximum length of the message. + * @return The wire format of the message, or null if the message could not be + * rendered into the specified length. + * @see Flags + * @see TSIG + */ +public byte [] +toWire(int maxLength) { + DNSOutput out = new DNSOutput(); + toWire(out, maxLength); + size = out.current(); + return out.toByteArray(); +} + +/** + * Sets the TSIG key and other necessary information to sign a message. + * @param key The TSIG key. + * @param error The value of the TSIG error field. + * @param querytsig If this is a response, the TSIG from the request. + */ +public void +setTSIG(TSIG key, int error, TSIGRecord querytsig) { + this.tsigkey = key; + this.tsigerror = error; + this.querytsig = querytsig; +} + +/** + * Returns the size of the message. Only valid if the message has been + * converted to or from wire format. + */ +public int +numBytes() { + return size; +} + +/** + * Converts the given section of the Message to a String. + * @see Section + */ +public String +sectionToString(int i) { + if (i > 3) + return null; + + StringBuffer sb = new StringBuffer(); + + Record [] records = getSectionArray(i); + for (int j = 0; j < records.length; j++) { + Record rec = records[j]; + if (i == Section.QUESTION) { + sb.append(";;\t" + rec.name); + sb.append(", type = " + Type.string(rec.type)); + sb.append(", class = " + DClass.string(rec.dclass)); + } + else + sb.append(rec); + sb.append("\n"); + } + return sb.toString(); +} + +/** + * Converts the Message to a String. + */ +public String +toString() { + StringBuffer sb = new StringBuffer(); + OPTRecord opt = getOPT(); + if (opt != null) + sb.append(header.toStringWithRcode(getRcode()) + "\n"); + else + sb.append(header + "\n"); + if (isSigned()) { + sb.append(";; TSIG "); + if (isVerified()) + sb.append("ok"); + else + sb.append("invalid"); + sb.append('\n'); + } + for (int i = 0; i < 4; i++) { + if (header.getOpcode() != Opcode.UPDATE) + sb.append(";; " + Section.longString(i) + ":\n"); + else + sb.append(";; " + Section.updString(i) + ":\n"); + sb.append(sectionToString(i) + "\n"); + } + sb.append(";; Message size: " + numBytes() + " bytes"); + return sb.toString(); +} + +/** + * Creates a copy of this Message. This is done by the Resolver before adding + * TSIG and OPT records, for example. + * @see Resolver + * @see TSIGRecord + * @see OPTRecord + */ +public Object +clone() { + Message m = new Message(); + for (int i = 0; i < sections.length; i++) { + if (sections[i] != null) + m.sections[i] = new LinkedList(sections[i]); + } + m.header = (Header) header.clone(); + m.size = size; + return m; +} + +} diff --git a/src/org/xbill/DNS/Mnemonic.java b/src/org/xbill/DNS/Mnemonic.java new file mode 100644 index 0000000..dd60f62 --- /dev/null +++ b/src/org/xbill/DNS/Mnemonic.java @@ -0,0 +1,210 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.HashMap; + +/** + * A utility class for converting between numeric codes and mnemonics + * for those codes. Mnemonics are case insensitive. + * + * @author Brian Wellington + */ + +class Mnemonic { + +private static Integer cachedInts[] = new Integer[64]; + +static { + for (int i = 0; i < cachedInts.length; i++) { + cachedInts[i] = new Integer(i); + } +} + +/* Strings are case-sensitive. */ +static final int CASE_SENSITIVE = 1; + +/* Strings will be stored/searched for in uppercase. */ +static final int CASE_UPPER = 2; + +/* Strings will be stored/searched for in lowercase. */ +static final int CASE_LOWER = 3; + +private HashMap strings; +private HashMap values; +private String description; +private int wordcase; +private String prefix; +private int max; +private boolean numericok; + +/** + * Creates a new Mnemonic table. + * @param description A short description of the mnemonic to use when + * @param wordcase Whether to convert strings into uppercase, lowercase, + * or leave them unchanged. + * throwing exceptions. + */ +public +Mnemonic(String description, int wordcase) { + this.description = description; + this.wordcase = wordcase; + strings = new HashMap(); + values = new HashMap(); + max = Integer.MAX_VALUE; +} + +/** Sets the maximum numeric value */ +public void +setMaximum(int max) { + this.max = max; +} + +/** + * Sets the prefix to use when converting to and from values that don't + * have mnemonics. + */ +public void +setPrefix(String prefix) { + this.prefix = sanitize(prefix); +} + +/** + * Sets whether numeric values stored in strings are acceptable. + */ +public void +setNumericAllowed(boolean numeric) { + this.numericok = numeric; +} + +/** + * Converts an int into a possibly cached Integer. + */ +public static Integer +toInteger(int val) { + if (val >= 0 && val < cachedInts.length) + return (cachedInts[val]); + return new Integer(val); +} + +/** + * Checks that a numeric value is within the range [0..max] + */ +public void +check(int val) { + if (val < 0 || val > max) { + throw new IllegalArgumentException(description + " " + val + + "is out of range"); + } +} + +/* Converts a String to the correct case. */ +private String +sanitize(String str) { + if (wordcase == CASE_UPPER) + return str.toUpperCase(); + else if (wordcase == CASE_LOWER) + return str.toLowerCase(); + return str; +} + +private int +parseNumeric(String s) { + try { + int val = Integer.parseInt(s); + if (val >= 0 && val <= max) + return val; + } + catch (NumberFormatException e) { + } + return -1; +} + +/** + * Defines the text representation of a numeric value. + * @param val The numeric value + * @param string The text string + */ +public void +add(int val, String str) { + check(val); + Integer value = toInteger(val); + str = sanitize(str); + strings.put(str, value); + values.put(value, str); +} + +/** + * Defines an additional text representation of a numeric value. This will + * be used by getValue(), but not getText(). + * @param val The numeric value + * @param string The text string + */ +public void +addAlias(int val, String str) { + check(val); + Integer value = toInteger(val); + str = sanitize(str); + strings.put(str, value); +} + +/** + * Copies all mnemonics from one table into another. + * @param val The numeric value + * @param string The text string + * @throws IllegalArgumentException The wordcases of the Mnemonics do not + * match. + */ +public void +addAll(Mnemonic source) { + if (wordcase != source.wordcase) + throw new IllegalArgumentException(source.description + + ": wordcases do not match"); + strings.putAll(source.strings); + values.putAll(source.values); +} + +/** + * Gets the text mnemonic corresponding to a numeric value. + * @param val The numeric value + * @return The corresponding text mnemonic. + */ +public String +getText(int val) { + check(val); + String str = (String) values.get(toInteger(val)); + if (str != null) + return str; + str = Integer.toString(val); + if (prefix != null) + return prefix + str; + return str; +} + +/** + * Gets the numeric value corresponding to a text mnemonic. + * @param str The text mnemonic + * @return The corresponding numeric value, or -1 if there is none + */ +public int +getValue(String str) { + str = sanitize(str); + Integer value = (Integer) strings.get(str); + if (value != null) { + return value.intValue(); + } + if (prefix != null) { + if (str.startsWith(prefix)) { + int val = parseNumeric(str.substring(prefix.length())); + if (val >= 0) { + return val; + } + } + } + if (numericok) { + return parseNumeric(str); + } + return -1; +} + +} diff --git a/src/org/xbill/DNS/NAPTRRecord.java b/src/org/xbill/DNS/NAPTRRecord.java new file mode 100644 index 0000000..da2ec6d --- /dev/null +++ b/src/org/xbill/DNS/NAPTRRecord.java @@ -0,0 +1,154 @@ +// Copyright (c) 2000-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Name Authority Pointer Record - specifies rewrite rule, that when applied + * to an existing string will produce a new domain. + * + * @author Chuck Santos + */ + +public class NAPTRRecord extends Record { + +private static final long serialVersionUID = 5191232392044947002L; + +private int order, preference; +private byte [] flags, service, regexp; +private Name replacement; + +NAPTRRecord() {} + +Record +getObject() { + return new NAPTRRecord(); +} + +/** + * Creates an NAPTR Record from the given data + * @param order The order of this NAPTR. Records with lower order are + * preferred. + * @param preference The preference, used to select between records at the + * same order. + * @param flags The control aspects of the NAPTRRecord. + * @param service The service or protocol available down the rewrite path. + * @param regexp The regular/substitution expression. + * @param replacement The domain-name to query for the next DNS resource + * record, depending on the value of the flags field. + * @throws IllegalArgumentException One of the strings has invalid escapes + */ +public +NAPTRRecord(Name name, int dclass, long ttl, int order, int preference, + String flags, String service, String regexp, Name replacement) +{ + super(name, Type.NAPTR, dclass, ttl); + this.order = checkU16("order", order); + this.preference = checkU16("preference", preference); + try { + this.flags = byteArrayFromString(flags); + this.service = byteArrayFromString(service); + this.regexp = byteArrayFromString(regexp); + } + catch (TextParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + this.replacement = checkName("replacement", replacement); +} + +void +rrFromWire(DNSInput in) throws IOException { + order = in.readU16(); + preference = in.readU16(); + flags = in.readCountedString(); + service = in.readCountedString(); + regexp = in.readCountedString(); + replacement = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + order = st.getUInt16(); + preference = st.getUInt16(); + try { + flags = byteArrayFromString(st.getString()); + service = byteArrayFromString(st.getString()); + regexp = byteArrayFromString(st.getString()); + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } + replacement = st.getName(origin); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(order); + sb.append(" "); + sb.append(preference); + sb.append(" "); + sb.append(byteArrayToString(flags, true)); + sb.append(" "); + sb.append(byteArrayToString(service, true)); + sb.append(" "); + sb.append(byteArrayToString(regexp, true)); + sb.append(" "); + sb.append(replacement); + return sb.toString(); +} + +/** Returns the order */ +public int +getOrder() { + return order; +} + +/** Returns the preference */ +public int +getPreference() { + return preference; +} + +/** Returns flags */ +public String +getFlags() { + return byteArrayToString(flags, false); +} + +/** Returns service */ +public String +getService() { + return byteArrayToString(service, false); +} + +/** Returns regexp */ +public String +getRegexp() { + return byteArrayToString(regexp, false); +} + +/** Returns the replacement domain-name */ +public Name +getReplacement() { + return replacement; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(order); + out.writeU16(preference); + out.writeCountedString(flags); + out.writeCountedString(service); + out.writeCountedString(regexp); + replacement.toWire(out, null, canonical); +} + +public Name +getAdditionalName() { + return replacement; +} + +} diff --git a/src/org/xbill/DNS/NSAPRecord.java b/src/org/xbill/DNS/NSAPRecord.java new file mode 100644 index 0000000..a6b2031 --- /dev/null +++ b/src/org/xbill/DNS/NSAPRecord.java @@ -0,0 +1,106 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * NSAP Address Record. + * + * @author Brian Wellington + */ + +public class NSAPRecord extends Record { + +private static final long serialVersionUID = -1037209403185658593L; + +private byte [] address; + +NSAPRecord() {} + +Record +getObject() { + return new NSAPRecord(); +} + +private static final byte [] +checkAndConvertAddress(String address) { + if (!address.substring(0, 2).equalsIgnoreCase("0x")) { + return null; + } + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + boolean partial = false; + int current = 0; + for (int i = 2; i < address.length(); i++) { + char c = address.charAt(i); + if (c == '.') { + continue; + } + int value = Character.digit(c, 16); + if (value == -1) { + return null; + } + if (partial) { + current += value; + bytes.write(current); + partial = false; + } else { + current = value << 4; + partial = true; + } + + } + if (partial) { + return null; + } + return bytes.toByteArray(); +} + +/** + * Creates an NSAP Record from the given data + * @param address The NSAP address. + * @throws IllegalArgumentException The address is not a valid NSAP address. + */ +public +NSAPRecord(Name name, int dclass, long ttl, String address) { + super(name, Type.NSAP, dclass, ttl); + this.address = checkAndConvertAddress(address); + if (this.address == null) { + throw new IllegalArgumentException("invalid NSAP address " + + address); + } +} + +void +rrFromWire(DNSInput in) throws IOException { + address = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String addr = st.getString(); + this.address = checkAndConvertAddress(addr); + if (this.address == null) + throw st.exception("invalid NSAP address " + addr); +} + +/** + * Returns the NSAP address. + */ +public String +getAddress() { + return byteArrayToString(address, false); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(address); +} + +String +rrToString() { + return "0x" + base16.toString(address); +} + +} diff --git a/src/org/xbill/DNS/NSAP_PTRRecord.java b/src/org/xbill/DNS/NSAP_PTRRecord.java new file mode 100644 index 0000000..ecc609f --- /dev/null +++ b/src/org/xbill/DNS/NSAP_PTRRecord.java @@ -0,0 +1,38 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * NSAP Pointer Record - maps a domain name representing an NSAP Address to + * a hostname. + * + * @author Brian Wellington + */ + +public class NSAP_PTRRecord extends SingleNameBase { + +private static final long serialVersionUID = 2386284746382064904L; + +NSAP_PTRRecord() {} + +Record +getObject() { + return new NSAP_PTRRecord(); +} + +/** + * Creates a new NSAP_PTR Record with the given data + * @param target The name of the host with this address + */ +public +NSAP_PTRRecord(Name name, int dclass, long ttl, Name target) { + super(name, Type.NSAP_PTR, dclass, ttl, target, "target"); +} + +/** Gets the target of the NSAP_PTR Record */ +public Name +getTarget() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/NSEC3PARAMRecord.java b/src/org/xbill/DNS/NSEC3PARAMRecord.java new file mode 100644 index 0000000..d663a62 --- /dev/null +++ b/src/org/xbill/DNS/NSEC3PARAMRecord.java @@ -0,0 +1,165 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +import org.xbill.DNS.utils.base16; + +/** + * Next SECure name 3 Parameters - this record contains the parameters (hash + * algorithm, salt, iterations) used for a valid, complete NSEC3 chain present + * in a zone. Zones signed using NSEC3 must include this record at the zone apex + * to inform authoritative servers that NSEC3 is being used with the given + * parameters. + * + * @author Brian Wellington + * @author David Blacka + */ + +public class NSEC3PARAMRecord extends Record { + +private static final long serialVersionUID = -8689038598776316533L; + +private int hashAlg; +private int flags; +private int iterations; +private byte salt[]; + +NSEC3PARAMRecord() {} + +Record getObject() { + return new NSEC3PARAMRecord(); +} + +/** + * Creates an NSEC3PARAM record from the given data. + * + * @param name The ownername of the NSEC3PARAM record (generally the zone name). + * @param dclass The class. + * @param ttl The TTL. + * @param hashAlg The hash algorithm. + * @param flags The value of the flags field. + * @param iterations The number of hash iterations. + * @param salt The salt to use (may be null). + */ +public NSEC3PARAMRecord(Name name, int dclass, long ttl, int hashAlg, + int flags, int iterations, byte [] salt) +{ + super(name, Type.NSEC3PARAM, dclass, ttl); + this.hashAlg = checkU8("hashAlg", hashAlg); + this.flags = checkU8("flags", flags); + this.iterations = checkU16("iterations", iterations); + + if (salt != null) { + if (salt.length > 255) + throw new IllegalArgumentException("Invalid salt " + + "length"); + if (salt.length > 0) { + this.salt = new byte[salt.length]; + System.arraycopy(salt, 0, this.salt, 0, salt.length); + } + } +} + +void +rrFromWire(DNSInput in) throws IOException { + hashAlg = in.readU8(); + flags = in.readU8(); + iterations = in.readU16(); + + int salt_length = in.readU8(); + if (salt_length > 0) + salt = in.readByteArray(salt_length); + else + salt = null; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(hashAlg); + out.writeU8(flags); + out.writeU16(iterations); + + if (salt != null) { + out.writeU8(salt.length); + out.writeByteArray(salt); + } else + out.writeU8(0); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException +{ + hashAlg = st.getUInt8(); + flags = st.getUInt8(); + iterations = st.getUInt16(); + + String s = st.getString(); + if (s.equals("-")) + salt = null; + else { + st.unget(); + salt = st.getHexString(); + if (salt.length > 255) + throw st.exception("salt value too long"); + } +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(hashAlg); + sb.append(' '); + sb.append(flags); + sb.append(' '); + sb.append(iterations); + sb.append(' '); + if (salt == null) + sb.append('-'); + else + sb.append(base16.toString(salt)); + + return sb.toString(); +} + +/** Returns the hash algorithm */ +public int +getHashAlgorithm() { + return hashAlg; +} + +/** Returns the flags */ +public int +getFlags() { + return flags; +} + +/** Returns the number of iterations */ +public int +getIterations() { + return iterations; +} + +/** Returns the salt */ +public byte [] +getSalt() +{ + return salt; +} + +/** + * Hashes a name with the parameters of this NSEC3PARAM record. + * @param name The name to hash + * @return The hashed version of the name + * @throws NoSuchAlgorithmException The hash algorithm is unknown. + */ +public byte [] +hashName(Name name) throws NoSuchAlgorithmException +{ + return NSEC3Record.hashName(name, hashAlg, iterations, salt); +} + +} diff --git a/src/org/xbill/DNS/NSEC3Record.java b/src/org/xbill/DNS/NSEC3Record.java new file mode 100644 index 0000000..aa086b8 --- /dev/null +++ b/src/org/xbill/DNS/NSEC3Record.java @@ -0,0 +1,266 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.security.*; + +import org.xbill.DNS.utils.*; + +/** + * Next SECure name 3 - this record contains the next hashed name in an + * ordered list of hashed names in the zone, and a set of types for which + * records exist for this name. The presence of this record in a response + * signifies a negative response from a DNSSEC-signed zone. + * + * This replaces the NSEC and NXT records, when used. + * + * @author Brian Wellington + * @author David Blacka + */ + +public class NSEC3Record extends Record { + +public static class Flags { + /** + * NSEC3 flags identifiers. + */ + + private Flags() {} + + /** Unsigned delegation are not included in the NSEC3 chain. + * + */ + public static final int OPT_OUT = 0x01; +} + +public static class Digest { + private Digest() {} + + /** SHA-1 */ + public static final int SHA1 = 1; +} + +public static final int SHA1_DIGEST_ID = Digest.SHA1; + +private static final long serialVersionUID = -7123504635968932855L; + +private int hashAlg; +private int flags; +private int iterations; +private byte [] salt; +private byte [] next; +private TypeBitmap types; + +private static final base32 b32 = new base32(base32.Alphabet.BASE32HEX, + false, false); + +NSEC3Record() {} + +Record getObject() { + return new NSEC3Record(); +} + +/** + * Creates an NSEC3 record from the given data. + * + * @param name The ownername of the NSEC3 record (base32'd hash plus zonename). + * @param dclass The class. + * @param ttl The TTL. + * @param hashAlg The hash algorithm. + * @param flags The value of the flags field. + * @param iterations The number of hash iterations. + * @param salt The salt to use (may be null). + * @param next The next hash (may not be null). + * @param types The types present at the original ownername. + */ +public NSEC3Record(Name name, int dclass, long ttl, int hashAlg, + int flags, int iterations, byte [] salt, byte [] next, + int [] types) +{ + super(name, Type.NSEC3, dclass, ttl); + this.hashAlg = checkU8("hashAlg", hashAlg); + this.flags = checkU8("flags", flags); + this.iterations = checkU16("iterations", iterations); + + if (salt != null) { + if (salt.length > 255) + throw new IllegalArgumentException("Invalid salt"); + if (salt.length > 0) { + this.salt = new byte[salt.length]; + System.arraycopy(salt, 0, this.salt, 0, salt.length); + } + } + + if (next.length > 255) { + throw new IllegalArgumentException("Invalid next hash"); + } + this.next = new byte[next.length]; + System.arraycopy(next, 0, this.next, 0, next.length); + this.types = new TypeBitmap(types); +} + +void +rrFromWire(DNSInput in) throws IOException { + hashAlg = in.readU8(); + flags = in.readU8(); + iterations = in.readU16(); + + int salt_length = in.readU8(); + if (salt_length > 0) + salt = in.readByteArray(salt_length); + else + salt = null; + + int next_length = in.readU8(); + next = in.readByteArray(next_length); + types = new TypeBitmap(in); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(hashAlg); + out.writeU8(flags); + out.writeU16(iterations); + + if (salt != null) { + out.writeU8(salt.length); + out.writeByteArray(salt); + } else + out.writeU8(0); + + out.writeU8(next.length); + out.writeByteArray(next); + types.toWire(out); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + hashAlg = st.getUInt8(); + flags = st.getUInt8(); + iterations = st.getUInt16(); + + String s = st.getString(); + if (s.equals("-")) + salt = null; + else { + st.unget(); + salt = st.getHexString(); + if (salt.length > 255) + throw st.exception("salt value too long"); + } + + next = st.getBase32String(b32); + types = new TypeBitmap(st); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(hashAlg); + sb.append(' '); + sb.append(flags); + sb.append(' '); + sb.append(iterations); + sb.append(' '); + if (salt == null) + sb.append('-'); + else + sb.append(base16.toString(salt)); + sb.append(' '); + sb.append(b32.toString(next)); + + if (!types.empty()) { + sb.append(' '); + sb.append(types.toString()); + } + + return sb.toString(); +} + +/** Returns the hash algorithm */ +public int +getHashAlgorithm() { + return hashAlg; +} + +/** Returns the flags */ +public int +getFlags() { + return flags; +} + +/** Returns the number of iterations */ +public int +getIterations() { + return iterations; +} + +/** Returns the salt */ +public byte [] +getSalt() +{ + return salt; +} + +/** Returns the next hash */ +public byte [] +getNext() { + return next; +} + + /** Returns the set of types defined for this name */ +public int [] +getTypes() { + return types.toArray(); +} + +/** Returns whether a specific type is in the set of types. */ +public boolean +hasType(int type) +{ + return types.contains(type); +} + +static byte [] +hashName(Name name, int hashAlg, int iterations, byte [] salt) +throws NoSuchAlgorithmException +{ + MessageDigest digest; + switch (hashAlg) { + case Digest.SHA1: + digest = MessageDigest.getInstance("sha-1"); + break; + default: + throw new NoSuchAlgorithmException("Unknown NSEC3 algorithm" + + "identifier: " + + hashAlg); + } + byte [] hash = null; + for (int i = 0; i <= iterations; i++) { + digest.reset(); + if (i == 0) + digest.update(name.toWireCanonical()); + else + digest.update(hash); + if (salt != null) + digest.update(salt); + hash = digest.digest(); + } + return hash; +} + +/** + * Hashes a name with the parameters of this NSEC3 record. + * @param name The name to hash + * @return The hashed version of the name + * @throws NoSuchAlgorithmException The hash algorithm is unknown. + */ +public byte [] +hashName(Name name) throws NoSuchAlgorithmException +{ + return hashName(name, hashAlg, iterations, salt); +} + +} diff --git a/src/org/xbill/DNS/NSECRecord.java b/src/org/xbill/DNS/NSECRecord.java new file mode 100644 index 0000000..e523e37 --- /dev/null +++ b/src/org/xbill/DNS/NSECRecord.java @@ -0,0 +1,98 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Next SECure name - this record contains the following name in an + * ordered list of names in the zone, and a set of types for which + * records exist for this name. The presence of this record in a response + * signifies a negative response from a DNSSEC-signed zone. + * + * This replaces the NXT record. + * + * @author Brian Wellington + * @author David Blacka + */ + +public class NSECRecord extends Record { + +private static final long serialVersionUID = -5165065768816265385L; + +private Name next; +private TypeBitmap types; + +NSECRecord() {} + +Record +getObject() { + return new NSECRecord(); +} + +/** + * Creates an NSEC Record from the given data. + * @param next The following name in an ordered list of the zone + * @param types An array containing the types present. + */ +public +NSECRecord(Name name, int dclass, long ttl, Name next, int [] types) { + super(name, Type.NSEC, dclass, ttl); + this.next = checkName("next", next); + for (int i = 0; i < types.length; i++) { + Type.check(types[i]); + } + this.types = new TypeBitmap(types); +} + +void +rrFromWire(DNSInput in) throws IOException { + next = new Name(in); + types = new TypeBitmap(in); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + // Note: The next name is not lowercased. + next.toWire(out, null, false); + types.toWire(out); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + next = st.getName(origin); + types = new TypeBitmap(st); +} + +/** Converts rdata to a String */ +String +rrToString() +{ + StringBuffer sb = new StringBuffer(); + sb.append(next); + if (!types.empty()) { + sb.append(' '); + sb.append(types.toString()); + } + return sb.toString(); +} + +/** Returns the next name */ +public Name +getNext() { + return next; +} + +/** Returns the set of types defined for this name */ +public int [] +getTypes() { + return types.toArray(); +} + +/** Returns whether a specific type is in the set of types. */ +public boolean +hasType(int type) { + return types.contains(type); +} + +} diff --git a/src/org/xbill/DNS/NSIDOption.java b/src/org/xbill/DNS/NSIDOption.java new file mode 100644 index 0000000..7bcbcd5 --- /dev/null +++ b/src/org/xbill/DNS/NSIDOption.java @@ -0,0 +1,29 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * The Name Server Identifier Option, define in RFC 5001. + * + * @see OPTRecord + * + * @author Brian Wellington + */ +public class NSIDOption extends GenericEDNSOption { + +private static final long serialVersionUID = 74739759292589056L; + +NSIDOption() { + super(EDNSOption.Code.NSID); +} + +/** + * Construct an NSID option. + * @param data The contents of the option. + */ +public +NSIDOption(byte [] data) { + super(EDNSOption.Code.NSID, data); +} + +} diff --git a/src/org/xbill/DNS/NSRecord.java b/src/org/xbill/DNS/NSRecord.java new file mode 100644 index 0000000..2908da4 --- /dev/null +++ b/src/org/xbill/DNS/NSRecord.java @@ -0,0 +1,42 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Name Server Record - contains the name server serving the named zone + * + * @author Brian Wellington + */ + +public class NSRecord extends SingleCompressedNameBase { + +private static final long serialVersionUID = 487170758138268838L; + +NSRecord() {} + +Record +getObject() { + return new NSRecord(); +} + +/** + * Creates a new NS Record with the given data + * @param target The name server for the given domain + */ +public +NSRecord(Name name, int dclass, long ttl, Name target) { + super(name, Type.NS, dclass, ttl, target, "target"); +} + +/** Gets the target of the NS Record */ +public Name +getTarget() { + return getSingleName(); +} + +public Name +getAdditionalName() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/NULLRecord.java b/src/org/xbill/DNS/NULLRecord.java new file mode 100644 index 0000000..fa46d61 --- /dev/null +++ b/src/org/xbill/DNS/NULLRecord.java @@ -0,0 +1,67 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * The NULL Record. This has no defined purpose, but can be used to + * hold arbitrary data. + * + * @author Brian Wellington + */ + +public class NULLRecord extends Record { + +private static final long serialVersionUID = -5796493183235216538L; + +private byte [] data; + +NULLRecord() {} + +Record +getObject() { + return new NULLRecord(); +} + +/** + * Creates a NULL record from the given data. + * @param data The contents of the record. + */ +public +NULLRecord(Name name, int dclass, long ttl, byte [] data) { + super(name, Type.NULL, dclass, ttl); + + if (data.length > 0xFFFF) { + throw new IllegalArgumentException("data must be <65536 bytes"); + } + this.data = data; +} + +void +rrFromWire(DNSInput in) throws IOException { + data = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + throw st.exception("no defined text format for NULL records"); +} + +String +rrToString() { + return unknownToString(data); +} + +/** Returns the contents of this record. */ +public byte [] +getData() { + return data; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(data); +} + +} diff --git a/src/org/xbill/DNS/NXTRecord.java b/src/org/xbill/DNS/NXTRecord.java new file mode 100644 index 0000000..ad04e01 --- /dev/null +++ b/src/org/xbill/DNS/NXTRecord.java @@ -0,0 +1,111 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * Next name - this record contains the following name in an ordered list + * of names in the zone, and a set of types for which records exist for + * this name. The presence of this record in a response signifies a + * failed query for data in a DNSSEC-signed zone. + * + * @author Brian Wellington + */ + +public class NXTRecord extends Record { + +private static final long serialVersionUID = -8851454400765507520L; + +private Name next; +private BitSet bitmap; + +NXTRecord() {} + +Record +getObject() { + return new NXTRecord(); +} + +/** + * Creates an NXT Record from the given data + * @param next The following name in an ordered list of the zone + * @param bitmap The set of type for which records exist at this name +*/ +public +NXTRecord(Name name, int dclass, long ttl, Name next, BitSet bitmap) { + super(name, Type.NXT, dclass, ttl); + this.next = checkName("next", next); + this.bitmap = bitmap; +} + +void +rrFromWire(DNSInput in) throws IOException { + next = new Name(in); + bitmap = new BitSet(); + int bitmapLength = in.remaining(); + for (int i = 0; i < bitmapLength; i++) { + int t = in.readU8(); + for (int j = 0; j < 8; j++) + if ((t & (1 << (7 - j))) != 0) + bitmap.set(i * 8 + j); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + next = st.getName(origin); + bitmap = new BitSet(); + while (true) { + Tokenizer.Token t = st.get(); + if (!t.isString()) + break; + int typecode = Type.value(t.value, true); + if (typecode <= 0 || typecode > 128) + throw st.exception("Invalid type: " + t.value); + bitmap.set(typecode); + } + st.unget(); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(next); + int length = bitmap.length(); + for (short i = 0; i < length; i++) + if (bitmap.get(i)) { + sb.append(" "); + sb.append(Type.string(i)); + } + return sb.toString(); +} + +/** Returns the next name */ +public Name +getNext() { + return next; +} + +/** Returns the set of types defined for this name */ +public BitSet +getBitmap() { + return bitmap; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + next.toWire(out, null, canonical); + int length = bitmap.length(); + for (int i = 0, t = 0; i < length; i++) { + t |= (bitmap.get(i) ? (1 << (7 - i % 8)) : 0); + if (i % 8 == 7 || i == length - 1) { + out.writeU8(t); + t = 0; + } + } +} + +} diff --git a/src/org/xbill/DNS/Name.java b/src/org/xbill/DNS/Name.java new file mode 100644 index 0000000..1331ad9 --- /dev/null +++ b/src/org/xbill/DNS/Name.java @@ -0,0 +1,822 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.text.*; + +/** + * A representation of a domain name. It may either be absolute (fully + * qualified) or relative. + * + * @author Brian Wellington + */ + +public class Name implements Comparable, Serializable { + +private static final long serialVersionUID = -7257019940971525644L; + +private static final int LABEL_NORMAL = 0; +private static final int LABEL_COMPRESSION = 0xC0; +private static final int LABEL_MASK = 0xC0; + +/* The name data */ +private byte [] name; + +/* + * Effectively an 8 byte array, where the low order byte stores the number + * of labels and the 7 higher order bytes store per-label offsets. + */ +private long offsets; + +/* Precomputed hashcode. */ +private int hashcode; + +private static final byte [] emptyLabel = new byte[] {(byte)0}; +private static final byte [] wildLabel = new byte[] {(byte)1, (byte)'*'}; + +/** The root name */ +public static final Name root; + +/** The root name */ +public static final Name empty; + +/** The maximum length of a Name */ +private static final int MAXNAME = 255; + +/** The maximum length of a label a Name */ +private static final int MAXLABEL = 63; + +/** The maximum number of labels in a Name */ +private static final int MAXLABELS = 128; + +/** The maximum number of cached offsets */ +private static final int MAXOFFSETS = 7; + +/* Used for printing non-printable characters */ +private static final DecimalFormat byteFormat = new DecimalFormat(); + +/* Used to efficiently convert bytes to lowercase */ +private static final byte lowercase[] = new byte[256]; + +/* Used in wildcard names. */ +private static final Name wild; + +static { + byteFormat.setMinimumIntegerDigits(3); + for (int i = 0; i < lowercase.length; i++) { + if (i < 'A' || i > 'Z') + lowercase[i] = (byte)i; + else + lowercase[i] = (byte)(i - 'A' + 'a'); + } + root = new Name(); + root.appendSafe(emptyLabel, 0, 1); + empty = new Name(); + empty.name = new byte[0]; + wild = new Name(); + wild.appendSafe(wildLabel, 0, 1); +} + +private +Name() { +} + +private final void +setoffset(int n, int offset) { + if (n >= MAXOFFSETS) + return; + int shift = 8 * (7 - n); + offsets &= (~(0xFFL << shift)); + offsets |= ((long)offset << shift); +} + +private final int +offset(int n) { + if (n == 0 && getlabels() == 0) + return 0; + if (n < 0 || n >= getlabels()) + throw new IllegalArgumentException("label out of range"); + if (n < MAXOFFSETS) { + int shift = 8 * (7 - n); + return ((int)(offsets >>> shift) & 0xFF); + } else { + int pos = offset(MAXOFFSETS - 1); + for (int i = MAXOFFSETS - 1; i < n; i++) + pos += (name[pos] + 1); + return (pos); + } +} + +private final void +setlabels(int labels) { + offsets &= ~(0xFF); + offsets |= labels; +} + +private final int +getlabels() { + return (int)(offsets & 0xFF); +} + +private static final void +copy(Name src, Name dst) { + if (src.offset(0) == 0) { + dst.name = src.name; + dst.offsets = src.offsets; + } else { + int offset0 = src.offset(0); + int namelen = src.name.length - offset0; + int labels = src.labels(); + dst.name = new byte[namelen]; + System.arraycopy(src.name, offset0, dst.name, 0, namelen); + for (int i = 0; i < labels && i < MAXOFFSETS; i++) + dst.setoffset(i, src.offset(i) - offset0); + dst.setlabels(labels); + } +} + +private final void +append(byte [] array, int start, int n) throws NameTooLongException { + int length = (name == null ? 0 : (name.length - offset(0))); + int alength = 0; + for (int i = 0, pos = start; i < n; i++) { + int len = array[pos]; + if (len > MAXLABEL) + throw new IllegalStateException("invalid label"); + len++; + pos += len; + alength += len; + } + int newlength = length + alength; + if (newlength > MAXNAME) + throw new NameTooLongException(); + int labels = getlabels(); + int newlabels = labels + n; + if (newlabels > MAXLABELS) + throw new IllegalStateException("too many labels"); + byte [] newname = new byte[newlength]; + if (length != 0) + System.arraycopy(name, offset(0), newname, 0, length); + System.arraycopy(array, start, newname, length, alength); + name = newname; + for (int i = 0, pos = length; i < n; i++) { + setoffset(labels + i, pos); + pos += (newname[pos] + 1); + } + setlabels(newlabels); +} + +private static TextParseException +parseException(String str, String message) { + return new TextParseException("'" + str + "': " + message); +} + +private final void +appendFromString(String fullName, byte [] array, int start, int n) +throws TextParseException +{ + try { + append(array, start, n); + } + catch (NameTooLongException e) { + throw parseException(fullName, "Name too long"); + } +} + +private final void +appendSafe(byte [] array, int start, int n) { + try { + append(array, start, n); + } + catch (NameTooLongException e) { + } +} + +/** + * Create a new name from a string and an origin. This does not automatically + * make the name absolute; it will be absolute if it has a trailing dot or an + * absolute origin is appended. + * @param s The string to be converted + * @param origin If the name is not absolute, the origin to be appended. + * @throws TextParseException The name is invalid. + */ +public +Name(String s, Name origin) throws TextParseException { + if (s.equals("")) + throw parseException(s, "empty name"); + else if (s.equals("@")) { + if (origin == null) + copy(empty, this); + else + copy(origin, this); + return; + } else if (s.equals(".")) { + copy(root, this); + return; + } + int labelstart = -1; + int pos = 1; + byte [] label = new byte[MAXLABEL + 1]; + boolean escaped = false; + int digits = 0; + int intval = 0; + boolean absolute = false; + for (int i = 0; i < s.length(); i++) { + byte b = (byte) s.charAt(i); + if (escaped) { + if (b >= '0' && b <= '9' && digits < 3) { + digits++; + intval *= 10; + intval += (b - '0'); + if (intval > 255) + throw parseException(s, "bad escape"); + if (digits < 3) + continue; + b = (byte) intval; + } + else if (digits > 0 && digits < 3) + throw parseException(s, "bad escape"); + if (pos > MAXLABEL) + throw parseException(s, "label too long"); + labelstart = pos; + label[pos++] = b; + escaped = false; + } else if (b == '\\') { + escaped = true; + digits = 0; + intval = 0; + } else if (b == '.') { + if (labelstart == -1) + throw parseException(s, "invalid empty label"); + label[0] = (byte)(pos - 1); + appendFromString(s, label, 0, 1); + labelstart = -1; + pos = 1; + } else { + if (labelstart == -1) + labelstart = i; + if (pos > MAXLABEL) + throw parseException(s, "label too long"); + label[pos++] = b; + } + } + if (digits > 0 && digits < 3) + throw parseException(s, "bad escape"); + if (escaped) + throw parseException(s, "bad escape"); + if (labelstart == -1) { + appendFromString(s, emptyLabel, 0, 1); + absolute = true; + } else { + label[0] = (byte)(pos - 1); + appendFromString(s, label, 0, 1); + } + if (origin != null && !absolute) + appendFromString(s, origin.name, 0, origin.getlabels()); +} + +/** + * Create a new name from a string. This does not automatically make the name + * absolute; it will be absolute if it has a trailing dot. + * @param s The string to be converted + * @throws TextParseException The name is invalid. + */ +public +Name(String s) throws TextParseException { + this(s, null); +} + +/** + * Create a new name from a string and an origin. This does not automatically + * make the name absolute; it will be absolute if it has a trailing dot or an + * absolute origin is appended. This is identical to the constructor, except + * that it will avoid creating new objects in some cases. + * @param s The string to be converted + * @param origin If the name is not absolute, the origin to be appended. + * @throws TextParseException The name is invalid. + */ +public static Name +fromString(String s, Name origin) throws TextParseException { + if (s.equals("@") && origin != null) + return origin; + else if (s.equals(".")) + return (root); + + return new Name(s, origin); +} + +/** + * Create a new name from a string. This does not automatically make the name + * absolute; it will be absolute if it has a trailing dot. This is identical + * to the constructor, except that it will avoid creating new objects in some + * cases. + * @param s The string to be converted + * @throws TextParseException The name is invalid. + */ +public static Name +fromString(String s) throws TextParseException { + return fromString(s, null); +} + +/** + * Create a new name from a constant string. This should only be used when + the name is known to be good - that is, when it is constant. + * @param s The string to be converted + * @throws IllegalArgumentException The name is invalid. + */ +public static Name +fromConstantString(String s) { + try { + return fromString(s, null); + } + catch (TextParseException e) { + throw new IllegalArgumentException("Invalid name '" + s + "'"); + } +} + +/** + * Create a new name from DNS a wire format message + * @param in A stream containing the DNS message which is currently + * positioned at the start of the name to be read. + */ +public +Name(DNSInput in) throws WireParseException { + int len, pos; + boolean done = false; + byte [] label = new byte[MAXLABEL + 1]; + boolean savedState = false; + + while (!done) { + len = in.readU8(); + switch (len & LABEL_MASK) { + case LABEL_NORMAL: + if (getlabels() >= MAXLABELS) + throw new WireParseException("too many labels"); + if (len == 0) { + append(emptyLabel, 0, 1); + done = true; + } else { + label[0] = (byte)len; + in.readByteArray(label, 1, len); + append(label, 0, 1); + } + break; + case LABEL_COMPRESSION: + pos = in.readU8(); + pos += ((len & ~LABEL_MASK) << 8); + if (Options.check("verbosecompression")) + System.err.println("currently " + in.current() + + ", pointer to " + pos); + + if (pos >= in.current() - 2) + throw new WireParseException("bad compression"); + if (!savedState) { + in.save(); + savedState = true; + } + in.jump(pos); + if (Options.check("verbosecompression")) + System.err.println("current name '" + this + + "', seeking to " + pos); + break; + default: + throw new WireParseException("bad label type"); + } + } + if (savedState) { + in.restore(); + } +} + +/** + * Create a new name from DNS wire format + * @param b A byte array containing the wire format of the name. + */ +public +Name(byte [] b) throws IOException { + this(new DNSInput(b)); +} + +/** + * Create a new name by removing labels from the beginning of an existing Name + * @param src An existing Name + * @param n The number of labels to remove from the beginning in the copy + */ +public +Name(Name src, int n) { + int slabels = src.labels(); + if (n > slabels) + throw new IllegalArgumentException("attempted to remove too " + + "many labels"); + name = src.name; + setlabels(slabels - n); + for (int i = 0; i < MAXOFFSETS && i < slabels - n; i++) + setoffset(i, src.offset(i + n)); +} + +/** + * Creates a new name by concatenating two existing names. + * @param prefix The prefix name. + * @param suffix The suffix name. + * @return The concatenated name. + * @throws NameTooLongException The name is too long. + */ +public static Name +concatenate(Name prefix, Name suffix) throws NameTooLongException { + if (prefix.isAbsolute()) + return (prefix); + Name newname = new Name(); + copy(prefix, newname); + newname.append(suffix.name, suffix.offset(0), suffix.getlabels()); + return newname; +} + +/** + * If this name is a subdomain of origin, return a new name relative to + * origin with the same value. Otherwise, return the existing name. + * @param origin The origin to remove. + * @return The possibly relativized name. + */ +public Name +relativize(Name origin) { + if (origin == null || !subdomain(origin)) + return this; + Name newname = new Name(); + copy(this, newname); + int length = length() - origin.length(); + int labels = newname.labels() - origin.labels(); + newname.setlabels(labels); + newname.name = new byte[length]; + System.arraycopy(name, offset(0), newname.name, 0, length); + return newname; +} + +/** + * Generates a new Name with the first n labels replaced by a wildcard + * @return The wildcard name + */ +public Name +wild(int n) { + if (n < 1) + throw new IllegalArgumentException("must replace 1 or more " + + "labels"); + try { + Name newname = new Name(); + copy(wild, newname); + newname.append(name, offset(n), getlabels() - n); + return newname; + } + catch (NameTooLongException e) { + throw new IllegalStateException + ("Name.wild: concatenate failed"); + } +} + +/** + * Generates a new Name to be used when following a DNAME. + * @param dname The DNAME record to follow. + * @return The constructed name. + * @throws NameTooLongException The resulting name is too long. + */ +public Name +fromDNAME(DNAMERecord dname) throws NameTooLongException { + Name dnameowner = dname.getName(); + Name dnametarget = dname.getTarget(); + if (!subdomain(dnameowner)) + return null; + + int plabels = labels() - dnameowner.labels(); + int plength = length() - dnameowner.length(); + int pstart = offset(0); + + int dlabels = dnametarget.labels(); + int dlength = dnametarget.length(); + + if (plength + dlength > MAXNAME) + throw new NameTooLongException(); + + Name newname = new Name(); + newname.setlabels(plabels + dlabels); + newname.name = new byte[plength + dlength]; + System.arraycopy(name, pstart, newname.name, 0, plength); + System.arraycopy(dnametarget.name, 0, newname.name, plength, dlength); + + for (int i = 0, pos = 0; i < MAXOFFSETS && i < plabels + dlabels; i++) { + newname.setoffset(i, pos); + pos += (newname.name[pos] + 1); + } + return newname; +} + +/** + * Is this name a wildcard? + */ +public boolean +isWild() { + if (labels() == 0) + return false; + return (name[0] == (byte)1 && name[1] == (byte)'*'); +} + +/** + * Is this name absolute? + */ +public boolean +isAbsolute() { + if (labels() == 0) + return false; + return (name[name.length - 1] == 0); +} + +/** + * The length of the name. + */ +public short +length() { + if (getlabels() == 0) + return 0; + return (short)(name.length - offset(0)); +} + +/** + * The number of labels in the name. + */ +public int +labels() { + return getlabels(); +} + +/** + * Is the current Name a subdomain of the specified name? + */ +public boolean +subdomain(Name domain) { + int labels = labels(); + int dlabels = domain.labels(); + if (dlabels > labels) + return false; + if (dlabels == labels) + return equals(domain); + return domain.equals(name, offset(labels - dlabels)); +} + +private String +byteString(byte [] array, int pos) { + StringBuffer sb = new StringBuffer(); + int len = array[pos++]; + for (int i = pos; i < pos + len; i++) { + int b = array[i] & 0xFF; + if (b <= 0x20 || b >= 0x7f) { + sb.append('\\'); + sb.append(byteFormat.format(b)); + } + else if (b == '"' || b == '(' || b == ')' || b == '.' || + b == ';' || b == '\\' || b == '@' || b == '$') + { + sb.append('\\'); + sb.append((char)b); + } + else + sb.append((char)b); + } + return sb.toString(); +} + +/** + * Convert a Name to a String + * @return The representation of this name as a (printable) String. + */ +public String +toString() { + int labels = labels(); + if (labels == 0) + return "@"; + else if (labels == 1 && name[offset(0)] == 0) + return "."; + StringBuffer sb = new StringBuffer(); + for (int i = 0, pos = offset(0); i < labels; i++) { + int len = name[pos]; + if (len > MAXLABEL) + throw new IllegalStateException("invalid label"); + if (len == 0) + break; + sb.append(byteString(name, pos)); + sb.append('.'); + pos += (1 + len); + } + if (!isAbsolute()) + sb.deleteCharAt(sb.length() - 1); + return sb.toString(); +} + +/** + * Retrieve the nth label of a Name. This makes a copy of the label; changing + * this does not change the Name. + * @param n The label to be retrieved. The first label is 0. + */ +public byte [] +getLabel(int n) { + int pos = offset(n); + byte len = (byte)(name[pos] + 1); + byte [] label = new byte[len]; + System.arraycopy(name, pos, label, 0, len); + return label; +} + +/** + * Convert the nth label in a Name to a String + * @param n The label to be converted to a (printable) String. The first + * label is 0. + */ +public String +getLabelString(int n) { + int pos = offset(n); + return byteString(name, pos); +} + +/** + * Emit a Name in DNS wire format + * @param out The output stream containing the DNS message. + * @param c The compression context, or null of no compression is desired. + * @throws IllegalArgumentException The name is not absolute. + */ +public void +toWire(DNSOutput out, Compression c) { + if (!isAbsolute()) + throw new IllegalArgumentException("toWire() called on " + + "non-absolute name"); + + int labels = labels(); + for (int i = 0; i < labels - 1; i++) { + Name tname; + if (i == 0) + tname = this; + else + tname = new Name(this, i); + int pos = -1; + if (c != null) + pos = c.get(tname); + if (pos >= 0) { + pos |= (LABEL_MASK << 8); + out.writeU16(pos); + return; + } else { + if (c != null) + c.add(out.current(), tname); + int off = offset(i); + out.writeByteArray(name, off, name[off] + 1); + } + } + out.writeU8(0); +} + +/** + * Emit a Name in DNS wire format + * @throws IllegalArgumentException The name is not absolute. + */ +public byte [] +toWire() { + DNSOutput out = new DNSOutput(); + toWire(out, null); + return out.toByteArray(); +} + +/** + * Emit a Name in canonical DNS wire format (all lowercase) + * @param out The output stream to which the message is written. + */ +public void +toWireCanonical(DNSOutput out) { + byte [] b = toWireCanonical(); + out.writeByteArray(b); +} + +/** + * Emit a Name in canonical DNS wire format (all lowercase) + * @return The canonical form of the name. + */ +public byte [] +toWireCanonical() { + int labels = labels(); + if (labels == 0) + return (new byte[0]); + byte [] b = new byte[name.length - offset(0)]; + for (int i = 0, spos = offset(0), dpos = 0; i < labels; i++) { + int len = name[spos]; + if (len > MAXLABEL) + throw new IllegalStateException("invalid label"); + b[dpos++] = name[spos++]; + for (int j = 0; j < len; j++) + b[dpos++] = lowercase[(name[spos++] & 0xFF)]; + } + return b; +} + +/** + * Emit a Name in DNS wire format + * @param out The output stream containing the DNS message. + * @param c The compression context, or null of no compression is desired. + * @param canonical If true, emit the name in canonicalized form + * (all lowercase). + * @throws IllegalArgumentException The name is not absolute. + */ +public void +toWire(DNSOutput out, Compression c, boolean canonical) { + if (canonical) + toWireCanonical(out); + else + toWire(out, c); +} + +private final boolean +equals(byte [] b, int bpos) { + int labels = labels(); + for (int i = 0, pos = offset(0); i < labels; i++) { + if (name[pos] != b[bpos]) + return false; + int len = name[pos++]; + bpos++; + if (len > MAXLABEL) + throw new IllegalStateException("invalid label"); + for (int j = 0; j < len; j++) + if (lowercase[(name[pos++] & 0xFF)] != + lowercase[(b[bpos++] & 0xFF)]) + return false; + } + return true; +} + +/** + * Are these two Names equivalent? + */ +public boolean +equals(Object arg) { + if (arg == this) + return true; + if (arg == null || !(arg instanceof Name)) + return false; + Name d = (Name) arg; + if (d.hashcode == 0) + d.hashCode(); + if (hashcode == 0) + hashCode(); + if (d.hashcode != hashcode) + return false; + if (d.labels() != labels()) + return false; + return equals(d.name, d.offset(0)); +} + +/** + * Computes a hashcode based on the value + */ +public int +hashCode() { + if (hashcode != 0) + return (hashcode); + int code = 0; + for (int i = offset(0); i < name.length; i++) + code += ((code << 3) + lowercase[(name[i] & 0xFF)]); + hashcode = code; + return hashcode; +} + +/** + * Compares this Name to another Object. + * @param o The Object to be compared. + * @return The value 0 if the argument is a name equivalent to this name; + * a value less than 0 if the argument is less than this name in the canonical + * ordering, and a value greater than 0 if the argument is greater than this + * name in the canonical ordering. + * @throws ClassCastException if the argument is not a Name. + */ +public int +compareTo(Object o) { + Name arg = (Name) o; + + if (this == arg) + return (0); + + int labels = labels(); + int alabels = arg.labels(); + int compares = labels > alabels ? alabels : labels; + + for (int i = 1; i <= compares; i++) { + int start = offset(labels - i); + int astart = arg.offset(alabels - i); + int length = name[start]; + int alength = arg.name[astart]; + for (int j = 0; j < length && j < alength; j++) { + int n = lowercase[(name[j + start + 1]) & 0xFF] - + lowercase[(arg.name[j + astart + 1]) & 0xFF]; + if (n != 0) + return (n); + } + if (length != alength) + return (length - alength); + } + return (labels - alabels); +} + +} diff --git a/src/org/xbill/DNS/NameTooLongException.java b/src/org/xbill/DNS/NameTooLongException.java new file mode 100644 index 0000000..114be39 --- /dev/null +++ b/src/org/xbill/DNS/NameTooLongException.java @@ -0,0 +1,24 @@ +// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when a name is longer than the maximum length of a DNS + * name. + * + * @author Brian Wellington + */ + +public class NameTooLongException extends WireParseException { + +public +NameTooLongException() { + super(); +} + +public +NameTooLongException(String s) { + super(s); +} + +} diff --git a/src/org/xbill/DNS/OPTRecord.java b/src/org/xbill/DNS/OPTRecord.java new file mode 100644 index 0000000..47fef2f --- /dev/null +++ b/src/org/xbill/DNS/OPTRecord.java @@ -0,0 +1,191 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * Options - describes Extended DNS (EDNS) properties of a Message. + * No specific options are defined other than those specified in the + * header. An OPT should be generated by Resolver. + * + * EDNS is a method to extend the DNS protocol while providing backwards + * compatibility and not significantly changing the protocol. This + * implementation of EDNS is mostly complete at level 0. + * + * @see Message + * @see Resolver + * + * @author Brian Wellington + */ + +public class OPTRecord extends Record { + +private static final long serialVersionUID = -6254521894809367938L; + +private List options; + +OPTRecord() {} + +Record +getObject() { + return new OPTRecord(); +} + +/** + * Creates an OPT Record. This is normally called by SimpleResolver, but can + * also be called by a server. + * @param payloadSize The size of a packet that can be reassembled on the + * sending host. + * @param xrcode The value of the extended rcode field. This is the upper + * 16 bits of the full rcode. + * @param flags Additional message flags. + * @param version The EDNS version that this DNS implementation supports. + * This should be 0 for dnsjava. + * @param options The list of options that comprise the data field. There + * are currently no defined options. + * @see ExtendedFlags + */ +public +OPTRecord(int payloadSize, int xrcode, int version, int flags, List options) { + super(Name.root, Type.OPT, payloadSize, 0); + checkU16("payloadSize", payloadSize); + checkU8("xrcode", xrcode); + checkU8("version", version); + checkU16("flags", flags); + ttl = ((long)xrcode << 24) + ((long)version << 16) + flags; + if (options != null) { + this.options = new ArrayList(options); + } +} + +/** + * Creates an OPT Record with no data. This is normally called by + * SimpleResolver, but can also be called by a server. + * @param payloadSize The size of a packet that can be reassembled on the + * sending host. + * @param xrcode The value of the extended rcode field. This is the upper + * 16 bits of the full rcode. + * @param flags Additional message flags. + * @param version The EDNS version that this DNS implementation supports. + * This should be 0 for dnsjava. + * @see ExtendedFlags + */ +public +OPTRecord(int payloadSize, int xrcode, int version, int flags) { + this(payloadSize, xrcode, version, flags, null); +} + +/** + * Creates an OPT Record with no data. This is normally called by + * SimpleResolver, but can also be called by a server. + */ +public +OPTRecord(int payloadSize, int xrcode, int version) { + this(payloadSize, xrcode, version, 0, null); +} + +void +rrFromWire(DNSInput in) throws IOException { + if (in.remaining() > 0) + options = new ArrayList(); + while (in.remaining() > 0) { + EDNSOption option = EDNSOption.fromWire(in); + options.add(option); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + throw st.exception("no text format defined for OPT"); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + if (options != null) { + sb.append(options); + sb.append(" "); + } + sb.append(" ; payload "); + sb.append(getPayloadSize()); + sb.append(", xrcode "); + sb.append(getExtendedRcode()); + sb.append(", version "); + sb.append(getVersion()); + sb.append(", flags "); + sb.append(getFlags()); + return sb.toString(); +} + +/** Returns the maximum allowed payload size. */ +public int +getPayloadSize() { + return dclass; +} + +/** + * Returns the extended Rcode + * @see Rcode + */ +public int +getExtendedRcode() { + return (int)(ttl >>> 24); +} + +/** Returns the highest supported EDNS version */ +public int +getVersion() { + return (int)((ttl >>> 16) & 0xFF); +} + +/** Returns the EDNS flags */ +public int +getFlags() { + return (int)(ttl & 0xFFFF); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + if (options == null) + return; + Iterator it = options.iterator(); + while (it.hasNext()) { + EDNSOption option = (EDNSOption) it.next(); + option.toWire(out); + } +} + +/** + * Gets all options in the OPTRecord. This returns a list of EDNSOptions. + */ +public List +getOptions() { + if (options == null) + return Collections.EMPTY_LIST; + return Collections.unmodifiableList(options); +} + +/** + * Gets all options in the OPTRecord with a specific code. This returns a list + * of EDNSOptions. + */ +public List +getOptions(int code) { + if (options == null) + return Collections.EMPTY_LIST; + List list = Collections.EMPTY_LIST; + for (Iterator it = options.iterator(); it.hasNext(); ) { + EDNSOption opt = (EDNSOption) it.next(); + if (opt.getCode() == code) { + if (list == Collections.EMPTY_LIST) + list = new ArrayList(); + list.add(opt); + } + } + return list; +} + +} diff --git a/src/org/xbill/DNS/Opcode.java b/src/org/xbill/DNS/Opcode.java new file mode 100644 index 0000000..dadbca1 --- /dev/null +++ b/src/org/xbill/DNS/Opcode.java @@ -0,0 +1,60 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to DNS opcodes + * + * @author Brian Wellington + */ + +public final class Opcode { + +/** A standard query */ +public static final int QUERY = 0; + +/** An inverse query (deprecated) */ +public static final int IQUERY = 1; + +/** A server status request (not used) */ +public static final int STATUS = 2; + +/** + * A message from a primary to a secondary server to initiate a zone transfer + */ +public static final int NOTIFY = 4; + +/** A dynamic update message */ +public static final int UPDATE = 5; + +private static Mnemonic opcodes = new Mnemonic("DNS Opcode", + Mnemonic.CASE_UPPER); + +static { + opcodes.setMaximum(0xF); + opcodes.setPrefix("RESERVED"); + opcodes.setNumericAllowed(true); + + opcodes.add(QUERY, "QUERY"); + opcodes.add(IQUERY, "IQUERY"); + opcodes.add(STATUS, "STATUS"); + opcodes.add(NOTIFY, "NOTIFY"); + opcodes.add(UPDATE, "UPDATE"); +} + +private +Opcode() {} + +/** Converts a numeric Opcode into a String */ +public static String +string(int i) { + return opcodes.getText(i); +} + +/** Converts a String representation of an Opcode into its numeric value */ +public static int +value(String s) { + return opcodes.getValue(s); +} + +} diff --git a/src/org/xbill/DNS/Options.java b/src/org/xbill/DNS/Options.java new file mode 100644 index 0000000..2f1dae3 --- /dev/null +++ b/src/org/xbill/DNS/Options.java @@ -0,0 +1,123 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * Boolean options:<BR> + * bindttl - Print TTLs in BIND format<BR> + * multiline - Print records in multiline format<BR> + * noprintin - Don't print the class of a record if it's IN<BR> + * verbose - Turn on general debugging statements<BR> + * verbosemsg - Print all messages sent or received by SimpleResolver<BR> + * verbosecompression - Print messages related to name compression<BR> + * verbosesec - Print messages related to signature verification<BR> + * verbosecache - Print messages related to cache lookups<BR> + * <BR> + * Valued options:<BR> + * tsigfudge=n - Sets the default TSIG fudge value (in seconds)<BR> + * sig0validity=n - Sets the default SIG(0) validity period (in seconds)<BR> + * + * @author Brian Wellington + */ + +public final class Options { + +private static Map table; + +static { + try { + refresh(); + } + catch (SecurityException e) { + } +} + +private +Options() {} + +public static void +refresh() { + String s = System.getProperty("dnsjava.options"); + if (s != null) { + StringTokenizer st = new StringTokenizer(s, ","); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + int index = token.indexOf('='); + if (index == -1) + set(token); + else { + String option = token.substring(0, index); + String value = token.substring(index + 1); + set(option, value); + } + } + } +} + +/** Clears all defined options */ +public static void +clear() { + table = null; +} + +/** Sets an option to "true" */ +public static void +set(String option) { + if (table == null) + table = new HashMap(); + table.put(option.toLowerCase(), "true"); +} + +/** Sets an option to the the supplied value */ +public static void +set(String option, String value) { + if (table == null) + table = new HashMap(); + table.put(option.toLowerCase(), value.toLowerCase()); +} + +/** Removes an option */ +public static void +unset(String option) { + if (table == null) + return; + table.remove(option.toLowerCase()); +} + +/** Checks if an option is defined */ +public static boolean +check(String option) { + if (table == null) + return false; + return (table.get(option.toLowerCase()) != null); +} + +/** Returns the value of an option */ +public static String +value(String option) { + if (table == null) + return null; + return ((String)table.get(option.toLowerCase())); +} + +/** + * Returns the value of an option as an integer, or -1 if not defined. + */ +public static int +intValue(String option) { + String s = value(option); + if (s != null) { + try { + int val = Integer.parseInt(s); + if (val > 0) + return (val); + } + catch (NumberFormatException e) { + } + } + return (-1); +} + +} diff --git a/src/org/xbill/DNS/PTRRecord.java b/src/org/xbill/DNS/PTRRecord.java new file mode 100644 index 0000000..89be578 --- /dev/null +++ b/src/org/xbill/DNS/PTRRecord.java @@ -0,0 +1,38 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Pointer Record - maps a domain name representing an Internet Address to + * a hostname. + * + * @author Brian Wellington + */ + +public class PTRRecord extends SingleCompressedNameBase { + +private static final long serialVersionUID = -8321636610425434192L; + +PTRRecord() {} + +Record +getObject() { + return new PTRRecord(); +} + +/** + * Creates a new PTR Record with the given data + * @param target The name of the machine with this address + */ +public +PTRRecord(Name name, int dclass, long ttl, Name target) { + super(name, Type.PTR, dclass, ttl, target, "target"); +} + +/** Gets the target of the PTR Record */ +public Name +getTarget() { + return getSingleName(); +} + +} diff --git a/src/org/xbill/DNS/PXRecord.java b/src/org/xbill/DNS/PXRecord.java new file mode 100644 index 0000000..a407241 --- /dev/null +++ b/src/org/xbill/DNS/PXRecord.java @@ -0,0 +1,96 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * X.400 mail mapping record. + * + * @author Brian Wellington + */ + +public class PXRecord extends Record { + +private static final long serialVersionUID = 1811540008806660667L; + +private int preference; +private Name map822; +private Name mapX400; + +PXRecord() {} + +Record +getObject() { + return new PXRecord(); +} + +/** + * Creates an PX Record from the given data + * @param preference The preference of this mail address. + * @param map822 The RFC 822 component of the mail address. + * @param mapX400 The X.400 component of the mail address. + */ +public +PXRecord(Name name, int dclass, long ttl, int preference, + Name map822, Name mapX400) +{ + super(name, Type.PX, dclass, ttl); + + this.preference = checkU16("preference", preference); + this.map822 = checkName("map822", map822); + this.mapX400 = checkName("mapX400", mapX400); +} + +void +rrFromWire(DNSInput in) throws IOException { + preference = in.readU16(); + map822 = new Name(in); + mapX400 = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + preference = st.getUInt16(); + map822 = st.getName(origin); + mapX400 = st.getName(origin); +} + +/** Converts the PX Record to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(preference); + sb.append(" "); + sb.append(map822); + sb.append(" "); + sb.append(mapX400); + return sb.toString(); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(preference); + map822.toWire(out, null, canonical); + mapX400.toWire(out, null, canonical); +} + +/** Gets the preference of the route. */ +public int +getPreference() { + return preference; +} + +/** Gets the RFC 822 component of the mail address. */ +public Name +getMap822() { + return map822; +} + +/** Gets the X.400 component of the mail address. */ +public Name +getMapX400() { + return mapX400; +} + +} diff --git a/src/org/xbill/DNS/RPRecord.java b/src/org/xbill/DNS/RPRecord.java new file mode 100644 index 0000000..7aa066c --- /dev/null +++ b/src/org/xbill/DNS/RPRecord.java @@ -0,0 +1,82 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Responsible Person Record - lists the mail address of a responsible person + * and a domain where TXT records are available. + * + * @author Tom Scola <tscola@research.att.com> + * @author Brian Wellington + */ + +public class RPRecord extends Record { + +private static final long serialVersionUID = 8124584364211337460L; + +private Name mailbox; +private Name textDomain; + +RPRecord() {} + +Record +getObject() { + return new RPRecord(); +} + +/** + * Creates an RP Record from the given data + * @param mailbox The responsible person + * @param textDomain The address where TXT records can be found + */ +public +RPRecord(Name name, int dclass, long ttl, Name mailbox, Name textDomain) { + super(name, Type.RP, dclass, ttl); + + this.mailbox = checkName("mailbox", mailbox); + this.textDomain = checkName("textDomain", textDomain); +} + +void +rrFromWire(DNSInput in) throws IOException { + mailbox = new Name(in); + textDomain = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + mailbox = st.getName(origin); + textDomain = st.getName(origin); +} + +/** Converts the RP Record to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(mailbox); + sb.append(" "); + sb.append(textDomain); + return sb.toString(); +} + +/** Gets the mailbox address of the RP Record */ +public Name +getMailbox() { + return mailbox; +} + +/** Gets the text domain info of the RP Record */ +public Name +getTextDomain() { + return textDomain; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + mailbox.toWire(out, null, canonical); + textDomain.toWire(out, null, canonical); +} + +} diff --git a/src/org/xbill/DNS/RRSIGRecord.java b/src/org/xbill/DNS/RRSIGRecord.java new file mode 100644 index 0000000..c092839 --- /dev/null +++ b/src/org/xbill/DNS/RRSIGRecord.java @@ -0,0 +1,50 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * Recource Record Signature - An RRSIG provides the digital signature of an + * RRset, so that the data can be authenticated by a DNSSEC-capable resolver. + * The signature is generated by a key contained in a DNSKEY Record. + * @see RRset + * @see DNSSEC + * @see KEYRecord + * + * @author Brian Wellington + */ + +public class RRSIGRecord extends SIGBase { + +private static final long serialVersionUID = -2609150673537226317L; + +RRSIGRecord() {} + +Record +getObject() { + return new RRSIGRecord(); +} + +/** + * Creates an RRSIG Record from the given data + * @param covered The RRset type covered by this signature + * @param alg The cryptographic algorithm of the key that generated the + * signature + * @param origttl The original TTL of the RRset + * @param expire The time at which the signature expires + * @param timeSigned The time at which this signature was generated + * @param footprint The footprint/key id of the signing key. + * @param signer The owner of the signing key + * @param signature Binary data representing the signature + */ +public +RRSIGRecord(Name name, int dclass, long ttl, int covered, int alg, long origttl, + Date expire, Date timeSigned, int footprint, Name signer, + byte [] signature) +{ + super(name, Type.RRSIG, dclass, ttl, covered, alg, origttl, expire, + timeSigned, footprint, signer, signature); +} + +} diff --git a/src/org/xbill/DNS/RRset.java b/src/org/xbill/DNS/RRset.java new file mode 100644 index 0000000..fa1a6ad --- /dev/null +++ b/src/org/xbill/DNS/RRset.java @@ -0,0 +1,258 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.Serializable; +import java.util.*; + +/** + * A set of Records with the same name, type, and class. Also included + * are all RRSIG records signing the data records. + * @see Record + * @see RRSIGRecord + * + * @author Brian Wellington + */ + +public class RRset implements Serializable { + +private static final long serialVersionUID = -3270249290171239695L; + +/* + * rrs contains both normal and RRSIG records, with the RRSIG records + * at the end. + */ +private List rrs; +private short nsigs; +private short position; + +/** Creates an empty RRset */ +public +RRset() { + rrs = new ArrayList(1); + nsigs = 0; + position = 0; +} + +/** Creates an RRset and sets its contents to the specified record */ +public +RRset(Record record) { + this(); + safeAddRR(record); +} + +/** Creates an RRset with the contents of an existing RRset */ +public +RRset(RRset rrset) { + synchronized (rrset) { + rrs = (List) ((ArrayList)rrset.rrs).clone(); + nsigs = rrset.nsigs; + position = rrset.position; + } +} + +private void +safeAddRR(Record r) { + if (!(r instanceof RRSIGRecord)) { + if (nsigs == 0) + rrs.add(r); + else + rrs.add(rrs.size() - nsigs, r); + } else { + rrs.add(r); + nsigs++; + } +} + +/** Adds a Record to an RRset */ +public synchronized void +addRR(Record r) { + if (rrs.size() == 0) { + safeAddRR(r); + return; + } + Record first = first(); + if (!r.sameRRset(first)) + throw new IllegalArgumentException("record does not match " + + "rrset"); + + if (r.getTTL() != first.getTTL()) { + if (r.getTTL() > first.getTTL()) { + r = r.cloneRecord(); + r.setTTL(first.getTTL()); + } else { + for (int i = 0; i < rrs.size(); i++) { + Record tmp = (Record) rrs.get(i); + tmp = tmp.cloneRecord(); + tmp.setTTL(r.getTTL()); + rrs.set(i, tmp); + } + } + } + + if (!rrs.contains(r)) + safeAddRR(r); +} + +/** Deletes a Record from an RRset */ +public synchronized void +deleteRR(Record r) { + if (rrs.remove(r) && (r instanceof RRSIGRecord)) + nsigs--; +} + +/** Deletes all Records from an RRset */ +public synchronized void +clear() { + rrs.clear(); + position = 0; + nsigs = 0; +} + +private synchronized Iterator +iterator(boolean data, boolean cycle) { + int size, start, total; + + total = rrs.size(); + + if (data) + size = total - nsigs; + else + size = nsigs; + if (size == 0) + return Collections.EMPTY_LIST.iterator(); + + if (data) { + if (!cycle) + start = 0; + else { + if (position >= size) + position = 0; + start = position++; + } + } else { + start = total - nsigs; + } + + List list = new ArrayList(size); + if (data) { + list.addAll(rrs.subList(start, size)); + if (start != 0) + list.addAll(rrs.subList(0, start)); + } else { + list.addAll(rrs.subList(start, total)); + } + + return list.iterator(); +} + +/** + * Returns an Iterator listing all (data) records. + * @param cycle If true, cycle through the records so that each Iterator will + * start with a different record. + */ +public synchronized Iterator +rrs(boolean cycle) { + return iterator(true, cycle); +} + +/** + * Returns an Iterator listing all (data) records. This cycles through + * the records, so each Iterator will start with a different record. + */ +public synchronized Iterator +rrs() { + return iterator(true, true); +} + +/** Returns an Iterator listing all signature records */ +public synchronized Iterator +sigs() { + return iterator(false, false); +} + +/** Returns the number of (data) records */ +public synchronized int +size() { + return rrs.size() - nsigs; +} + +/** + * Returns the name of the records + * @see Name + */ +public Name +getName() { + return first().getName(); +} + +/** + * Returns the type of the records + * @see Type + */ +public int +getType() { + return first().getRRsetType(); +} + +/** + * Returns the class of the records + * @see DClass + */ +public int +getDClass() { + return first().getDClass(); +} + +/** Returns the ttl of the records */ +public synchronized long +getTTL() { + return first().getTTL(); +} + +/** + * Returns the first record + * @throws IllegalStateException if the rrset is empty + */ +public synchronized Record +first() { + if (rrs.size() == 0) + throw new IllegalStateException("rrset is empty"); + return (Record) rrs.get(0); +} + +private String +iteratorToString(Iterator it) { + StringBuffer sb = new StringBuffer(); + while (it.hasNext()) { + Record rr = (Record) it.next(); + sb.append("["); + sb.append(rr.rdataToString()); + sb.append("]"); + if (it.hasNext()) + sb.append(" "); + } + return sb.toString(); +} + +/** Converts the RRset to a String */ +public String +toString() { + if (rrs == null) + return ("{empty}"); + StringBuffer sb = new StringBuffer(); + sb.append("{ "); + sb.append(getName() + " "); + sb.append(getTTL() + " "); + sb.append(DClass.string(getDClass()) + " "); + sb.append(Type.string(getType()) + " "); + sb.append(iteratorToString(iterator(true, false))); + if (nsigs > 0) { + sb.append(" sigs: "); + sb.append(iteratorToString(iterator(false, false))); + } + sb.append(" }"); + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/RTRecord.java b/src/org/xbill/DNS/RTRecord.java new file mode 100644 index 0000000..549731e --- /dev/null +++ b/src/org/xbill/DNS/RTRecord.java @@ -0,0 +1,48 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Route Through Record - lists a route preference and intermediate host. + * + * @author Brian Wellington + */ + +public class RTRecord extends U16NameBase { + +private static final long serialVersionUID = -3206215651648278098L; + +RTRecord() {} + +Record +getObject() { + return new RTRecord(); +} + +/** + * Creates an RT Record from the given data + * @param preference The preference of the route. Smaller numbers indicate + * more preferred routes. + * @param intermediateHost The domain name of the host to use as a router. + */ +public +RTRecord(Name name, int dclass, long ttl, int preference, + Name intermediateHost) +{ + super(name, Type.RT, dclass, ttl, preference, "preference", + intermediateHost, "intermediateHost"); +} + +/** Gets the preference of the route. */ +public int +getPreference() { + return getU16Field(); +} + +/** Gets the host to use as a router. */ +public Name +getIntermediateHost() { + return getNameField(); +} + +} diff --git a/src/org/xbill/DNS/Rcode.java b/src/org/xbill/DNS/Rcode.java new file mode 100644 index 0000000..7f0dd1f --- /dev/null +++ b/src/org/xbill/DNS/Rcode.java @@ -0,0 +1,123 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to DNS rcodes (error values) + * + * @author Brian Wellington + */ + +public final class Rcode { + +private static Mnemonic rcodes = new Mnemonic("DNS Rcode", + Mnemonic.CASE_UPPER); + +private static Mnemonic tsigrcodes = new Mnemonic("TSIG rcode", + Mnemonic.CASE_UPPER); + +/** No error */ +public static final int NOERROR = 0; + +/** Format error */ +public static final int FORMERR = 1; + +/** Server failure */ +public static final int SERVFAIL = 2; + +/** The name does not exist */ +public static final int NXDOMAIN = 3; + +/** The operation requested is not implemented */ +public static final int NOTIMP = 4; + +/** Deprecated synonym for NOTIMP. */ +public static final int NOTIMPL = 4; + +/** The operation was refused by the server */ +public static final int REFUSED = 5; + +/** The name exists */ +public static final int YXDOMAIN = 6; + +/** The RRset (name, type) exists */ +public static final int YXRRSET = 7; + +/** The RRset (name, type) does not exist */ +public static final int NXRRSET = 8; + +/** The requestor is not authorized to perform this operation */ +public static final int NOTAUTH = 9; + +/** The zone specified is not a zone */ +public static final int NOTZONE = 10; + +/* EDNS extended rcodes */ +/** Unsupported EDNS level */ +public static final int BADVERS = 16; + +/* TSIG/TKEY only rcodes */ +/** The signature is invalid (TSIG/TKEY extended error) */ +public static final int BADSIG = 16; + +/** The key is invalid (TSIG/TKEY extended error) */ +public static final int BADKEY = 17; + +/** The time is out of range (TSIG/TKEY extended error) */ +public static final int BADTIME = 18; + +/** The mode is invalid (TKEY extended error) */ +public static final int BADMODE = 19; + +static { + rcodes.setMaximum(0xFFF); + rcodes.setPrefix("RESERVED"); + rcodes.setNumericAllowed(true); + + rcodes.add(NOERROR, "NOERROR"); + rcodes.add(FORMERR, "FORMERR"); + rcodes.add(SERVFAIL, "SERVFAIL"); + rcodes.add(NXDOMAIN, "NXDOMAIN"); + rcodes.add(NOTIMP, "NOTIMP"); + rcodes.addAlias(NOTIMP, "NOTIMPL"); + rcodes.add(REFUSED, "REFUSED"); + rcodes.add(YXDOMAIN, "YXDOMAIN"); + rcodes.add(YXRRSET, "YXRRSET"); + rcodes.add(NXRRSET, "NXRRSET"); + rcodes.add(NOTAUTH, "NOTAUTH"); + rcodes.add(NOTZONE, "NOTZONE"); + rcodes.add(BADVERS, "BADVERS"); + + tsigrcodes.setMaximum(0xFFFF); + tsigrcodes.setPrefix("RESERVED"); + tsigrcodes.setNumericAllowed(true); + tsigrcodes.addAll(rcodes); + + tsigrcodes.add(BADSIG, "BADSIG"); + tsigrcodes.add(BADKEY, "BADKEY"); + tsigrcodes.add(BADTIME, "BADTIME"); + tsigrcodes.add(BADMODE, "BADMODE"); +} + +private +Rcode() {} + +/** Converts a numeric Rcode into a String */ +public static String +string(int i) { + return rcodes.getText(i); +} + +/** Converts a numeric TSIG extended Rcode into a String */ +public static String +TSIGstring(int i) { + return tsigrcodes.getText(i); +} + +/** Converts a String representation of an Rcode into its numeric value */ +public static int +value(String s) { + return rcodes.getValue(s); +} + +} diff --git a/src/org/xbill/DNS/Record.java b/src/org/xbill/DNS/Record.java new file mode 100644 index 0000000..8da7015 --- /dev/null +++ b/src/org/xbill/DNS/Record.java @@ -0,0 +1,736 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.text.*; +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * A generic DNS resource record. The specific record types extend this class. + * A record contains a name, type, class, ttl, and rdata. + * + * @author Brian Wellington + */ + +public abstract class Record implements Cloneable, Comparable, Serializable { + +private static final long serialVersionUID = 2694906050116005466L; + +protected Name name; +protected int type, dclass; +protected long ttl; + +private static final DecimalFormat byteFormat = new DecimalFormat(); + +static { + byteFormat.setMinimumIntegerDigits(3); +} + +protected +Record() {} + +Record(Name name, int type, int dclass, long ttl) { + if (!name.isAbsolute()) + throw new RelativeNameException(name); + Type.check(type); + DClass.check(dclass); + TTL.check(ttl); + this.name = name; + this.type = type; + this.dclass = dclass; + this.ttl = ttl; +} + +/** + * Creates an empty record of the correct type; must be overriden + */ +abstract Record +getObject(); + +private static final Record +getEmptyRecord(Name name, int type, int dclass, long ttl, boolean hasData) { + Record proto, rec; + + if (hasData) { + proto = Type.getProto(type); + if (proto != null) + rec = proto.getObject(); + else + rec = new UNKRecord(); + } else + rec = new EmptyRecord(); + rec.name = name; + rec.type = type; + rec.dclass = dclass; + rec.ttl = ttl; + return rec; +} + +/** + * Converts the type-specific RR to wire format - must be overriden + */ +abstract void +rrFromWire(DNSInput in) throws IOException; + +private static Record +newRecord(Name name, int type, int dclass, long ttl, int length, DNSInput in) +throws IOException +{ + Record rec; + rec = getEmptyRecord(name, type, dclass, ttl, in != null); + if (in != null) { + if (in.remaining() < length) + throw new WireParseException("truncated record"); + in.setActive(length); + + rec.rrFromWire(in); + + if (in.remaining() > 0) + throw new WireParseException("invalid record length"); + in.clearActive(); + } + return rec; +} + +/** + * Creates a new record, with the given parameters. + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @param ttl The record's time to live. + * @param length The length of the record's data. + * @param data The rdata of the record, in uncompressed DNS wire format. Only + * the first length bytes are used. + */ +public static Record +newRecord(Name name, int type, int dclass, long ttl, int length, byte [] data) { + if (!name.isAbsolute()) + throw new RelativeNameException(name); + Type.check(type); + DClass.check(dclass); + TTL.check(ttl); + + DNSInput in; + if (data != null) + in = new DNSInput(data); + else + in = null; + try { + return newRecord(name, type, dclass, ttl, length, in); + } + catch (IOException e) { + return null; + } +} + +/** + * Creates a new record, with the given parameters. + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @param ttl The record's time to live. + * @param data The complete rdata of the record, in uncompressed DNS wire + * format. + */ +public static Record +newRecord(Name name, int type, int dclass, long ttl, byte [] data) { + return newRecord(name, type, dclass, ttl, data.length, data); +} + +/** + * Creates a new empty record, with the given parameters. + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @param ttl The record's time to live. + * @return An object of a subclass of Record + */ +public static Record +newRecord(Name name, int type, int dclass, long ttl) { + if (!name.isAbsolute()) + throw new RelativeNameException(name); + Type.check(type); + DClass.check(dclass); + TTL.check(ttl); + + return getEmptyRecord(name, type, dclass, ttl, false); +} + +/** + * Creates a new empty record, with the given parameters. This method is + * designed to create records that will be added to the QUERY section + * of a message. + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @return An object of a subclass of Record + */ +public static Record +newRecord(Name name, int type, int dclass) { + return newRecord(name, type, dclass, 0); +} + +static Record +fromWire(DNSInput in, int section, boolean isUpdate) throws IOException { + int type, dclass; + long ttl; + int length; + Name name; + Record rec; + + name = new Name(in); + type = in.readU16(); + dclass = in.readU16(); + + if (section == Section.QUESTION) + return newRecord(name, type, dclass); + + ttl = in.readU32(); + length = in.readU16(); + if (length == 0 && isUpdate && + (section == Section.PREREQ || section == Section.UPDATE)) + return newRecord(name, type, dclass, ttl); + rec = newRecord(name, type, dclass, ttl, length, in); + return rec; +} + +static Record +fromWire(DNSInput in, int section) throws IOException { + return fromWire(in, section, false); +} + +/** + * Builds a Record from DNS uncompressed wire format. + */ +public static Record +fromWire(byte [] b, int section) throws IOException { + return fromWire(new DNSInput(b), section, false); +} + +void +toWire(DNSOutput out, int section, Compression c) { + name.toWire(out, c); + out.writeU16(type); + out.writeU16(dclass); + if (section == Section.QUESTION) + return; + out.writeU32(ttl); + int lengthPosition = out.current(); + out.writeU16(0); /* until we know better */ + rrToWire(out, c, false); + int rrlength = out.current() - lengthPosition - 2; + out.writeU16At(rrlength, lengthPosition); +} + +/** + * Converts a Record into DNS uncompressed wire format. + */ +public byte [] +toWire(int section) { + DNSOutput out = new DNSOutput(); + toWire(out, section, null); + return out.toByteArray(); +} + +private void +toWireCanonical(DNSOutput out, boolean noTTL) { + name.toWireCanonical(out); + out.writeU16(type); + out.writeU16(dclass); + if (noTTL) { + out.writeU32(0); + } else { + out.writeU32(ttl); + } + int lengthPosition = out.current(); + out.writeU16(0); /* until we know better */ + rrToWire(out, null, true); + int rrlength = out.current() - lengthPosition - 2; + out.writeU16At(rrlength, lengthPosition); +} + +/* + * Converts a Record into canonical DNS uncompressed wire format (all names are + * converted to lowercase), optionally ignoring the TTL. + */ +private byte [] +toWireCanonical(boolean noTTL) { + DNSOutput out = new DNSOutput(); + toWireCanonical(out, noTTL); + return out.toByteArray(); +} + +/** + * Converts a Record into canonical DNS uncompressed wire format (all names are + * converted to lowercase). + */ +public byte [] +toWireCanonical() { + return toWireCanonical(false); +} + +/** + * Converts the rdata in a Record into canonical DNS uncompressed wire format + * (all names are converted to lowercase). + */ +public byte [] +rdataToWireCanonical() { + DNSOutput out = new DNSOutput(); + rrToWire(out, null, true); + return out.toByteArray(); +} + +/** + * Converts the type-specific RR to text format - must be overriden + */ +abstract String rrToString(); + +/** + * Converts the rdata portion of a Record into a String representation + */ +public String +rdataToString() { + return rrToString(); +} + +/** + * Converts a Record into a String representation + */ +public String +toString() { + StringBuffer sb = new StringBuffer(); + sb.append(name); + if (sb.length() < 8) + sb.append("\t"); + if (sb.length() < 16) + sb.append("\t"); + sb.append("\t"); + if (Options.check("BINDTTL")) + sb.append(TTL.format(ttl)); + else + sb.append(ttl); + sb.append("\t"); + if (dclass != DClass.IN || !Options.check("noPrintIN")) { + sb.append(DClass.string(dclass)); + sb.append("\t"); + } + sb.append(Type.string(type)); + String rdata = rrToString(); + if (!rdata.equals("")) { + sb.append("\t"); + sb.append(rdata); + } + return sb.toString(); +} + +/** + * Converts the text format of an RR to the internal format - must be overriden + */ +abstract void +rdataFromString(Tokenizer st, Name origin) throws IOException; + +/** + * Converts a String into a byte array. + */ +protected static byte [] +byteArrayFromString(String s) throws TextParseException { + byte [] array = s.getBytes(); + boolean escaped = false; + boolean hasEscapes = false; + + for (int i = 0; i < array.length; i++) { + if (array[i] == '\\') { + hasEscapes = true; + break; + } + } + if (!hasEscapes) { + if (array.length > 255) { + throw new TextParseException("text string too long"); + } + return array; + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + int digits = 0; + int intval = 0; + for (int i = 0; i < array.length; i++) { + byte b = array[i]; + if (escaped) { + if (b >= '0' && b <= '9' && digits < 3) { + digits++; + intval *= 10; + intval += (b - '0'); + if (intval > 255) + throw new TextParseException + ("bad escape"); + if (digits < 3) + continue; + b = (byte) intval; + } + else if (digits > 0 && digits < 3) + throw new TextParseException("bad escape"); + os.write(b); + escaped = false; + } + else if (array[i] == '\\') { + escaped = true; + digits = 0; + intval = 0; + } + else + os.write(array[i]); + } + if (digits > 0 && digits < 3) + throw new TextParseException("bad escape"); + array = os.toByteArray(); + if (array.length > 255) { + throw new TextParseException("text string too long"); + } + + return os.toByteArray(); +} + +/** + * Converts a byte array into a String. + */ +protected static String +byteArrayToString(byte [] array, boolean quote) { + StringBuffer sb = new StringBuffer(); + if (quote) + sb.append('"'); + for (int i = 0; i < array.length; i++) { + int b = array[i] & 0xFF; + if (b < 0x20 || b >= 0x7f) { + sb.append('\\'); + sb.append(byteFormat.format(b)); + } else if (b == '"' || b == '\\') { + sb.append('\\'); + sb.append((char)b); + } else + sb.append((char)b); + } + if (quote) + sb.append('"'); + return sb.toString(); +} + +/** + * Converts a byte array into the unknown RR format. + */ +protected static String +unknownToString(byte [] data) { + StringBuffer sb = new StringBuffer(); + sb.append("\\# "); + sb.append(data.length); + sb.append(" "); + sb.append(base16.toString(data)); + return sb.toString(); +} + +/** + * Builds a new Record from its textual representation + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @param ttl The record's time to live. + * @param st A tokenizer containing the textual representation of the rdata. + * @param origin The default origin to be appended to relative domain names. + * @return The new record + * @throws IOException The text format was invalid. + */ +public static Record +fromString(Name name, int type, int dclass, long ttl, Tokenizer st, Name origin) +throws IOException +{ + Record rec; + + if (!name.isAbsolute()) + throw new RelativeNameException(name); + Type.check(type); + DClass.check(dclass); + TTL.check(ttl); + + Tokenizer.Token t = st.get(); + if (t.type == Tokenizer.IDENTIFIER && t.value.equals("\\#")) { + int length = st.getUInt16(); + byte [] data = st.getHex(); + if (data == null) { + data = new byte[0]; + } + if (length != data.length) + throw st.exception("invalid unknown RR encoding: " + + "length mismatch"); + DNSInput in = new DNSInput(data); + return newRecord(name, type, dclass, ttl, length, in); + } + st.unget(); + rec = getEmptyRecord(name, type, dclass, ttl, true); + rec.rdataFromString(st, origin); + t = st.get(); + if (t.type != Tokenizer.EOL && t.type != Tokenizer.EOF) { + throw st.exception("unexpected tokens at end of record"); + } + return rec; +} + +/** + * Builds a new Record from its textual representation + * @param name The owner name of the record. + * @param type The record's type. + * @param dclass The record's class. + * @param ttl The record's time to live. + * @param s The textual representation of the rdata. + * @param origin The default origin to be appended to relative domain names. + * @return The new record + * @throws IOException The text format was invalid. + */ +public static Record +fromString(Name name, int type, int dclass, long ttl, String s, Name origin) +throws IOException +{ + return fromString(name, type, dclass, ttl, new Tokenizer(s), origin); +} + +/** + * Returns the record's name + * @see Name + */ +public Name +getName() { + return name; +} + +/** + * Returns the record's type + * @see Type + */ +public int +getType() { + return type; +} + +/** + * Returns the type of RRset that this record would belong to. For all types + * except RRSIG, this is equivalent to getType(). + * @return The type of record, if not RRSIG. If the type is RRSIG, + * the type covered is returned. + * @see Type + * @see RRset + * @see SIGRecord + */ +public int +getRRsetType() { + if (type == Type.RRSIG) { + RRSIGRecord sig = (RRSIGRecord) this; + return sig.getTypeCovered(); + } + return type; +} + +/** + * Returns the record's class + */ +public int +getDClass() { + return dclass; +} + +/** + * Returns the record's TTL + */ +public long +getTTL() { + return ttl; +} + +/** + * Converts the type-specific RR to wire format - must be overriden + */ +abstract void +rrToWire(DNSOutput out, Compression c, boolean canonical); + +/** + * Determines if two Records could be part of the same RRset. + * This compares the name, type, and class of the Records; the ttl and + * rdata are not compared. + */ +public boolean +sameRRset(Record rec) { + return (getRRsetType() == rec.getRRsetType() && + dclass == rec.dclass && + name.equals(rec.name)); +} + +/** + * Determines if two Records are identical. This compares the name, type, + * class, and rdata (with names canonicalized). The TTLs are not compared. + * @param arg The record to compare to + * @return true if the records are equal, false otherwise. + */ +public boolean +equals(Object arg) { + if (arg == null || !(arg instanceof Record)) + return false; + Record r = (Record) arg; + if (type != r.type || dclass != r.dclass || !name.equals(r.name)) + return false; + byte [] array1 = rdataToWireCanonical(); + byte [] array2 = r.rdataToWireCanonical(); + return Arrays.equals(array1, array2); +} + +/** + * Generates a hash code based on the Record's data. + */ +public int +hashCode() { + byte [] array = toWireCanonical(true); + int code = 0; + for (int i = 0; i < array.length; i++) + code += ((code << 3) + (array[i] & 0xFF)); + return code; +} + +Record +cloneRecord() { + try { + return (Record) clone(); + } + catch (CloneNotSupportedException e) { + throw new IllegalStateException(); + } +} + +/** + * Creates a new record identical to the current record, but with a different + * name. This is most useful for replacing the name of a wildcard record. + */ +public Record +withName(Name name) { + if (!name.isAbsolute()) + throw new RelativeNameException(name); + Record rec = cloneRecord(); + rec.name = name; + return rec; +} + +/** + * Creates a new record identical to the current record, but with a different + * class and ttl. This is most useful for dynamic update. + */ +Record +withDClass(int dclass, long ttl) { + Record rec = cloneRecord(); + rec.dclass = dclass; + rec.ttl = ttl; + return rec; +} + +/* Sets the TTL to the specified value. This is intentionally not public. */ +void +setTTL(long ttl) { + this.ttl = ttl; +} + +/** + * Compares this Record to another Object. + * @param o The Object to be compared. + * @return The value 0 if the argument is a record equivalent to this record; + * a value less than 0 if the argument is less than this record in the + * canonical ordering, and a value greater than 0 if the argument is greater + * than this record in the canonical ordering. The canonical ordering + * is defined to compare by name, class, type, and rdata. + * @throws ClassCastException if the argument is not a Record. + */ +public int +compareTo(Object o) { + Record arg = (Record) o; + + if (this == arg) + return (0); + + int n = name.compareTo(arg.name); + if (n != 0) + return (n); + n = dclass - arg.dclass; + if (n != 0) + return (n); + n = type - arg.type; + if (n != 0) + return (n); + byte [] rdata1 = rdataToWireCanonical(); + byte [] rdata2 = arg.rdataToWireCanonical(); + for (int i = 0; i < rdata1.length && i < rdata2.length; i++) { + n = (rdata1[i] & 0xFF) - (rdata2[i] & 0xFF); + if (n != 0) + return (n); + } + return (rdata1.length - rdata2.length); +} + +/** + * Returns the name for which additional data processing should be done + * for this record. This can be used both for building responses and + * parsing responses. + * @return The name to used for additional data processing, or null if this + * record type does not require additional data processing. + */ +public Name +getAdditionalName() { + return null; +} + +/* Checks that an int contains an unsigned 8 bit value */ +static int +checkU8(String field, int val) { + if (val < 0 || val > 0xFF) + throw new IllegalArgumentException("\"" + field + "\" " + val + + " must be an unsigned 8 " + + "bit value"); + return val; +} + +/* Checks that an int contains an unsigned 16 bit value */ +static int +checkU16(String field, int val) { + if (val < 0 || val > 0xFFFF) + throw new IllegalArgumentException("\"" + field + "\" " + val + + " must be an unsigned 16 " + + "bit value"); + return val; +} + +/* Checks that a long contains an unsigned 32 bit value */ +static long +checkU32(String field, long val) { + if (val < 0 || val > 0xFFFFFFFFL) + throw new IllegalArgumentException("\"" + field + "\" " + val + + " must be an unsigned 32 " + + "bit value"); + return val; +} + +/* Checks that a name is absolute */ +static Name +checkName(String field, Name name) { + if (!name.isAbsolute()) + throw new RelativeNameException(name); + return name; +} + +static byte [] +checkByteArrayLength(String field, byte [] array, int maxLength) { + if (array.length > 0xFFFF) + throw new IllegalArgumentException("\"" + field + "\" array " + + "must have no more than " + + maxLength + " elements"); + byte [] out = new byte[array.length]; + System.arraycopy(array, 0, out, 0, array.length); + return out; +} + +} diff --git a/src/org/xbill/DNS/RelativeNameException.java b/src/org/xbill/DNS/RelativeNameException.java new file mode 100644 index 0000000..869fd39 --- /dev/null +++ b/src/org/xbill/DNS/RelativeNameException.java @@ -0,0 +1,24 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when a relative name is passed as an argument to + * a method requiring an absolute name. + * + * @author Brian Wellington + */ + +public class RelativeNameException extends IllegalArgumentException { + +public +RelativeNameException(Name name) { + super("'" + name + "' is not an absolute name"); +} + +public +RelativeNameException(String s) { + super(s); +} + +} diff --git a/src/org/xbill/DNS/ResolveThread.java b/src/org/xbill/DNS/ResolveThread.java new file mode 100644 index 0000000..3087cdb --- /dev/null +++ b/src/org/xbill/DNS/ResolveThread.java @@ -0,0 +1,45 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * A special-purpose thread used by Resolvers (both SimpleResolver and + * ExtendedResolver) to perform asynchronous queries. + * + * @author Brian Wellington + */ + +class ResolveThread extends Thread { + +private Message query; +private Object id; +private ResolverListener listener; +private Resolver res; + +/** Creates a new ResolveThread */ +public +ResolveThread(Resolver res, Message query, Object id, + ResolverListener listener) +{ + this.res = res; + this.query = query; + this.id = id; + this.listener = listener; +} + + +/** + * Performs the query, and executes the callback. + */ +public void +run() { + try { + Message response = res.send(query); + listener.receiveMessage(id, response); + } + catch (Exception e) { + listener.handleException(id, e); + } +} + +} diff --git a/src/org/xbill/DNS/Resolver.java b/src/org/xbill/DNS/Resolver.java new file mode 100644 index 0000000..7d28d40 --- /dev/null +++ b/src/org/xbill/DNS/Resolver.java @@ -0,0 +1,95 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * Interface describing a resolver. + * + * @author Brian Wellington + */ + +public interface Resolver { + +/** + * Sets the port to communicate with on the server + * @param port The port to send messages to + */ +void setPort(int port); + +/** + * Sets whether TCP connections will be sent by default + * @param flag Indicates whether TCP connections are made + */ +void setTCP(boolean flag); + +/** + * Sets whether truncated responses will be ignored. If not, a truncated + * response over UDP will cause a retransmission over TCP. + * @param flag Indicates whether truncated responses should be ignored. + */ +void setIgnoreTruncation(boolean flag); + +/** + * Sets the EDNS version used on outgoing messages. + * @param level The EDNS level to use. 0 indicates EDNS0 and -1 indicates no + * EDNS. + * @throws IllegalArgumentException An invalid level was indicated. + */ +void setEDNS(int level); + +/** + * Sets the EDNS information on outgoing messages. + * @param level The EDNS level to use. 0 indicates EDNS0 and -1 indicates no + * EDNS. + * @param payloadSize The maximum DNS packet size that this host is capable + * of receiving over UDP. If 0 is specified, the default (1280) is used. + * @param flags EDNS extended flags to be set in the OPT record. + * @param options EDNS options to be set in the OPT record, specified as a + * List of OPTRecord.Option elements. + * @throws IllegalArgumentException An invalid field was specified. + * @see OPTRecord + */ +void setEDNS(int level, int payloadSize, int flags, List options); + +/** + * Specifies the TSIG key that messages will be signed with + * @param key The key + */ +void setTSIGKey(TSIG key); + +/** + * Sets the amount of time to wait for a response before giving up. + * @param secs The number of seconds to wait. + * @param msecs The number of milliseconds to wait. + */ +void setTimeout(int secs, int msecs); + +/** + * Sets the amount of time to wait for a response before giving up. + * @param secs The number of seconds to wait. + */ +void setTimeout(int secs); + +/** + * Sends a message and waits for a response. + * @param query The query to send. + * @return The response + * @throws IOException An error occurred while sending or receiving. + */ +Message send(Message query) throws IOException; + +/** + * Asynchronously sends a message registering a listener to receive a callback + * on success or exception. Multiple asynchronous lookups can be performed + * in parallel. Since the callback may be invoked before the function returns, + * external synchronization is necessary. + * @param query The query to send + * @param listener The object containing the callbacks. + * @return An identifier, which is also a parameter in the callback + */ +Object sendAsync(final Message query, final ResolverListener listener); + +} diff --git a/src/org/xbill/DNS/ResolverConfig.java b/src/org/xbill/DNS/ResolverConfig.java new file mode 100644 index 0000000..7b09daf --- /dev/null +++ b/src/org/xbill/DNS/ResolverConfig.java @@ -0,0 +1,509 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.lang.reflect.*; +import java.util.*; + +/** + * A class that tries to locate name servers and the search path to + * be appended to unqualified names. + * + * The following are attempted, in order, until one succeeds. + * <UL> + * <LI>The properties 'dns.server' and 'dns.search' (comma delimited lists) + * are checked. The servers can either be IP addresses or hostnames + * (which are resolved using Java's built in DNS support). + * <LI>The sun.net.dns.ResolverConfiguration class is queried. + * <LI>On Unix, /etc/resolv.conf is parsed. + * <LI>On Windows, ipconfig/winipcfg is called and its output parsed. This + * may fail for non-English versions on Windows. + * <LI>"localhost" is used as the nameserver, and the search path is empty. + * </UL> + * + * These routines will be called internally when creating Resolvers/Lookups + * without explicitly specifying server names, and can also be called + * directly if desired. + * + * @author Brian Wellington + * @author <a href="mailto:yannick@meudal.net">Yannick Meudal</a> + * @author <a href="mailto:arnt@gulbrandsen.priv.no">Arnt Gulbrandsen</a> + */ + +public class ResolverConfig { + +private String [] servers = null; +private Name [] searchlist = null; +private int ndots = -1; + +private static ResolverConfig currentConfig; + +static { + refresh(); +} + +public +ResolverConfig() { + if (findProperty()) + return; + if (findSunJVM()) + return; + if (servers == null || searchlist == null) { + String OS = System.getProperty("os.name"); + String vendor = System.getProperty("java.vendor"); + if (OS.indexOf("Windows") != -1) { + if (OS.indexOf("95") != -1 || + OS.indexOf("98") != -1 || + OS.indexOf("ME") != -1) + find95(); + else + findNT(); + } else if (OS.indexOf("NetWare") != -1) { + findNetware(); + } else if (vendor.indexOf("Android") != -1) { + findAndroid(); + } else { + findUnix(); + } + } +} + +private void +addServer(String server, List list) { + if (list.contains(server)) + return; + if (Options.check("verbose")) + System.out.println("adding server " + server); + list.add(server); +} + +private void +addSearch(String search, List list) { + Name name; + if (Options.check("verbose")) + System.out.println("adding search " + search); + try { + name = Name.fromString(search, Name.root); + } + catch (TextParseException e) { + return; + } + if (list.contains(name)) + return; + list.add(name); +} + +private int +parseNdots(String token) { + token = token.substring(6); + try { + int ndots = Integer.parseInt(token); + if (ndots >= 0) { + if (Options.check("verbose")) + System.out.println("setting ndots " + token); + return ndots; + } + } + catch (NumberFormatException e) { + } + return -1; +} + +private void +configureFromLists(List lserver, List lsearch) { + if (servers == null && lserver.size() > 0) + servers = (String []) lserver.toArray(new String[0]); + if (searchlist == null && lsearch.size() > 0) + searchlist = (Name []) lsearch.toArray(new Name[0]); +} + +private void +configureNdots(int lndots) { + if (ndots < 0 && lndots > 0) + ndots = lndots; +} + +/** + * Looks in the system properties to find servers and a search path. + * Servers are defined by dns.server=server1,server2... + * The search path is defined by dns.search=domain1,domain2... + */ +private boolean +findProperty() { + String prop; + List lserver = new ArrayList(0); + List lsearch = new ArrayList(0); + StringTokenizer st; + + prop = System.getProperty("dns.server"); + if (prop != null) { + st = new StringTokenizer(prop, ","); + while (st.hasMoreTokens()) + addServer(st.nextToken(), lserver); + } + + prop = System.getProperty("dns.search"); + if (prop != null) { + st = new StringTokenizer(prop, ","); + while (st.hasMoreTokens()) + addSearch(st.nextToken(), lsearch); + } + configureFromLists(lserver, lsearch); + return (servers != null && searchlist != null); +} + +/** + * Uses the undocumented Sun DNS implementation to determine the configuration. + * This doesn't work or even compile with all JVMs (gcj, for example). + */ +private boolean +findSunJVM() { + List lserver = new ArrayList(0); + List lserver_tmp; + List lsearch = new ArrayList(0); + List lsearch_tmp; + + try { + Class [] noClasses = new Class[0]; + Object [] noObjects = new Object[0]; + String resConfName = "sun.net.dns.ResolverConfiguration"; + Class resConfClass = Class.forName(resConfName); + Object resConf; + + // ResolverConfiguration resConf = ResolverConfiguration.open(); + Method open = resConfClass.getDeclaredMethod("open", noClasses); + resConf = open.invoke(null, noObjects); + + // lserver_tmp = resConf.nameservers(); + Method nameservers = resConfClass.getMethod("nameservers", + noClasses); + lserver_tmp = (List) nameservers.invoke(resConf, noObjects); + + // lsearch_tmp = resConf.searchlist(); + Method searchlist = resConfClass.getMethod("searchlist", + noClasses); + lsearch_tmp = (List) searchlist.invoke(resConf, noObjects); + } + catch (Exception e) { + return false; + } + + if (lserver_tmp.size() == 0) + return false; + + if (lserver_tmp.size() > 0) { + Iterator it = lserver_tmp.iterator(); + while (it.hasNext()) + addServer((String) it.next(), lserver); + } + + if (lsearch_tmp.size() > 0) { + Iterator it = lsearch_tmp.iterator(); + while (it.hasNext()) + addSearch((String) it.next(), lsearch); + } + configureFromLists(lserver, lsearch); + return true; +} + +/** + * Looks in /etc/resolv.conf to find servers and a search path. + * "nameserver" lines specify servers. "domain" and "search" lines + * define the search path. + */ +private void +findResolvConf(String file) { + InputStream in = null; + try { + in = new FileInputStream(file); + } + catch (FileNotFoundException e) { + return; + } + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + List lserver = new ArrayList(0); + List lsearch = new ArrayList(0); + int lndots = -1; + try { + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith("nameserver")) { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); /* skip nameserver */ + addServer(st.nextToken(), lserver); + } + else if (line.startsWith("domain")) { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); /* skip domain */ + if (!st.hasMoreTokens()) + continue; + if (lsearch.isEmpty()) + addSearch(st.nextToken(), lsearch); + } + else if (line.startsWith("search")) { + if (!lsearch.isEmpty()) + lsearch.clear(); + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); /* skip search */ + while (st.hasMoreTokens()) + addSearch(st.nextToken(), lsearch); + } + else if(line.startsWith("options")) { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); /* skip options */ + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (token.startsWith("ndots:")) { + lndots = parseNdots(token); + } + } + } + } + br.close(); + } + catch (IOException e) { + } + + configureFromLists(lserver, lsearch); + configureNdots(lndots); +} + +private void +findUnix() { + findResolvConf("/etc/resolv.conf"); +} + +private void +findNetware() { + findResolvConf("sys:/etc/resolv.cfg"); +} + +/** + * Parses the output of winipcfg or ipconfig. + */ +private void +findWin(InputStream in, Locale locale) { + String packageName = ResolverConfig.class.getPackage().getName(); + String resPackageName = packageName + ".windows.DNSServer"; + ResourceBundle res; + if (locale != null) + res = ResourceBundle.getBundle(resPackageName, locale); + else + res = ResourceBundle.getBundle(resPackageName); + + String host_name = res.getString("host_name"); + String primary_dns_suffix = res.getString("primary_dns_suffix"); + String dns_suffix = res.getString("dns_suffix"); + String dns_servers = res.getString("dns_servers"); + + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + try { + List lserver = new ArrayList(); + List lsearch = new ArrayList(); + String line = null; + boolean readingServers = false; + boolean readingSearches = false; + while ((line = br.readLine()) != null) { + StringTokenizer st = new StringTokenizer(line); + if (!st.hasMoreTokens()) { + readingServers = false; + readingSearches = false; + continue; + } + String s = st.nextToken(); + if (line.indexOf(":") != -1) { + readingServers = false; + readingSearches = false; + } + + if (line.indexOf(host_name) != -1) { + while (st.hasMoreTokens()) + s = st.nextToken(); + Name name; + try { + name = Name.fromString(s, null); + } + catch (TextParseException e) { + continue; + } + if (name.labels() == 1) + continue; + addSearch(s, lsearch); + } else if (line.indexOf(primary_dns_suffix) != -1) { + while (st.hasMoreTokens()) + s = st.nextToken(); + if (s.equals(":")) + continue; + addSearch(s, lsearch); + readingSearches = true; + } else if (readingSearches || + line.indexOf(dns_suffix) != -1) + { + while (st.hasMoreTokens()) + s = st.nextToken(); + if (s.equals(":")) + continue; + addSearch(s, lsearch); + readingSearches = true; + } else if (readingServers || + line.indexOf(dns_servers) != -1) + { + while (st.hasMoreTokens()) + s = st.nextToken(); + if (s.equals(":")) + continue; + addServer(s, lserver); + readingServers = true; + } + } + + configureFromLists(lserver, lsearch); + } + catch (IOException e) { + } + return; +} + +private void +findWin(InputStream in) { + String property = "org.xbill.DNS.windows.parse.buffer"; + final int defaultBufSize = 8 * 1024; + int bufSize = Integer.getInteger(property, defaultBufSize).intValue(); + BufferedInputStream b = new BufferedInputStream(in, bufSize); + b.mark(bufSize); + findWin(b, null); + if (servers == null) { + try { + b.reset(); + } + catch (IOException e) { + return; + } + findWin(b, new Locale("", "")); + } +} + +/** + * Calls winipcfg and parses the result to find servers and a search path. + */ +private void +find95() { + String s = "winipcfg.out"; + try { + Process p; + p = Runtime.getRuntime().exec("winipcfg /all /batch " + s); + p.waitFor(); + File f = new File(s); + findWin(new FileInputStream(f)); + new File(s).delete(); + } + catch (Exception e) { + return; + } +} + +/** + * Calls ipconfig and parses the result to find servers and a search path. + */ +private void +findNT() { + try { + Process p; + p = Runtime.getRuntime().exec("ipconfig /all"); + findWin(p.getInputStream()); + p.destroy(); + } + catch (Exception e) { + return; + } +} + +/** + * Parses the output of getprop, which is the only way to get DNS + * info on Android. getprop might disappear in future releases, so + * this code comes with a use-by date. + */ +private void +findAndroid() { + // This originally looked for all lines containing .dns; but + // http://code.google.com/p/android/issues/detail?id=2207#c73 + // indicates that net.dns* should always be the active nameservers, so + // we use those. + String re1 = "^\\d+(\\.\\d+){3}$"; + String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$"; + try { + ArrayList lserver = new ArrayList(); + ArrayList lsearch = new ArrayList(); + String line; + Process p = Runtime.getRuntime().exec("getprop"); + InputStream in = p.getInputStream(); + InputStreamReader isr = new InputStreamReader(in); + BufferedReader br = new BufferedReader(isr); + while ((line = br.readLine()) != null ) { + StringTokenizer t = new StringTokenizer(line, ":"); + String name = t.nextToken(); + if (name.indexOf( "net.dns" ) > -1) { + String v = t.nextToken(); + v = v.replaceAll("[ \\[\\]]", ""); + if ((v.matches(re1) || v.matches(re2)) && + !lserver.contains(v)) + lserver.add(v); + } + } + configureFromLists(lserver, lsearch); + } catch ( Exception e ) { + // ignore resolutely + } +} + +/** Returns all located servers */ +public String [] +servers() { + return servers; +} + +/** Returns the first located server */ +public String +server() { + if (servers == null) + return null; + return servers[0]; +} + +/** Returns all entries in the located search path */ +public Name [] +searchPath() { + return searchlist; +} + +/** + * Returns the located ndots value, or the default (1) if not configured. + * Note that ndots can only be configured in a resolv.conf file, and will only + * take effect if ResolverConfig uses resolv.conf directly (that is, if the + * JVM does not include the sun.net.dns.ResolverConfiguration class). + */ +public int +ndots() { + if (ndots < 0) + return 1; + return ndots; +} + +/** Gets the current configuration */ +public static synchronized ResolverConfig +getCurrentConfig() { + return currentConfig; +} + +/** Gets the current configuration */ +public static void +refresh() { + ResolverConfig newConfig = new ResolverConfig(); + synchronized (ResolverConfig.class) { + currentConfig = newConfig; + } +} + +} diff --git a/src/org/xbill/DNS/ResolverListener.java b/src/org/xbill/DNS/ResolverListener.java new file mode 100644 index 0000000..accf82c --- /dev/null +++ b/src/org/xbill/DNS/ResolverListener.java @@ -0,0 +1,30 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.EventListener; + +/** + * An interface to the asynchronous resolver. + * @see Resolver + * + * @author Brian Wellington + */ + +public interface ResolverListener extends EventListener { + +/** + * The callback used by an asynchronous resolver + * @param id The identifier returned by Resolver.sendAsync() + * @param m The response message as returned by the Resolver + */ +void receiveMessage(Object id, Message m); + +/** + * The callback used by an asynchronous resolver when an exception is thrown + * @param id The identifier returned by Resolver.sendAsync() + * @param e The thrown exception + */ +void handleException(Object id, Exception e); + +} diff --git a/src/org/xbill/DNS/ReverseMap.java b/src/org/xbill/DNS/ReverseMap.java new file mode 100644 index 0000000..f4293de --- /dev/null +++ b/src/org/xbill/DNS/ReverseMap.java @@ -0,0 +1,130 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.net.*; + +/** + * A set functions designed to deal with DNS names used in reverse mappings. + * For the IPv4 address a.b.c.d, the reverse map name is d.c.b.a.in-addr.arpa. + * For an IPv6 address, the reverse map name is ...ip6.arpa. + * + * @author Brian Wellington + */ + +public final class ReverseMap { + +private static Name inaddr4 = Name.fromConstantString("in-addr.arpa."); +private static Name inaddr6 = Name.fromConstantString("ip6.arpa."); + +/* Otherwise the class could be instantiated */ +private +ReverseMap() {} + +/** + * Creates a reverse map name corresponding to an address contained in + * an array of 4 bytes (for an IPv4 address) or 16 bytes (for an IPv6 address). + * @param addr The address from which to build a name. + * @return The name corresponding to the address in the reverse map. + */ +public static Name +fromAddress(byte [] addr) { + if (addr.length != 4 && addr.length != 16) + throw new IllegalArgumentException("array must contain " + + "4 or 16 elements"); + + StringBuffer sb = new StringBuffer(); + if (addr.length == 4) { + for (int i = addr.length - 1; i >= 0; i--) { + sb.append(addr[i] & 0xFF); + if (i > 0) + sb.append("."); + } + } else { + int [] nibbles = new int[2]; + for (int i = addr.length - 1; i >= 0; i--) { + nibbles[0] = (addr[i] & 0xFF) >> 4; + nibbles[1] = (addr[i] & 0xFF) & 0xF; + for (int j = nibbles.length - 1; j >= 0; j--) { + sb.append(Integer.toHexString(nibbles[j])); + if (i > 0 || j > 0) + sb.append("."); + } + } + } + + try { + if (addr.length == 4) + return Name.fromString(sb.toString(), inaddr4); + else + return Name.fromString(sb.toString(), inaddr6); + } + catch (TextParseException e) { + throw new IllegalStateException("name cannot be invalid"); + } +} + +/** + * Creates a reverse map name corresponding to an address contained in + * an array of 4 integers between 0 and 255 (for an IPv4 address) or 16 + * integers between 0 and 255 (for an IPv6 address). + * @param addr The address from which to build a name. + * @return The name corresponding to the address in the reverse map. + */ +public static Name +fromAddress(int [] addr) { + byte [] bytes = new byte[addr.length]; + for (int i = 0; i < addr.length; i++) { + if (addr[i] < 0 || addr[i] > 0xFF) + throw new IllegalArgumentException("array must " + + "contain values " + + "between 0 and 255"); + bytes[i] = (byte) addr[i]; + } + return fromAddress(bytes); +} + +/** + * Creates a reverse map name corresponding to an address contained in + * an InetAddress. + * @param addr The address from which to build a name. + * @return The name corresponding to the address in the reverse map. + */ +public static Name +fromAddress(InetAddress addr) { + return fromAddress(addr.getAddress()); +} + +/** + * Creates a reverse map name corresponding to an address contained in + * a String. + * @param addr The address from which to build a name. + * @return The name corresponding to the address in the reverse map. + * @throws UnknownHostException The string does not contain a valid address. + */ +public static Name +fromAddress(String addr, int family) throws UnknownHostException { + byte [] array = Address.toByteArray(addr, family); + if (array == null) + throw new UnknownHostException("Invalid IP address"); + return fromAddress(array); +} + +/** + * Creates a reverse map name corresponding to an address contained in + * a String. + * @param addr The address from which to build a name. + * @return The name corresponding to the address in the reverse map. + * @throws UnknownHostException The string does not contain a valid address. + */ +public static Name +fromAddress(String addr) throws UnknownHostException { + byte [] array = Address.toByteArray(addr, Address.IPv4); + if (array == null) + array = Address.toByteArray(addr, Address.IPv6); + if (array == null) + throw new UnknownHostException("Invalid IP address"); + return fromAddress(array); +} + +} diff --git a/src/org/xbill/DNS/SIG0.java b/src/org/xbill/DNS/SIG0.java new file mode 100644 index 0000000..5a00e72 --- /dev/null +++ b/src/org/xbill/DNS/SIG0.java @@ -0,0 +1,79 @@ +// Copyright (c) 2001-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.security.PrivateKey; +import java.util.Date; + +/** + * Creates SIG(0) transaction signatures. + * + * @author Pasi Eronen + * @author Brian Wellington + */ + +public class SIG0 { + +/** + * The default validity period for outgoing SIG(0) signed messages. + * Can be overriden by the sig0validity option. + */ +private static final short VALIDITY = 300; + +private +SIG0() { } + +/** + * Sign a message with SIG(0). The DNS key and private key must refer to the + * same underlying cryptographic key. + * @param message The message to be signed + * @param key The DNSKEY record to use as part of signing + * @param privkey The PrivateKey to use when signing + * @param previous If this message is a response, the SIG(0) from the query + */ +public static void +signMessage(Message message, KEYRecord key, PrivateKey privkey, + SIGRecord previous) throws DNSSEC.DNSSECException +{ + + int validity = Options.intValue("sig0validity"); + if (validity < 0) + validity = VALIDITY; + + long now = System.currentTimeMillis(); + Date timeSigned = new Date(now); + Date timeExpires = new Date(now + validity * 1000); + + SIGRecord sig = DNSSEC.signMessage(message, previous, key, privkey, + timeSigned, timeExpires); + + message.addRecord(sig, Section.ADDITIONAL); +} + +/** + * Verify a message using SIG(0). + * @param message The message to be signed + * @param b An array containing the message in unparsed form. This is + * necessary since SIG(0) signs the message in wire format, and we can't + * recreate the exact wire format (with the same name compression). + * @param key The KEY record to verify the signature with. + * @param previous If this message is a response, the SIG(0) from the query + */ +public static void +verifyMessage(Message message, byte [] b, KEYRecord key, SIGRecord previous) + throws DNSSEC.DNSSECException +{ + SIGRecord sig = null; + Record [] additional = message.getSectionArray(Section.ADDITIONAL); + for (int i = 0; i < additional.length; i++) { + if (additional[i].getType() != Type.SIG) + continue; + if (((SIGRecord) additional[i]).getTypeCovered() != 0) + continue; + sig = (SIGRecord) additional[i]; + break; + } + DNSSEC.verifyMessage(message, b, sig, previous, key); +} + +} diff --git a/src/org/xbill/DNS/SIGBase.java b/src/org/xbill/DNS/SIGBase.java new file mode 100644 index 0000000..6e5f12d --- /dev/null +++ b/src/org/xbill/DNS/SIGBase.java @@ -0,0 +1,193 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * The base class for SIG/RRSIG records, which have identical formats + * + * @author Brian Wellington + */ + +abstract class SIGBase extends Record { + +private static final long serialVersionUID = -3738444391533812369L; + +protected int covered; +protected int alg, labels; +protected long origttl; +protected Date expire, timeSigned; +protected int footprint; +protected Name signer; +protected byte [] signature; + +protected +SIGBase() {} + +public +SIGBase(Name name, int type, int dclass, long ttl, int covered, int alg, + long origttl, Date expire, Date timeSigned, int footprint, Name signer, + byte [] signature) +{ + super(name, type, dclass, ttl); + Type.check(covered); + TTL.check(origttl); + this.covered = covered; + this.alg = checkU8("alg", alg); + this.labels = name.labels() - 1; + if (name.isWild()) + this.labels--; + this.origttl = origttl; + this.expire = expire; + this.timeSigned = timeSigned; + this.footprint = checkU16("footprint", footprint); + this.signer = checkName("signer", signer); + this.signature = signature; +} + +void +rrFromWire(DNSInput in) throws IOException { + covered = in.readU16(); + alg = in.readU8(); + labels = in.readU8(); + origttl = in.readU32(); + expire = new Date(1000 * in.readU32()); + timeSigned = new Date(1000 * in.readU32()); + footprint = in.readU16(); + signer = new Name(in); + signature = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String typeString = st.getString(); + covered = Type.value(typeString); + if (covered < 0) + throw st.exception("Invalid type: " + typeString); + String algString = st.getString(); + alg = DNSSEC.Algorithm.value(algString); + if (alg < 0) + throw st.exception("Invalid algorithm: " + algString); + labels = st.getUInt8(); + origttl = st.getTTL(); + expire = FormattedTime.parse(st.getString()); + timeSigned = FormattedTime.parse(st.getString()); + footprint = st.getUInt16(); + signer = st.getName(origin); + signature = st.getBase64(); +} + +/** Converts the RRSIG/SIG Record to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append (Type.string(covered)); + sb.append (" "); + sb.append (alg); + sb.append (" "); + sb.append (labels); + sb.append (" "); + sb.append (origttl); + sb.append (" "); + if (Options.check("multiline")) + sb.append ("(\n\t"); + sb.append (FormattedTime.format(expire)); + sb.append (" "); + sb.append (FormattedTime.format(timeSigned)); + sb.append (" "); + sb.append (footprint); + sb.append (" "); + sb.append (signer); + if (Options.check("multiline")) { + sb.append("\n"); + sb.append(base64.formatString(signature, 64, "\t", + true)); + } else { + sb.append (" "); + sb.append(base64.toString(signature)); + } + return sb.toString(); +} + +/** Returns the RRset type covered by this signature */ +public int +getTypeCovered() { + return covered; +} + +/** + * Returns the cryptographic algorithm of the key that generated the signature + */ +public int +getAlgorithm() { + return alg; +} + +/** + * Returns the number of labels in the signed domain name. This may be + * different than the record's domain name if the record is a wildcard + * record. + */ +public int +getLabels() { + return labels; +} + +/** Returns the original TTL of the RRset */ +public long +getOrigTTL() { + return origttl; +} + +/** Returns the time at which the signature expires */ +public Date +getExpire() { + return expire; +} + +/** Returns the time at which this signature was generated */ +public Date +getTimeSigned() { + return timeSigned; +} + +/** Returns The footprint/key id of the signing key. */ +public int +getFootprint() { + return footprint; +} + +/** Returns the owner of the signing key */ +public Name +getSigner() { + return signer; +} + +/** Returns the binary data representing the signature */ +public byte [] +getSignature() { + return signature; +} + +void +setSignature(byte [] signature) { + this.signature = signature; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(covered); + out.writeU8(alg); + out.writeU8(labels); + out.writeU32(origttl); + out.writeU32(expire.getTime() / 1000); + out.writeU32(timeSigned.getTime() / 1000); + out.writeU16(footprint); + signer.toWire(out, null, canonical); + out.writeByteArray(signature); +} + +} diff --git a/src/org/xbill/DNS/SIGRecord.java b/src/org/xbill/DNS/SIGRecord.java new file mode 100644 index 0000000..8b6f58d --- /dev/null +++ b/src/org/xbill/DNS/SIGRecord.java @@ -0,0 +1,50 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * Signature - A SIG provides the digital signature of an RRset, so that + * the data can be authenticated by a DNSSEC-capable resolver. The + * signature is usually generated by a key contained in a KEYRecord + * @see RRset + * @see DNSSEC + * @see KEYRecord + * + * @author Brian Wellington + */ + +public class SIGRecord extends SIGBase { + +private static final long serialVersionUID = 4963556060953589058L; + +SIGRecord() {} + +Record +getObject() { + return new SIGRecord(); +} + +/** + * Creates an SIG Record from the given data + * @param covered The RRset type covered by this signature + * @param alg The cryptographic algorithm of the key that generated the + * signature + * @param origttl The original TTL of the RRset + * @param expire The time at which the signature expires + * @param timeSigned The time at which this signature was generated + * @param footprint The footprint/key id of the signing key. + * @param signer The owner of the signing key + * @param signature Binary data representing the signature + */ +public +SIGRecord(Name name, int dclass, long ttl, int covered, int alg, long origttl, + Date expire, Date timeSigned, int footprint, Name signer, + byte [] signature) +{ + super(name, Type.SIG, dclass, ttl, covered, alg, origttl, expire, + timeSigned, footprint, signer, signature); +} + +} diff --git a/src/org/xbill/DNS/SOARecord.java b/src/org/xbill/DNS/SOARecord.java new file mode 100644 index 0000000..7f27077 --- /dev/null +++ b/src/org/xbill/DNS/SOARecord.java @@ -0,0 +1,162 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Start of Authority - describes properties of a zone. + * + * @author Brian Wellington + */ + +public class SOARecord extends Record { + +private static final long serialVersionUID = 1049740098229303931L; + +private Name host, admin; +private long serial, refresh, retry, expire, minimum; + +SOARecord() {} + +Record +getObject() { + return new SOARecord(); +} + +/** + * Creates an SOA Record from the given data + * @param host The primary name server for the zone + * @param admin The zone administrator's address + * @param serial The zone's serial number + * @param refresh The amount of time until a secondary checks for a new serial + * number + * @param retry The amount of time between a secondary's checks for a new + * serial number + * @param expire The amount of time until a secondary expires a zone + * @param minimum The minimum TTL for records in the zone +*/ +public +SOARecord(Name name, int dclass, long ttl, Name host, Name admin, + long serial, long refresh, long retry, long expire, long minimum) +{ + super(name, Type.SOA, dclass, ttl); + this.host = checkName("host", host); + this.admin = checkName("admin", admin); + this.serial = checkU32("serial", serial); + this.refresh = checkU32("refresh", refresh); + this.retry = checkU32("retry", retry); + this.expire = checkU32("expire", expire); + this.minimum = checkU32("minimum", minimum); +} + +void +rrFromWire(DNSInput in) throws IOException { + host = new Name(in); + admin = new Name(in); + serial = in.readU32(); + refresh = in.readU32(); + retry = in.readU32(); + expire = in.readU32(); + minimum = in.readU32(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + host = st.getName(origin); + admin = st.getName(origin); + serial = st.getUInt32(); + refresh = st.getTTLLike(); + retry = st.getTTLLike(); + expire = st.getTTLLike(); + minimum = st.getTTLLike(); +} + +/** Convert to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(host); + sb.append(" "); + sb.append(admin); + if (Options.check("multiline")) { + sb.append(" (\n\t\t\t\t\t"); + sb.append(serial); + sb.append("\t; serial\n\t\t\t\t\t"); + sb.append(refresh); + sb.append("\t; refresh\n\t\t\t\t\t"); + sb.append(retry); + sb.append("\t; retry\n\t\t\t\t\t"); + sb.append(expire); + sb.append("\t; expire\n\t\t\t\t\t"); + sb.append(minimum); + sb.append(" )\t; minimum"); + } else { + sb.append(" "); + sb.append(serial); + sb.append(" "); + sb.append(refresh); + sb.append(" "); + sb.append(retry); + sb.append(" "); + sb.append(expire); + sb.append(" "); + sb.append(minimum); + } + return sb.toString(); +} + +/** Returns the primary name server */ +public Name +getHost() { + return host; +} + +/** Returns the zone administrator's address */ +public Name +getAdmin() { + return admin; +} + +/** Returns the zone's serial number */ +public long +getSerial() { + return serial; +} + +/** Returns the zone refresh interval */ +public long +getRefresh() { + return refresh; +} + +/** Returns the zone retry interval */ +public long +getRetry() { + return retry; +} + +/** Returns the time until a secondary expires a zone */ +public long +getExpire() { + return expire; +} + +/** Returns the minimum TTL for records in the zone */ +public long +getMinimum() { + return minimum; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + host.toWire(out, c, canonical); + admin.toWire(out, c, canonical); + out.writeU32(serial); + out.writeU32(refresh); + out.writeU32(retry); + out.writeU32(expire); + out.writeU32(minimum); +} + +} diff --git a/src/org/xbill/DNS/SPFRecord.java b/src/org/xbill/DNS/SPFRecord.java new file mode 100644 index 0000000..a286220 --- /dev/null +++ b/src/org/xbill/DNS/SPFRecord.java @@ -0,0 +1,44 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * Sender Policy Framework (RFC 4408, experimental) + * + * @author Brian Wellington + */ + +public class SPFRecord extends TXTBase { + +private static final long serialVersionUID = -2100754352801658722L; + +SPFRecord() {} + +Record +getObject() { + return new SPFRecord(); +} + +/** + * Creates a SPF Record from the given data + * @param strings The text strings + * @throws IllegalArgumentException One of the strings has invalid escapes + */ +public +SPFRecord(Name name, int dclass, long ttl, List strings) { + super(name, Type.SPF, dclass, ttl, strings); +} + +/** + * Creates a SPF Record from the given data + * @param string One text string + * @throws IllegalArgumentException The string has invalid escapes + */ +public +SPFRecord(Name name, int dclass, long ttl, String string) { + super(name, Type.SPF, dclass, ttl, string); +} + +} diff --git a/src/org/xbill/DNS/SRVRecord.java b/src/org/xbill/DNS/SRVRecord.java new file mode 100644 index 0000000..c0635fb --- /dev/null +++ b/src/org/xbill/DNS/SRVRecord.java @@ -0,0 +1,114 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Server Selection Record - finds hosts running services in a domain. An + * SRV record will normally be named _<service>._<protocol>.domain + * - examples would be _sips._tcp.example.org (for the secure SIP protocol) and + * _http._tcp.example.com (if HTTP used SRV records) + * + * @author Brian Wellington + */ + +public class SRVRecord extends Record { + +private static final long serialVersionUID = -3886460132387522052L; + +private int priority, weight, port; +private Name target; + +SRVRecord() {} + +Record +getObject() { + return new SRVRecord(); +} + +/** + * Creates an SRV Record from the given data + * @param priority The priority of this SRV. Records with lower priority + * are preferred. + * @param weight The weight, used to select between records at the same + * priority. + * @param port The TCP/UDP port that the service uses + * @param target The host running the service + */ +public +SRVRecord(Name name, int dclass, long ttl, int priority, + int weight, int port, Name target) +{ + super(name, Type.SRV, dclass, ttl); + this.priority = checkU16("priority", priority); + this.weight = checkU16("weight", weight); + this.port = checkU16("port", port); + this.target = checkName("target", target); +} + +void +rrFromWire(DNSInput in) throws IOException { + priority = in.readU16(); + weight = in.readU16(); + port = in.readU16(); + target = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + priority = st.getUInt16(); + weight = st.getUInt16(); + port = st.getUInt16(); + target = st.getName(origin); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(priority + " "); + sb.append(weight + " "); + sb.append(port + " "); + sb.append(target); + return sb.toString(); +} + +/** Returns the priority */ +public int +getPriority() { + return priority; +} + +/** Returns the weight */ +public int +getWeight() { + return weight; +} + +/** Returns the port that the service runs on */ +public int +getPort() { + return port; +} + +/** Returns the host running that the service */ +public Name +getTarget() { + return target; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(priority); + out.writeU16(weight); + out.writeU16(port); + target.toWire(out, null, canonical); +} + +public Name +getAdditionalName() { + return target; +} + +} diff --git a/src/org/xbill/DNS/SSHFPRecord.java b/src/org/xbill/DNS/SSHFPRecord.java new file mode 100644 index 0000000..079741e --- /dev/null +++ b/src/org/xbill/DNS/SSHFPRecord.java @@ -0,0 +1,108 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * SSH Fingerprint - stores the fingerprint of an SSH host key. + * + * @author Brian Wellington + */ + +public class SSHFPRecord extends Record { + +private static final long serialVersionUID = -8104701402654687025L; + +public static class Algorithm { + private Algorithm() {} + + public static final int RSA = 1; + public static final int DSS = 2; +} + +public static class Digest { + private Digest() {} + + public static final int SHA1 = 1; +} + +private int alg; +private int digestType; +private byte [] fingerprint; + +SSHFPRecord() {} + +Record +getObject() { + return new SSHFPRecord(); +} + +/** + * Creates an SSHFP Record from the given data. + * @param alg The public key's algorithm. + * @param digestType The public key's digest type. + * @param fingerprint The public key's fingerprint. + */ +public +SSHFPRecord(Name name, int dclass, long ttl, int alg, int digestType, + byte [] fingerprint) +{ + super(name, Type.SSHFP, dclass, ttl); + this.alg = checkU8("alg", alg); + this.digestType = checkU8("digestType", digestType); + this.fingerprint = fingerprint; +} + +void +rrFromWire(DNSInput in) throws IOException { + alg = in.readU8(); + digestType = in.readU8(); + fingerprint = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + alg = st.getUInt8(); + digestType = st.getUInt8(); + fingerprint = st.getHex(true); +} + +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(alg); + sb.append(" "); + sb.append(digestType); + sb.append(" "); + sb.append(base16.toString(fingerprint)); + return sb.toString(); +} + +/** Returns the public key's algorithm. */ +public int +getAlgorithm() { + return alg; +} + +/** Returns the public key's digest type. */ +public int +getDigestType() { + return digestType; +} + +/** Returns the fingerprint */ +public byte [] +getFingerPrint() { + return fingerprint; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(alg); + out.writeU8(digestType); + out.writeByteArray(fingerprint); +} + +} diff --git a/src/org/xbill/DNS/Section.java b/src/org/xbill/DNS/Section.java new file mode 100644 index 0000000..e0c8caa --- /dev/null +++ b/src/org/xbill/DNS/Section.java @@ -0,0 +1,92 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Constants and functions relating to DNS message sections + * + * @author Brian Wellington + */ + +public final class Section { + +/** The question (first) section */ +public static final int QUESTION = 0; + +/** The answer (second) section */ +public static final int ANSWER = 1; + +/** The authority (third) section */ +public static final int AUTHORITY = 2; + +/** The additional (fourth) section */ +public static final int ADDITIONAL = 3; + +/* Aliases for dynamic update */ +/** The zone (first) section of a dynamic update message */ +public static final int ZONE = 0; + +/** The prerequisite (second) section of a dynamic update message */ +public static final int PREREQ = 1; + +/** The update (third) section of a dynamic update message */ +public static final int UPDATE = 2; + +private static Mnemonic sections = new Mnemonic("Message Section", + Mnemonic.CASE_LOWER); +private static String [] longSections = new String[4]; +private static String [] updateSections = new String[4]; + +static { + sections.setMaximum(3); + sections.setNumericAllowed(true); + + sections.add(QUESTION, "qd"); + sections.add(ANSWER, "an"); + sections.add(AUTHORITY, "au"); + sections.add(ADDITIONAL, "ad"); + + longSections[QUESTION] = "QUESTIONS"; + longSections[ANSWER] = "ANSWERS"; + longSections[AUTHORITY] = "AUTHORITY RECORDS"; + longSections[ADDITIONAL] = "ADDITIONAL RECORDS"; + + updateSections[ZONE] = "ZONE"; + updateSections[PREREQ] = "PREREQUISITES"; + updateSections[UPDATE] = "UPDATE RECORDS"; + updateSections[ADDITIONAL] = "ADDITIONAL RECORDS"; +} + +private +Section() {} + +/** Converts a numeric Section into an abbreviation String */ +public static String +string(int i) { + return sections.getText(i); +} + +/** Converts a numeric Section into a full description String */ +public static String +longString(int i) { + sections.check(i); + return longSections[i]; +} + +/** + * Converts a numeric Section into a full description String for an update + * Message. + */ +public static String +updString(int i) { + sections.check(i); + return updateSections[i]; +} + +/** Converts a String representation of a Section into its numeric value */ +public static int +value(String s) { + return sections.getValue(s); +} + +} diff --git a/src/org/xbill/DNS/Serial.java b/src/org/xbill/DNS/Serial.java new file mode 100644 index 0000000..3a146c6 --- /dev/null +++ b/src/org/xbill/DNS/Serial.java @@ -0,0 +1,61 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Helper functions for doing serial arithmetic. These should be used when + * setting/checking SOA serial numbers. SOA serial number arithmetic is + * defined in RFC 1982. + * + * @author Brian Wellington + */ + +public final class Serial { + +private static final long MAX32 = 0xFFFFFFFFL; + +private +Serial() { +} + +/** + * Compares two numbers using serial arithmetic. The numbers are assumed + * to be 32 bit unsigned integers stored in longs. + * @param serial1 The first integer + * @param serial2 The second integer + * @return 0 if the 2 numbers are equal, a positive number if serial1 is greater + * than serial2, and a negative number if serial2 is greater than serial1. + * @throws IllegalArgumentException serial1 or serial2 is out of range + */ +public static int +compare(long serial1, long serial2) { + if (serial1 < 0 || serial1 > MAX32) + throw new IllegalArgumentException(serial1 + " out of range"); + if (serial2 < 0 || serial2 > MAX32) + throw new IllegalArgumentException(serial2 + " out of range"); + long diff = serial1 - serial2; + if (diff >= MAX32) + diff -= (MAX32 + 1); + else if (diff < -MAX32) + diff += (MAX32 + 1); + return (int)diff; +} + +/** + * Increments a serial number. The number is assumed to be a 32 bit unsigned + * integer stored in a long. This basically adds 1 and resets the value to + * 0 if it is 2^32. + * @param serial The serial number + * @return The incremented serial number + * @throws IllegalArgumentException serial is out of range + */ +public static long +increment(long serial) { + if (serial < 0 || serial > MAX32) + throw new IllegalArgumentException(serial + " out of range"); + if (serial == MAX32) + return 0; + return serial + 1; +} + +} diff --git a/src/org/xbill/DNS/SetResponse.java b/src/org/xbill/DNS/SetResponse.java new file mode 100644 index 0000000..05d9f32 --- /dev/null +++ b/src/org/xbill/DNS/SetResponse.java @@ -0,0 +1,202 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * The Response from a query to Cache.lookupRecords() or Zone.findRecords() + * @see Cache + * @see Zone + * + * @author Brian Wellington + */ + +public class SetResponse { + +/** + * The Cache contains no information about the requested name/type + */ +static final int UNKNOWN = 0; + +/** + * The Zone does not contain the requested name, or the Cache has + * determined that the name does not exist. + */ +static final int NXDOMAIN = 1; + +/** + * The Zone contains the name, but no data of the requested type, + * or the Cache has determined that the name exists and has no data + * of the requested type. + */ +static final int NXRRSET = 2; + +/** + * A delegation enclosing the requested name was found. + */ +static final int DELEGATION = 3; + +/** + * The Cache/Zone found a CNAME when looking for the name. + * @see CNAMERecord + */ +static final int CNAME = 4; + +/** + * The Cache/Zone found a DNAME when looking for the name. + * @see DNAMERecord + */ +static final int DNAME = 5; + +/** + * The Cache/Zone has successfully answered the question for the + * requested name/type/class. + */ +static final int SUCCESSFUL = 6; + +private static final SetResponse unknown = new SetResponse(UNKNOWN); +private static final SetResponse nxdomain = new SetResponse(NXDOMAIN); +private static final SetResponse nxrrset = new SetResponse(NXRRSET); + +private int type; +private Object data; + +private +SetResponse() {} + +SetResponse(int type, RRset rrset) { + if (type < 0 || type > 6) + throw new IllegalArgumentException("invalid type"); + this.type = type; + this.data = rrset; +} + +SetResponse(int type) { + if (type < 0 || type > 6) + throw new IllegalArgumentException("invalid type"); + this.type = type; + this.data = null; +} + +static SetResponse +ofType(int type) { + switch (type) { + case UNKNOWN: + return unknown; + case NXDOMAIN: + return nxdomain; + case NXRRSET: + return nxrrset; + case DELEGATION: + case CNAME: + case DNAME: + case SUCCESSFUL: + SetResponse sr = new SetResponse(); + sr.type = type; + sr.data = null; + return sr; + default: + throw new IllegalArgumentException("invalid type"); + } +} + +void +addRRset(RRset rrset) { + if (data == null) + data = new ArrayList(); + List l = (List) data; + l.add(rrset); +} + +/** Is the answer to the query unknown? */ +public boolean +isUnknown() { + return (type == UNKNOWN); +} + +/** Is the answer to the query that the name does not exist? */ +public boolean +isNXDOMAIN() { + return (type == NXDOMAIN); +} + +/** Is the answer to the query that the name exists, but the type does not? */ +public boolean +isNXRRSET() { + return (type == NXRRSET); +} + +/** Is the result of the lookup that the name is below a delegation? */ +public boolean +isDelegation() { + return (type == DELEGATION); +} + +/** Is the result of the lookup a CNAME? */ +public boolean +isCNAME() { + return (type == CNAME); +} + +/** Is the result of the lookup a DNAME? */ +public boolean +isDNAME() { + return (type == DNAME); +} + +/** Was the query successful? */ +public boolean +isSuccessful() { + return (type == SUCCESSFUL); +} + +/** If the query was successful, return the answers */ +public RRset [] +answers() { + if (type != SUCCESSFUL) + return null; + List l = (List) data; + return (RRset []) l.toArray(new RRset[l.size()]); +} + +/** + * If the query encountered a CNAME, return it. + */ +public CNAMERecord +getCNAME() { + return (CNAMERecord)((RRset)data).first(); +} + +/** + * If the query encountered a DNAME, return it. + */ +public DNAMERecord +getDNAME() { + return (DNAMERecord)((RRset)data).first(); +} + +/** + * If the query hit a delegation point, return the NS set. + */ +public RRset +getNS() { + return (RRset)data; +} + +/** Prints the value of the SetResponse */ +public String +toString() { + switch (type) { + case UNKNOWN: return "unknown"; + case NXDOMAIN: return "NXDOMAIN"; + case NXRRSET: return "NXRRSET"; + case DELEGATION: return "delegation: " + data; + case CNAME: return "CNAME: " + data; + case DNAME: return "DNAME: " + data; + case SUCCESSFUL: return "successful"; + default: throw new IllegalStateException(); + } +} + +} diff --git a/src/org/xbill/DNS/SimpleResolver.java b/src/org/xbill/DNS/SimpleResolver.java new file mode 100644 index 0000000..7436133 --- /dev/null +++ b/src/org/xbill/DNS/SimpleResolver.java @@ -0,0 +1,351 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; +import java.io.*; +import java.net.*; + +/** + * An implementation of Resolver that sends one query to one server. + * SimpleResolver handles TCP retries, transaction security (TSIG), and + * EDNS 0. + * @see Resolver + * @see TSIG + * @see OPTRecord + * + * @author Brian Wellington + */ + + +public class SimpleResolver implements Resolver { + +/** The default port to send queries to */ +public static final int DEFAULT_PORT = 53; + +/** The default EDNS payload size */ +public static final int DEFAULT_EDNS_PAYLOADSIZE = 1280; + +private InetSocketAddress address; +private InetSocketAddress localAddress; +private boolean useTCP, ignoreTruncation; +private OPTRecord queryOPT; +private TSIG tsig; +private long timeoutValue = 10 * 1000; + +private static final short DEFAULT_UDPSIZE = 512; + +private static String defaultResolver = "localhost"; +private static int uniqueID = 0; + +/** + * Creates a SimpleResolver that will query the specified host + * @exception UnknownHostException Failure occurred while finding the host + */ +public +SimpleResolver(String hostname) throws UnknownHostException { + if (hostname == null) { + hostname = ResolverConfig.getCurrentConfig().server(); + if (hostname == null) + hostname = defaultResolver; + } + InetAddress addr; + if (hostname.equals("0")) + addr = InetAddress.getLocalHost(); + else + addr = InetAddress.getByName(hostname); + address = new InetSocketAddress(addr, DEFAULT_PORT); +} + +/** + * Creates a SimpleResolver. The host to query is either found by using + * ResolverConfig, or the default host is used. + * @see ResolverConfig + * @exception UnknownHostException Failure occurred while finding the host + */ +public +SimpleResolver() throws UnknownHostException { + this(null); +} + +/** + * Gets the destination address associated with this SimpleResolver. + * Messages sent using this SimpleResolver will be sent to this address. + * @return The destination address associated with this SimpleResolver. + */ +InetSocketAddress +getAddress() { + return address; +} + +/** Sets the default host (initially localhost) to query */ +public static void +setDefaultResolver(String hostname) { + defaultResolver = hostname; +} + +public void +setPort(int port) { + address = new InetSocketAddress(address.getAddress(), port); +} + +/** + * Sets the address of the server to communicate with. + * @param addr The address of the DNS server + */ +public void +setAddress(InetSocketAddress addr) { + address = addr; +} + +/** + * Sets the address of the server to communicate with (on the default + * DNS port) + * @param addr The address of the DNS server + */ +public void +setAddress(InetAddress addr) { + address = new InetSocketAddress(addr, address.getPort()); +} + +/** + * Sets the local address to bind to when sending messages. + * @param addr The local address to send messages from. + */ +public void +setLocalAddress(InetSocketAddress addr) { + localAddress = addr; +} + +/** + * Sets the local address to bind to when sending messages. A random port + * will be used. + * @param addr The local address to send messages from. + */ +public void +setLocalAddress(InetAddress addr) { + localAddress = new InetSocketAddress(addr, 0); +} + +public void +setTCP(boolean flag) { + this.useTCP = flag; +} + +public void +setIgnoreTruncation(boolean flag) { + this.ignoreTruncation = flag; +} + +public void +setEDNS(int level, int payloadSize, int flags, List options) { + if (level != 0 && level != -1) + throw new IllegalArgumentException("invalid EDNS level - " + + "must be 0 or -1"); + if (payloadSize == 0) + payloadSize = DEFAULT_EDNS_PAYLOADSIZE; + queryOPT = new OPTRecord(payloadSize, 0, level, flags, options); +} + +public void +setEDNS(int level) { + setEDNS(level, 0, 0, null); +} + +public void +setTSIGKey(TSIG key) { + tsig = key; +} + +TSIG +getTSIGKey() { + return tsig; +} + +public void +setTimeout(int secs, int msecs) { + timeoutValue = (long)secs * 1000 + msecs; +} + +public void +setTimeout(int secs) { + setTimeout(secs, 0); +} + +long +getTimeout() { + return timeoutValue; +} + +private Message +parseMessage(byte [] b) throws WireParseException { + try { + return (new Message(b)); + } + catch (IOException e) { + if (Options.check("verbose")) + e.printStackTrace(); + if (!(e instanceof WireParseException)) + e = new WireParseException("Error parsing message"); + throw (WireParseException) e; + } +} + +private void +verifyTSIG(Message query, Message response, byte [] b, TSIG tsig) { + if (tsig == null) + return; + int error = tsig.verify(response, b, query.getTSIG()); + if (Options.check("verbose")) + System.err.println("TSIG verify: " + Rcode.TSIGstring(error)); +} + +private void +applyEDNS(Message query) { + if (queryOPT == null || query.getOPT() != null) + return; + query.addRecord(queryOPT, Section.ADDITIONAL); +} + +private int +maxUDPSize(Message query) { + OPTRecord opt = query.getOPT(); + if (opt == null) + return DEFAULT_UDPSIZE; + else + return opt.getPayloadSize(); +} + +/** + * Sends a message to a single server and waits for a response. No checking + * is done to ensure that the response is associated with the query. + * @param query The query to send. + * @return The response. + * @throws IOException An error occurred while sending or receiving. + */ +public Message +send(Message query) throws IOException { + if (Options.check("verbose")) + System.err.println("Sending to " + + address.getAddress().getHostAddress() + + ":" + address.getPort()); + + if (query.getHeader().getOpcode() == Opcode.QUERY) { + Record question = query.getQuestion(); + if (question != null && question.getType() == Type.AXFR) + return sendAXFR(query); + } + + query = (Message) query.clone(); + applyEDNS(query); + if (tsig != null) + tsig.apply(query, null); + + byte [] out = query.toWire(Message.MAXLENGTH); + int udpSize = maxUDPSize(query); + boolean tcp = false; + long endTime = System.currentTimeMillis() + timeoutValue; + do { + byte [] in; + + if (useTCP || out.length > udpSize) + tcp = true; + if (tcp) + in = TCPClient.sendrecv(localAddress, address, out, + endTime); + else + in = UDPClient.sendrecv(localAddress, address, out, + udpSize, endTime); + + /* + * Check that the response is long enough. + */ + if (in.length < Header.LENGTH) { + throw new WireParseException("invalid DNS header - " + + "too short"); + } + /* + * Check that the response ID matches the query ID. We want + * to check this before actually parsing the message, so that + * if there's a malformed response that's not ours, it + * doesn't confuse us. + */ + int id = ((in[0] & 0xFF) << 8) + (in[1] & 0xFF); + int qid = query.getHeader().getID(); + if (id != qid) { + String error = "invalid message id: expected " + qid + + "; got id " + id; + if (tcp) { + throw new WireParseException(error); + } else { + if (Options.check("verbose")) { + System.err.println(error); + } + continue; + } + } + Message response = parseMessage(in); + verifyTSIG(query, response, in, tsig); + if (!tcp && !ignoreTruncation && + response.getHeader().getFlag(Flags.TC)) + { + tcp = true; + continue; + } + return response; + } while (true); +} + +/** + * Asynchronously sends a message to a single server, registering a listener + * to receive a callback on success or exception. Multiple asynchronous + * lookups can be performed in parallel. Since the callback may be invoked + * before the function returns, external synchronization is necessary. + * @param query The query to send + * @param listener The object containing the callbacks. + * @return An identifier, which is also a parameter in the callback + */ +public Object +sendAsync(final Message query, final ResolverListener listener) { + final Object id; + synchronized (this) { + id = new Integer(uniqueID++); + } + Record question = query.getQuestion(); + String qname; + if (question != null) + qname = question.getName().toString(); + else + qname = "(none)"; + String name = this.getClass() + ": " + qname; + Thread thread = new ResolveThread(this, query, id, listener); + thread.setName(name); + thread.setDaemon(true); + thread.start(); + return id; +} + +private Message +sendAXFR(Message query) throws IOException { + Name qname = query.getQuestion().getName(); + ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(qname, address, tsig); + xfrin.setTimeout((int)(getTimeout() / 1000)); + xfrin.setLocalAddress(localAddress); + try { + xfrin.run(); + } + catch (ZoneTransferException e) { + throw new WireParseException(e.getMessage()); + } + List records = xfrin.getAXFR(); + Message response = new Message(query.getHeader().getID()); + response.getHeader().setFlag(Flags.AA); + response.getHeader().setFlag(Flags.QR); + response.addRecord(query.getQuestion(), Section.QUESTION); + Iterator it = records.iterator(); + while (it.hasNext()) + response.addRecord((Record)it.next(), Section.ANSWER); + return response; +} + +} diff --git a/src/org/xbill/DNS/SingleCompressedNameBase.java b/src/org/xbill/DNS/SingleCompressedNameBase.java new file mode 100644 index 0000000..790ca1f --- /dev/null +++ b/src/org/xbill/DNS/SingleCompressedNameBase.java @@ -0,0 +1,31 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Implements common functionality for the many record types whose format + * is a single compressed name. + * + * @author Brian Wellington + */ + +abstract class SingleCompressedNameBase extends SingleNameBase { + +private static final long serialVersionUID = -236435396815460677L; + +protected +SingleCompressedNameBase() {} + +protected +SingleCompressedNameBase(Name name, int type, int dclass, long ttl, + Name singleName, String description) +{ + super(name, type, dclass, ttl, singleName, description); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + singleName.toWire(out, c, canonical); +} + +} diff --git a/src/org/xbill/DNS/SingleNameBase.java b/src/org/xbill/DNS/SingleNameBase.java new file mode 100644 index 0000000..6d6c041 --- /dev/null +++ b/src/org/xbill/DNS/SingleNameBase.java @@ -0,0 +1,61 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Implements common functionality for the many record types whose format + * is a single name. + * + * @author Brian Wellington + */ + +abstract class SingleNameBase extends Record { + +private static final long serialVersionUID = -18595042501413L; + +protected Name singleName; + +protected +SingleNameBase() {} + +protected +SingleNameBase(Name name, int type, int dclass, long ttl) { + super(name, type, dclass, ttl); +} + +protected +SingleNameBase(Name name, int type, int dclass, long ttl, Name singleName, + String description) +{ + super(name, type, dclass, ttl); + this.singleName = checkName(description, singleName); +} + +void +rrFromWire(DNSInput in) throws IOException { + singleName = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + singleName = st.getName(origin); +} + +String +rrToString() { + return singleName.toString(); +} + +protected Name +getSingleName() { + return singleName; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + singleName.toWire(out, null, canonical); +} + +} diff --git a/src/org/xbill/DNS/TCPClient.java b/src/org/xbill/DNS/TCPClient.java new file mode 100644 index 0000000..1f17d72 --- /dev/null +++ b/src/org/xbill/DNS/TCPClient.java @@ -0,0 +1,132 @@ +// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; + +final class TCPClient extends Client { + +public +TCPClient(long endTime) throws IOException { + super(SocketChannel.open(), endTime); +} + +void +bind(SocketAddress addr) throws IOException { + SocketChannel channel = (SocketChannel) key.channel(); + channel.socket().bind(addr); +} + +void +connect(SocketAddress addr) throws IOException { + SocketChannel channel = (SocketChannel) key.channel(); + if (channel.connect(addr)) + return; + key.interestOps(SelectionKey.OP_CONNECT); + try { + while (!channel.finishConnect()) { + if (!key.isConnectable()) + blockUntil(key, endTime); + } + } + finally { + if (key.isValid()) + key.interestOps(0); + } +} + +void +send(byte [] data) throws IOException { + SocketChannel channel = (SocketChannel) key.channel(); + verboseLog("TCP write", data); + byte [] lengthArray = new byte[2]; + lengthArray[0] = (byte)(data.length >>> 8); + lengthArray[1] = (byte)(data.length & 0xFF); + ByteBuffer [] buffers = new ByteBuffer[2]; + buffers[0] = ByteBuffer.wrap(lengthArray); + buffers[1] = ByteBuffer.wrap(data); + int nsent = 0; + key.interestOps(SelectionKey.OP_WRITE); + try { + while (nsent < data.length + 2) { + if (key.isWritable()) { + long n = channel.write(buffers); + if (n < 0) + throw new EOFException(); + nsent += (int) n; + if (nsent < data.length + 2 && + System.currentTimeMillis() > endTime) + throw new SocketTimeoutException(); + } else + blockUntil(key, endTime); + } + } + finally { + if (key.isValid()) + key.interestOps(0); + } +} + +private byte [] +_recv(int length) throws IOException { + SocketChannel channel = (SocketChannel) key.channel(); + int nrecvd = 0; + byte [] data = new byte[length]; + ByteBuffer buffer = ByteBuffer.wrap(data); + key.interestOps(SelectionKey.OP_READ); + try { + while (nrecvd < length) { + if (key.isReadable()) { + long n = channel.read(buffer); + if (n < 0) + throw new EOFException(); + nrecvd += (int) n; + if (nrecvd < length && + System.currentTimeMillis() > endTime) + throw new SocketTimeoutException(); + } else + blockUntil(key, endTime); + } + } + finally { + if (key.isValid()) + key.interestOps(0); + } + return data; +} + +byte [] +recv() throws IOException { + byte [] buf = _recv(2); + int length = ((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF); + byte [] data = _recv(length); + verboseLog("TCP read", data); + return data; +} + +static byte [] +sendrecv(SocketAddress local, SocketAddress remote, byte [] data, long endTime) +throws IOException +{ + TCPClient client = new TCPClient(endTime); + try { + if (local != null) + client.bind(local); + client.connect(remote); + client.send(data); + return client.recv(); + } + finally { + client.cleanup(); + } +} + +static byte [] +sendrecv(SocketAddress addr, byte [] data, long endTime) throws IOException { + return sendrecv(null, addr, data, endTime); +} + +} diff --git a/src/org/xbill/DNS/TKEYRecord.java b/src/org/xbill/DNS/TKEYRecord.java new file mode 100644 index 0000000..4dcbb5c --- /dev/null +++ b/src/org/xbill/DNS/TKEYRecord.java @@ -0,0 +1,225 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * Transaction Key - used to compute and/or securely transport a shared + * secret to be used with TSIG. + * @see TSIG + * + * @author Brian Wellington + */ + +public class TKEYRecord extends Record { + +private static final long serialVersionUID = 8828458121926391756L; + +private Name alg; +private Date timeInception; +private Date timeExpire; +private int mode, error; +private byte [] key; +private byte [] other; + +/** The key is assigned by the server (unimplemented) */ +public static final int SERVERASSIGNED = 1; + +/** The key is computed using a Diffie-Hellman key exchange */ +public static final int DIFFIEHELLMAN = 2; + +/** The key is computed using GSS_API (unimplemented) */ +public static final int GSSAPI = 3; + +/** The key is assigned by the resolver (unimplemented) */ +public static final int RESOLVERASSIGNED = 4; + +/** The key should be deleted */ +public static final int DELETE = 5; + +TKEYRecord() {} + +Record +getObject() { + return new TKEYRecord(); +} + +/** + * Creates a TKEY Record from the given data. + * @param alg The shared key's algorithm + * @param timeInception The beginning of the validity period of the shared + * secret or keying material + * @param timeExpire The end of the validity period of the shared + * secret or keying material + * @param mode The mode of key agreement + * @param error The extended error field. Should be 0 in queries + * @param key The shared secret + * @param other The other data field. Currently unused + * responses. + */ +public +TKEYRecord(Name name, int dclass, long ttl, Name alg, + Date timeInception, Date timeExpire, int mode, int error, + byte [] key, byte other[]) +{ + super(name, Type.TKEY, dclass, ttl); + this.alg = checkName("alg", alg); + this.timeInception = timeInception; + this.timeExpire = timeExpire; + this.mode = checkU16("mode", mode); + this.error = checkU16("error", error); + this.key = key; + this.other = other; +} + +void +rrFromWire(DNSInput in) throws IOException { + alg = new Name(in); + timeInception = new Date(1000 * in.readU32()); + timeExpire = new Date(1000 * in.readU32()); + mode = in.readU16(); + error = in.readU16(); + + int keylen = in.readU16(); + if (keylen > 0) + key = in.readByteArray(keylen); + else + key = null; + + int otherlen = in.readU16(); + if (otherlen > 0) + other = in.readByteArray(otherlen); + else + other = null; +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + throw st.exception("no text format defined for TKEY"); +} + +protected String +modeString() { + switch (mode) { + case SERVERASSIGNED: return "SERVERASSIGNED"; + case DIFFIEHELLMAN: return "DIFFIEHELLMAN"; + case GSSAPI: return "GSSAPI"; + case RESOLVERASSIGNED: return "RESOLVERASSIGNED"; + case DELETE: return "DELETE"; + default: return Integer.toString(mode); + } +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(alg); + sb.append(" "); + if (Options.check("multiline")) + sb.append("(\n\t"); + sb.append(FormattedTime.format(timeInception)); + sb.append(" "); + sb.append(FormattedTime.format(timeExpire)); + sb.append(" "); + sb.append(modeString()); + sb.append(" "); + sb.append(Rcode.TSIGstring(error)); + if (Options.check("multiline")) { + sb.append("\n"); + if (key != null) { + sb.append(base64.formatString(key, 64, "\t", false)); + sb.append("\n"); + } + if (other != null) + sb.append(base64.formatString(other, 64, "\t", false)); + sb.append(" )"); + } else { + sb.append(" "); + if (key != null) { + sb.append(base64.toString(key)); + sb.append(" "); + } + if (other != null) + sb.append(base64.toString(other)); + } + return sb.toString(); +} + +/** Returns the shared key's algorithm */ +public Name +getAlgorithm() { + return alg; +} + +/** + * Returns the beginning of the validity period of the shared secret or + * keying material + */ +public Date +getTimeInception() { + return timeInception; +} + +/** + * Returns the end of the validity period of the shared secret or + * keying material + */ +public Date +getTimeExpire() { + return timeExpire; +} + +/** Returns the key agreement mode */ +public int +getMode() { + return mode; +} + +/** Returns the extended error */ +public int +getError() { + return error; +} + +/** Returns the shared secret or keying material */ +public byte [] +getKey() { + return key; +} + +/** Returns the other data */ +public byte [] +getOther() { + return other; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + alg.toWire(out, null, canonical); + + out.writeU32(timeInception.getTime() / 1000); + out.writeU32(timeExpire.getTime() / 1000); + + out.writeU16(mode); + out.writeU16(error); + + if (key != null) { + out.writeU16(key.length); + out.writeByteArray(key); + } + else + out.writeU16(0); + + if (other != null) { + out.writeU16(other.length); + out.writeByteArray(other); + } + else + out.writeU16(0); +} + +} diff --git a/src/org/xbill/DNS/TLSARecord.java b/src/org/xbill/DNS/TLSARecord.java new file mode 100644 index 0000000..48e2e80 --- /dev/null +++ b/src/org/xbill/DNS/TLSARecord.java @@ -0,0 +1,156 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import org.xbill.DNS.utils.*; + +/** + * Transport Layer Security Authentication + * + * @author Brian Wellington + */ + +public class TLSARecord extends Record { + +private static final long serialVersionUID = 356494267028580169L; + +public static class CertificateUsage { + private CertificateUsage() {} + + public static final int CA_CONSTRAINT = 0; + public static final int SERVICE_CERTIFICATE_CONSTRAINT = 1; + public static final int TRUST_ANCHOR_ASSERTION = 2; + public static final int DOMAIN_ISSUED_CERTIFICATE = 3; +} + +public static class Selector { + private Selector() {} + + /** + * Full certificate; the Certificate binary structure defined in + * [RFC5280] + */ + public static final int FULL_CERTIFICATE = 0; + + /** + * SubjectPublicKeyInfo; DER-encoded binary structure defined in + * [RFC5280] + */ + public static final int SUBJECT_PUBLIC_KEY_INFO = 1; +} + +public static class MatchingType { + private MatchingType() {} + + /** Exact match on selected content */ + public static final int EXACT = 0; + + /** SHA-256 hash of selected content [RFC6234] */ + public static final int SHA256 = 1; + + /** SHA-512 hash of selected content [RFC6234] */ + public static final int SHA512 = 2; +} + +private int certificateUsage; +private int selector; +private int matchingType; +private byte [] certificateAssociationData; + +TLSARecord() {} + +Record +getObject() { + return new TLSARecord(); +} + +/** + * Creates an TLSA Record from the given data + * @param certificateUsage The provided association that will be used to + * match the certificate presented in the TLS handshake. + * @param selector The part of the TLS certificate presented by the server + * that will be matched against the association data. + * @param matchingType How the certificate association is presented. + * @param certificateAssociationData The "certificate association data" to be + * matched. + */ +public +TLSARecord(Name name, int dclass, long ttl, + int certificateUsage, int selector, int matchingType, + byte [] certificateAssociationData) +{ + super(name, Type.TLSA, dclass, ttl); + this.certificateUsage = checkU8("certificateUsage", certificateUsage); + this.selector = checkU8("selector", selector); + this.matchingType = checkU8("matchingType", matchingType); + this.certificateAssociationData = checkByteArrayLength( + "certificateAssociationData", + certificateAssociationData, + 0xFFFF); +} + +void +rrFromWire(DNSInput in) throws IOException { + certificateUsage = in.readU8(); + selector = in.readU8(); + matchingType = in.readU8(); + certificateAssociationData = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + certificateUsage = st.getUInt8(); + selector = st.getUInt8(); + matchingType = st.getUInt8(); + certificateAssociationData = st.getHex(); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(certificateUsage); + sb.append(" "); + sb.append(selector); + sb.append(" "); + sb.append(matchingType); + sb.append(" "); + sb.append(base16.toString(certificateAssociationData)); + + return sb.toString(); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU8(certificateUsage); + out.writeU8(selector); + out.writeU8(matchingType); + out.writeByteArray(certificateAssociationData); +} + +/** Returns the certificate usage of the TLSA record */ +public int +getCertificateUsage() { + return certificateUsage; +} + +/** Returns the selector of the TLSA record */ +public int +getSelector() { + return selector; +} + +/** Returns the matching type of the TLSA record */ +public int +getMatchingType() { + return matchingType; +} + +/** Returns the certificate associate data of this TLSA record */ +public final byte [] +getCertificateAssociationData() { + return certificateAssociationData; +} + +} diff --git a/src/org/xbill/DNS/TSIG.java b/src/org/xbill/DNS/TSIG.java new file mode 100644 index 0000000..d9e6972 --- /dev/null +++ b/src/org/xbill/DNS/TSIG.java @@ -0,0 +1,592 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * Transaction signature handling. This class generates and verifies + * TSIG records on messages, which provide transaction security. + * @see TSIGRecord + * + * @author Brian Wellington + */ + +public class TSIG { + +private static final String HMAC_MD5_STR = "HMAC-MD5.SIG-ALG.REG.INT."; +private static final String HMAC_SHA1_STR = "hmac-sha1."; +private static final String HMAC_SHA224_STR = "hmac-sha224."; +private static final String HMAC_SHA256_STR = "hmac-sha256."; +private static final String HMAC_SHA384_STR = "hmac-sha384."; +private static final String HMAC_SHA512_STR = "hmac-sha512."; + +/** The domain name representing the HMAC-MD5 algorithm. */ +public static final Name HMAC_MD5 = Name.fromConstantString(HMAC_MD5_STR); + +/** The domain name representing the HMAC-MD5 algorithm (deprecated). */ +public static final Name HMAC = HMAC_MD5; + +/** The domain name representing the HMAC-SHA1 algorithm. */ +public static final Name HMAC_SHA1 = Name.fromConstantString(HMAC_SHA1_STR); + +/** + * The domain name representing the HMAC-SHA224 algorithm. + * Note that SHA224 is not supported by Java out-of-the-box, this requires use + * of a third party provider like BouncyCastle.org. + */ +public static final Name HMAC_SHA224 = Name.fromConstantString(HMAC_SHA224_STR); + +/** The domain name representing the HMAC-SHA256 algorithm. */ +public static final Name HMAC_SHA256 = Name.fromConstantString(HMAC_SHA256_STR); + +/** The domain name representing the HMAC-SHA384 algorithm. */ +public static final Name HMAC_SHA384 = Name.fromConstantString(HMAC_SHA384_STR); + +/** The domain name representing the HMAC-SHA512 algorithm. */ +public static final Name HMAC_SHA512 = Name.fromConstantString(HMAC_SHA512_STR); + +/** + * The default fudge value for outgoing packets. Can be overriden by the + * tsigfudge option. + */ +public static final short FUDGE = 300; + +private Name name, alg; +private String digest; +private int digestBlockLength; +private byte [] key; + +private void +getDigest() { + if (alg.equals(HMAC_MD5)) { + digest = "md5"; + digestBlockLength = 64; + } else if (alg.equals(HMAC_SHA1)) { + digest = "sha-1"; + digestBlockLength = 64; + } else if (alg.equals(HMAC_SHA224)) { + digest = "sha-224"; + digestBlockLength = 64; + } else if (alg.equals(HMAC_SHA256)) { + digest = "sha-256"; + digestBlockLength = 64; + } else if (alg.equals(HMAC_SHA512)) { + digest = "sha-512"; + digestBlockLength = 128; + } else if (alg.equals(HMAC_SHA384)) { + digest = "sha-384"; + digestBlockLength = 128; + } else + throw new IllegalArgumentException("Invalid algorithm"); +} + +/** + * Creates a new TSIG key, which can be used to sign or verify a message. + * @param algorithm The algorithm of the shared key. + * @param name The name of the shared key. + * @param key The shared key's data. + */ +public +TSIG(Name algorithm, Name name, byte [] key) { + this.name = name; + this.alg = algorithm; + this.key = key; + getDigest(); +} + +/** + * Creates a new TSIG key with the hmac-md5 algorithm, which can be used to + * sign or verify a message. + * @param name The name of the shared key. + * @param key The shared key's data. + */ +public +TSIG(Name name, byte [] key) { + this(HMAC_MD5, name, key); +} + +/** + * Creates a new TSIG object, which can be used to sign or verify a message. + * @param name The name of the shared key. + * @param key The shared key's data represented as a base64 encoded string. + * @throws IllegalArgumentException The key name is an invalid name + * @throws IllegalArgumentException The key data is improperly encoded + */ +public +TSIG(Name algorithm, String name, String key) { + this.key = base64.fromString(key); + if (this.key == null) + throw new IllegalArgumentException("Invalid TSIG key string"); + try { + this.name = Name.fromString(name, Name.root); + } + catch (TextParseException e) { + throw new IllegalArgumentException("Invalid TSIG key name"); + } + this.alg = algorithm; + getDigest(); +} + +/** + * Creates a new TSIG object, which can be used to sign or verify a message. + * @param name The name of the shared key. + * @param algorithm The algorithm of the shared key. The legal values are + * "hmac-md5", "hmac-sha1", "hmac-sha224", "hmac-sha256", "hmac-sha384", and + * "hmac-sha512". + * @param key The shared key's data represented as a base64 encoded string. + * @throws IllegalArgumentException The key name is an invalid name + * @throws IllegalArgumentException The key data is improperly encoded + */ +public +TSIG(String algorithm, String name, String key) { + this(HMAC_MD5, name, key); + if (algorithm.equalsIgnoreCase("hmac-md5")) + this.alg = HMAC_MD5; + else if (algorithm.equalsIgnoreCase("hmac-sha1")) + this.alg = HMAC_SHA1; + else if (algorithm.equalsIgnoreCase("hmac-sha224")) + this.alg = HMAC_SHA224; + else if (algorithm.equalsIgnoreCase("hmac-sha256")) + this.alg = HMAC_SHA256; + else if (algorithm.equalsIgnoreCase("hmac-sha384")) + this.alg = HMAC_SHA384; + else if (algorithm.equalsIgnoreCase("hmac-sha512")) + this.alg = HMAC_SHA512; + else + throw new IllegalArgumentException("Invalid TSIG algorithm"); + getDigest(); +} + +/** + * Creates a new TSIG object with the hmac-md5 algorithm, which can be used to + * sign or verify a message. + * @param name The name of the shared key + * @param key The shared key's data, represented as a base64 encoded string. + * @throws IllegalArgumentException The key name is an invalid name + * @throws IllegalArgumentException The key data is improperly encoded + */ +public +TSIG(String name, String key) { + this(HMAC_MD5, name, key); +} + +/** + * Creates a new TSIG object, which can be used to sign or verify a message. + * @param str The TSIG key, in the form name:secret, name/secret, + * alg:name:secret, or alg/name/secret. If an algorithm is specified, it must + * be "hmac-md5", "hmac-sha1", or "hmac-sha256". + * @throws IllegalArgumentException The string does not contain both a name + * and secret. + * @throws IllegalArgumentException The key name is an invalid name + * @throws IllegalArgumentException The key data is improperly encoded + */ +static public TSIG +fromString(String str) { + String [] parts = str.split("[:/]", 3); + if (parts.length < 2) + throw new IllegalArgumentException("Invalid TSIG key " + + "specification"); + if (parts.length == 3) { + try { + return new TSIG(parts[0], parts[1], parts[2]); + } catch (IllegalArgumentException e) { + parts = str.split("[:/]", 2); + } + } + return new TSIG(HMAC_MD5, parts[0], parts[1]); +} + +/** + * Generates a TSIG record with a specific error for a message that has + * been rendered. + * @param m The message + * @param b The rendered message + * @param error The error + * @param old If this message is a response, the TSIG from the request + * @return The TSIG record to be added to the message + */ +public TSIGRecord +generate(Message m, byte [] b, int error, TSIGRecord old) { + Date timeSigned; + if (error != Rcode.BADTIME) + timeSigned = new Date(); + else + timeSigned = old.getTimeSigned(); + int fudge; + HMAC hmac = null; + if (error == Rcode.NOERROR || error == Rcode.BADTIME) + hmac = new HMAC(digest, digestBlockLength, key); + + fudge = Options.intValue("tsigfudge"); + if (fudge < 0 || fudge > 0x7FFF) + fudge = FUDGE; + + if (old != null) { + DNSOutput out = new DNSOutput(); + out.writeU16(old.getSignature().length); + if (hmac != null) { + hmac.update(out.toByteArray()); + hmac.update(old.getSignature()); + } + } + + /* Digest the message */ + if (hmac != null) + hmac.update(b); + + DNSOutput out = new DNSOutput(); + name.toWireCanonical(out); + out.writeU16(DClass.ANY); /* class */ + out.writeU32(0); /* ttl */ + alg.toWireCanonical(out); + long time = timeSigned.getTime() / 1000; + int timeHigh = (int) (time >> 32); + long timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + out.writeU16(fudge); + + out.writeU16(error); + out.writeU16(0); /* No other data */ + + if (hmac != null) + hmac.update(out.toByteArray()); + + byte [] signature; + if (hmac != null) + signature = hmac.sign(); + else + signature = new byte[0]; + + byte [] other = null; + if (error == Rcode.BADTIME) { + out = new DNSOutput(); + time = new Date().getTime() / 1000; + timeHigh = (int) (time >> 32); + timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + other = out.toByteArray(); + } + + return (new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge, + signature, m.getHeader().getID(), error, other)); +} + +/** + * Generates a TSIG record with a specific error for a message and adds it + * to the message. + * @param m The message + * @param error The error + * @param old If this message is a response, the TSIG from the request + */ +public void +apply(Message m, int error, TSIGRecord old) { + Record r = generate(m, m.toWire(), error, old); + m.addRecord(r, Section.ADDITIONAL); + m.tsigState = Message.TSIG_SIGNED; +} + +/** + * Generates a TSIG record for a message and adds it to the message + * @param m The message + * @param old If this message is a response, the TSIG from the request + */ +public void +apply(Message m, TSIGRecord old) { + apply(m, Rcode.NOERROR, old); +} + +/** + * Generates a TSIG record for a message and adds it to the message + * @param m The message + * @param old If this message is a response, the TSIG from the request + */ +public void +applyStream(Message m, TSIGRecord old, boolean first) { + if (first) { + apply(m, old); + return; + } + Date timeSigned = new Date(); + int fudge; + HMAC hmac = new HMAC(digest, digestBlockLength, key); + + fudge = Options.intValue("tsigfudge"); + if (fudge < 0 || fudge > 0x7FFF) + fudge = FUDGE; + + DNSOutput out = new DNSOutput(); + out.writeU16(old.getSignature().length); + hmac.update(out.toByteArray()); + hmac.update(old.getSignature()); + + /* Digest the message */ + hmac.update(m.toWire()); + + out = new DNSOutput(); + long time = timeSigned.getTime() / 1000; + int timeHigh = (int) (time >> 32); + long timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + out.writeU16(fudge); + + hmac.update(out.toByteArray()); + + byte [] signature = hmac.sign(); + byte [] other = null; + + Record r = new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge, + signature, m.getHeader().getID(), + Rcode.NOERROR, other); + m.addRecord(r, Section.ADDITIONAL); + m.tsigState = Message.TSIG_SIGNED; +} + +/** + * Verifies a TSIG record on an incoming message. Since this is only called + * in the context where a TSIG is expected to be present, it is an error + * if one is not present. After calling this routine, Message.isVerified() may + * be called on this message. + * @param m The message + * @param b An array containing the message in unparsed form. This is + * necessary since TSIG signs the message in wire format, and we can't + * recreate the exact wire format (with the same name compression). + * @param length The length of the message in the array. + * @param old If this message is a response, the TSIG from the request + * @return The result of the verification (as an Rcode) + * @see Rcode + */ +public byte +verify(Message m, byte [] b, int length, TSIGRecord old) { + m.tsigState = Message.TSIG_FAILED; + TSIGRecord tsig = m.getTSIG(); + HMAC hmac = new HMAC(digest, digestBlockLength, key); + if (tsig == null) + return Rcode.FORMERR; + + if (!tsig.getName().equals(name) || !tsig.getAlgorithm().equals(alg)) { + if (Options.check("verbose")) + System.err.println("BADKEY failure"); + return Rcode.BADKEY; + } + long now = System.currentTimeMillis(); + long then = tsig.getTimeSigned().getTime(); + long fudge = tsig.getFudge(); + if (Math.abs(now - then) > fudge * 1000) { + if (Options.check("verbose")) + System.err.println("BADTIME failure"); + return Rcode.BADTIME; + } + + if (old != null && tsig.getError() != Rcode.BADKEY && + tsig.getError() != Rcode.BADSIG) + { + DNSOutput out = new DNSOutput(); + out.writeU16(old.getSignature().length); + hmac.update(out.toByteArray()); + hmac.update(old.getSignature()); + } + m.getHeader().decCount(Section.ADDITIONAL); + byte [] header = m.getHeader().toWire(); + m.getHeader().incCount(Section.ADDITIONAL); + hmac.update(header); + + int len = m.tsigstart - header.length; + hmac.update(b, header.length, len); + + DNSOutput out = new DNSOutput(); + tsig.getName().toWireCanonical(out); + out.writeU16(tsig.dclass); + out.writeU32(tsig.ttl); + tsig.getAlgorithm().toWireCanonical(out); + long time = tsig.getTimeSigned().getTime() / 1000; + int timeHigh = (int) (time >> 32); + long timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + out.writeU16(tsig.getFudge()); + out.writeU16(tsig.getError()); + if (tsig.getOther() != null) { + out.writeU16(tsig.getOther().length); + out.writeByteArray(tsig.getOther()); + } else { + out.writeU16(0); + } + + hmac.update(out.toByteArray()); + + byte [] signature = tsig.getSignature(); + int digestLength = hmac.digestLength(); + int minDigestLength = digest.equals("md5") ? 10 : digestLength / 2; + + if (signature.length > digestLength) { + if (Options.check("verbose")) + System.err.println("BADSIG: signature too long"); + return Rcode.BADSIG; + } else if (signature.length < minDigestLength) { + if (Options.check("verbose")) + System.err.println("BADSIG: signature too short"); + return Rcode.BADSIG; + } else if (!hmac.verify(signature, true)) { + if (Options.check("verbose")) + System.err.println("BADSIG: signature verification"); + return Rcode.BADSIG; + } + + m.tsigState = Message.TSIG_VERIFIED; + return Rcode.NOERROR; +} + +/** + * Verifies a TSIG record on an incoming message. Since this is only called + * in the context where a TSIG is expected to be present, it is an error + * if one is not present. After calling this routine, Message.isVerified() may + * be called on this message. + * @param m The message + * @param b The message in unparsed form. This is necessary since TSIG + * signs the message in wire format, and we can't recreate the exact wire + * format (with the same name compression). + * @param old If this message is a response, the TSIG from the request + * @return The result of the verification (as an Rcode) + * @see Rcode + */ +public int +verify(Message m, byte [] b, TSIGRecord old) { + return verify(m, b, b.length, old); +} + +/** + * Returns the maximum length of a TSIG record generated by this key. + * @see TSIGRecord + */ +public int +recordLength() { + return (name.length() + 10 + + alg.length() + + 8 + // time signed, fudge + 18 + // 2 byte MAC length, 16 byte MAC + 4 + // original id, error + 8); // 2 byte error length, 6 byte max error field. +} + +public static class StreamVerifier { + /** + * A helper class for verifying multiple message responses. + */ + + private TSIG key; + private HMAC verifier; + private int nresponses; + private int lastsigned; + private TSIGRecord lastTSIG; + + /** Creates an object to verify a multiple message response */ + public + StreamVerifier(TSIG tsig, TSIGRecord old) { + key = tsig; + verifier = new HMAC(key.digest, key.digestBlockLength, key.key); + nresponses = 0; + lastTSIG = old; + } + + /** + * Verifies a TSIG record on an incoming message that is part of a + * multiple message response. + * TSIG records must be present on the first and last messages, and + * at least every 100 records in between. + * After calling this routine, Message.isVerified() may be called on + * this message. + * @param m The message + * @param b The message in unparsed form + * @return The result of the verification (as an Rcode) + * @see Rcode + */ + public int + verify(Message m, byte [] b) { + TSIGRecord tsig = m.getTSIG(); + + nresponses++; + + if (nresponses == 1) { + int result = key.verify(m, b, lastTSIG); + if (result == Rcode.NOERROR) { + byte [] signature = tsig.getSignature(); + DNSOutput out = new DNSOutput(); + out.writeU16(signature.length); + verifier.update(out.toByteArray()); + verifier.update(signature); + } + lastTSIG = tsig; + return result; + } + + if (tsig != null) + m.getHeader().decCount(Section.ADDITIONAL); + byte [] header = m.getHeader().toWire(); + if (tsig != null) + m.getHeader().incCount(Section.ADDITIONAL); + verifier.update(header); + + int len; + if (tsig == null) + len = b.length - header.length; + else + len = m.tsigstart - header.length; + verifier.update(b, header.length, len); + + if (tsig != null) { + lastsigned = nresponses; + lastTSIG = tsig; + } + else { + boolean required = (nresponses - lastsigned >= 100); + if (required) { + m.tsigState = Message.TSIG_FAILED; + return Rcode.FORMERR; + } else { + m.tsigState = Message.TSIG_INTERMEDIATE; + return Rcode.NOERROR; + } + } + + if (!tsig.getName().equals(key.name) || + !tsig.getAlgorithm().equals(key.alg)) + { + if (Options.check("verbose")) + System.err.println("BADKEY failure"); + m.tsigState = Message.TSIG_FAILED; + return Rcode.BADKEY; + } + + DNSOutput out = new DNSOutput(); + long time = tsig.getTimeSigned().getTime() / 1000; + int timeHigh = (int) (time >> 32); + long timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + out.writeU16(tsig.getFudge()); + verifier.update(out.toByteArray()); + + if (verifier.verify(tsig.getSignature()) == false) { + if (Options.check("verbose")) + System.err.println("BADSIG failure"); + m.tsigState = Message.TSIG_FAILED; + return Rcode.BADSIG; + } + + verifier.clear(); + out = new DNSOutput(); + out.writeU16(tsig.getSignature().length); + verifier.update(out.toByteArray()); + verifier.update(tsig.getSignature()); + + m.tsigState = Message.TSIG_VERIFIED; + return Rcode.NOERROR; + } +} + +} diff --git a/src/org/xbill/DNS/TSIGRecord.java b/src/org/xbill/DNS/TSIGRecord.java new file mode 100644 index 0000000..c7ce9ed --- /dev/null +++ b/src/org/xbill/DNS/TSIGRecord.java @@ -0,0 +1,220 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; +import org.xbill.DNS.utils.*; + +/** + * Transaction Signature - this record is automatically generated by the + * resolver. TSIG records provide transaction security between the + * sender and receiver of a message, using a shared key. + * @see Resolver + * @see TSIG + * + * @author Brian Wellington + */ + +public class TSIGRecord extends Record { + +private static final long serialVersionUID = -88820909016649306L; + +private Name alg; +private Date timeSigned; +private int fudge; +private byte [] signature; +private int originalID; +private int error; +private byte [] other; + +TSIGRecord() {} + +Record +getObject() { + return new TSIGRecord(); +} + +/** + * Creates a TSIG Record from the given data. This is normally called by + * the TSIG class + * @param alg The shared key's algorithm + * @param timeSigned The time that this record was generated + * @param fudge The fudge factor for time - if the time that the message is + * received is not in the range [now - fudge, now + fudge], the signature + * fails + * @param signature The signature + * @param originalID The message ID at the time of its generation + * @param error The extended error field. Should be 0 in queries. + * @param other The other data field. Currently used only in BADTIME + * responses. + * @see TSIG + */ +public +TSIGRecord(Name name, int dclass, long ttl, Name alg, Date timeSigned, + int fudge, byte [] signature, int originalID, int error, + byte other[]) +{ + super(name, Type.TSIG, dclass, ttl); + this.alg = checkName("alg", alg); + this.timeSigned = timeSigned; + this.fudge = checkU16("fudge", fudge); + this.signature = signature; + this.originalID = checkU16("originalID", originalID); + this.error = checkU16("error", error); + this.other = other; +} + +void +rrFromWire(DNSInput in) throws IOException { + alg = new Name(in); + + long timeHigh = in.readU16(); + long timeLow = in.readU32(); + long time = (timeHigh << 32) + timeLow; + timeSigned = new Date(time * 1000); + fudge = in.readU16(); + + int sigLen = in.readU16(); + signature = in.readByteArray(sigLen); + + originalID = in.readU16(); + error = in.readU16(); + + int otherLen = in.readU16(); + if (otherLen > 0) + other = in.readByteArray(otherLen); + else + other = null; +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + throw st.exception("no text format defined for TSIG"); +} + +/** Converts rdata to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(alg); + sb.append(" "); + if (Options.check("multiline")) + sb.append("(\n\t"); + + sb.append (timeSigned.getTime() / 1000); + sb.append (" "); + sb.append (fudge); + sb.append (" "); + sb.append (signature.length); + if (Options.check("multiline")) { + sb.append ("\n"); + sb.append (base64.formatString(signature, 64, "\t", false)); + } else { + sb.append (" "); + sb.append (base64.toString(signature)); + } + sb.append (" "); + sb.append (Rcode.TSIGstring(error)); + sb.append (" "); + if (other == null) + sb.append (0); + else { + sb.append (other.length); + if (Options.check("multiline")) + sb.append("\n\n\n\t"); + else + sb.append(" "); + if (error == Rcode.BADTIME) { + if (other.length != 6) { + sb.append("<invalid BADTIME other data>"); + } else { + long time = ((long)(other[0] & 0xFF) << 40) + + ((long)(other[1] & 0xFF) << 32) + + ((other[2] & 0xFF) << 24) + + ((other[3] & 0xFF) << 16) + + ((other[4] & 0xFF) << 8) + + ((other[5] & 0xFF) ); + sb.append("<server time: "); + sb.append(new Date(time * 1000)); + sb.append(">"); + } + } else { + sb.append("<"); + sb.append(base64.toString(other)); + sb.append(">"); + } + } + if (Options.check("multiline")) + sb.append(" )"); + return sb.toString(); +} + +/** Returns the shared key's algorithm */ +public Name +getAlgorithm() { + return alg; +} + +/** Returns the time that this record was generated */ +public Date +getTimeSigned() { + return timeSigned; +} + +/** Returns the time fudge factor */ +public int +getFudge() { + return fudge; +} + +/** Returns the signature */ +public byte [] +getSignature() { + return signature; +} + +/** Returns the original message ID */ +public int +getOriginalID() { + return originalID; +} + +/** Returns the extended error */ +public int +getError() { + return error; +} + +/** Returns the other data */ +public byte [] +getOther() { + return other; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + alg.toWire(out, null, canonical); + + long time = timeSigned.getTime() / 1000; + int timeHigh = (int) (time >> 32); + long timeLow = (time & 0xFFFFFFFFL); + out.writeU16(timeHigh); + out.writeU32(timeLow); + out.writeU16(fudge); + + out.writeU16(signature.length); + out.writeByteArray(signature); + + out.writeU16(originalID); + out.writeU16(error); + + if (other != null) { + out.writeU16(other.length); + out.writeByteArray(other); + } + else + out.writeU16(0); +} + +} diff --git a/src/org/xbill/DNS/TTL.java b/src/org/xbill/DNS/TTL.java new file mode 100644 index 0000000..01bf416 --- /dev/null +++ b/src/org/xbill/DNS/TTL.java @@ -0,0 +1,113 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Routines for parsing BIND-style TTL values. These values consist of + * numbers followed by 1 letter units of time (W - week, D - day, H - hour, + * M - minute, S - second). + * + * @author Brian Wellington + */ + +public final class TTL { + +public static final long MAX_VALUE = 0x7FFFFFFFL; + +private +TTL() {} + +static void +check(long i) { + if (i < 0 || i > MAX_VALUE) + throw new InvalidTTLException(i); +} + +/** + * Parses a TTL-like value, which can either be expressed as a number or a + * BIND-style string with numbers and units. + * @param s The string representing the numeric value. + * @param clamp Whether to clamp values in the range [MAX_VALUE + 1, 2^32 -1] + * to MAX_VALUE. This should be donw for TTLs, but not other values which + * can be expressed in this format. + * @return The value as a number of seconds + * @throws NumberFormatException The string was not in a valid TTL format. + */ +public static long +parse(String s, boolean clamp) { + if (s == null || s.length() == 0 || !Character.isDigit(s.charAt(0))) + throw new NumberFormatException(); + long value = 0; + long ttl = 0; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + long oldvalue = value; + if (Character.isDigit(c)) { + value = (value * 10) + Character.getNumericValue(c); + if (value < oldvalue) + throw new NumberFormatException(); + } else { + switch (Character.toUpperCase(c)) { + case 'W': value *= 7; + case 'D': value *= 24; + case 'H': value *= 60; + case 'M': value *= 60; + case 'S': break; + default: throw new NumberFormatException(); + } + ttl += value; + value = 0; + if (ttl > 0xFFFFFFFFL) + throw new NumberFormatException(); + } + } + if (ttl == 0) + ttl = value; + + if (ttl > 0xFFFFFFFFL) + throw new NumberFormatException(); + else if (ttl > MAX_VALUE && clamp) + ttl = MAX_VALUE; + return ttl; +} + +/** + * Parses a TTL, which can either be expressed as a number or a BIND-style + * string with numbers and units. + * @param s The string representing the TTL + * @return The TTL as a number of seconds + * @throws NumberFormatException The string was not in a valid TTL format. + */ +public static long +parseTTL(String s) { + return parse(s, true); +} + +public static String +format(long ttl) { + TTL.check(ttl); + StringBuffer sb = new StringBuffer(); + long secs, mins, hours, days, weeks; + secs = ttl % 60; + ttl /= 60; + mins = ttl % 60; + ttl /= 60; + hours = ttl % 24; + ttl /= 24; + days = ttl % 7; + ttl /= 7; + weeks = ttl; + if (weeks > 0) + sb.append(weeks + "W"); + if (days > 0) + sb.append(days + "D"); + if (hours > 0) + sb.append(hours + "H"); + if (mins > 0) + sb.append(mins + "M"); + if (secs > 0 || (weeks == 0 && days == 0 && hours == 0 && mins == 0)) + sb.append(secs + "S"); + return sb.toString(); +} + +} diff --git a/src/org/xbill/DNS/TXTBase.java b/src/org/xbill/DNS/TXTBase.java new file mode 100644 index 0000000..fd99bfb --- /dev/null +++ b/src/org/xbill/DNS/TXTBase.java @@ -0,0 +1,123 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * Implements common functionality for the many record types whose format + * is a list of strings. + * + * @author Brian Wellington + */ + +abstract class TXTBase extends Record { + +private static final long serialVersionUID = -4319510507246305931L; + +protected List strings; + +protected +TXTBase() {} + +protected +TXTBase(Name name, int type, int dclass, long ttl) { + super(name, type, dclass, ttl); +} + +protected +TXTBase(Name name, int type, int dclass, long ttl, List strings) { + super(name, type, dclass, ttl); + if (strings == null) + throw new IllegalArgumentException("strings must not be null"); + this.strings = new ArrayList(strings.size()); + Iterator it = strings.iterator(); + try { + while (it.hasNext()) { + String s = (String) it.next(); + this.strings.add(byteArrayFromString(s)); + } + } + catch (TextParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } +} + +protected +TXTBase(Name name, int type, int dclass, long ttl, String string) { + this(name, type, dclass, ttl, Collections.singletonList(string)); +} + +void +rrFromWire(DNSInput in) throws IOException { + strings = new ArrayList(2); + while (in.remaining() > 0) { + byte [] b = in.readCountedString(); + strings.add(b); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + strings = new ArrayList(2); + while (true) { + Tokenizer.Token t = st.get(); + if (!t.isString()) + break; + try { + strings.add(byteArrayFromString(t.value)); + } + catch (TextParseException e) { + throw st.exception(e.getMessage()); + } + + } + st.unget(); +} + +/** converts to a String */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + Iterator it = strings.iterator(); + while (it.hasNext()) { + byte [] array = (byte []) it.next(); + sb.append(byteArrayToString(array, true)); + if (it.hasNext()) + sb.append(" "); + } + return sb.toString(); +} + +/** + * Returns the text strings + * @return A list of Strings corresponding to the text strings. + */ +public List +getStrings() { + List list = new ArrayList(strings.size()); + for (int i = 0; i < strings.size(); i++) + list.add(byteArrayToString((byte []) strings.get(i), false)); + return list; +} + +/** + * Returns the text strings + * @return A list of byte arrays corresponding to the text strings. + */ +public List +getStringsAsByteArrays() { + return strings; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + Iterator it = strings.iterator(); + while (it.hasNext()) { + byte [] b = (byte []) it.next(); + out.writeCountedString(b); + } +} + +} diff --git a/src/org/xbill/DNS/TXTRecord.java b/src/org/xbill/DNS/TXTRecord.java new file mode 100644 index 0000000..ea5de04 --- /dev/null +++ b/src/org/xbill/DNS/TXTRecord.java @@ -0,0 +1,44 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.*; + +/** + * Text - stores text strings + * + * @author Brian Wellington + */ + +public class TXTRecord extends TXTBase { + +private static final long serialVersionUID = -5780785764284221342L; + +TXTRecord() {} + +Record +getObject() { + return new TXTRecord(); +} + +/** + * Creates a TXT Record from the given data + * @param strings The text strings + * @throws IllegalArgumentException One of the strings has invalid escapes + */ +public +TXTRecord(Name name, int dclass, long ttl, List strings) { + super(name, Type.TXT, dclass, ttl, strings); +} + +/** + * Creates a TXT Record from the given data + * @param string One text string + * @throws IllegalArgumentException The string has invalid escapes + */ +public +TXTRecord(Name name, int dclass, long ttl, String string) { + super(name, Type.TXT, dclass, ttl, string); +} + +} diff --git a/src/org/xbill/DNS/TextParseException.java b/src/org/xbill/DNS/TextParseException.java new file mode 100644 index 0000000..3b9a425 --- /dev/null +++ b/src/org/xbill/DNS/TextParseException.java @@ -0,0 +1,25 @@ +// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * An exception thrown when unable to parse text. + * + * @author Brian Wellington + */ + +public class TextParseException extends IOException { + +public +TextParseException() { + super(); +} + +public +TextParseException(String s) { + super(s); +} + +} diff --git a/src/org/xbill/DNS/Tokenizer.java b/src/org/xbill/DNS/Tokenizer.java new file mode 100644 index 0000000..bc637ab --- /dev/null +++ b/src/org/xbill/DNS/Tokenizer.java @@ -0,0 +1,713 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) +// +// Copyright (C) 2003-2004 Nominum, Inc. +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; + +import org.xbill.DNS.utils.*; + +/** + * Tokenizer is used to parse DNS records and zones from text format, + * + * @author Brian Wellington + * @author Bob Halley + */ + +public class Tokenizer { + +private static String delim = " \t\n;()\""; +private static String quotes = "\""; + +/** End of file */ +public static final int EOF = 0; + +/** End of line */ +public static final int EOL = 1; + +/** Whitespace; only returned when wantWhitespace is set */ +public static final int WHITESPACE = 2; + +/** An identifier (unquoted string) */ +public static final int IDENTIFIER = 3; + +/** A quoted string */ +public static final int QUOTED_STRING = 4; + +/** A comment; only returned when wantComment is set */ +public static final int COMMENT = 5; + +private PushbackInputStream is; +private boolean ungottenToken; +private int multiline; +private boolean quoting; +private String delimiters; +private Token current; +private StringBuffer sb; +private boolean wantClose; + +private String filename; +private int line; + +public static class Token { + /** The type of token. */ + public int type; + + /** The value of the token, or null for tokens without values. */ + public String value; + + private + Token() { + type = -1; + value = null; + } + + private Token + set(int type, StringBuffer value) { + if (type < 0) + throw new IllegalArgumentException(); + this.type = type; + this.value = value == null ? null : value.toString(); + return this; + } + + /** + * Converts the token to a string containing a representation useful + * for debugging. + */ + public String + toString() { + switch (type) { + case EOF: + return "<eof>"; + case EOL: + return "<eol>"; + case WHITESPACE: + return "<whitespace>"; + case IDENTIFIER: + return "<identifier: " + value + ">"; + case QUOTED_STRING: + return "<quoted_string: " + value + ">"; + case COMMENT: + return "<comment: " + value + ">"; + default: + return "<unknown>"; + } + } + + /** Indicates whether this token contains a string. */ + public boolean + isString() { + return (type == IDENTIFIER || type == QUOTED_STRING); + } + + /** Indicates whether this token contains an EOL or EOF. */ + public boolean + isEOL() { + return (type == EOL || type == EOF); + } +} + +static class TokenizerException extends TextParseException { + String message; + + public + TokenizerException(String filename, int line, String message) { + super(filename + ":" + line + ": " + message); + this.message = message; + } + + public String + getBaseMessage() { + return message; + } +} + +/** + * Creates a Tokenizer from an arbitrary input stream. + * @param is The InputStream to tokenize. + */ +public +Tokenizer(InputStream is) { + if (!(is instanceof BufferedInputStream)) + is = new BufferedInputStream(is); + this.is = new PushbackInputStream(is, 2); + ungottenToken = false; + multiline = 0; + quoting = false; + delimiters = delim; + current = new Token(); + sb = new StringBuffer(); + filename = "<none>"; + line = 1; +} + +/** + * Creates a Tokenizer from a string. + * @param s The String to tokenize. + */ +public +Tokenizer(String s) { + this(new ByteArrayInputStream(s.getBytes())); +} + +/** + * Creates a Tokenizer from a file. + * @param f The File to tokenize. + */ +public +Tokenizer(File f) throws FileNotFoundException { + this(new FileInputStream(f)); + wantClose = true; + filename = f.getName(); +} + +private int +getChar() throws IOException { + int c = is.read(); + if (c == '\r') { + int next = is.read(); + if (next != '\n') + is.unread(next); + c = '\n'; + } + if (c == '\n') + line++; + return c; +} + +private void +ungetChar(int c) throws IOException { + if (c == -1) + return; + is.unread(c); + if (c == '\n') + line--; +} + +private int +skipWhitespace() throws IOException { + int skipped = 0; + while (true) { + int c = getChar(); + if (c != ' ' && c != '\t') { + if (!(c == '\n' && multiline > 0)) { + ungetChar(c); + return skipped; + } + } + skipped++; + } +} + +private void +checkUnbalancedParens() throws TextParseException { + if (multiline > 0) + throw exception("unbalanced parentheses"); +} + +/** + * Gets the next token from a tokenizer. + * @param wantWhitespace If true, leading whitespace will be returned as a + * token. + * @param wantComment If true, comments are returned as tokens. + * @return The next token in the stream. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public Token +get(boolean wantWhitespace, boolean wantComment) throws IOException { + int type; + int c; + + if (ungottenToken) { + ungottenToken = false; + if (current.type == WHITESPACE) { + if (wantWhitespace) + return current; + } else if (current.type == COMMENT) { + if (wantComment) + return current; + } else { + if (current.type == EOL) + line++; + return current; + } + } + int skipped = skipWhitespace(); + if (skipped > 0 && wantWhitespace) + return current.set(WHITESPACE, null); + type = IDENTIFIER; + sb.setLength(0); + while (true) { + c = getChar(); + if (c == -1 || delimiters.indexOf(c) != -1) { + if (c == -1) { + if (quoting) + throw exception("EOF in " + + "quoted string"); + else if (sb.length() == 0) + return current.set(EOF, null); + else + return current.set(type, sb); + } + if (sb.length() == 0 && type != QUOTED_STRING) { + if (c == '(') { + multiline++; + skipWhitespace(); + continue; + } else if (c == ')') { + if (multiline <= 0) + throw exception("invalid " + + "close " + + "parenthesis"); + multiline--; + skipWhitespace(); + continue; + } else if (c == '"') { + if (!quoting) { + quoting = true; + delimiters = quotes; + type = QUOTED_STRING; + } else { + quoting = false; + delimiters = delim; + skipWhitespace(); + } + continue; + } else if (c == '\n') { + return current.set(EOL, null); + } else if (c == ';') { + while (true) { + c = getChar(); + if (c == '\n' || c == -1) + break; + sb.append((char)c); + } + if (wantComment) { + ungetChar(c); + return current.set(COMMENT, sb); + } else if (c == -1 && + type != QUOTED_STRING) + { + checkUnbalancedParens(); + return current.set(EOF, null); + } else if (multiline > 0) { + skipWhitespace(); + sb.setLength(0); + continue; + } else + return current.set(EOL, null); + } else + throw new IllegalStateException(); + } else + ungetChar(c); + break; + } else if (c == '\\') { + c = getChar(); + if (c == -1) + throw exception("unterminated escape sequence"); + sb.append('\\'); + } else if (quoting && c == '\n') { + throw exception("newline in quoted string"); + } + sb.append((char)c); + } + if (sb.length() == 0 && type != QUOTED_STRING) { + checkUnbalancedParens(); + return current.set(EOF, null); + } + return current.set(type, sb); +} + +/** + * Gets the next token from a tokenizer, ignoring whitespace and comments. + * @return The next token in the stream. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public Token +get() throws IOException { + return get(false, false); +} + +/** + * Returns a token to the stream, so that it will be returned by the next call + * to get(). + * @throws IllegalStateException There are already ungotten tokens. + */ +public void +unget() { + if (ungottenToken) + throw new IllegalStateException + ("Cannot unget multiple tokens"); + if (current.type == EOL) + line--; + ungottenToken = true; +} + +/** + * Gets the next token from a tokenizer and converts it to a string. + * @return The next token in the stream, as a string. + * @throws TextParseException The input was invalid or not a string. + * @throws IOException An I/O error occurred. + */ +public String +getString() throws IOException { + Token next = get(); + if (!next.isString()) { + throw exception("expected a string"); + } + return next.value; +} + +private String +_getIdentifier(String expected) throws IOException { + Token next = get(); + if (next.type != IDENTIFIER) + throw exception("expected " + expected); + return next.value; +} + +/** + * Gets the next token from a tokenizer, ensures it is an unquoted string, + * and converts it to a string. + * @return The next token in the stream, as a string. + * @throws TextParseException The input was invalid or not an unquoted string. + * @throws IOException An I/O error occurred. + */ +public String +getIdentifier() throws IOException { + return _getIdentifier("an identifier"); +} + +/** + * Gets the next token from a tokenizer and converts it to a long. + * @return The next token in the stream, as a long. + * @throws TextParseException The input was invalid or not a long. + * @throws IOException An I/O error occurred. + */ +public long +getLong() throws IOException { + String next = _getIdentifier("an integer"); + if (!Character.isDigit(next.charAt(0))) + throw exception("expected an integer"); + try { + return Long.parseLong(next); + } catch (NumberFormatException e) { + throw exception("expected an integer"); + } +} + +/** + * Gets the next token from a tokenizer and converts it to an unsigned 32 bit + * integer. + * @return The next token in the stream, as an unsigned 32 bit integer. + * @throws TextParseException The input was invalid or not an unsigned 32 + * bit integer. + * @throws IOException An I/O error occurred. + */ +public long +getUInt32() throws IOException { + long l = getLong(); + if (l < 0 || l > 0xFFFFFFFFL) + throw exception("expected an 32 bit unsigned integer"); + return l; +} + +/** + * Gets the next token from a tokenizer and converts it to an unsigned 16 bit + * integer. + * @return The next token in the stream, as an unsigned 16 bit integer. + * @throws TextParseException The input was invalid or not an unsigned 16 + * bit integer. + * @throws IOException An I/O error occurred. + */ +public int +getUInt16() throws IOException { + long l = getLong(); + if (l < 0 || l > 0xFFFFL) + throw exception("expected an 16 bit unsigned integer"); + return (int) l; +} + +/** + * Gets the next token from a tokenizer and converts it to an unsigned 8 bit + * integer. + * @return The next token in the stream, as an unsigned 8 bit integer. + * @throws TextParseException The input was invalid or not an unsigned 8 + * bit integer. + * @throws IOException An I/O error occurred. + */ +public int +getUInt8() throws IOException { + long l = getLong(); + if (l < 0 || l > 0xFFL) + throw exception("expected an 8 bit unsigned integer"); + return (int) l; +} + +/** + * Gets the next token from a tokenizer and parses it as a TTL. + * @return The next token in the stream, as an unsigned 32 bit integer. + * @throws TextParseException The input was not valid. + * @throws IOException An I/O error occurred. + * @see TTL + */ +public long +getTTL() throws IOException { + String next = _getIdentifier("a TTL value"); + try { + return TTL.parseTTL(next); + } + catch (NumberFormatException e) { + throw exception("expected a TTL value"); + } +} + +/** + * Gets the next token from a tokenizer and parses it as if it were a TTL. + * @return The next token in the stream, as an unsigned 32 bit integer. + * @throws TextParseException The input was not valid. + * @throws IOException An I/O error occurred. + * @see TTL + */ +public long +getTTLLike() throws IOException { + String next = _getIdentifier("a TTL-like value"); + try { + return TTL.parse(next, false); + } + catch (NumberFormatException e) { + throw exception("expected a TTL-like value"); + } +} + +/** + * Gets the next token from a tokenizer and converts it to a name. + * @param origin The origin to append to relative names. + * @return The next token in the stream, as a name. + * @throws TextParseException The input was invalid or not a valid name. + * @throws IOException An I/O error occurred. + * @throws RelativeNameException The parsed name was relative, even with the + * origin. + * @see Name + */ +public Name +getName(Name origin) throws IOException { + String next = _getIdentifier("a name"); + try { + Name name = Name.fromString(next, origin); + if (!name.isAbsolute()) + throw new RelativeNameException(name); + return name; + } + catch (TextParseException e) { + throw exception(e.getMessage()); + } +} + +/** + * Gets the next token from a tokenizer and converts it to an IP Address. + * @param family The address family. + * @return The next token in the stream, as an InetAddress + * @throws TextParseException The input was invalid or not a valid address. + * @throws IOException An I/O error occurred. + * @see Address + */ +public InetAddress +getAddress(int family) throws IOException { + String next = _getIdentifier("an address"); + try { + return Address.getByAddress(next, family); + } + catch (UnknownHostException e) { + throw exception(e.getMessage()); + } +} + +/** + * Gets the next token from a tokenizer, which must be an EOL or EOF. + * @throws TextParseException The input was invalid or not an EOL or EOF token. + * @throws IOException An I/O error occurred. + */ +public void +getEOL() throws IOException { + Token next = get(); + if (next.type != EOL && next.type != EOF) { + throw exception("expected EOL or EOF"); + } +} + +/** + * Returns a concatenation of the remaining strings from a Tokenizer. + */ +private String +remainingStrings() throws IOException { + StringBuffer buffer = null; + while (true) { + Tokenizer.Token t = get(); + if (!t.isString()) + break; + if (buffer == null) + buffer = new StringBuffer(); + buffer.append(t.value); + } + unget(); + if (buffer == null) + return null; + return buffer.toString(); +} + +/** + * Gets the remaining string tokens until an EOL/EOF is seen, concatenates + * them together, and converts the base64 encoded data to a byte array. + * @param required If true, an exception will be thrown if no strings remain; + * otherwise null be be returned. + * @return The byte array containing the decoded strings, or null if there + * were no strings to decode. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getBase64(boolean required) throws IOException { + String s = remainingStrings(); + if (s == null) { + if (required) + throw exception("expected base64 encoded string"); + else + return null; + } + byte [] array = base64.fromString(s); + if (array == null) + throw exception("invalid base64 encoding"); + return array; +} + +/** + * Gets the remaining string tokens until an EOL/EOF is seen, concatenates + * them together, and converts the base64 encoded data to a byte array. + * @return The byte array containing the decoded strings, or null if there + * were no strings to decode. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getBase64() throws IOException { + return getBase64(false); +} + +/** + * Gets the remaining string tokens until an EOL/EOF is seen, concatenates + * them together, and converts the hex encoded data to a byte array. + * @param required If true, an exception will be thrown if no strings remain; + * otherwise null be be returned. + * @return The byte array containing the decoded strings, or null if there + * were no strings to decode. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getHex(boolean required) throws IOException { + String s = remainingStrings(); + if (s == null) { + if (required) + throw exception("expected hex encoded string"); + else + return null; + } + byte [] array = base16.fromString(s); + if (array == null) + throw exception("invalid hex encoding"); + return array; +} + +/** + * Gets the remaining string tokens until an EOL/EOF is seen, concatenates + * them together, and converts the hex encoded data to a byte array. + * @return The byte array containing the decoded strings, or null if there + * were no strings to decode. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getHex() throws IOException { + return getHex(false); +} + +/** + * Gets the next token from a tokenizer and decodes it as hex. + * @return The byte array containing the decoded string. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getHexString() throws IOException { + String next = _getIdentifier("a hex string"); + byte [] array = base16.fromString(next); + if (array == null) + throw exception("invalid hex encoding"); + return array; +} + +/** + * Gets the next token from a tokenizer and decodes it as base32. + * @param b32 The base32 context to decode with. + * @return The byte array containing the decoded string. + * @throws TextParseException The input was invalid. + * @throws IOException An I/O error occurred. + */ +public byte [] +getBase32String(base32 b32) throws IOException { + String next = _getIdentifier("a base32 string"); + byte [] array = b32.fromString(next); + if (array == null) + throw exception("invalid base32 encoding"); + return array; +} + +/** + * Creates an exception which includes the current state in the error message + * @param s The error message to include. + * @return The exception to be thrown + */ +public TextParseException +exception(String s) { + return new TokenizerException(filename, line, s); +} + +/** + * Closes any files opened by this tokenizer. + */ +public void +close() { + if (wantClose) { + try { + is.close(); + } + catch (IOException e) { + } + } +} + +protected void +finalize() { + close(); +} + +} diff --git a/src/org/xbill/DNS/Type.java b/src/org/xbill/DNS/Type.java new file mode 100644 index 0000000..df84e83 --- /dev/null +++ b/src/org/xbill/DNS/Type.java @@ -0,0 +1,361 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.util.HashMap; + +/** + * Constants and functions relating to DNS Types + * + * @author Brian Wellington + */ + +public final class Type { + +/** Address */ +public static final int A = 1; + +/** Name server */ +public static final int NS = 2; + +/** Mail destination */ +public static final int MD = 3; + +/** Mail forwarder */ +public static final int MF = 4; + +/** Canonical name (alias) */ +public static final int CNAME = 5; + +/** Start of authority */ +public static final int SOA = 6; + +/** Mailbox domain name */ +public static final int MB = 7; + +/** Mail group member */ +public static final int MG = 8; + +/** Mail rename name */ +public static final int MR = 9; + +/** Null record */ +public static final int NULL = 10; + +/** Well known services */ +public static final int WKS = 11; + +/** Domain name pointer */ +public static final int PTR = 12; + +/** Host information */ +public static final int HINFO = 13; + +/** Mailbox information */ +public static final int MINFO = 14; + +/** Mail routing information */ +public static final int MX = 15; + +/** Text strings */ +public static final int TXT = 16; + +/** Responsible person */ +public static final int RP = 17; + +/** AFS cell database */ +public static final int AFSDB = 18; + +/** X.25 calling address */ +public static final int X25 = 19; + +/** ISDN calling address */ +public static final int ISDN = 20; + +/** Router */ +public static final int RT = 21; + +/** NSAP address */ +public static final int NSAP = 22; + +/** Reverse NSAP address (deprecated) */ +public static final int NSAP_PTR = 23; + +/** Signature */ +public static final int SIG = 24; + +/** Key */ +public static final int KEY = 25; + +/** X.400 mail mapping */ +public static final int PX = 26; + +/** Geographical position (withdrawn) */ +public static final int GPOS = 27; + +/** IPv6 address */ +public static final int AAAA = 28; + +/** Location */ +public static final int LOC = 29; + +/** Next valid name in zone */ +public static final int NXT = 30; + +/** Endpoint identifier */ +public static final int EID = 31; + +/** Nimrod locator */ +public static final int NIMLOC = 32; + +/** Server selection */ +public static final int SRV = 33; + +/** ATM address */ +public static final int ATMA = 34; + +/** Naming authority pointer */ +public static final int NAPTR = 35; + +/** Key exchange */ +public static final int KX = 36; + +/** Certificate */ +public static final int CERT = 37; + +/** IPv6 address (experimental) */ +public static final int A6 = 38; + +/** Non-terminal name redirection */ +public static final int DNAME = 39; + +/** Options - contains EDNS metadata */ +public static final int OPT = 41; + +/** Address Prefix List */ +public static final int APL = 42; + +/** Delegation Signer */ +public static final int DS = 43; + +/** SSH Key Fingerprint */ +public static final int SSHFP = 44; + +/** IPSEC key */ +public static final int IPSECKEY = 45; + +/** Resource Record Signature */ +public static final int RRSIG = 46; + +/** Next Secure Name */ +public static final int NSEC = 47; + +/** DNSSEC Key */ +public static final int DNSKEY = 48; + +/** Dynamic Host Configuration Protocol (DHCP) ID */ +public static final int DHCID = 49; + +/** Next SECure, 3rd edition, RFC 5155 */ +public static final int NSEC3 = 50; + +/** Next SECure PARAMeter, RFC 5155 */ +public static final int NSEC3PARAM = 51; + +/** Transport Layer Security Authentication, draft-ietf-dane-protocol-23 */ +public static final int TLSA = 52; + +/** Sender Policy Framework (experimental) */ +public static final int SPF = 99; + +/** Transaction key - used to compute a shared secret or exchange a key */ +public static final int TKEY = 249; + +/** Transaction signature */ +public static final int TSIG = 250; + +/** Incremental zone transfer */ +public static final int IXFR = 251; + +/** Zone transfer */ +public static final int AXFR = 252; + +/** Transfer mailbox records */ +public static final int MAILB = 253; + +/** Transfer mail agent records */ +public static final int MAILA = 254; + +/** Matches any type */ +public static final int ANY = 255; + +/** DNSSEC Lookaside Validation, RFC 4431 . */ +public static final int DLV = 32769; + + +private static class TypeMnemonic extends Mnemonic { + private HashMap objects; + + public + TypeMnemonic() { + super("Type", CASE_UPPER); + setPrefix("TYPE"); + objects = new HashMap(); + } + + public void + add(int val, String str, Record proto) { + super.add(val, str); + objects.put(Mnemonic.toInteger(val), proto); + } + + public void + check(int val) { + Type.check(val); + } + + public Record + getProto(int val) { + check(val); + return (Record) objects.get(toInteger(val)); + } +} + +private static TypeMnemonic types = new TypeMnemonic(); + +static { + types.add(A, "A", new ARecord()); + types.add(NS, "NS", new NSRecord()); + types.add(MD, "MD", new MDRecord()); + types.add(MF, "MF", new MFRecord()); + types.add(CNAME, "CNAME", new CNAMERecord()); + types.add(SOA, "SOA", new SOARecord()); + types.add(MB, "MB", new MBRecord()); + types.add(MG, "MG", new MGRecord()); + types.add(MR, "MR", new MRRecord()); + types.add(NULL, "NULL", new NULLRecord()); + types.add(WKS, "WKS", new WKSRecord()); + types.add(PTR, "PTR", new PTRRecord()); + types.add(HINFO, "HINFO", new HINFORecord()); + types.add(MINFO, "MINFO", new MINFORecord()); + types.add(MX, "MX", new MXRecord()); + types.add(TXT, "TXT", new TXTRecord()); + types.add(RP, "RP", new RPRecord()); + types.add(AFSDB, "AFSDB", new AFSDBRecord()); + types.add(X25, "X25", new X25Record()); + types.add(ISDN, "ISDN", new ISDNRecord()); + types.add(RT, "RT", new RTRecord()); + types.add(NSAP, "NSAP", new NSAPRecord()); + types.add(NSAP_PTR, "NSAP-PTR", new NSAP_PTRRecord()); + types.add(SIG, "SIG", new SIGRecord()); + types.add(KEY, "KEY", new KEYRecord()); + types.add(PX, "PX", new PXRecord()); + types.add(GPOS, "GPOS", new GPOSRecord()); + types.add(AAAA, "AAAA", new AAAARecord()); + types.add(LOC, "LOC", new LOCRecord()); + types.add(NXT, "NXT", new NXTRecord()); + types.add(EID, "EID"); + types.add(NIMLOC, "NIMLOC"); + types.add(SRV, "SRV", new SRVRecord()); + types.add(ATMA, "ATMA"); + types.add(NAPTR, "NAPTR", new NAPTRRecord()); + types.add(KX, "KX", new KXRecord()); + types.add(CERT, "CERT", new CERTRecord()); + types.add(A6, "A6", new A6Record()); + types.add(DNAME, "DNAME", new DNAMERecord()); + types.add(OPT, "OPT", new OPTRecord()); + types.add(APL, "APL", new APLRecord()); + types.add(DS, "DS", new DSRecord()); + types.add(SSHFP, "SSHFP", new SSHFPRecord()); + types.add(IPSECKEY, "IPSECKEY", new IPSECKEYRecord()); + types.add(RRSIG, "RRSIG", new RRSIGRecord()); + types.add(NSEC, "NSEC", new NSECRecord()); + types.add(DNSKEY, "DNSKEY", new DNSKEYRecord()); + types.add(DHCID, "DHCID", new DHCIDRecord()); + types.add(NSEC3, "NSEC3", new NSEC3Record()); + types.add(NSEC3PARAM, "NSEC3PARAM", new NSEC3PARAMRecord()); + types.add(TLSA, "TLSA", new TLSARecord()); + types.add(SPF, "SPF", new SPFRecord()); + types.add(TKEY, "TKEY", new TKEYRecord()); + types.add(TSIG, "TSIG", new TSIGRecord()); + types.add(IXFR, "IXFR"); + types.add(AXFR, "AXFR"); + types.add(MAILB, "MAILB"); + types.add(MAILA, "MAILA"); + types.add(ANY, "ANY"); + types.add(DLV, "DLV", new DLVRecord()); +} + +private +Type() { +} + +/** + * Checks that a numeric Type is valid. + * @throws InvalidTypeException The type is out of range. + */ +public static void +check(int val) { + if (val < 0 || val > 0xFFFF) + throw new InvalidTypeException(val); +} + +/** + * Converts a numeric Type into a String + * @param val The type value. + * @return The canonical string representation of the type + * @throws InvalidTypeException The type is out of range. + */ +public static String +string(int val) { + return types.getText(val); +} + +/** + * Converts a String representation of an Type into its numeric value. + * @param s The string representation of the type + * @param numberok Whether a number will be accepted or not. + * @return The type code, or -1 on error. + */ +public static int +value(String s, boolean numberok) { + int val = types.getValue(s); + if (val == -1 && numberok) { + val = types.getValue("TYPE" + s); + } + return val; +} + +/** + * Converts a String representation of an Type into its numeric value + * @return The type code, or -1 on error. + */ +public static int +value(String s) { + return value(s, false); +} + +static Record +getProto(int val) { + return types.getProto(val); +} + +/** Is this type valid for a record (a non-meta type)? */ +public static boolean +isRR(int type) { + switch (type) { + case OPT: + case TKEY: + case TSIG: + case IXFR: + case AXFR: + case MAILB: + case MAILA: + case ANY: + return false; + default: + return true; + } +} + +} diff --git a/src/org/xbill/DNS/TypeBitmap.java b/src/org/xbill/DNS/TypeBitmap.java new file mode 100644 index 0000000..628cc35 --- /dev/null +++ b/src/org/xbill/DNS/TypeBitmap.java @@ -0,0 +1,147 @@ +// Copyright (c) 2004-2009 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * Routines for deal with the lists of types found in NSEC/NSEC3 records. + * + * @author Brian Wellington + */ + +import java.io.*; +import java.util.*; + +final class TypeBitmap implements Serializable { + +private static final long serialVersionUID = -125354057735389003L; + +private TreeSet types; + +private +TypeBitmap() { + types = new TreeSet(); +} + +public +TypeBitmap(int [] array) { + this(); + for (int i = 0; i < array.length; i++) { + Type.check(array[i]); + types.add(new Integer(array[i])); + } +} + +public +TypeBitmap(DNSInput in) throws WireParseException { + this(); + int lastbase = -1; + while (in.remaining() > 0) { + if (in.remaining() < 2) + throw new WireParseException + ("invalid bitmap descriptor"); + int mapbase = in.readU8(); + if (mapbase < lastbase) + throw new WireParseException("invalid ordering"); + int maplength = in.readU8(); + if (maplength > in.remaining()) + throw new WireParseException("invalid bitmap"); + for (int i = 0; i < maplength; i++) { + int current = in.readU8(); + if (current == 0) + continue; + for (int j = 0; j < 8; j++) { + if ((current & (1 << (7 - j))) == 0) + continue; + int typecode = mapbase * 256 + + i * 8 + j; + types.add(Mnemonic.toInteger(typecode)); + } + } + } +} + +public +TypeBitmap(Tokenizer st) throws IOException { + this(); + while (true) { + Tokenizer.Token t = st.get(); + if (!t.isString()) + break; + int typecode = Type.value(t.value); + if (typecode < 0) { + throw st.exception("Invalid type: " + t.value); + } + types.add(Mnemonic.toInteger(typecode)); + } + st.unget(); +} + +public int [] +toArray() { + int [] array = new int[types.size()]; + int n = 0; + for (Iterator it = types.iterator(); it.hasNext(); ) + array[n++] = ((Integer)it.next()).intValue(); + return array; +} + +public String +toString() { + StringBuffer sb = new StringBuffer(); + for (Iterator it = types.iterator(); it.hasNext(); ) { + int t = ((Integer)it.next()).intValue(); + sb.append(Type.string(t)); + if (it.hasNext()) + sb.append(' '); + } + return sb.toString(); +} + +private static void +mapToWire(DNSOutput out, TreeSet map, int mapbase) { + int arraymax = (((Integer)map.last()).intValue()) & 0xFF; + int arraylength = (arraymax / 8) + 1; + int [] array = new int[arraylength]; + out.writeU8(mapbase); + out.writeU8(arraylength); + for (Iterator it = map.iterator(); it.hasNext(); ) { + int typecode = ((Integer)it.next()).intValue(); + array[(typecode & 0xFF) / 8] |= (1 << ( 7 - typecode % 8)); + } + for (int j = 0; j < arraylength; j++) + out.writeU8(array[j]); +} + +public void +toWire(DNSOutput out) { + if (types.size() == 0) + return; + + int mapbase = -1; + TreeSet map = new TreeSet(); + + for (Iterator it = types.iterator(); it.hasNext(); ) { + int t = ((Integer)it.next()).intValue(); + int base = t >> 8; + if (base != mapbase) { + if (map.size() > 0) { + mapToWire(out, map, mapbase); + map.clear(); + } + mapbase = base; + } + map.add(new Integer(t)); + } + mapToWire(out, map, mapbase); +} + +public boolean +empty() { + return types.isEmpty(); +} + +public boolean +contains(int typecode) { + return types.contains(Mnemonic.toInteger(typecode)); +} + +} diff --git a/src/org/xbill/DNS/U16NameBase.java b/src/org/xbill/DNS/U16NameBase.java new file mode 100644 index 0000000..df3c836 --- /dev/null +++ b/src/org/xbill/DNS/U16NameBase.java @@ -0,0 +1,75 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * Implements common functionality for the many record types whose format + * is an unsigned 16 bit integer followed by a name. + * + * @author Brian Wellington + */ + +abstract class U16NameBase extends Record { + +private static final long serialVersionUID = -8315884183112502995L; + +protected int u16Field; +protected Name nameField; + +protected +U16NameBase() {} + +protected +U16NameBase(Name name, int type, int dclass, long ttl) { + super(name, type, dclass, ttl); +} + +protected +U16NameBase(Name name, int type, int dclass, long ttl, int u16Field, + String u16Description, Name nameField, String nameDescription) +{ + super(name, type, dclass, ttl); + this.u16Field = checkU16(u16Description, u16Field); + this.nameField = checkName(nameDescription, nameField); +} + +void +rrFromWire(DNSInput in) throws IOException { + u16Field = in.readU16(); + nameField = new Name(in); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + u16Field = st.getUInt16(); + nameField = st.getName(origin); +} + +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(u16Field); + sb.append(" "); + sb.append(nameField); + return sb.toString(); +} + +protected int +getU16Field() { + return u16Field; +} + +protected Name +getNameField() { + return nameField; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeU16(u16Field); + nameField.toWire(out, null, canonical); +} + +} diff --git a/src/org/xbill/DNS/UDPClient.java b/src/org/xbill/DNS/UDPClient.java new file mode 100644 index 0000000..e752ce4 --- /dev/null +++ b/src/org/xbill/DNS/UDPClient.java @@ -0,0 +1,164 @@ +// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import java.security.SecureRandom; +import java.nio.*; +import java.nio.channels.*; + +final class UDPClient extends Client { + +private static final int EPHEMERAL_START = 1024; +private static final int EPHEMERAL_STOP = 65535; +private static final int EPHEMERAL_RANGE = EPHEMERAL_STOP - EPHEMERAL_START; + +private static SecureRandom prng = new SecureRandom(); +private static volatile boolean prng_initializing = true; + +/* + * On some platforms (Windows), the SecureRandom module initialization involves + * a call to InetAddress.getLocalHost(), which can end up here if using a + * dnsjava name service provider. + * + * This can cause problems in multiple ways. + * - If the SecureRandom seed generation process calls into here, and this + * module attempts to seed the local SecureRandom object, the thread hangs. + * - If something else calls InetAddress.getLocalHost(), and that causes this + * module to seed the local SecureRandom object, the thread hangs. + * + * To avoid both of these, check at initialization time to see if InetAddress + * is in the call chain. If so, initialize the SecureRandom object in a new + * thread, and disable port randomization until it completes. + */ +static { + new Thread(new Runnable() { + public void run() { + int n = prng.nextInt(); + prng_initializing = false; + }}).start(); +} + +private boolean bound = false; + +public +UDPClient(long endTime) throws IOException { + super(DatagramChannel.open(), endTime); +} + +private void +bind_random(InetSocketAddress addr) throws IOException +{ + if (prng_initializing) { + try { + Thread.sleep(2); + } + catch (InterruptedException e) { + } + if (prng_initializing) + return; + } + + DatagramChannel channel = (DatagramChannel) key.channel(); + InetSocketAddress temp; + + for (int i = 0; i < 1024; i++) { + try { + int port = prng.nextInt(EPHEMERAL_RANGE) + + EPHEMERAL_START; + if (addr != null) + temp = new InetSocketAddress(addr.getAddress(), + port); + else + temp = new InetSocketAddress(port); + channel.socket().bind(temp); + bound = true; + return; + } + catch (SocketException e) { + } + } +} + +void +bind(SocketAddress addr) throws IOException { + if (addr == null || + (addr instanceof InetSocketAddress && + ((InetSocketAddress)addr).getPort() == 0)) + { + bind_random((InetSocketAddress) addr); + if (bound) + return; + } + + if (addr != null) { + DatagramChannel channel = (DatagramChannel) key.channel(); + channel.socket().bind(addr); + bound = true; + } +} + +void +connect(SocketAddress addr) throws IOException { + if (!bound) + bind(null); + DatagramChannel channel = (DatagramChannel) key.channel(); + channel.connect(addr); +} + +void +send(byte [] data) throws IOException { + DatagramChannel channel = (DatagramChannel) key.channel(); + verboseLog("UDP write", data); + channel.write(ByteBuffer.wrap(data)); +} + +byte [] +recv(int max) throws IOException { + DatagramChannel channel = (DatagramChannel) key.channel(); + byte [] temp = new byte[max]; + key.interestOps(SelectionKey.OP_READ); + try { + while (!key.isReadable()) + blockUntil(key, endTime); + } + finally { + if (key.isValid()) + key.interestOps(0); + } + long ret = channel.read(ByteBuffer.wrap(temp)); + if (ret <= 0) + throw new EOFException(); + int len = (int) ret; + byte [] data = new byte[len]; + System.arraycopy(temp, 0, data, 0, len); + verboseLog("UDP read", data); + return data; +} + +static byte [] +sendrecv(SocketAddress local, SocketAddress remote, byte [] data, int max, + long endTime) +throws IOException +{ + UDPClient client = new UDPClient(endTime); + try { + client.bind(local); + client.connect(remote); + client.send(data); + return client.recv(max); + } + finally { + client.cleanup(); + } +} + +static byte [] +sendrecv(SocketAddress addr, byte [] data, int max, long endTime) +throws IOException +{ + return sendrecv(null, addr, data, max, endTime); +} + +} diff --git a/src/org/xbill/DNS/UNKRecord.java b/src/org/xbill/DNS/UNKRecord.java new file mode 100644 index 0000000..91c9697 --- /dev/null +++ b/src/org/xbill/DNS/UNKRecord.java @@ -0,0 +1,54 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * A class implementing Records of unknown and/or unimplemented types. This + * class can only be initialized using static Record initializers. + * + * @author Brian Wellington + */ + +public class UNKRecord extends Record { + +private static final long serialVersionUID = -4193583311594626915L; + +private byte [] data; + +UNKRecord() {} + +Record +getObject() { + return new UNKRecord(); +} + +void +rrFromWire(DNSInput in) throws IOException { + data = in.readByteArray(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + throw st.exception("invalid unknown RR encoding"); +} + +/** Converts this Record to the String "unknown format" */ +String +rrToString() { + return unknownToString(data); +} + +/** Returns the contents of this record. */ +public byte [] +getData() { + return data; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(data); +} + +} diff --git a/src/org/xbill/DNS/Update.java b/src/org/xbill/DNS/Update.java new file mode 100644 index 0000000..02a920b --- /dev/null +++ b/src/org/xbill/DNS/Update.java @@ -0,0 +1,300 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A helper class for constructing dynamic DNS (DDNS) update messages. + * + * @author Brian Wellington + */ + +public class Update extends Message { + +private Name origin; +private int dclass; + +/** + * Creates an update message. + * @param zone The name of the zone being updated. + * @param dclass The class of the zone being updated. + */ +public +Update(Name zone, int dclass) { + super(); + if (!zone.isAbsolute()) + throw new RelativeNameException(zone); + DClass.check(dclass); + getHeader().setOpcode(Opcode.UPDATE); + Record soa = Record.newRecord(zone, Type.SOA, DClass.IN); + addRecord(soa, Section.QUESTION); + this.origin = zone; + this.dclass = dclass; +} + +/** + * Creates an update message. The class is assumed to be IN. + * @param zone The name of the zone being updated. + */ +public +Update(Name zone) { + this(zone, DClass.IN); +} + +private void +newPrereq(Record rec) { + addRecord(rec, Section.PREREQ); +} + +private void +newUpdate(Record rec) { + addRecord(rec, Section.UPDATE); +} + +/** + * Inserts a prerequisite that the specified name exists; that is, there + * exist records with the given name in the zone. + */ +public void +present(Name name) { + newPrereq(Record.newRecord(name, Type.ANY, DClass.ANY, 0)); +} + +/** + * Inserts a prerequisite that the specified rrset exists; that is, there + * exist records with the given name and type in the zone. + */ +public void +present(Name name, int type) { + newPrereq(Record.newRecord(name, type, DClass.ANY, 0)); +} + +/** + * Parses a record from the string, and inserts a prerequisite that the + * record exists. Due to the way value-dependent prequisites work, the + * condition that must be met is that the set of all records with the same + * and type in the update message must be identical to the set of all records + * with that name and type on the server. + * @throws IOException The record could not be parsed. + */ +public void +present(Name name, int type, String record) throws IOException { + newPrereq(Record.fromString(name, type, dclass, 0, record, origin)); +} + +/** + * Parses a record from the tokenizer, and inserts a prerequisite that the + * record exists. Due to the way value-dependent prequisites work, the + * condition that must be met is that the set of all records with the same + * and type in the update message must be identical to the set of all records + * with that name and type on the server. + * @throws IOException The record could not be parsed. + */ +public void +present(Name name, int type, Tokenizer tokenizer) throws IOException { + newPrereq(Record.fromString(name, type, dclass, 0, tokenizer, origin)); +} + +/** + * Inserts a prerequisite that the specified record exists. Due to the way + * value-dependent prequisites work, the condition that must be met is that + * the set of all records with the same and type in the update message must + * be identical to the set of all records with that name and type on the server. + */ +public void +present(Record record) { + newPrereq(record); +} + +/** + * Inserts a prerequisite that the specified name does not exist; that is, + * there are no records with the given name in the zone. + */ +public void +absent(Name name) { + newPrereq(Record.newRecord(name, Type.ANY, DClass.NONE, 0)); +} + +/** + * Inserts a prerequisite that the specified rrset does not exist; that is, + * there are no records with the given name and type in the zone. + */ +public void +absent(Name name, int type) { + newPrereq(Record.newRecord(name, type, DClass.NONE, 0)); +} + +/** + * Parses a record from the string, and indicates that the record + * should be inserted into the zone. + * @throws IOException The record could not be parsed. + */ +public void +add(Name name, int type, long ttl, String record) throws IOException { + newUpdate(Record.fromString(name, type, dclass, ttl, record, origin)); +} + +/** + * Parses a record from the tokenizer, and indicates that the record + * should be inserted into the zone. + * @throws IOException The record could not be parsed. + */ +public void +add(Name name, int type, long ttl, Tokenizer tokenizer) throws IOException { + newUpdate(Record.fromString(name, type, dclass, ttl, tokenizer, + origin)); +} + +/** + * Indicates that the record should be inserted into the zone. + */ +public void +add(Record record) { + newUpdate(record); +} + +/** + * Indicates that the records should be inserted into the zone. + */ +public void +add(Record [] records) { + for (int i = 0; i < records.length; i++) + add(records[i]); +} + +/** + * Indicates that all of the records in the rrset should be inserted into the + * zone. + */ +public void +add(RRset rrset) { + for (Iterator it = rrset.rrs(); it.hasNext(); ) + add((Record) it.next()); +} + +/** + * Indicates that all records with the given name should be deleted from + * the zone. + */ +public void +delete(Name name) { + newUpdate(Record.newRecord(name, Type.ANY, DClass.ANY, 0)); +} + +/** + * Indicates that all records with the given name and type should be deleted + * from the zone. + */ +public void +delete(Name name, int type) { + newUpdate(Record.newRecord(name, type, DClass.ANY, 0)); +} + +/** + * Parses a record from the string, and indicates that the record + * should be deleted from the zone. + * @throws IOException The record could not be parsed. + */ +public void +delete(Name name, int type, String record) throws IOException { + newUpdate(Record.fromString(name, type, DClass.NONE, 0, record, + origin)); +} + +/** + * Parses a record from the tokenizer, and indicates that the record + * should be deleted from the zone. + * @throws IOException The record could not be parsed. + */ +public void +delete(Name name, int type, Tokenizer tokenizer) throws IOException { + newUpdate(Record.fromString(name, type, DClass.NONE, 0, tokenizer, + origin)); +} + +/** + * Indicates that the specified record should be deleted from the zone. + */ +public void +delete(Record record) { + newUpdate(record.withDClass(DClass.NONE, 0)); +} + +/** + * Indicates that the records should be deleted from the zone. + */ +public void +delete(Record [] records) { + for (int i = 0; i < records.length; i++) + delete(records[i]); +} + +/** + * Indicates that all of the records in the rrset should be deleted from the + * zone. + */ +public void +delete(RRset rrset) { + for (Iterator it = rrset.rrs(); it.hasNext(); ) + delete((Record) it.next()); +} + +/** + * Parses a record from the string, and indicates that the record + * should be inserted into the zone replacing any other records with the + * same name and type. + * @throws IOException The record could not be parsed. + */ +public void +replace(Name name, int type, long ttl, String record) throws IOException { + delete(name, type); + add(name, type, ttl, record); +} + +/** + * Parses a record from the tokenizer, and indicates that the record + * should be inserted into the zone replacing any other records with the + * same name and type. + * @throws IOException The record could not be parsed. + */ +public void +replace(Name name, int type, long ttl, Tokenizer tokenizer) throws IOException +{ + delete(name, type); + add(name, type, ttl, tokenizer); +} + +/** + * Indicates that the record should be inserted into the zone replacing any + * other records with the same name and type. + */ +public void +replace(Record record) { + delete(record.getName(), record.getType()); + add(record); +} + +/** + * Indicates that the records should be inserted into the zone replacing any + * other records with the same name and type as each one. + */ +public void +replace(Record [] records) { + for (int i = 0; i < records.length; i++) + replace(records[i]); +} + +/** + * Indicates that all of the records in the rrset should be inserted into the + * zone replacing any other records with the same name and type. + */ +public void +replace(RRset rrset) { + delete(rrset.getName(), rrset.getType()); + for (Iterator it = rrset.rrs(); it.hasNext(); ) + add((Record) it.next()); +} + +} diff --git a/src/org/xbill/DNS/WKSRecord.java b/src/org/xbill/DNS/WKSRecord.java new file mode 100644 index 0000000..10b61be --- /dev/null +++ b/src/org/xbill/DNS/WKSRecord.java @@ -0,0 +1,719 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.net.*; +import java.io.*; +import java.util.*; + +/** + * Well Known Services - Lists services offered by this host. + * + * @author Brian Wellington + */ + +public class WKSRecord extends Record { + +private static final long serialVersionUID = -9104259763909119805L; + +public static class Protocol { + /** + * IP protocol identifiers. This is basically copied out of RFC 1010. + */ + + private Protocol() {} + + /** Internet Control Message */ + public static final int ICMP = 1; + + /** Internet Group Management */ + public static final int IGMP = 2; + + /** Gateway-to-Gateway */ + public static final int GGP = 3; + + /** Stream */ + public static final int ST = 5; + + /** Transmission Control */ + public static final int TCP = 6; + + /** UCL */ + public static final int UCL = 7; + + /** Exterior Gateway Protocol */ + public static final int EGP = 8; + + /** any private interior gateway */ + public static final int IGP = 9; + + /** BBN RCC Monitoring */ + public static final int BBN_RCC_MON = 10; + + /** Network Voice Protocol */ + public static final int NVP_II = 11; + + /** PUP */ + public static final int PUP = 12; + + /** ARGUS */ + public static final int ARGUS = 13; + + /** EMCON */ + public static final int EMCON = 14; + + /** Cross Net Debugger */ + public static final int XNET = 15; + + /** Chaos */ + public static final int CHAOS = 16; + + /** User Datagram */ + public static final int UDP = 17; + + /** Multiplexing */ + public static final int MUX = 18; + + /** DCN Measurement Subsystems */ + public static final int DCN_MEAS = 19; + + /** Host Monitoring */ + public static final int HMP = 20; + + /** Packet Radio Measurement */ + public static final int PRM = 21; + + /** XEROX NS IDP */ + public static final int XNS_IDP = 22; + + /** Trunk-1 */ + public static final int TRUNK_1 = 23; + + /** Trunk-2 */ + public static final int TRUNK_2 = 24; + + /** Leaf-1 */ + public static final int LEAF_1 = 25; + + /** Leaf-2 */ + public static final int LEAF_2 = 26; + + /** Reliable Data Protocol */ + public static final int RDP = 27; + + /** Internet Reliable Transaction */ + public static final int IRTP = 28; + + /** ISO Transport Protocol Class 4 */ + public static final int ISO_TP4 = 29; + + /** Bulk Data Transfer Protocol */ + public static final int NETBLT = 30; + + /** MFE Network Services Protocol */ + public static final int MFE_NSP = 31; + + /** MERIT Internodal Protocol */ + public static final int MERIT_INP = 32; + + /** Sequential Exchange Protocol */ + public static final int SEP = 33; + + /** CFTP */ + public static final int CFTP = 62; + + /** SATNET and Backroom EXPAK */ + public static final int SAT_EXPAK = 64; + + /** MIT Subnet Support */ + public static final int MIT_SUBNET = 65; + + /** MIT Remote Virtual Disk Protocol */ + public static final int RVD = 66; + + /** Internet Pluribus Packet Core */ + public static final int IPPC = 67; + + /** SATNET Monitoring */ + public static final int SAT_MON = 69; + + /** Internet Packet Core Utility */ + public static final int IPCV = 71; + + /** Backroom SATNET Monitoring */ + public static final int BR_SAT_MON = 76; + + /** WIDEBAND Monitoring */ + public static final int WB_MON = 78; + + /** WIDEBAND EXPAK */ + public static final int WB_EXPAK = 79; + + private static Mnemonic protocols = new Mnemonic("IP protocol", + Mnemonic.CASE_LOWER); + + static { + protocols.setMaximum(0xFF); + protocols.setNumericAllowed(true); + + protocols.add(ICMP, "icmp"); + protocols.add(IGMP, "igmp"); + protocols.add(GGP, "ggp"); + protocols.add(ST, "st"); + protocols.add(TCP, "tcp"); + protocols.add(UCL, "ucl"); + protocols.add(EGP, "egp"); + protocols.add(IGP, "igp"); + protocols.add(BBN_RCC_MON, "bbn-rcc-mon"); + protocols.add(NVP_II, "nvp-ii"); + protocols.add(PUP, "pup"); + protocols.add(ARGUS, "argus"); + protocols.add(EMCON, "emcon"); + protocols.add(XNET, "xnet"); + protocols.add(CHAOS, "chaos"); + protocols.add(UDP, "udp"); + protocols.add(MUX, "mux"); + protocols.add(DCN_MEAS, "dcn-meas"); + protocols.add(HMP, "hmp"); + protocols.add(PRM, "prm"); + protocols.add(XNS_IDP, "xns-idp"); + protocols.add(TRUNK_1, "trunk-1"); + protocols.add(TRUNK_2, "trunk-2"); + protocols.add(LEAF_1, "leaf-1"); + protocols.add(LEAF_2, "leaf-2"); + protocols.add(RDP, "rdp"); + protocols.add(IRTP, "irtp"); + protocols.add(ISO_TP4, "iso-tp4"); + protocols.add(NETBLT, "netblt"); + protocols.add(MFE_NSP, "mfe-nsp"); + protocols.add(MERIT_INP, "merit-inp"); + protocols.add(SEP, "sep"); + protocols.add(CFTP, "cftp"); + protocols.add(SAT_EXPAK, "sat-expak"); + protocols.add(MIT_SUBNET, "mit-subnet"); + protocols.add(RVD, "rvd"); + protocols.add(IPPC, "ippc"); + protocols.add(SAT_MON, "sat-mon"); + protocols.add(IPCV, "ipcv"); + protocols.add(BR_SAT_MON, "br-sat-mon"); + protocols.add(WB_MON, "wb-mon"); + protocols.add(WB_EXPAK, "wb-expak"); + } + + /** + * Converts an IP protocol value into its textual representation + */ + public static String + string(int type) { + return protocols.getText(type); + } + + /** + * Converts a textual representation of an IP protocol into its + * numeric code. Integers in the range 0..255 are also accepted. + * @param s The textual representation of the protocol + * @return The protocol code, or -1 on error. + */ + public static int + value(String s) { + return protocols.getValue(s); + } +} + +public static class Service { + /** + * TCP/UDP services. This is basically copied out of RFC 1010, + * with MIT-ML-DEV removed, as it is not unique, and the description + * of SWIFT-RVF fixed. + */ + + private Service() {} + + /** Remote Job Entry */ + public static final int RJE = 5; + + /** Echo */ + public static final int ECHO = 7; + + /** Discard */ + public static final int DISCARD = 9; + + /** Active Users */ + public static final int USERS = 11; + + /** Daytime */ + public static final int DAYTIME = 13; + + /** Quote of the Day */ + public static final int QUOTE = 17; + + /** Character Generator */ + public static final int CHARGEN = 19; + + /** File Transfer [Default Data] */ + public static final int FTP_DATA = 20; + + /** File Transfer [Control] */ + public static final int FTP = 21; + + /** Telnet */ + public static final int TELNET = 23; + + /** Simple Mail Transfer */ + public static final int SMTP = 25; + + /** NSW User System FE */ + public static final int NSW_FE = 27; + + /** MSG ICP */ + public static final int MSG_ICP = 29; + + /** MSG Authentication */ + public static final int MSG_AUTH = 31; + + /** Display Support Protocol */ + public static final int DSP = 33; + + /** Time */ + public static final int TIME = 37; + + /** Resource Location Protocol */ + public static final int RLP = 39; + + /** Graphics */ + public static final int GRAPHICS = 41; + + /** Host Name Server */ + public static final int NAMESERVER = 42; + + /** Who Is */ + public static final int NICNAME = 43; + + /** MPM FLAGS Protocol */ + public static final int MPM_FLAGS = 44; + + /** Message Processing Module [recv] */ + public static final int MPM = 45; + + /** MPM [default send] */ + public static final int MPM_SND = 46; + + /** NI FTP */ + public static final int NI_FTP = 47; + + /** Login Host Protocol */ + public static final int LOGIN = 49; + + /** IMP Logical Address Maintenance */ + public static final int LA_MAINT = 51; + + /** Domain Name Server */ + public static final int DOMAIN = 53; + + /** ISI Graphics Language */ + public static final int ISI_GL = 55; + + /** NI MAIL */ + public static final int NI_MAIL = 61; + + /** VIA Systems - FTP */ + public static final int VIA_FTP = 63; + + /** TACACS-Database Service */ + public static final int TACACS_DS = 65; + + /** Bootstrap Protocol Server */ + public static final int BOOTPS = 67; + + /** Bootstrap Protocol Client */ + public static final int BOOTPC = 68; + + /** Trivial File Transfer */ + public static final int TFTP = 69; + + /** Remote Job Service */ + public static final int NETRJS_1 = 71; + + /** Remote Job Service */ + public static final int NETRJS_2 = 72; + + /** Remote Job Service */ + public static final int NETRJS_3 = 73; + + /** Remote Job Service */ + public static final int NETRJS_4 = 74; + + /** Finger */ + public static final int FINGER = 79; + + /** HOSTS2 Name Server */ + public static final int HOSTS2_NS = 81; + + /** SU/MIT Telnet Gateway */ + public static final int SU_MIT_TG = 89; + + /** MIT Dover Spooler */ + public static final int MIT_DOV = 91; + + /** Device Control Protocol */ + public static final int DCP = 93; + + /** SUPDUP */ + public static final int SUPDUP = 95; + + /** Swift Remote Virtual File Protocol */ + public static final int SWIFT_RVF = 97; + + /** TAC News */ + public static final int TACNEWS = 98; + + /** Metagram Relay */ + public static final int METAGRAM = 99; + + /** NIC Host Name Server */ + public static final int HOSTNAME = 101; + + /** ISO-TSAP */ + public static final int ISO_TSAP = 102; + + /** X400 */ + public static final int X400 = 103; + + /** X400-SND */ + public static final int X400_SND = 104; + + /** Mailbox Name Nameserver */ + public static final int CSNET_NS = 105; + + /** Remote Telnet Service */ + public static final int RTELNET = 107; + + /** Post Office Protocol - Version 2 */ + public static final int POP_2 = 109; + + /** SUN Remote Procedure Call */ + public static final int SUNRPC = 111; + + /** Authentication Service */ + public static final int AUTH = 113; + + /** Simple File Transfer Protocol */ + public static final int SFTP = 115; + + /** UUCP Path Service */ + public static final int UUCP_PATH = 117; + + /** Network News Transfer Protocol */ + public static final int NNTP = 119; + + /** HYDRA Expedited Remote Procedure */ + public static final int ERPC = 121; + + /** Network Time Protocol */ + public static final int NTP = 123; + + /** Locus PC-Interface Net Map Server */ + public static final int LOCUS_MAP = 125; + + /** Locus PC-Interface Conn Server */ + public static final int LOCUS_CON = 127; + + /** Password Generator Protocol */ + public static final int PWDGEN = 129; + + /** CISCO FNATIVE */ + public static final int CISCO_FNA = 130; + + /** CISCO TNATIVE */ + public static final int CISCO_TNA = 131; + + /** CISCO SYSMAINT */ + public static final int CISCO_SYS = 132; + + /** Statistics Service */ + public static final int STATSRV = 133; + + /** INGRES-NET Service */ + public static final int INGRES_NET = 134; + + /** Location Service */ + public static final int LOC_SRV = 135; + + /** PROFILE Naming System */ + public static final int PROFILE = 136; + + /** NETBIOS Name Service */ + public static final int NETBIOS_NS = 137; + + /** NETBIOS Datagram Service */ + public static final int NETBIOS_DGM = 138; + + /** NETBIOS Session Service */ + public static final int NETBIOS_SSN = 139; + + /** EMFIS Data Service */ + public static final int EMFIS_DATA = 140; + + /** EMFIS Control Service */ + public static final int EMFIS_CNTL = 141; + + /** Britton-Lee IDM */ + public static final int BL_IDM = 142; + + /** Survey Measurement */ + public static final int SUR_MEAS = 243; + + /** LINK */ + public static final int LINK = 245; + + private static Mnemonic services = new Mnemonic("TCP/UDP service", + Mnemonic.CASE_LOWER); + + static { + services.setMaximum(0xFFFF); + services.setNumericAllowed(true); + + services.add(RJE, "rje"); + services.add(ECHO, "echo"); + services.add(DISCARD, "discard"); + services.add(USERS, "users"); + services.add(DAYTIME, "daytime"); + services.add(QUOTE, "quote"); + services.add(CHARGEN, "chargen"); + services.add(FTP_DATA, "ftp-data"); + services.add(FTP, "ftp"); + services.add(TELNET, "telnet"); + services.add(SMTP, "smtp"); + services.add(NSW_FE, "nsw-fe"); + services.add(MSG_ICP, "msg-icp"); + services.add(MSG_AUTH, "msg-auth"); + services.add(DSP, "dsp"); + services.add(TIME, "time"); + services.add(RLP, "rlp"); + services.add(GRAPHICS, "graphics"); + services.add(NAMESERVER, "nameserver"); + services.add(NICNAME, "nicname"); + services.add(MPM_FLAGS, "mpm-flags"); + services.add(MPM, "mpm"); + services.add(MPM_SND, "mpm-snd"); + services.add(NI_FTP, "ni-ftp"); + services.add(LOGIN, "login"); + services.add(LA_MAINT, "la-maint"); + services.add(DOMAIN, "domain"); + services.add(ISI_GL, "isi-gl"); + services.add(NI_MAIL, "ni-mail"); + services.add(VIA_FTP, "via-ftp"); + services.add(TACACS_DS, "tacacs-ds"); + services.add(BOOTPS, "bootps"); + services.add(BOOTPC, "bootpc"); + services.add(TFTP, "tftp"); + services.add(NETRJS_1, "netrjs-1"); + services.add(NETRJS_2, "netrjs-2"); + services.add(NETRJS_3, "netrjs-3"); + services.add(NETRJS_4, "netrjs-4"); + services.add(FINGER, "finger"); + services.add(HOSTS2_NS, "hosts2-ns"); + services.add(SU_MIT_TG, "su-mit-tg"); + services.add(MIT_DOV, "mit-dov"); + services.add(DCP, "dcp"); + services.add(SUPDUP, "supdup"); + services.add(SWIFT_RVF, "swift-rvf"); + services.add(TACNEWS, "tacnews"); + services.add(METAGRAM, "metagram"); + services.add(HOSTNAME, "hostname"); + services.add(ISO_TSAP, "iso-tsap"); + services.add(X400, "x400"); + services.add(X400_SND, "x400-snd"); + services.add(CSNET_NS, "csnet-ns"); + services.add(RTELNET, "rtelnet"); + services.add(POP_2, "pop-2"); + services.add(SUNRPC, "sunrpc"); + services.add(AUTH, "auth"); + services.add(SFTP, "sftp"); + services.add(UUCP_PATH, "uucp-path"); + services.add(NNTP, "nntp"); + services.add(ERPC, "erpc"); + services.add(NTP, "ntp"); + services.add(LOCUS_MAP, "locus-map"); + services.add(LOCUS_CON, "locus-con"); + services.add(PWDGEN, "pwdgen"); + services.add(CISCO_FNA, "cisco-fna"); + services.add(CISCO_TNA, "cisco-tna"); + services.add(CISCO_SYS, "cisco-sys"); + services.add(STATSRV, "statsrv"); + services.add(INGRES_NET, "ingres-net"); + services.add(LOC_SRV, "loc-srv"); + services.add(PROFILE, "profile"); + services.add(NETBIOS_NS, "netbios-ns"); + services.add(NETBIOS_DGM, "netbios-dgm"); + services.add(NETBIOS_SSN, "netbios-ssn"); + services.add(EMFIS_DATA, "emfis-data"); + services.add(EMFIS_CNTL, "emfis-cntl"); + services.add(BL_IDM, "bl-idm"); + services.add(SUR_MEAS, "sur-meas"); + services.add(LINK, "link"); + } + + /** + * Converts a TCP/UDP service port number into its textual + * representation. + */ + public static String + string(int type) { + return services.getText(type); + } + + /** + * Converts a textual representation of a TCP/UDP service into its + * port number. Integers in the range 0..65535 are also accepted. + * @param s The textual representation of the service. + * @return The port number, or -1 on error. + */ + public static int + value(String s) { + return services.getValue(s); + } +} +private byte [] address; +private int protocol; +private int [] services; + +WKSRecord() {} + +Record +getObject() { + return new WKSRecord(); +} + +/** + * Creates a WKS Record from the given data + * @param address The IP address + * @param protocol The IP protocol number + * @param services An array of supported services, represented by port number. + */ +public +WKSRecord(Name name, int dclass, long ttl, InetAddress address, int protocol, + int [] services) +{ + super(name, Type.WKS, dclass, ttl); + if (Address.familyOf(address) != Address.IPv4) + throw new IllegalArgumentException("invalid IPv4 address"); + this.address = address.getAddress(); + this.protocol = checkU8("protocol", protocol); + for (int i = 0; i < services.length; i++) { + checkU16("service", services[i]); + } + this.services = new int[services.length]; + System.arraycopy(services, 0, this.services, 0, services.length); + Arrays.sort(this.services); +} + +void +rrFromWire(DNSInput in) throws IOException { + address = in.readByteArray(4); + protocol = in.readU8(); + byte [] array = in.readByteArray(); + List list = new ArrayList(); + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < 8; j++) { + int octet = array[i] & 0xFF; + if ((octet & (1 << (7 - j))) != 0) { + list.add(new Integer(i * 8 + j)); + } + } + } + services = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + services[i] = ((Integer) list.get(i)).intValue(); + } +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String s = st.getString(); + address = Address.toByteArray(s, Address.IPv4); + if (address == null) + throw st.exception("invalid address"); + + s = st.getString(); + protocol = Protocol.value(s); + if (protocol < 0) { + throw st.exception("Invalid IP protocol: " + s); + } + + List list = new ArrayList(); + while (true) { + Tokenizer.Token t = st.get(); + if (!t.isString()) + break; + int service = Service.value(t.value); + if (service < 0) { + throw st.exception("Invalid TCP/UDP service: " + + t.value); + } + list.add(new Integer(service)); + } + st.unget(); + services = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + services[i] = ((Integer) list.get(i)).intValue(); + } +} + +/** + * Converts rdata to a String + */ +String +rrToString() { + StringBuffer sb = new StringBuffer(); + sb.append(Address.toDottedQuad(address)); + sb.append(" "); + sb.append(protocol); + for (int i = 0; i < services.length; i++) { + sb.append(" " + services[i]); + } + return sb.toString(); +} + +/** + * Returns the IP address. + */ +public InetAddress +getAddress() { + try { + return InetAddress.getByAddress(address); + } catch (UnknownHostException e) { + return null; + } +} + +/** + * Returns the IP protocol. + */ +public int +getProtocol() { + return protocol; +} + +/** + * Returns the services provided by the host on the specified address. + */ +public int [] +getServices() { + return services; +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeByteArray(address); + out.writeU8(protocol); + int highestPort = services[services.length - 1]; + byte [] array = new byte[highestPort / 8 + 1]; + for (int i = 0; i < services.length; i++) { + int port = services[i]; + array[port / 8] |= (1 << (7 - port % 8)); + } + out.writeByteArray(array); +} + +} diff --git a/src/org/xbill/DNS/WireParseException.java b/src/org/xbill/DNS/WireParseException.java new file mode 100644 index 0000000..2842731 --- /dev/null +++ b/src/org/xbill/DNS/WireParseException.java @@ -0,0 +1,31 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * An exception thrown when a DNS message is invalid. + * + * @author Brian Wellington + */ + +public class WireParseException extends IOException { + +public +WireParseException() { + super(); +} + +public +WireParseException(String s) { + super(s); +} + +public +WireParseException(String s, Throwable cause) { + super(s); + initCause(cause); +} + +} diff --git a/src/org/xbill/DNS/X25Record.java b/src/org/xbill/DNS/X25Record.java new file mode 100644 index 0000000..1349a1e --- /dev/null +++ b/src/org/xbill/DNS/X25Record.java @@ -0,0 +1,86 @@ +// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; + +/** + * X25 - identifies the PSDN (Public Switched Data Network) address in the + * X.121 numbering plan associated with a name. + * + * @author Brian Wellington + */ + +public class X25Record extends Record { + +private static final long serialVersionUID = 4267576252335579764L; + +private byte [] address; + +X25Record() {} + +Record +getObject() { + return new X25Record(); +} + +private static final byte [] +checkAndConvertAddress(String address) { + int length = address.length(); + byte [] out = new byte [length]; + for (int i = 0; i < length; i++) { + char c = address.charAt(i); + if (!Character.isDigit(c)) + return null; + out[i] = (byte) c; + } + return out; +} + +/** + * Creates an X25 Record from the given data + * @param address The X.25 PSDN address. + * @throws IllegalArgumentException The address is not a valid PSDN address. + */ +public +X25Record(Name name, int dclass, long ttl, String address) { + super(name, Type.X25, dclass, ttl); + this.address = checkAndConvertAddress(address); + if (this.address == null) { + throw new IllegalArgumentException("invalid PSDN address " + + address); + } +} + +void +rrFromWire(DNSInput in) throws IOException { + address = in.readCountedString(); +} + +void +rdataFromString(Tokenizer st, Name origin) throws IOException { + String addr = st.getString(); + this.address = checkAndConvertAddress(addr); + if (this.address == null) + throw st.exception("invalid PSDN address " + addr); +} + +/** + * Returns the X.25 PSDN address. + */ +public String +getAddress() { + return byteArrayToString(address, false); +} + +void +rrToWire(DNSOutput out, Compression c, boolean canonical) { + out.writeCountedString(address); +} + +String +rrToString() { + return byteArrayToString(address, true); +} + +} diff --git a/src/org/xbill/DNS/Zone.java b/src/org/xbill/DNS/Zone.java new file mode 100644 index 0000000..866be77 --- /dev/null +++ b/src/org/xbill/DNS/Zone.java @@ -0,0 +1,559 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +import java.io.*; +import java.util.*; + +/** + * A DNS Zone. This encapsulates all data related to a Zone, and provides + * convenient lookup methods. + * + * @author Brian Wellington + */ + +public class Zone implements Serializable { + +private static final long serialVersionUID = -9220510891189510942L; + +/** A primary zone */ +public static final int PRIMARY = 1; + +/** A secondary zone */ +public static final int SECONDARY = 2; + +private Map data; +private Name origin; +private Object originNode; +private int dclass = DClass.IN; +private RRset NS; +private SOARecord SOA; +private boolean hasWild; + +class ZoneIterator implements Iterator { + private Iterator zentries; + private RRset [] current; + private int count; + private boolean wantLastSOA; + + ZoneIterator(boolean axfr) { + synchronized (Zone.this) { + zentries = data.entrySet().iterator(); + } + wantLastSOA = axfr; + RRset [] sets = allRRsets(originNode); + current = new RRset[sets.length]; + for (int i = 0, j = 2; i < sets.length; i++) { + int type = sets[i].getType(); + if (type == Type.SOA) + current[0] = sets[i]; + else if (type == Type.NS) + current[1] = sets[i]; + else + current[j++] = sets[i]; + } + } + + public boolean + hasNext() { + return (current != null || wantLastSOA); + } + + public Object + next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + if (current == null) { + wantLastSOA = false; + return oneRRset(originNode, Type.SOA); + } + Object set = current[count++]; + if (count == current.length) { + current = null; + while (zentries.hasNext()) { + Map.Entry entry = (Map.Entry) zentries.next(); + if (entry.getKey().equals(origin)) + continue; + RRset [] sets = allRRsets(entry.getValue()); + if (sets.length == 0) + continue; + current = sets; + count = 0; + break; + } + } + return set; + } + + public void + remove() { + throw new UnsupportedOperationException(); + } +} + +private void +validate() throws IOException { + originNode = exactName(origin); + if (originNode == null) + throw new IOException(origin + ": no data specified"); + + RRset rrset = oneRRset(originNode, Type.SOA); + if (rrset == null || rrset.size() != 1) + throw new IOException(origin + + ": exactly 1 SOA must be specified"); + Iterator it = rrset.rrs(); + SOA = (SOARecord) it.next(); + + NS = oneRRset(originNode, Type.NS); + if (NS == null) + throw new IOException(origin + ": no NS set specified"); +} + +private final void +maybeAddRecord(Record record) throws IOException { + int rtype = record.getType(); + Name name = record.getName(); + + if (rtype == Type.SOA && !name.equals(origin)) { + throw new IOException("SOA owner " + name + + " does not match zone origin " + + origin); + } + if (name.subdomain(origin)) + addRecord(record); +} + +/** + * Creates a Zone from the records in the specified master file. + * @param zone The name of the zone. + * @param file The master file to read from. + * @see Master + */ +public +Zone(Name zone, String file) throws IOException { + data = new TreeMap(); + + if (zone == null) + throw new IllegalArgumentException("no zone name specified"); + Master m = new Master(file, zone); + Record record; + + origin = zone; + while ((record = m.nextRecord()) != null) + maybeAddRecord(record); + validate(); +} + +/** + * Creates a Zone from an array of records. + * @param zone The name of the zone. + * @param records The records to add to the zone. + * @see Master + */ +public +Zone(Name zone, Record [] records) throws IOException { + data = new TreeMap(); + + if (zone == null) + throw new IllegalArgumentException("no zone name specified"); + origin = zone; + for (int i = 0; i < records.length; i++) + maybeAddRecord(records[i]); + validate(); +} + +private void +fromXFR(ZoneTransferIn xfrin) throws IOException, ZoneTransferException { + data = new TreeMap(); + + origin = xfrin.getName(); + List records = xfrin.run(); + for (Iterator it = records.iterator(); it.hasNext(); ) { + Record record = (Record) it.next(); + maybeAddRecord(record); + } + if (!xfrin.isAXFR()) + throw new IllegalArgumentException("zones can only be " + + "created from AXFRs"); + validate(); +} + +/** + * Creates a Zone by doing the specified zone transfer. + * @param xfrin The incoming zone transfer to execute. + * @see ZoneTransferIn + */ +public +Zone(ZoneTransferIn xfrin) throws IOException, ZoneTransferException { + fromXFR(xfrin); +} + +/** + * Creates a Zone by performing a zone transfer to the specified host. + * @see ZoneTransferIn + */ +public +Zone(Name zone, int dclass, String remote) +throws IOException, ZoneTransferException +{ + ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(zone, remote, null); + xfrin.setDClass(dclass); + fromXFR(xfrin); +} + +/** Returns the Zone's origin */ +public Name +getOrigin() { + return origin; +} + +/** Returns the Zone origin's NS records */ +public RRset +getNS() { + return NS; +} + +/** Returns the Zone's SOA record */ +public SOARecord +getSOA() { + return SOA; +} + +/** Returns the Zone's class */ +public int +getDClass() { + return dclass; +} + +private synchronized Object +exactName(Name name) { + return data.get(name); +} + +private synchronized RRset [] +allRRsets(Object types) { + if (types instanceof List) { + List typelist = (List) types; + return (RRset []) typelist.toArray(new RRset[typelist.size()]); + } else { + RRset set = (RRset) types; + return new RRset [] {set}; + } +} + +private synchronized RRset +oneRRset(Object types, int type) { + if (type == Type.ANY) + throw new IllegalArgumentException("oneRRset(ANY)"); + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + RRset set = (RRset) list.get(i); + if (set.getType() == type) + return set; + } + } else { + RRset set = (RRset) types; + if (set.getType() == type) + return set; + } + return null; +} + +private synchronized RRset +findRRset(Name name, int type) { + Object types = exactName(name); + if (types == null) + return null; + return oneRRset(types, type); +} + +private synchronized void +addRRset(Name name, RRset rrset) { + if (!hasWild && name.isWild()) + hasWild = true; + Object types = data.get(name); + if (types == null) { + data.put(name, rrset); + return; + } + int rtype = rrset.getType(); + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + RRset set = (RRset) list.get(i); + if (set.getType() == rtype) { + list.set(i, rrset); + return; + } + } + list.add(rrset); + } else { + RRset set = (RRset) types; + if (set.getType() == rtype) + data.put(name, rrset); + else { + LinkedList list = new LinkedList(); + list.add(set); + list.add(rrset); + data.put(name, list); + } + } +} + +private synchronized void +removeRRset(Name name, int type) { + Object types = data.get(name); + if (types == null) { + return; + } + if (types instanceof List) { + List list = (List) types; + for (int i = 0; i < list.size(); i++) { + RRset set = (RRset) list.get(i); + if (set.getType() == type) { + list.remove(i); + if (list.size() == 0) + data.remove(name); + return; + } + } + } else { + RRset set = (RRset) types; + if (set.getType() != type) + return; + data.remove(name); + } +} + +private synchronized SetResponse +lookup(Name name, int type) { + int labels; + int olabels; + int tlabels; + RRset rrset; + Name tname; + Object types; + SetResponse sr; + + if (!name.subdomain(origin)) + return SetResponse.ofType(SetResponse.NXDOMAIN); + + labels = name.labels(); + olabels = origin.labels(); + + for (tlabels = olabels; tlabels <= labels; tlabels++) { + boolean isOrigin = (tlabels == olabels); + boolean isExact = (tlabels == labels); + + if (isOrigin) + tname = origin; + else if (isExact) + tname = name; + else + tname = new Name(name, labels - tlabels); + + types = exactName(tname); + if (types == null) + continue; + + /* If this is a delegation, return that. */ + if (!isOrigin) { + RRset ns = oneRRset(types, Type.NS); + if (ns != null) + return new SetResponse(SetResponse.DELEGATION, + ns); + } + + /* If this is an ANY lookup, return everything. */ + if (isExact && type == Type.ANY) { + sr = new SetResponse(SetResponse.SUCCESSFUL); + RRset [] sets = allRRsets(types); + for (int i = 0; i < sets.length; i++) + sr.addRRset(sets[i]); + return sr; + } + + /* + * If this is the name, look for the actual type or a CNAME. + * Otherwise, look for a DNAME. + */ + if (isExact) { + rrset = oneRRset(types, type); + if (rrset != null) { + sr = new SetResponse(SetResponse.SUCCESSFUL); + sr.addRRset(rrset); + return sr; + } + rrset = oneRRset(types, Type.CNAME); + if (rrset != null) + return new SetResponse(SetResponse.CNAME, + rrset); + } else { + rrset = oneRRset(types, Type.DNAME); + if (rrset != null) + return new SetResponse(SetResponse.DNAME, + rrset); + } + + /* We found the name, but not the type. */ + if (isExact) + return SetResponse.ofType(SetResponse.NXRRSET); + } + + if (hasWild) { + for (int i = 0; i < labels - olabels; i++) { + tname = name.wild(i + 1); + + types = exactName(tname); + if (types == null) + continue; + + rrset = oneRRset(types, type); + if (rrset != null) { + sr = new SetResponse(SetResponse.SUCCESSFUL); + sr.addRRset(rrset); + return sr; + } + } + } + + return SetResponse.ofType(SetResponse.NXDOMAIN); +} + +/** + * Looks up Records in the Zone. This follows CNAMEs and wildcards. + * @param name The name to look up + * @param type The type to look up + * @return A SetResponse object + * @see SetResponse + */ +public SetResponse +findRecords(Name name, int type) { + return lookup(name, type); +} + +/** + * Looks up Records in the zone, finding exact matches only. + * @param name The name to look up + * @param type The type to look up + * @return The matching RRset + * @see RRset + */ +public RRset +findExactMatch(Name name, int type) { + Object types = exactName(name); + if (types == null) + return null; + return oneRRset(types, type); +} + +/** + * Adds an RRset to the Zone + * @param rrset The RRset to be added + * @see RRset + */ +public void +addRRset(RRset rrset) { + Name name = rrset.getName(); + addRRset(name, rrset); +} + +/** + * Adds a Record to the Zone + * @param r The record to be added + * @see Record + */ +public void +addRecord(Record r) { + Name name = r.getName(); + int rtype = r.getRRsetType(); + synchronized (this) { + RRset rrset = findRRset(name, rtype); + if (rrset == null) { + rrset = new RRset(r); + addRRset(name, rrset); + } else { + rrset.addRR(r); + } + } +} + +/** + * Removes a record from the Zone + * @param r The record to be removed + * @see Record + */ +public void +removeRecord(Record r) { + Name name = r.getName(); + int rtype = r.getRRsetType(); + synchronized (this) { + RRset rrset = findRRset(name, rtype); + if (rrset == null) + return; + if (rrset.size() == 1 && rrset.first().equals(r)) + removeRRset(name, rtype); + else + rrset.deleteRR(r); + } +} + +/** + * Returns an Iterator over the RRsets in the zone. + */ +public Iterator +iterator() { + return new ZoneIterator(false); +} + +/** + * Returns an Iterator over the RRsets in the zone that can be used to + * construct an AXFR response. This is identical to {@link #iterator} except + * that the SOA is returned at the end as well as the beginning. + */ +public Iterator +AXFR() { + return new ZoneIterator(true); +} + +private void +nodeToString(StringBuffer sb, Object node) { + RRset [] sets = allRRsets(node); + for (int i = 0; i < sets.length; i++) { + RRset rrset = sets[i]; + Iterator it = rrset.rrs(); + while (it.hasNext()) + sb.append(it.next() + "\n"); + it = rrset.sigs(); + while (it.hasNext()) + sb.append(it.next() + "\n"); + } +} + +/** + * Returns the contents of the Zone in master file format. + */ +public synchronized String +toMasterFile() { + Iterator zentries = data.entrySet().iterator(); + StringBuffer sb = new StringBuffer(); + nodeToString(sb, originNode); + while (zentries.hasNext()) { + Map.Entry entry = (Map.Entry) zentries.next(); + if (!origin.equals(entry.getKey())) + nodeToString(sb, entry.getValue()); + } + return sb.toString(); +} + +/** + * Returns the contents of the Zone as a string (in master file format). + */ +public String +toString() { + return toMasterFile(); +} + +} diff --git a/src/org/xbill/DNS/ZoneTransferException.java b/src/org/xbill/DNS/ZoneTransferException.java new file mode 100644 index 0000000..3ba487b --- /dev/null +++ b/src/org/xbill/DNS/ZoneTransferException.java @@ -0,0 +1,23 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS; + +/** + * An exception thrown when a zone transfer fails. + * + * @author Brian Wellington + */ + +public class ZoneTransferException extends Exception { + +public +ZoneTransferException() { + super(); +} + +public +ZoneTransferException(String s) { + super(s); +} + +} diff --git a/src/org/xbill/DNS/ZoneTransferIn.java b/src/org/xbill/DNS/ZoneTransferIn.java new file mode 100644 index 0000000..8a19992 --- /dev/null +++ b/src/org/xbill/DNS/ZoneTransferIn.java @@ -0,0 +1,680 @@ +// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org) +// Parts of this are derived from lib/dns/xfrin.c from BIND 9; its copyright +// notice follows. + +/* + * Copyright (C) 1999-2001 Internet Software Consortium. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM + * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL + * INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING + * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +package org.xbill.DNS; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * An incoming DNS Zone Transfer. To use this class, first initialize an + * object, then call the run() method. If run() doesn't throw an exception + * the result will either be an IXFR-style response, an AXFR-style response, + * or an indication that the zone is up to date. + * + * @author Brian Wellington + */ + +public class ZoneTransferIn { + +private static final int INITIALSOA = 0; +private static final int FIRSTDATA = 1; +private static final int IXFR_DELSOA = 2; +private static final int IXFR_DEL = 3; +private static final int IXFR_ADDSOA = 4; +private static final int IXFR_ADD = 5; +private static final int AXFR = 6; +private static final int END = 7; + +private Name zname; +private int qtype; +private int dclass; +private long ixfr_serial; +private boolean want_fallback; +private ZoneTransferHandler handler; + +private SocketAddress localAddress; +private SocketAddress address; +private TCPClient client; +private TSIG tsig; +private TSIG.StreamVerifier verifier; +private long timeout = 900 * 1000; + +private int state; +private long end_serial; +private long current_serial; +private Record initialsoa; + +private int rtype; + +public static class Delta { + /** + * All changes between two versions of a zone in an IXFR response. + */ + + /** The starting serial number of this delta. */ + public long start; + + /** The ending serial number of this delta. */ + public long end; + + /** A list of records added between the start and end versions */ + public List adds; + + /** A list of records deleted between the start and end versions */ + public List deletes; + + private + Delta() { + adds = new ArrayList(); + deletes = new ArrayList(); + } +} + +public static interface ZoneTransferHandler { + /** + * Handles a Zone Transfer. + */ + + /** + * Called when an AXFR transfer begins. + */ + public void startAXFR() throws ZoneTransferException; + + /** + * Called when an IXFR transfer begins. + */ + public void startIXFR() throws ZoneTransferException; + + /** + * Called when a series of IXFR deletions begins. + * @param soa The starting SOA. + */ + public void startIXFRDeletes(Record soa) throws ZoneTransferException; + + /** + * Called when a series of IXFR adds begins. + * @param soa The starting SOA. + */ + public void startIXFRAdds(Record soa) throws ZoneTransferException; + + /** + * Called for each content record in an AXFR. + * @param r The DNS record. + */ + public void handleRecord(Record r) throws ZoneTransferException; +}; + +private static class BasicHandler implements ZoneTransferHandler { + private List axfr; + private List ixfr; + + public void startAXFR() { + axfr = new ArrayList(); + } + + public void startIXFR() { + ixfr = new ArrayList(); + } + + public void startIXFRDeletes(Record soa) { + Delta delta = new Delta(); + delta.deletes.add(soa); + delta.start = getSOASerial(soa); + ixfr.add(delta); + } + + public void startIXFRAdds(Record soa) { + Delta delta = (Delta) ixfr.get(ixfr.size() - 1); + delta.adds.add(soa); + delta.end = getSOASerial(soa); + } + + public void handleRecord(Record r) { + List list; + if (ixfr != null) { + Delta delta = (Delta) ixfr.get(ixfr.size() - 1); + if (delta.adds.size() > 0) + list = delta.adds; + else + list = delta.deletes; + } else + list = axfr; + list.add(r); + } +}; + +private +ZoneTransferIn() {} + +private +ZoneTransferIn(Name zone, int xfrtype, long serial, boolean fallback, + SocketAddress address, TSIG key) +{ + this.address = address; + this.tsig = key; + if (zone.isAbsolute()) + zname = zone; + else { + try { + zname = Name.concatenate(zone, Name.root); + } + catch (NameTooLongException e) { + throw new IllegalArgumentException("ZoneTransferIn: " + + "name too long"); + } + } + qtype = xfrtype; + dclass = DClass.IN; + ixfr_serial = serial; + want_fallback = fallback; + state = INITIALSOA; +} + +/** + * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer). + * @param zone The zone to transfer. + * @param address The host/port from which to transfer the zone. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newAXFR(Name zone, SocketAddress address, TSIG key) { + return new ZoneTransferIn(zone, Type.AXFR, 0, false, address, key); +} + +/** + * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer). + * @param zone The zone to transfer. + * @param host The host from which to transfer the zone. + * @param port The port to connect to on the server, or 0 for the default. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newAXFR(Name zone, String host, int port, TSIG key) +throws UnknownHostException +{ + if (port == 0) + port = SimpleResolver.DEFAULT_PORT; + return newAXFR(zone, new InetSocketAddress(host, port), key); +} + +/** + * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer). + * @param zone The zone to transfer. + * @param host The host from which to transfer the zone. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newAXFR(Name zone, String host, TSIG key) +throws UnknownHostException +{ + return newAXFR(zone, host, 0, key); +} + +/** + * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone + * transfer). + * @param zone The zone to transfer. + * @param serial The existing serial number. + * @param fallback If true, fall back to AXFR if IXFR is not supported. + * @param address The host/port from which to transfer the zone. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newIXFR(Name zone, long serial, boolean fallback, SocketAddress address, + TSIG key) +{ + return new ZoneTransferIn(zone, Type.IXFR, serial, fallback, address, + key); +} + +/** + * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone + * transfer). + * @param zone The zone to transfer. + * @param serial The existing serial number. + * @param fallback If true, fall back to AXFR if IXFR is not supported. + * @param host The host from which to transfer the zone. + * @param port The port to connect to on the server, or 0 for the default. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newIXFR(Name zone, long serial, boolean fallback, String host, int port, + TSIG key) +throws UnknownHostException +{ + if (port == 0) + port = SimpleResolver.DEFAULT_PORT; + return newIXFR(zone, serial, fallback, + new InetSocketAddress(host, port), key); +} + +/** + * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone + * transfer). + * @param zone The zone to transfer. + * @param serial The existing serial number. + * @param fallback If true, fall back to AXFR if IXFR is not supported. + * @param host The host from which to transfer the zone. + * @param key The TSIG key used to authenticate the transfer, or null. + * @return The ZoneTransferIn object. + * @throws UnknownHostException The host does not exist. + */ +public static ZoneTransferIn +newIXFR(Name zone, long serial, boolean fallback, String host, TSIG key) +throws UnknownHostException +{ + return newIXFR(zone, serial, fallback, host, 0, key); +} + +/** + * Gets the name of the zone being transferred. + */ +public Name +getName() { + return zname; +} + +/** + * Gets the type of zone transfer (either AXFR or IXFR). + */ +public int +getType() { + return qtype; +} + +/** + * Sets a timeout on this zone transfer. The default is 900 seconds (15 + * minutes). + * @param secs The maximum amount of time that this zone transfer can take. + */ +public void +setTimeout(int secs) { + if (secs < 0) + throw new IllegalArgumentException("timeout cannot be " + + "negative"); + timeout = 1000L * secs; +} + +/** + * Sets an alternate DNS class for this zone transfer. + * @param dclass The class to use instead of class IN. + */ +public void +setDClass(int dclass) { + DClass.check(dclass); + this.dclass = dclass; +} + +/** + * Sets the local address to bind to when sending messages. + * @param addr The local address to send messages from. + */ +public void +setLocalAddress(SocketAddress addr) { + this.localAddress = addr; +} + +private void +openConnection() throws IOException { + long endTime = System.currentTimeMillis() + timeout; + client = new TCPClient(endTime); + if (localAddress != null) + client.bind(localAddress); + client.connect(address); +} + +private void +sendQuery() throws IOException { + Record question = Record.newRecord(zname, qtype, dclass); + + Message query = new Message(); + query.getHeader().setOpcode(Opcode.QUERY); + query.addRecord(question, Section.QUESTION); + if (qtype == Type.IXFR) { + Record soa = new SOARecord(zname, dclass, 0, Name.root, + Name.root, ixfr_serial, + 0, 0, 0, 0); + query.addRecord(soa, Section.AUTHORITY); + } + if (tsig != null) { + tsig.apply(query, null); + verifier = new TSIG.StreamVerifier(tsig, query.getTSIG()); + } + byte [] out = query.toWire(Message.MAXLENGTH); + client.send(out); +} + +private static long +getSOASerial(Record rec) { + SOARecord soa = (SOARecord) rec; + return soa.getSerial(); +} + +private void +logxfr(String s) { + if (Options.check("verbose")) + System.out.println(zname + ": " + s); +} + +private void +fail(String s) throws ZoneTransferException { + throw new ZoneTransferException(s); +} + +private void +fallback() throws ZoneTransferException { + if (!want_fallback) + fail("server doesn't support IXFR"); + + logxfr("falling back to AXFR"); + qtype = Type.AXFR; + state = INITIALSOA; +} + +private void +parseRR(Record rec) throws ZoneTransferException { + int type = rec.getType(); + Delta delta; + + switch (state) { + case INITIALSOA: + if (type != Type.SOA) + fail("missing initial SOA"); + initialsoa = rec; + // Remember the serial number in the initial SOA; we need it + // to recognize the end of an IXFR. + end_serial = getSOASerial(rec); + if (qtype == Type.IXFR && + Serial.compare(end_serial, ixfr_serial) <= 0) + { + logxfr("up to date"); + state = END; + break; + } + state = FIRSTDATA; + break; + + case FIRSTDATA: + // If the transfer begins with 1 SOA, it's an AXFR. + // If it begins with 2 SOAs, it's an IXFR. + if (qtype == Type.IXFR && type == Type.SOA && + getSOASerial(rec) == ixfr_serial) + { + rtype = Type.IXFR; + handler.startIXFR(); + logxfr("got incremental response"); + state = IXFR_DELSOA; + } else { + rtype = Type.AXFR; + handler.startAXFR(); + handler.handleRecord(initialsoa); + logxfr("got nonincremental response"); + state = AXFR; + } + parseRR(rec); // Restart... + return; + + case IXFR_DELSOA: + handler.startIXFRDeletes(rec); + state = IXFR_DEL; + break; + + case IXFR_DEL: + if (type == Type.SOA) { + current_serial = getSOASerial(rec); + state = IXFR_ADDSOA; + parseRR(rec); // Restart... + return; + } + handler.handleRecord(rec); + break; + + case IXFR_ADDSOA: + handler.startIXFRAdds(rec); + state = IXFR_ADD; + break; + + case IXFR_ADD: + if (type == Type.SOA) { + long soa_serial = getSOASerial(rec); + if (soa_serial == end_serial) { + state = END; + break; + } else if (soa_serial != current_serial) { + fail("IXFR out of sync: expected serial " + + current_serial + " , got " + soa_serial); + } else { + state = IXFR_DELSOA; + parseRR(rec); // Restart... + return; + } + } + handler.handleRecord(rec); + break; + + case AXFR: + // Old BINDs sent cross class A records for non IN classes. + if (type == Type.A && rec.getDClass() != dclass) + break; + handler.handleRecord(rec); + if (type == Type.SOA) { + state = END; + } + break; + + case END: + fail("extra data"); + break; + + default: + fail("invalid state"); + break; + } +} + +private void +closeConnection() { + try { + if (client != null) + client.cleanup(); + } + catch (IOException e) { + } +} + +private Message +parseMessage(byte [] b) throws WireParseException { + try { + return new Message(b); + } + catch (IOException e) { + if (e instanceof WireParseException) + throw (WireParseException) e; + throw new WireParseException("Error parsing message"); + } +} + +private void +doxfr() throws IOException, ZoneTransferException { + sendQuery(); + while (state != END) { + byte [] in = client.recv(); + Message response = parseMessage(in); + if (response.getHeader().getRcode() == Rcode.NOERROR && + verifier != null) + { + TSIGRecord tsigrec = response.getTSIG(); + + int error = verifier.verify(response, in); + if (error != Rcode.NOERROR) + fail("TSIG failure"); + } + + Record [] answers = response.getSectionArray(Section.ANSWER); + + if (state == INITIALSOA) { + int rcode = response.getRcode(); + if (rcode != Rcode.NOERROR) { + if (qtype == Type.IXFR && + rcode == Rcode.NOTIMP) + { + fallback(); + doxfr(); + return; + } + fail(Rcode.string(rcode)); + } + + Record question = response.getQuestion(); + if (question != null && question.getType() != qtype) { + fail("invalid question section"); + } + + if (answers.length == 0 && qtype == Type.IXFR) { + fallback(); + doxfr(); + return; + } + } + + for (int i = 0; i < answers.length; i++) { + parseRR(answers[i]); + } + + if (state == END && verifier != null && + !response.isVerified()) + fail("last message must be signed"); + } +} + +/** + * Does the zone transfer. + * @param handler The callback object that handles the zone transfer data. + * @throws IOException The zone transfer failed to due an IO problem. + * @throws ZoneTransferException The zone transfer failed to due a problem + * with the zone transfer itself. + */ +public void +run(ZoneTransferHandler handler) throws IOException, ZoneTransferException { + this.handler = handler; + try { + openConnection(); + doxfr(); + } + finally { + closeConnection(); + } +} + +/** + * Does the zone transfer. + * @return A list, which is either an AXFR-style response (List of Records), + * and IXFR-style response (List of Deltas), or null, which indicates that + * an IXFR was performed and the zone is up to date. + * @throws IOException The zone transfer failed to due an IO problem. + * @throws ZoneTransferException The zone transfer failed to due a problem + * with the zone transfer itself. + */ +public List +run() throws IOException, ZoneTransferException { + BasicHandler handler = new BasicHandler(); + run(handler); + if (handler.axfr != null) + return handler.axfr; + return handler.ixfr; +} + +private BasicHandler +getBasicHandler() throws IllegalArgumentException { + if (handler instanceof BasicHandler) + return (BasicHandler) handler; + throw new IllegalArgumentException("ZoneTransferIn used callback " + + "interface"); +} + +/** + * Returns true if the response is an AXFR-style response (List of Records). + * This will be true if either an IXFR was performed, an IXFR was performed + * and the server provided a full zone transfer, or an IXFR failed and + * fallback to AXFR occurred. + */ +public boolean +isAXFR() { + return (rtype == Type.AXFR); +} + +/** + * Gets the AXFR-style response. + * @throws IllegalArgumentException The transfer used the callback interface, + * so the response was not stored. + */ +public List +getAXFR() { + BasicHandler handler = getBasicHandler(); + return handler.axfr; +} + +/** + * Returns true if the response is an IXFR-style response (List of Deltas). + * This will be true only if an IXFR was performed and the server provided + * an incremental zone transfer. + */ +public boolean +isIXFR() { + return (rtype == Type.IXFR); +} + +/** + * Gets the IXFR-style response. + * @throws IllegalArgumentException The transfer used the callback interface, + * so the response was not stored. + */ +public List +getIXFR() { + BasicHandler handler = getBasicHandler(); + return handler.ixfr; +} + +/** + * Returns true if the response indicates that the zone is up to date. + * This will be true only if an IXFR was performed. + * @throws IllegalArgumentException The transfer used the callback interface, + * so the response was not stored. + */ +public boolean +isCurrent() { + BasicHandler handler = getBasicHandler(); + return (handler.axfr == null && handler.ixfr == null); +} + +} diff --git a/src/org/xbill/DNS/spi/DNSJavaNameService.java b/src/org/xbill/DNS/spi/DNSJavaNameService.java new file mode 100644 index 0000000..14d8adb --- /dev/null +++ b/src/org/xbill/DNS/spi/DNSJavaNameService.java @@ -0,0 +1,176 @@ +// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.spi; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.StringTokenizer; + +import org.xbill.DNS.AAAARecord; +import org.xbill.DNS.ARecord; +import org.xbill.DNS.ExtendedResolver; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Name; +import org.xbill.DNS.PTRRecord; +import org.xbill.DNS.Record; +import org.xbill.DNS.Resolver; +import org.xbill.DNS.ReverseMap; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +/** + * This class implements a Name Service Provider, which Java can use + * (starting with version 1.4), to perform DNS resolutions instead of using + * the standard calls. + * <p> + * This Name Service Provider uses dnsjava. + * <p> + * To use this provider, you must set the following system property: + * <b>sun.net.spi.nameservice.provider.1=dns,dnsjava</b> + * + * @author Brian Wellington + * @author Paul Cowan (pwc21@yahoo.com) + */ + +public class DNSJavaNameService implements InvocationHandler { + +private static final String nsProperty = "sun.net.spi.nameservice.nameservers"; +private static final String domainProperty = "sun.net.spi.nameservice.domain"; +private static final String v6Property = "java.net.preferIPv6Addresses"; + +private boolean preferV6 = false; + +/** + * Creates a DNSJavaNameService instance. + * <p> + * Uses the + * <b>sun.net.spi.nameservice.nameservers</b>, + * <b>sun.net.spi.nameservice.domain</b>, and + * <b>java.net.preferIPv6Addresses</b> properties for configuration. + */ +protected +DNSJavaNameService() { + String nameServers = System.getProperty(nsProperty); + String domain = System.getProperty(domainProperty); + String v6 = System.getProperty(v6Property); + + if (nameServers != null) { + StringTokenizer st = new StringTokenizer(nameServers, ","); + String [] servers = new String[st.countTokens()]; + int n = 0; + while (st.hasMoreTokens()) + servers[n++] = st.nextToken(); + try { + Resolver res = new ExtendedResolver(servers); + Lookup.setDefaultResolver(res); + } + catch (UnknownHostException e) { + System.err.println("DNSJavaNameService: invalid " + + nsProperty); + } + } + + if (domain != null) { + try { + Lookup.setDefaultSearchPath(new String[] {domain}); + } + catch (TextParseException e) { + System.err.println("DNSJavaNameService: invalid " + + domainProperty); + } + } + + if (v6 != null && v6.equalsIgnoreCase("true")) + preferV6 = true; +} + + +public Object +invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (method.getName().equals("getHostByAddr")) { + return this.getHostByAddr((byte[]) args[0]); + } else if (method.getName().equals("lookupAllHostAddr")) { + InetAddress[] addresses; + addresses = this.lookupAllHostAddr((String) args[0]); + Class returnType = method.getReturnType(); + if (returnType.equals(InetAddress[].class)) { + // method for Java >= 1.6 + return addresses; + } else if (returnType.equals(byte[][].class)) { + // method for Java <= 1.5 + int naddrs = addresses.length; + byte [][] byteAddresses = new byte[naddrs][]; + byte [] addr; + for (int i = 0; i < naddrs; i++) { + addr = addresses[i].getAddress(); + byteAddresses[i] = addr; + } + return byteAddresses; + } + } + } catch (Throwable e) { + System.err.println("DNSJavaNameService: Unexpected error."); + e.printStackTrace(); + throw e; + } + throw new IllegalArgumentException( + "Unknown function name or arguments."); +} + +/** + * Performs a forward DNS lookup for the host name. + * @param host The host name to resolve. + * @return All the ip addresses found for the host name. + */ +public InetAddress [] +lookupAllHostAddr(String host) throws UnknownHostException { + Name name = null; + + try { + name = new Name(host); + } + catch (TextParseException e) { + throw new UnknownHostException(host); + } + + Record [] records = null; + if (preferV6) + records = new Lookup(name, Type.AAAA).run(); + if (records == null) + records = new Lookup(name, Type.A).run(); + if (records == null && !preferV6) + records = new Lookup(name, Type.AAAA).run(); + if (records == null) + throw new UnknownHostException(host); + + InetAddress[] array = new InetAddress[records.length]; + for (int i = 0; i < records.length; i++) { + Record record = records[i]; + if (records[i] instanceof ARecord) { + ARecord a = (ARecord) records[i]; + array[i] = a.getAddress(); + } else { + AAAARecord aaaa = (AAAARecord) records[i]; + array[i] = aaaa.getAddress(); + } + } + return array; +} + +/** + * Performs a reverse DNS lookup. + * @param addr The ip address to lookup. + * @return The host name found for the ip address. + */ +public String +getHostByAddr(byte [] addr) throws UnknownHostException { + Name name = ReverseMap.fromAddress(InetAddress.getByAddress(addr)); + Record [] records = new Lookup(name, Type.PTR).run(); + if (records == null) + throw new UnknownHostException(); + return ((PTRRecord) records[0]).getTarget().toString(); +} +} diff --git a/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor b/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor new file mode 100644 index 0000000..1ca895c --- /dev/null +++ b/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor @@ -0,0 +1 @@ +org.xbill.DNS.spi.DNSJavaNameServiceDescriptor diff --git a/src/org/xbill/DNS/tests/primary.java b/src/org/xbill/DNS/tests/primary.java new file mode 100644 index 0000000..85455b9 --- /dev/null +++ b/src/org/xbill/DNS/tests/primary.java @@ -0,0 +1,59 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.tests; + +import java.util.*; +import org.xbill.DNS.*; + +public class primary { + +private static void +usage() { + System.out.println("usage: primary [-t] [-a | -i] origin file"); + System.exit(1); +} + +public static void +main(String [] args) throws Exception { + boolean time = false; + boolean axfr = false; + boolean iterator = false; + int arg = 0; + + if (args.length < 2) + usage(); + + while (args.length - arg > 2) { + if (args[0].equals("-t")) + time = true; + else if (args[0].equals("-a")) + axfr = true; + else if (args[0].equals("-i")) + iterator = true; + arg++; + } + + Name origin = Name.fromString(args[arg++], Name.root); + String file = args[arg++]; + + long start = System.currentTimeMillis(); + Zone zone = new Zone(origin, file); + long end = System.currentTimeMillis(); + if (axfr) { + Iterator it = zone.AXFR(); + while (it.hasNext()) { + System.out.println(it.next()); + } + } else if (iterator) { + Iterator it = zone.iterator(); + while (it.hasNext()) { + System.out.println(it.next()); + } + } else { + System.out.println(zone); + } + if (time) + System.out.println("; Load time: " + (end - start) + " ms"); +} + +} diff --git a/src/org/xbill/DNS/tests/xfrin.java b/src/org/xbill/DNS/tests/xfrin.java new file mode 100644 index 0000000..066c70e --- /dev/null +++ b/src/org/xbill/DNS/tests/xfrin.java @@ -0,0 +1,109 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.tests; + +import java.util.*; +import org.xbill.DNS.*; + +public class xfrin { + +private static void +usage(String s) { + System.out.println("Error: " + s); + System.out.println("usage: xfrin [-i serial] [-k keyname/secret] " + + "[-s server] [-p port] [-f] zone"); + System.exit(1); +} + +public static void +main(String [] args) throws Exception { + ZoneTransferIn xfrin; + TSIG key = null; + int ixfr_serial = -1; + String server = null; + int port = SimpleResolver.DEFAULT_PORT; + boolean fallback = false; + Name zname; + + int arg = 0; + while (arg < args.length) { + if (args[arg].equals("-i")) { + ixfr_serial = Integer.parseInt(args[++arg]); + if (ixfr_serial < 0) + usage("invalid serial number"); + } else if (args[arg].equals("-k")) { + String s = args[++arg]; + int index = s.indexOf('/'); + if (index < 0) + usage("invalid key"); + key = new TSIG(s.substring(0, index), + s.substring(index+1)); + } else if (args[arg].equals("-s")) { + server = args[++arg]; + } else if (args[arg].equals("-p")) { + port = Integer.parseInt(args[++arg]); + if (port < 0 || port > 0xFFFF) + usage("invalid port"); + } else if (args[arg].equals("-f")) { + fallback = true; + } else if (args[arg].startsWith("-")) { + usage("invalid option"); + } else { + break; + } + arg++; + } + if (arg >= args.length) + usage("no zone name specified"); + zname = Name.fromString(args[arg]); + + if (server == null) { + Lookup l = new Lookup(zname, Type.NS); + Record [] ns = l.run(); + if (ns == null) { + System.out.println("failed to look up NS record: " + + l.getErrorString()); + System.exit(1); + } + server = ns[0].rdataToString(); + System.out.println("sending to server '" + server + "'"); + } + + if (ixfr_serial >= 0) + xfrin = ZoneTransferIn.newIXFR(zname, ixfr_serial, fallback, + server, port, key); + else + xfrin = ZoneTransferIn.newAXFR(zname, server, port, key); + + List response = xfrin.run(); + if (xfrin.isAXFR()) { + if (ixfr_serial >= 0) + System.out.println("AXFR-like IXFR response"); + else + System.out.println("AXFR response"); + Iterator it = response.iterator(); + while (it.hasNext()) + System.out.println(it.next()); + } else if (xfrin.isIXFR()) { + System.out.println("IXFR response"); + Iterator it = response.iterator(); + while (it.hasNext()) { + ZoneTransferIn.Delta delta; + delta = (ZoneTransferIn.Delta) it.next(); + System.out.println("delta from " + delta.start + + " to " + delta.end); + System.out.println("deletes"); + Iterator it2 = delta.deletes.iterator(); + while (it2.hasNext()) + System.out.println(it2.next()); + System.out.println("adds"); + it2 = delta.adds.iterator(); + while (it2.hasNext()) + System.out.println(it2.next()); + } + } else if (xfrin.isCurrent()) { + System.out.println("up to date"); + } +} + +} diff --git a/src/org/xbill/DNS/utils/HMAC.java b/src/org/xbill/DNS/utils/HMAC.java new file mode 100644 index 0000000..5eb5afd --- /dev/null +++ b/src/org/xbill/DNS/utils/HMAC.java @@ -0,0 +1,182 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.utils; + +import java.util.Arrays; +import java.security.*; + +/** + * An implementation of the HMAC message authentication code. + * + * @author Brian Wellington + */ + +public class HMAC { + +private MessageDigest digest; +private int blockLength; + +private byte [] ipad, opad; + +private static final byte IPAD = 0x36; +private static final byte OPAD = 0x5c; + +private void +init(byte [] key) { + int i; + + if (key.length > blockLength) { + key = digest.digest(key); + digest.reset(); + } + ipad = new byte[blockLength]; + opad = new byte[blockLength]; + for (i = 0; i < key.length; i++) { + ipad[i] = (byte) (key[i] ^ IPAD); + opad[i] = (byte) (key[i] ^ OPAD); + } + for (; i < blockLength; i++) { + ipad[i] = IPAD; + opad[i] = OPAD; + } + digest.update(ipad); +} + +/** + * Creates a new HMAC instance + * @param digest The message digest object. + * @param blockLength The block length of the message digest. + * @param key The secret key + */ +public +HMAC(MessageDigest digest, int blockLength, byte [] key) { + digest.reset(); + this.digest = digest; + this.blockLength = blockLength; + init(key); +} + +/** + * Creates a new HMAC instance + * @param digestName The name of the message digest function. + * @param blockLength The block length of the message digest. + * @param key The secret key. + */ +public +HMAC(String digestName, int blockLength, byte [] key) { + try { + digest = MessageDigest.getInstance(digestName); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("unknown digest algorithm " + + digestName); + } + this.blockLength = blockLength; + init(key); +} + +/** + * Creates a new HMAC instance + * @param digest The message digest object. + * @param key The secret key + * @deprecated won't work with digests using a padding length other than 64; + * use {@code HMAC(MessageDigest digest, int blockLength, + * byte [] key)} instead. + * @see HMAC#HMAC(MessageDigest digest, int blockLength, byte [] key) + */ +public +HMAC(MessageDigest digest, byte [] key) { + this(digest, 64, key); +} + +/** + * Creates a new HMAC instance + * @param digestName The name of the message digest function. + * @param key The secret key. + * @deprecated won't work with digests using a padding length other than 64; + * use {@code HMAC(String digestName, int blockLength, byte [] key)} + * instead + * @see HMAC#HMAC(String digestName, int blockLength, byte [] key) + */ +public +HMAC(String digestName, byte [] key) { + this(digestName, 64, key); +} + +/** + * Adds data to the current hash + * @param b The data + * @param offset The index at which to start adding to the hash + * @param length The number of bytes to hash + */ +public void +update(byte [] b, int offset, int length) { + digest.update(b, offset, length); +} + +/** + * Adds data to the current hash + * @param b The data + */ +public void +update(byte [] b) { + digest.update(b); +} + +/** + * Signs the data (computes the secure hash) + * @return An array with the signature + */ +public byte [] +sign() { + byte [] output = digest.digest(); + digest.reset(); + digest.update(opad); + return digest.digest(output); +} + +/** + * Verifies the data (computes the secure hash and compares it to the input) + * @param signature The signature to compare against + * @return true if the signature matches, false otherwise + */ +public boolean +verify(byte [] signature) { + return verify(signature, false); +} + +/** + * Verifies the data (computes the secure hash and compares it to the input) + * @param signature The signature to compare against + * @param truncation_ok If true, the signature may be truncated; only the + * number of bytes in the provided signature are compared. + * @return true if the signature matches, false otherwise + */ +public boolean +verify(byte [] signature, boolean truncation_ok) { + byte [] expected = sign(); + if (truncation_ok && signature.length < expected.length) { + byte [] truncated = new byte[signature.length]; + System.arraycopy(expected, 0, truncated, 0, truncated.length); + expected = truncated; + } + return Arrays.equals(signature, expected); +} + +/** + * Resets the HMAC object for further use + */ +public void +clear() { + digest.reset(); + digest.update(ipad); +} + +/** + * Returns the length of the digest. + */ +public int +digestLength() { + return digest.getDigestLength(); +} + +} diff --git a/src/org/xbill/DNS/utils/base16.java b/src/org/xbill/DNS/utils/base16.java new file mode 100644 index 0000000..58024e6 --- /dev/null +++ b/src/org/xbill/DNS/utils/base16.java @@ -0,0 +1,73 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.utils; + +import java.io.*; + +/** + * Routines for converting between Strings of hex-encoded data and arrays of + * binary data. This is not actually used by DNS. + * + * @author Brian Wellington + */ + +public class base16 { + +private static final String Base16 = "0123456789ABCDEF"; + +private +base16() {} + +/** + * Convert binary data to a hex-encoded String + * @param b An array containing binary data + * @return A String containing the encoded data + */ +public static String +toString(byte [] b) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + for (int i = 0; i < b.length; i++) { + short value = (short) (b[i] & 0xFF); + byte high = (byte) (value >> 4); + byte low = (byte) (value & 0xF); + os.write(Base16.charAt(high)); + os.write(Base16.charAt(low)); + } + return new String(os.toByteArray()); +} + +/** + * Convert a hex-encoded String to binary data + * @param str A String containing the encoded data + * @return An array containing the binary data, or null if the string is invalid + */ +public static byte [] +fromString(String str) { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + byte [] raw = str.getBytes(); + for (int i = 0; i < raw.length; i++) { + if (!Character.isWhitespace((char)raw[i])) + bs.write(raw[i]); + } + byte [] in = bs.toByteArray(); + if (in.length % 2 != 0) { + return null; + } + + bs.reset(); + DataOutputStream ds = new DataOutputStream(bs); + + for (int i = 0; i < in.length; i += 2) { + byte high = (byte) Base16.indexOf(Character.toUpperCase((char)in[i])); + byte low = (byte) Base16.indexOf(Character.toUpperCase((char)in[i+1])); + try { + ds.writeByte((high << 4) + low); + } + catch (IOException e) { + } + } + return bs.toByteArray(); +} + +} diff --git a/src/org/xbill/DNS/utils/base32.java b/src/org/xbill/DNS/utils/base32.java new file mode 100644 index 0000000..a2f26ea --- /dev/null +++ b/src/org/xbill/DNS/utils/base32.java @@ -0,0 +1,213 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.utils; + +import java.io.*; + +/** + * Routines for converting between Strings of base32-encoded data and arrays + * of binary data. This currently supports the base32 and base32hex alphabets + * specified in RFC 4648, sections 6 and 7. + * + * @author Brian Wellington + */ + +public class base32 { + +public static class Alphabet { + private Alphabet() {} + + public static final String BASE32 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567="; + public static final String BASE32HEX = + "0123456789ABCDEFGHIJKLMNOPQRSTUV="; +}; + +private String alphabet; +private boolean padding, lowercase; + +/** + * Creates an object that can be used to do base32 conversions. + * @param alphabet Which alphabet should be used + * @param padding Whether padding should be used + * @param lowercase Whether lowercase characters should be used. + * default parameters (The standard base32 alphabet, no padding, uppercase) + */ +public +base32(String alphabet, boolean padding, boolean lowercase) { + this.alphabet = alphabet; + this.padding = padding; + this.lowercase = lowercase; +} + +static private int +blockLenToPadding(int blocklen) { + switch (blocklen) { + case 1: + return 6; + case 2: + return 4; + case 3: + return 3; + case 4: + return 1; + case 5: + return 0; + default: + return -1; + } +} + +static private int +paddingToBlockLen(int padlen) { + switch (padlen) { + case 6: + return 1; + case 4: + return 2; + case 3: + return 3; + case 1: + return 4; + case 0: + return 5; + default : + return -1; + } +} + +/** + * Convert binary data to a base32-encoded String + * + * @param b An array containing binary data + * @return A String containing the encoded data + */ +public String +toString(byte [] b) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + for (int i = 0; i < (b.length + 4) / 5; i++) { + short s[] = new short[5]; + int t[] = new int[8]; + + int blocklen = 5; + for (int j = 0; j < 5; j++) { + if ((i * 5 + j) < b.length) + s[j] = (short) (b[i * 5 + j] & 0xFF); + else { + s[j] = 0; + blocklen--; + } + } + int padlen = blockLenToPadding(blocklen); + + // convert the 5 byte block into 8 characters (values 0-31). + + // upper 5 bits from first byte + t[0] = (byte) ((s[0] >> 3) & 0x1F); + // lower 3 bits from 1st byte, upper 2 bits from 2nd. + t[1] = (byte) (((s[0] & 0x07) << 2) | ((s[1] >> 6) & 0x03)); + // bits 5-1 from 2nd. + t[2] = (byte) ((s[1] >> 1) & 0x1F); + // lower 1 bit from 2nd, upper 4 from 3rd + t[3] = (byte) (((s[1] & 0x01) << 4) | ((s[2] >> 4) & 0x0F)); + // lower 4 from 3rd, upper 1 from 4th. + t[4] = (byte) (((s[2] & 0x0F) << 1) | ((s[3] >> 7) & 0x01)); + // bits 6-2 from 4th + t[5] = (byte) ((s[3] >> 2) & 0x1F); + // lower 2 from 4th, upper 3 from 5th; + t[6] = (byte) (((s[3] & 0x03) << 3) | ((s[4] >> 5) & 0x07)); + // lower 5 from 5th; + t[7] = (byte) (s[4] & 0x1F); + + // write out the actual characters. + for (int j = 0; j < t.length - padlen; j++) { + char c = alphabet.charAt(t[j]); + if (lowercase) + c = Character.toLowerCase(c); + os.write(c); + } + + // write out the padding (if any) + if (padding) { + for (int j = t.length - padlen; j < t.length; j++) + os.write('='); + } + } + + return new String(os.toByteArray()); +} + +/** + * Convert a base32-encoded String to binary data + * + * @param str A String containing the encoded data + * @return An array containing the binary data, or null if the string is invalid + */ +public byte[] +fromString(String str) { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + byte [] raw = str.getBytes(); + for (int i = 0; i < raw.length; i++) + { + char c = (char) raw[i]; + if (!Character.isWhitespace(c)) { + c = Character.toUpperCase(c); + bs.write((byte) c); + } + } + + if (padding) { + if (bs.size() % 8 != 0) + return null; + } else { + while (bs.size() % 8 != 0) + bs.write('='); + } + + byte [] in = bs.toByteArray(); + + bs.reset(); + DataOutputStream ds = new DataOutputStream(bs); + + for (int i = 0; i < in.length / 8; i++) { + short[] s = new short[8]; + int[] t = new int[5]; + + int padlen = 8; + for (int j = 0; j < 8; j++) { + char c = (char) in[i * 8 + j]; + if (c == '=') + break; + s[j] = (short) alphabet.indexOf(in[i * 8 + j]); + if (s[j] < 0) + return null; + padlen--; + } + int blocklen = paddingToBlockLen(padlen); + if (blocklen < 0) + return null; + + // all 5 bits of 1st, high 3 (of 5) of 2nd + t[0] = (s[0] << 3) | s[1] >> 2; + // lower 2 of 2nd, all 5 of 3rd, high 1 of 4th + t[1] = ((s[1] & 0x03) << 6) | (s[2] << 1) | (s[3] >> 4); + // lower 4 of 4th, high 4 of 5th + t[2] = ((s[3] & 0x0F) << 4) | ((s[4] >> 1) & 0x0F); + // lower 1 of 5th, all 5 of 6th, high 2 of 7th + t[3] = (s[4] << 7) | (s[5] << 2) | (s[6] >> 3); + // lower 3 of 7th, all of 8th + t[4] = ((s[6] & 0x07) << 5) | s[7]; + + try { + for (int j = 0; j < blocklen; j++) + ds.writeByte((byte) (t[j] & 0xFF)); + } + catch (IOException e) { + } + } + + return bs.toByteArray(); +} + +} diff --git a/src/org/xbill/DNS/utils/base64.java b/src/org/xbill/DNS/utils/base64.java new file mode 100644 index 0000000..54567cf --- /dev/null +++ b/src/org/xbill/DNS/utils/base64.java @@ -0,0 +1,145 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.utils; + +import java.io.*; + +/** + * Routines for converting between Strings of base64-encoded data and arrays of + * binary data. + * + * @author Brian Wellington + */ + +public class base64 { + +private static final String Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + +private +base64() {} + +/** + * Convert binary data to a base64-encoded String + * @param b An array containing binary data + * @return A String containing the encoded data + */ +public static String +toString(byte [] b) { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + for (int i = 0; i < (b.length + 2) / 3; i++) { + short [] s = new short[3]; + short [] t = new short[4]; + for (int j = 0; j < 3; j++) { + if ((i * 3 + j) < b.length) + s[j] = (short) (b[i*3+j] & 0xFF); + else + s[j] = -1; + } + + t[0] = (short) (s[0] >> 2); + if (s[1] == -1) + t[1] = (short) (((s[0] & 0x3) << 4)); + else + t[1] = (short) (((s[0] & 0x3) << 4) + (s[1] >> 4)); + if (s[1] == -1) + t[2] = t[3] = 64; + else if (s[2] == -1) { + t[2] = (short) (((s[1] & 0xF) << 2)); + t[3] = 64; + } + else { + t[2] = (short) (((s[1] & 0xF) << 2) + (s[2] >> 6)); + t[3] = (short) (s[2] & 0x3F); + } + for (int j = 0; j < 4; j++) + os.write(Base64.charAt(t[j])); + } + return new String(os.toByteArray()); +} + +/** + * Formats data into a nicely formatted base64 encoded String + * @param b An array containing binary data + * @param lineLength The number of characters per line + * @param prefix A string prefixing the characters on each line + * @param addClose Whether to add a close parenthesis or not + * @return A String representing the formatted output + */ +public static String +formatString(byte [] b, int lineLength, String prefix, boolean addClose) { + String s = toString(b); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < s.length(); i += lineLength) { + sb.append (prefix); + if (i + lineLength >= s.length()) { + sb.append(s.substring(i)); + if (addClose) + sb.append(" )"); + } + else { + sb.append(s.substring(i, i + lineLength)); + sb.append("\n"); + } + } + return sb.toString(); +} + + +/** + * Convert a base64-encoded String to binary data + * @param str A String containing the encoded data + * @return An array containing the binary data, or null if the string is invalid + */ +public static byte [] +fromString(String str) { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + byte [] raw = str.getBytes(); + for (int i = 0; i < raw.length; i++) { + if (!Character.isWhitespace((char)raw[i])) + bs.write(raw[i]); + } + byte [] in = bs.toByteArray(); + if (in.length % 4 != 0) { + return null; + } + + bs.reset(); + DataOutputStream ds = new DataOutputStream(bs); + + for (int i = 0; i < (in.length + 3) / 4; i++) { + short [] s = new short[4]; + short [] t = new short[3]; + + for (int j = 0; j < 4; j++) + s[j] = (short) Base64.indexOf(in[i*4+j]); + + t[0] = (short) ((s[0] << 2) + (s[1] >> 4)); + if (s[2] == 64) { + t[1] = t[2] = (short) (-1); + if ((s[1] & 0xF) != 0) + return null; + } + else if (s[3] == 64) { + t[1] = (short) (((s[1] << 4) + (s[2] >> 2)) & 0xFF); + t[2] = (short) (-1); + if ((s[2] & 0x3) != 0) + return null; + } + else { + t[1] = (short) (((s[1] << 4) + (s[2] >> 2)) & 0xFF); + t[2] = (short) (((s[2] << 6) + s[3]) & 0xFF); + } + + try { + for (int j = 0; j < 3; j++) + if (t[j] >= 0) + ds.writeByte(t[j]); + } + catch (IOException e) { + } + } + return bs.toByteArray(); +} + +} diff --git a/src/org/xbill/DNS/utils/hexdump.java b/src/org/xbill/DNS/utils/hexdump.java new file mode 100644 index 0000000..1a79a40 --- /dev/null +++ b/src/org/xbill/DNS/utils/hexdump.java @@ -0,0 +1,56 @@ +// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) + +package org.xbill.DNS.utils; + +/** + * A routine to produce a nice looking hex dump + * + * @author Brian Wellington + */ + +public class hexdump { + +private static final char [] hex = "0123456789ABCDEF".toCharArray(); + +/** + * Dumps a byte array into hex format. + * @param description If not null, a description of the data. + * @param b The data to be printed. + * @param offset The start of the data in the array. + * @param length The length of the data in the array. + */ +public static String +dump(String description, byte [] b, int offset, int length) { + StringBuffer sb = new StringBuffer(); + + sb.append(length + "b"); + if (description != null) + sb.append(" (" + description + ")"); + sb.append(':'); + + int prefixlen = sb.toString().length(); + prefixlen = (prefixlen + 8) & ~ 7; + sb.append('\t'); + + int perline = (80 - prefixlen) / 3; + for (int i = 0; i < length; i++) { + if (i != 0 && i % perline == 0) { + sb.append('\n'); + for (int j = 0; j < prefixlen / 8 ; j++) + sb.append('\t'); + } + int value = (int)(b[i + offset]) & 0xFF; + sb.append(hex[(value >> 4)]); + sb.append(hex[(value & 0xF)]); + sb.append(' '); + } + sb.append('\n'); + return sb.toString(); +} + +public static String +dump(String s, byte [] b) { + return dump(s, b, 0, b.length); +} + +} diff --git a/src/org/xbill/DNS/windows/DNSServer.properties b/src/org/xbill/DNS/windows/DNSServer.properties new file mode 100644 index 0000000..25342f9 --- /dev/null +++ b/src/org/xbill/DNS/windows/DNSServer.properties @@ -0,0 +1,4 @@ +host_name=Host Name +primary_dns_suffix=Primary Dns Suffix +dns_suffix=DNS Suffix +dns_servers=DNS Servers diff --git a/src/org/xbill/DNS/windows/DNSServer_de.properties b/src/org/xbill/DNS/windows/DNSServer_de.properties new file mode 100644 index 0000000..aa3f4a6 --- /dev/null +++ b/src/org/xbill/DNS/windows/DNSServer_de.properties @@ -0,0 +1,4 @@ +host_name=Hostname +primary_dns_suffix=Prim\u00E4res DNS-Suffix +dns_suffix=DNS-Suffixsuchliste +dns_servers=DNS-Server diff --git a/src/org/xbill/DNS/windows/DNSServer_fr.properties b/src/org/xbill/DNS/windows/DNSServer_fr.properties new file mode 100644 index 0000000..7c87a25 --- /dev/null +++ b/src/org/xbill/DNS/windows/DNSServer_fr.properties @@ -0,0 +1,4 @@ +host_name=Nom de l'h\u00F4te +primary_dns_suffix=Suffixe DNS principal +dns_suffix=Suffixe DNS propre \u00E0 la connexion +dns_servers=Serveurs DNS diff --git a/src/org/xbill/DNS/windows/DNSServer_ja.properties b/src/org/xbill/DNS/windows/DNSServer_ja.properties new file mode 100644 index 0000000..f873164 --- /dev/null +++ b/src/org/xbill/DNS/windows/DNSServer_ja.properties @@ -0,0 +1,4 @@ +host_name=\u30db\u30b9\u30c8\u540d +primary_dns_suffix=\u30d7\u30e9\u30a4\u30de\u30ea DNS \u30b5\u30d5\u30a3\u30c3\u30af\u30b9 +dns_suffix=DNS \u30b5\u30d5\u30a3\u30c3\u30af\u30b9 +dns_servers=DNS \u30b5\u30fc\u30d0\u30fc diff --git a/src/org/xbill/DNS/windows/DNSServer_pl.properties b/src/org/xbill/DNS/windows/DNSServer_pl.properties new file mode 100644 index 0000000..eab5774 --- /dev/null +++ b/src/org/xbill/DNS/windows/DNSServer_pl.properties @@ -0,0 +1,4 @@ +host_name=Nazwa hosta +primary_dns_suffix=Sufiks podstawowej domeny DNS +dns_suffix=Sufiks DNS konkretnego po\u0142\u0105czenia +dns_servers=Serwery DNS |