/* * 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; import io.milton.dns.record.Compression; import io.milton.dns.record.DNAMERecord; import io.milton.dns.record.DNSInput; import io.milton.dns.record.DNSOutput; import io.milton.dns.record.NameTooLongException; import io.milton.dns.record.Options; import io.milton.dns.record.WireParseException; import java.io.*; import java.text.*; /** * A representation of a domain name. It may either be absolute (fully * qualified) or relative. * * @author Brian Wellington */ public class Name implements Comparable, Serializable { private static final long serialVersionUID = -7257019940971525644L; private static final int LABEL_NORMAL = 0; private static final int LABEL_COMPRESSION = 0xC0; private static final int LABEL_MASK = 0xC0; /* The name data */ private byte[] name; /* * Effectively an 8 byte array, where the low order byte stores the number * of labels and the 7 higher order bytes store per-label offsets. */ private long offsets; /* Precomputed hashcode. */ private int hashcode; private static final byte[] emptyLabel = new byte[]{(byte) 0}; private static final byte[] wildLabel = new byte[]{(byte) 1, (byte) '*'}; /** * The root name */ public static final Name root; /** * The root name */ public static final Name empty; /** * The maximum length of a Name */ private static final int MAXNAME = 255; /** * The maximum length of a label a Name */ private static final int MAXLABEL = 63; /** * The maximum number of labels in a Name */ private static final int MAXLABELS = 128; /** * The maximum number of cached offsets */ private static final int MAXOFFSETS = 7; /* Used for printing non-printable characters */ private static final DecimalFormat byteFormat = new DecimalFormat(); /* Used to efficiently convert bytes to lowercase */ private static final byte lowercase[] = new byte[256]; /* Used in wildcard names. */ private static final Name wild; static { byteFormat.setMinimumIntegerDigits(3); for (int i = 0; i < lowercase.length; i++) { if (i < 'A' || i > 'Z') { lowercase[i] = (byte) i; } else { lowercase[i] = (byte) (i - 'A' + 'a'); } } root = new Name(); root.appendSafe(emptyLabel, 0, 1); empty = new Name(); empty.name = new byte[0]; wild = new Name(); wild.appendSafe(wildLabel, 0, 1); } private Name() { } private final void setoffset(int n, int offset) { if (n >= MAXOFFSETS) { return; } int shift = 8 * (7 - n); offsets &= (~(0xFFL << shift)); offsets |= ((long) offset << shift); } private final int offset(int n) { if (n == 0 && getlabels() == 0) { return 0; } if (n < 0 || n >= getlabels()) { throw new IllegalArgumentException("label out of range"); } if (n < MAXOFFSETS) { int shift = 8 * (7 - n); return ((int) (offsets >>> shift) & 0xFF); } else { int pos = offset(MAXOFFSETS - 1); for (int i = MAXOFFSETS - 1; i < n; i++) { pos += (name[pos] + 1); } return (pos); } } private final void setlabels(int labels) { offsets &= ~(0xFF); offsets |= labels; } private final int getlabels() { return (int) (offsets & 0xFF); } private static final void copy(Name src, Name dst) { if (src.offset(0) == 0) { dst.name = src.name; dst.offsets = src.offsets; } else { int offset0 = src.offset(0); int namelen = src.name.length - offset0; int labels = src.labels(); dst.name = new byte[namelen]; System.arraycopy(src.name, offset0, dst.name, 0, namelen); for (int i = 0; i < labels && i < MAXOFFSETS; i++) { dst.setoffset(i, src.offset(i) - offset0); } dst.setlabels(labels); } } private final void append(byte[] array, int start, int n) throws NameTooLongException { int length = (name == null ? 0 : (name.length - offset(0))); int alength = 0; for (int i = 0, pos = start; i < n; i++) { int len = array[pos]; if (len > MAXLABEL) { throw new IllegalStateException("invalid label"); } len++; pos += len; alength += len; } int newlength = length + alength; if (newlength > MAXNAME) { throw new NameTooLongException(); } int labels = getlabels(); int newlabels = labels + n; if (newlabels > MAXLABELS) { throw new IllegalStateException("too many labels"); } byte[] newname = new byte[newlength]; if (length != 0) { System.arraycopy(name, offset(0), newname, 0, length); } System.arraycopy(array, start, newname, length, alength); name = newname; for (int i = 0, pos = length; i < n; i++) { setoffset(labels + i, pos); pos += (newname[pos] + 1); } setlabels(newlabels); } private static TextParseException parseException(String str, String message) { return new TextParseException("'" + str + "': " + message); } private final void appendFromString(String fullName, byte[] array, int start, int n) throws TextParseException { try { append(array, start, n); } catch (NameTooLongException e) { throw parseException(fullName, "Name too long"); } } private final void appendSafe(byte[] array, int start, int n) { try { append(array, start, n); } catch (NameTooLongException e) { } } /** * Create a new name from a string and an origin. This does not * automatically make the name absolute; it will be absolute if it has a * trailing dot or an absolute origin is appended. * * @param s The string to be converted * @param origin If the name is not absolute, the origin to be appended. * @throws TextParseException The name is invalid. */ public Name(String s, Name origin) throws TextParseException { if (s.isEmpty()) { throw parseException(s, "empty name"); } else if (s.equals("@")) { if (origin == null) { copy(empty, this); } else { copy(origin, this); } return; } else if (s.equals(".")) { copy(root, this); return; } int labelstart = -1; int pos = 1; byte[] label = new byte[MAXLABEL + 1]; boolean escaped = false; int digits = 0; int intval = 0; boolean absolute = false; for (int i = 0; i < s.length(); i++) { byte b = (byte) s.charAt(i); if (escaped) { if (b >= '0' && b <= '9' && digits < 3) { digits++; intval *= 10; intval += (b - '0'); if (intval > 255) { throw parseException(s, "bad escape"); } if (digits < 3) { continue; } b = (byte) intval; } else if (digits > 0 && digits < 3) { throw parseException(s, "bad escape"); } if (pos > MAXLABEL) { throw parseException(s, "label too long"); } labelstart = pos; label[pos++] = b; escaped = false; } else if (b == '\\') { escaped = true; digits = 0; intval = 0; } else if (b == '.') { if (labelstart == -1) { throw parseException(s, "invalid empty label"); } label[0] = (byte) (pos - 1); appendFromString(s, label, 0, 1); labelstart = -1; pos = 1; } else { if (labelstart == -1) { labelstart = i; } if (pos > MAXLABEL) { throw parseException(s, "label too long"); } label[pos++] = b; } } if (digits > 0 && digits < 3) { throw parseException(s, "bad escape"); } if (escaped) { throw parseException(s, "bad escape"); } if (labelstart == -1) { appendFromString(s, emptyLabel, 0, 1); absolute = true; } else { label[0] = (byte) (pos - 1); appendFromString(s, label, 0, 1); } if (origin != null && !absolute) { appendFromString(s, origin.name, 0, origin.getlabels()); } } /** * Create a new name from a string. This does not automatically make the * name absolute; it will be absolute if it has a trailing dot. * * @param s The string to be converted * @throws TextParseException The name is invalid. */ public Name(String s) throws TextParseException { this(s, null); } /** * Create a new name from a string and an origin. This does not * automatically make the name absolute; it will be absolute if it has a * trailing dot or an absolute origin is appended. This is identical to the * constructor, except that it will avoid creating new objects in some * cases. * * @param s The string to be converted * @param origin If the name is not absolute, the origin to be appended. * @throws TextParseException The name is invalid. */ public static Name fromString(String s, Name origin) throws TextParseException { if (s.equals("@") && origin != null) { return origin; } else if (s.equals(".")) { return (root); } return new Name(s, origin); } /** * Create a new name from a string. This does not automatically make the * name absolute; it will be absolute if it has a trailing dot. This is * identical to the constructor, except that it will avoid creating new * objects in some cases. * * @param s The string to be converted * @throws TextParseException The name is invalid. */ public static Name fromString(String s) throws TextParseException { return fromString(s, null); } /** * Create a new name from a constant string. This should only be used when * the name is known to be good - that is, when it is constant. * * @param s The string to be converted * @throws IllegalArgumentException The name is invalid. */ public static Name fromConstantString(String s) { try { return fromString(s, null); } catch (TextParseException e) { throw new IllegalArgumentException("Invalid name '" + s + "'"); } } /** * Create a new name from DNS a wire format message * * @param in A stream containing the DNS message which is currently * positioned at the start of the name to be read. */ public Name(DNSInput in) throws WireParseException { int len, pos; boolean done = false; byte[] label = new byte[MAXLABEL + 1]; boolean savedState = false; while (!done) { len = in.readU8(); switch (len & LABEL_MASK) { case LABEL_NORMAL: if (getlabels() >= MAXLABELS) { throw new WireParseException("too many labels"); } if (len == 0) { append(emptyLabel, 0, 1); done = true; } else { label[0] = (byte) len; in.readByteArray(label, 1, len); append(label, 0, 1); } break; case LABEL_COMPRESSION: pos = in.readU8(); pos += ((len & ~LABEL_MASK) << 8); if (Options.check("verbosecompression")) { System.err.println("currently " + in.current() + ", pointer to " + pos); } if (pos >= in.current() - 2) { throw new WireParseException("bad compression"); } if (!savedState) { in.save(); savedState = true; } in.jump(pos); if (Options.check("verbosecompression")) { System.err.println("current name '" + this + "', seeking to " + pos); } break; default: throw new WireParseException("bad label type"); } } if (savedState) { in.restore(); } } /** * Create a new name from DNS wire format * * @param b A byte array containing the wire format of the name. */ public Name(byte[] b) throws IOException { this(new DNSInput(b)); } /** * Create a new name by removing labels from the beginning of an existing * Name * * @param src An existing Name * @param n The number of labels to remove from the beginning in the copy */ public Name(Name src, int n) { int slabels = src.labels(); if (n > slabels) { throw new IllegalArgumentException("attempted to remove too " + "many labels"); } name = src.name; setlabels(slabels - n); for (int i = 0; i < MAXOFFSETS && i < slabels - n; i++) { setoffset(i, src.offset(i + n)); } } /** * Creates a new name by concatenating two existing names. * * @param prefix The prefix name. * @param suffix The suffix name. * @return The concatenated name. * @throws NameTooLongException The name is too long. */ public static Name concatenate(Name prefix, Name suffix) throws NameTooLongException { if (prefix.isAbsolute()) { return (prefix); } Name newname = new Name(); copy(prefix, newname); newname.append(suffix.name, suffix.offset(0), suffix.getlabels()); return newname; } /** * If this name is a subdomain of origin, return a new name relative to * origin with the same value. Otherwise, return the existing name. * * @param origin The origin to remove. * @return The possibly relativized name. */ public Name relativize(Name origin) { if (origin == null || !subdomain(origin)) { return this; } Name newname = new Name(); copy(this, newname); int length = length() - origin.length(); int labels = newname.labels() - origin.labels(); newname.setlabels(labels); newname.name = new byte[length]; System.arraycopy(name, offset(0), newname.name, 0, length); return newname; } /** * Generates a new Name with the first n labels replaced by a wildcard * * @return The wildcard name */ public Name wild(int n) { if (n < 1) { throw new IllegalArgumentException("must replace 1 or more " + "labels"); } try { Name newname = new Name(); copy(wild, newname); newname.append(name, offset(n), getlabels() - n); return newname; } catch (NameTooLongException e) { throw new IllegalStateException("Name.wild: concatenate failed"); } } /** * Generates a new Name to be used when following a DNAME. * * @param dname The DNAME record to follow. * @return The constructed name. * @throws NameTooLongException The resulting name is too long. */ public Name fromDNAME(DNAMERecord dname) throws NameTooLongException { Name dnameowner = dname.getName(); Name dnametarget = dname.getTarget(); if (!subdomain(dnameowner)) { return null; } int plabels = labels() - dnameowner.labels(); int plength = length() - dnameowner.length(); int pstart = offset(0); int dlabels = dnametarget.labels(); int dlength = dnametarget.length(); if (plength + dlength > MAXNAME) { throw new NameTooLongException(); } Name newname = new Name(); newname.setlabels(plabels + dlabels); newname.name = new byte[plength + dlength]; System.arraycopy(name, pstart, newname.name, 0, plength); System.arraycopy(dnametarget.name, 0, newname.name, plength, dlength); for (int i = 0, pos = 0; i < MAXOFFSETS && i < plabels + dlabels; i++) { newname.setoffset(i, pos); pos += (newname.name[pos] + 1); } return newname; } /** * Is this name a wildcard? */ public boolean isWild() { if (labels() == 0) { return false; } return (name[0] == (byte) 1 && name[1] == (byte) '*'); } /** * Is this name absolute? */ public boolean isAbsolute() { if (labels() == 0) { return false; } return (name[name.length - 1] == 0); } /** * The length of the name. */ public short length() { if (getlabels() == 0) { return 0; } return (short) (name.length - offset(0)); } /** * The number of labels in the name. */ public int labels() { return getlabels(); } /** * Is the current Name a subdomain of the specified name? */ public boolean subdomain(Name domain) { int labels = labels(); int dlabels = domain.labels(); if (dlabels > labels) { return false; } if (dlabels == labels) { return equals(domain); } return domain.equals(name, offset(labels - dlabels)); } private String byteString(byte[] array, int pos) { StringBuilder sb = new StringBuilder(); int len = array[pos++]; for (int i = pos; i < pos + len; i++) { int b = array[i] & 0xFF; if (b <= 0x20 || b >= 0x7f) { sb.append('\\'); sb.append(byteFormat.format(b)); } else if (b == '"' || b == '(' || b == ')' || b == '.' || b == ';' || b == '\\' || b == '@' || b == '$') { sb.append('\\'); sb.append((char) b); } else { sb.append((char) b); } } return sb.toString(); } /** * Convert a Name to a String * * @return The representation of this name as a (printable) String. */ public String toString() { int labels = labels(); if (labels == 0) { return "@"; } else if (labels == 1 && name[offset(0)] == 0) { return "."; } StringBuilder sb = new StringBuilder(); for (int i = 0, pos = offset(0); i < labels; i++) { int len = name[pos]; if (len > MAXLABEL) { throw new IllegalStateException("invalid label"); } if (len == 0) { break; } sb.append(byteString(name, pos)); sb.append('.'); pos += (1 + len); } if (!isAbsolute()) { sb.deleteCharAt(sb.length() - 1); } return sb.toString(); } /** * Retrieve the nth label of a Name. This makes a copy of the label; * changing this does not change the Name. * * @param n The label to be retrieved. The first label is 0. */ public byte[] getLabel(int n) { int pos = offset(n); byte len = (byte) (name[pos] + 1); byte[] label = new byte[len]; System.arraycopy(name, pos, label, 0, len); return label; } /** * Convert the nth label in a Name to a String * * @param n The label to be converted to a (printable) String. The first * label is 0. */ public String getLabelString(int n) { int pos = offset(n); return byteString(name, pos); } /** * Emit a Name in DNS wire format * * @param out The output stream containing the DNS message. * @param c The compression context, or null of no compression is desired. * @throws IllegalArgumentException The name is not absolute. */ public void toWire(DNSOutput out, Compression c) { if (!isAbsolute()) { throw new IllegalArgumentException("toWire() called on " + "non-absolute name"); } int labels = labels(); for (int i = 0; i < labels - 1; i++) { Name tname; if (i == 0) { tname = this; } else { tname = new Name(this, i); } int pos = -1; if (c != null) { pos = c.get(tname); } if (pos >= 0) { pos |= (LABEL_MASK << 8); out.writeU16(pos); return; } else { if (c != null) { c.add(out.current(), tname); } int off = offset(i); out.writeByteArray(name, off, name[off] + 1); } } out.writeU8(0); } /** * Emit a Name in DNS wire format * * @throws IllegalArgumentException The name is not absolute. */ public byte[] toWire() { DNSOutput out = new DNSOutput(); toWire(out, null); return out.toByteArray(); } /** * Emit a Name in canonical DNS wire format (all lowercase) * * @param out The output stream to which the message is written. */ public void toWireCanonical(DNSOutput out) { byte[] b = toWireCanonical(); out.writeByteArray(b); } /** * Emit a Name in canonical DNS wire format (all lowercase) * * @return The canonical form of the name. */ public byte[] toWireCanonical() { int labels = labels(); if (labels == 0) { return (new byte[0]); } byte[] b = new byte[name.length - offset(0)]; for (int i = 0, spos = offset(0), dpos = 0; i < labels; i++) { int len = name[spos]; if (len > MAXLABEL) { throw new IllegalStateException("invalid label"); } b[dpos++] = name[spos++]; for (int j = 0; j < len; j++) { b[dpos++] = lowercase[(name[spos++] & 0xFF)]; } } return b; } /** * Emit a Name in DNS wire format * * @param out The output stream containing the DNS message. * @param c The compression context, or null of no compression is desired. * @param canonical If true, emit the name in canonicalized form (all * lowercase). * @throws IllegalArgumentException The name is not absolute. */ public void toWire(DNSOutput out, Compression c, boolean canonical) { if (canonical) { toWireCanonical(out); } else { toWire(out, c); } } private final boolean equals(byte[] b, int bpos) { int labels = labels(); for (int i = 0, pos = offset(0); i < labels; i++) { if (name[pos] != b[bpos]) { return false; } int len = name[pos++]; bpos++; if (len > MAXLABEL) { throw new IllegalStateException("invalid label"); } for (int j = 0; j < len; j++) { if (lowercase[(name[pos++] & 0xFF)] != lowercase[(b[bpos++] & 0xFF)]) { return false; } } } return true; } /** * Are these two Names equivalent? */ public boolean equals(Object arg) { if (arg == this) { return true; } if (arg == null || !(arg instanceof Name)) { return false; } Name d = (Name) arg; if (d.hashcode == 0) { d.hashCode(); } if (hashcode == 0) { hashCode(); } if (d.hashcode != hashcode) { return false; } if (d.labels() != labels()) { return false; } return equals(d.name, d.offset(0)); } /** * Computes a hashcode based on the value */ public int hashCode() { if (hashcode != 0) { return (hashcode); } int code = 0; for (int i = offset(0); i < name.length; i++) { code += ((code << 3) + lowercase[(name[i] & 0xFF)]); } hashcode = code; return hashcode; } /** * Compares this Name to another Object. * * @param o The Object to be compared. * @return The value 0 if the argument is a name equivalent to this name; a * value less than 0 if the argument is less than this name in the canonical * ordering, and a value greater than 0 if the argument is greater than this * name in the canonical ordering. * @throws ClassCastException if the argument is not a Name. */ public int compareTo(Object o) { Name arg = (Name) o; if (this == arg) { return (0); } int labels = labels(); int alabels = arg.labels(); int compares = labels > alabels ? alabels : labels; for (int i = 1; i <= compares; i++) { int start = offset(labels - i); int astart = arg.offset(alabels - i); int length = name[start]; int alength = arg.name[astart]; for (int j = 0; j < length && j < alength; j++) { int n = lowercase[(name[j + start + 1]) & 0xFF] - lowercase[(arg.name[j + astart + 1]) & 0xFF]; if (n != 0) { return (n); } } if (length != alength) { return (length - alength); } } return (labels - alabels); } }