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