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