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