| // 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; |
| } |
| |
| } |