/* * Copied from the DnsJava project * * Copyright (c) 1998-2011, Brian Wellington. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package io.milton.dns.record; import io.milton.dns.Name; 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; public 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 (rec.getType() == Type.TSIG) { tsigstart = pos; } if (rec.getType() == Type.SIG && ((SIGRecord) rec).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.isEmpty()) { 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()]); } public 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; } StringBuilder sb = new StringBuilder(); Record[] records = getSectionArray(i); for (Record rec : records) { if (i == Section.QUESTION) { sb.append(";;\t").append(rec.name); sb.append(", type = ").append(Type.string(rec.type)); sb.append(", class = ").append(DClass.string(rec.dclass)); } else { sb.append(rec); } sb.append("\n"); } return sb.toString(); } /** * Converts the Message to a String. */ public String toString() { StringBuilder sb = new StringBuilder(); OPTRecord opt = getOPT(); if (opt != null) { sb.append(header.toStringWithRcode(getRcode())).append("\n"); } else { sb.append(header).append("\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(";; ").append(Section.longString(i)).append(":\n"); } else { sb.append(";; ").append(Section.updString(i)).append(":\n"); } sb.append(sectionToString(i)).append("\n"); } sb.append(";; Message size: ").append(numBytes()).append(" 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; } }