src/jdk.dns.client/share/classes/jdk/dns/client/internal/ResourceRecord.java
branchaefimov-dns-client-branch
changeset 58870 35c438a6d45c
child 58971 465a15dd6bed
equal deleted inserted replaced
58869:cc66ac8c7646 58870:35c438a6d45c
       
     1 /*
       
     2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.dns.client.internal;
       
    27 
       
    28 import jdk.dns.client.ex.DnsCommunicationException;
       
    29 import jdk.dns.client.ex.DnsInvalidNameException;
       
    30 
       
    31 import java.io.IOException;
       
    32 import java.nio.charset.StandardCharsets;
       
    33 
       
    34 
       
    35 /**
       
    36  * The ResourceRecord class represents a DNS resource record.
       
    37  * The string format is based on the master file representation in
       
    38  * RFC 1035.
       
    39  *
       
    40  * @author Scott Seligman
       
    41  */
       
    42 
       
    43 public class ResourceRecord {
       
    44     /*
       
    45      * Resource record type codes
       
    46      */
       
    47     public static final int TYPE_A = 1;
       
    48     public static final int TYPE_CNAME = 5;
       
    49     public static final int TYPE_AAAA = 28;
       
    50     public static final int TYPE_PTR = 12;
       
    51     static final int QTYPE_STAR = 255;          // query type "*"
       
    52 
       
    53     private static final int TYPE_NS = 2;
       
    54     private static final int TYPE_SOA = 6;
       
    55     private static final int TYPE_HINFO = 13;
       
    56     private static final int TYPE_MX = 15;
       
    57     private static final int TYPE_TXT = 16;
       
    58     private static final int TYPE_SRV = 33;
       
    59     private static final int TYPE_NAPTR = 35;
       
    60     //static final int QTYPE_AXFR = 252;          // zone transfer //Do not want to support that
       
    61 
       
    62     /*
       
    63      * Mapping from resource record type codes to type name strings.
       
    64      */
       
    65     private static final String[] rrTypeNames = {
       
    66             null, "A", "NS", null, null,
       
    67             "CNAME", "SOA", null, null, null,
       
    68             null, null, "PTR", "HINFO", null,
       
    69             "MX", "TXT", null, null, null,
       
    70             null, null, null, null, null,
       
    71             null, null, null, "AAAA", null,
       
    72             null, null, null, "SRV", null,
       
    73             "NAPTR"
       
    74     };
       
    75 
       
    76     /*
       
    77      * Resource record class codes
       
    78      */
       
    79     public static final int CLASS_INTERNET = 1;
       
    80     static final int QCLASS_STAR = 255;      // query class "*"
       
    81 
       
    82     /*
       
    83      * Mapping from resource record type codes to class name strings.
       
    84      */
       
    85     private static final String[] rrClassNames = {
       
    86             null, "IN"
       
    87     };
       
    88 
       
    89     /*
       
    90      * Maximum number of compression references in labels.
       
    91      * Used to detect compression loops.
       
    92      */
       
    93     private static final int MAXIMUM_COMPRESSION_REFERENCES = 16;
       
    94 
       
    95     byte[] msg;                 // DNS message
       
    96     int msgLen;                 // msg size (in octets)
       
    97     boolean qSection;           // true if this RR is part of question section
       
    98     // and therefore has no ttl or rdata
       
    99     int offset;                 // offset of RR w/in msg
       
   100     int rrlen;                  // number of octets in encoded RR
       
   101     DnsName name;               // name field of RR, including root label
       
   102     int rrtype;                 // type field of RR
       
   103     String rrtypeName;          // name of rrtype
       
   104     int rrclass;                // class field of RR
       
   105     String rrclassName;         // name of rrclass
       
   106     int ttl = 0;                // ttl field of RR
       
   107     int rdlen = 0;              // number of octets of rdata
       
   108     Object rdata = null;        // rdata -- most are String, unknown are byte[]
       
   109 
       
   110 
       
   111     /*
       
   112      * Constructs a new ResourceRecord.  The encoded data of the DNS
       
   113      * message is contained in msg; data for this RR begins at msg[offset].
       
   114      * If qSection is true this RR is part of a question section.  It's
       
   115      * not a true resource record in that case, but is treated as if it
       
   116      * were a shortened one (with no ttl or rdata).  If decodeRdata is
       
   117      * false, the rdata is not decoded (and getRdata() will return null)
       
   118      * unless this is an SOA record.
       
   119      *
       
   120      * @throws CommunicationException if a decoded domain name isn't valid.
       
   121      * @throws ArrayIndexOutOfBoundsException given certain other corrupt data.
       
   122      */
       
   123     ResourceRecord(byte[] msg, int msgLen, int offset,
       
   124                    boolean qSection, boolean decodeRdata)
       
   125             throws DnsCommunicationException {
       
   126 
       
   127         this.msg = msg;
       
   128         this.msgLen = msgLen;
       
   129         this.offset = offset;
       
   130         this.qSection = qSection;
       
   131         decode(decodeRdata);
       
   132     }
       
   133 
       
   134     public String toString() {
       
   135         String text = name + " " + rrclassName + " " + rrtypeName;
       
   136         if (!qSection) {
       
   137             text += " " + ttl + " " +
       
   138                     ((rdata != null) ? rdata : "[n/a]");
       
   139         }
       
   140         return text;
       
   141     }
       
   142 
       
   143     /*
       
   144      * Returns the name field of this RR, including the root label.
       
   145      */
       
   146     public DnsName getName() {
       
   147         return name;
       
   148     }
       
   149 
       
   150     /*
       
   151      * Returns the number of octets in the encoded RR.
       
   152      */
       
   153     public int size() {
       
   154         return rrlen;
       
   155     }
       
   156 
       
   157     public int getType() {
       
   158         return rrtype;
       
   159     }
       
   160 
       
   161     public int getRrclass() {
       
   162         return rrclass;
       
   163     }
       
   164 
       
   165     public Object getRdata() {
       
   166         return rdata;
       
   167     }
       
   168 
       
   169 
       
   170     public static String getTypeName(int rrtype) {
       
   171         return valueToName(rrtype, rrTypeNames);
       
   172     }
       
   173 
       
   174     public static int getType(String typeName) {
       
   175         return nameToValue(typeName, rrTypeNames);
       
   176     }
       
   177 
       
   178     public static int getRrclass(String className) {
       
   179         return nameToValue(className, rrClassNames);
       
   180     }
       
   181 
       
   182     private static String valueToName(int val, String[] names) {
       
   183         String name = null;
       
   184         if ((val > 0) && (val < names.length)) {
       
   185             name = names[val];
       
   186         } else if (val == QTYPE_STAR) {         // QTYPE_STAR == QCLASS_STAR
       
   187             name = "*";
       
   188         }
       
   189         if (name == null) {
       
   190             name = Integer.toString(val);
       
   191         }
       
   192         return name;
       
   193     }
       
   194 
       
   195     private static int nameToValue(String name, String[] names) {
       
   196         if (name.isEmpty()) {
       
   197             return -1;                          // invalid name
       
   198         } else if (name.equals("*")) {
       
   199             return QTYPE_STAR;                  // QTYPE_STAR == QCLASS_STAR
       
   200         }
       
   201         if (Character.isDigit(name.charAt(0))) {
       
   202             try {
       
   203                 return Integer.parseInt(name);
       
   204             } catch (NumberFormatException e) {
       
   205             }
       
   206         }
       
   207         for (int i = 1; i < names.length; i++) {
       
   208             if ((names[i] != null) &&
       
   209                     name.equalsIgnoreCase(names[i])) {
       
   210                 return i;
       
   211             }
       
   212         }
       
   213         return -1;                              // unknown name
       
   214     }
       
   215 
       
   216     /*
       
   217      * Decodes the binary format of the RR.
       
   218      * May throw ArrayIndexOutOfBoundsException given corrupt data.
       
   219      */
       
   220     private void decode(boolean decodeRdata) throws DnsCommunicationException {
       
   221         int pos = offset;       // index of next unread octet
       
   222 
       
   223         name = new DnsName();                           // NAME
       
   224         pos = decodeName(pos, name);
       
   225 
       
   226         rrtype = getUShort(pos);                        // TYPE
       
   227         rrtypeName = (rrtype < rrTypeNames.length)
       
   228                 ? rrTypeNames[rrtype]
       
   229                 : null;
       
   230         if (rrtypeName == null) {
       
   231             rrtypeName = Integer.toString(rrtype);
       
   232         }
       
   233         pos += 2;
       
   234 
       
   235         rrclass = getUShort(pos);                       // CLASS
       
   236         rrclassName = (rrclass < rrClassNames.length)
       
   237                 ? rrClassNames[rrclass]
       
   238                 : null;
       
   239         if (rrclassName == null) {
       
   240             rrclassName = Integer.toString(rrclass);
       
   241         }
       
   242         pos += 2;
       
   243 
       
   244         if (!qSection) {
       
   245             ttl = getInt(pos);                          // TTL
       
   246             pos += 4;
       
   247 
       
   248             rdlen = getUShort(pos);                     // RDLENGTH
       
   249             pos += 2;
       
   250 
       
   251             rdata = (decodeRdata ||                     // RDATA
       
   252                     (rrtype == TYPE_SOA))
       
   253                     ? decodeRdata(pos)
       
   254                     : null;
       
   255             if (rdata instanceof DnsName) {
       
   256                 rdata = rdata.toString();
       
   257             }
       
   258             pos += rdlen;
       
   259         }
       
   260 
       
   261         rrlen = pos - offset;
       
   262 
       
   263         msg = null;     // free up for GC
       
   264     }
       
   265 
       
   266     /*
       
   267      * Returns the 1-byte unsigned value at msg[pos].
       
   268      */
       
   269     private int getUByte(int pos) {
       
   270         return (msg[pos] & 0xFF);
       
   271     }
       
   272 
       
   273     /*
       
   274      * Returns the 2-byte unsigned value at msg[pos].  The high
       
   275      * order byte comes first.
       
   276      */
       
   277     private int getUShort(int pos) {
       
   278         return (((msg[pos] & 0xFF) << 8) |
       
   279                 (msg[pos + 1] & 0xFF));
       
   280     }
       
   281 
       
   282     /*
       
   283      * Returns the 4-byte signed value at msg[pos].  The high
       
   284      * order byte comes first.
       
   285      */
       
   286     private int getInt(int pos) {
       
   287         return ((getUShort(pos) << 16) | getUShort(pos + 2));
       
   288     }
       
   289 
       
   290     /*
       
   291      * Returns the 4-byte unsigned value at msg[pos].  The high
       
   292      * order byte comes first.
       
   293      */
       
   294     private long getUInt(int pos) {
       
   295         return (getInt(pos) & 0xffffffffL);
       
   296     }
       
   297 
       
   298     /*
       
   299      * Returns the name encoded at msg[pos], including the root label.
       
   300      */
       
   301     private DnsName decodeName(int pos) throws DnsCommunicationException {
       
   302         DnsName n = new DnsName();
       
   303         decodeName(pos, n);
       
   304         return n;
       
   305     }
       
   306 
       
   307     /*
       
   308      * Prepends to "n" the domain name encoded at msg[pos], including the root
       
   309      * label.  Returns the index into "msg" following the name.
       
   310      */
       
   311     private int decodeName(int pos, DnsName n) throws DnsCommunicationException {
       
   312         int endPos = -1;
       
   313         int level = 0;
       
   314         try {
       
   315             while (true) {
       
   316                 if (level > MAXIMUM_COMPRESSION_REFERENCES)
       
   317                     throw new IOException("Too many compression references");
       
   318                 int typeAndLen = msg[pos] & 0xFF;
       
   319                 if (typeAndLen == 0) {                  // end of name
       
   320                     ++pos;
       
   321                     n.add(0, "");
       
   322                     break;
       
   323                 } else if (typeAndLen <= 63) {          // regular label
       
   324                     ++pos;
       
   325                     n.add(0, new String(msg, pos, typeAndLen,
       
   326                             StandardCharsets.ISO_8859_1));
       
   327                     pos += typeAndLen;
       
   328                 } else if ((typeAndLen & 0xC0) == 0xC0) { // name compression
       
   329                     ++level;
       
   330                     // cater for the case where the name pointed to is itself
       
   331                     // compressed: we don't want endPos to be reset by the second
       
   332                     // compression level.
       
   333                     int ppos = pos;
       
   334                     if (endPos == -1) endPos = pos + 2;
       
   335                     pos = getUShort(pos) & 0x3FFF;
       
   336                     if (debug) {
       
   337                         dprint("decode: name compression at " + ppos
       
   338                                 + " -> " + pos + " endPos=" + endPos);
       
   339                         assert endPos > 0;
       
   340                         assert pos < ppos;
       
   341                         assert pos >= Header.HEADER_SIZE;
       
   342                     }
       
   343                 } else
       
   344                     throw new IOException("Invalid label type: " + typeAndLen);
       
   345             }
       
   346         } catch (IOException | DnsInvalidNameException e) {
       
   347             DnsCommunicationException ce = new DnsCommunicationException(
       
   348                     "DNS error: malformed packet");
       
   349             ce.initCause(e);
       
   350             throw ce;
       
   351         }
       
   352         if (endPos == -1)
       
   353             endPos = pos;
       
   354         return endPos;
       
   355     }
       
   356 
       
   357     /*
       
   358      * Returns the rdata encoded at msg[pos].  The format is dependent
       
   359      * on the rrtype and rrclass values, which have already been set.
       
   360      * The length of the encoded data is rdlen, which has already been
       
   361      * set.
       
   362      * The rdata of records with unknown type/class combinations is
       
   363      * returned in a newly-allocated byte array.
       
   364      */
       
   365     private Object decodeRdata(int pos) throws DnsCommunicationException {
       
   366         if (rrclass == CLASS_INTERNET) {
       
   367             switch (rrtype) {
       
   368                 case TYPE_A:
       
   369                     return decodeA(pos);
       
   370                 case TYPE_AAAA:
       
   371                     return decodeAAAA(pos);
       
   372                 case TYPE_CNAME:
       
   373                 case TYPE_NS:
       
   374                 case TYPE_PTR:
       
   375                     return decodeName(pos);
       
   376                 case TYPE_MX:
       
   377                     return decodeMx(pos);
       
   378                 case TYPE_SOA:
       
   379                     return decodeSoa(pos);
       
   380                 case TYPE_SRV:
       
   381                     return decodeSrv(pos);
       
   382                 case TYPE_NAPTR:
       
   383                     return decodeNaptr(pos);
       
   384                 case TYPE_TXT:
       
   385                     return decodeTxt(pos);
       
   386                 case TYPE_HINFO:
       
   387                     return decodeHinfo(pos);
       
   388             }
       
   389         }
       
   390         // Unknown RR type/class
       
   391         if (debug) {
       
   392             dprint("Unknown RR type for RR data: " + rrtype + " rdlen=" + rdlen
       
   393                     + ", pos=" + pos + ", msglen=" + msg.length + ", remaining="
       
   394                     + (msg.length - pos));
       
   395         }
       
   396         byte[] rd = new byte[rdlen];
       
   397         System.arraycopy(msg, pos, rd, 0, rdlen);
       
   398         return rd;
       
   399     }
       
   400 
       
   401     /*
       
   402      * Returns the rdata of an MX record that is encoded at msg[pos].
       
   403      */
       
   404     private String decodeMx(int pos) throws DnsCommunicationException {
       
   405         int preference = getUShort(pos);
       
   406         pos += 2;
       
   407         DnsName name = decodeName(pos);
       
   408         return (preference + " " + name);
       
   409     }
       
   410 
       
   411     /*
       
   412      * Returns the rdata of an SOA record that is encoded at msg[pos].
       
   413      */
       
   414     private String decodeSoa(int pos) throws DnsCommunicationException {
       
   415         DnsName mname = new DnsName();
       
   416         pos = decodeName(pos, mname);
       
   417         DnsName rname = new DnsName();
       
   418         pos = decodeName(pos, rname);
       
   419 
       
   420         long serial = getUInt(pos);
       
   421         pos += 4;
       
   422         long refresh = getUInt(pos);
       
   423         pos += 4;
       
   424         long retry = getUInt(pos);
       
   425         pos += 4;
       
   426         long expire = getUInt(pos);
       
   427         pos += 4;
       
   428         long minimum = getUInt(pos);    // now used as negative TTL
       
   429         pos += 4;
       
   430 
       
   431         return (mname + " " + rname + " " + serial + " " +
       
   432                 refresh + " " + retry + " " + expire + " " + minimum);
       
   433     }
       
   434 
       
   435     /*
       
   436      * Returns the rdata of an SRV record that is encoded at msg[pos].
       
   437      * See RFC 2782.
       
   438      */
       
   439     private String decodeSrv(int pos) throws DnsCommunicationException {
       
   440         int priority = getUShort(pos);
       
   441         pos += 2;
       
   442         int weight = getUShort(pos);
       
   443         pos += 2;
       
   444         int port = getUShort(pos);
       
   445         pos += 2;
       
   446         DnsName target = decodeName(pos);
       
   447         return (priority + " " + weight + " " + port + " " + target);
       
   448     }
       
   449 
       
   450     /*
       
   451      * Returns the rdata of an NAPTR record that is encoded at msg[pos].
       
   452      * See RFC 2915.
       
   453      */
       
   454     private String decodeNaptr(int pos) throws DnsCommunicationException {
       
   455         int order = getUShort(pos);
       
   456         pos += 2;
       
   457         int preference = getUShort(pos);
       
   458         pos += 2;
       
   459         StringBuffer flags = new StringBuffer();
       
   460         pos += decodeCharString(pos, flags);
       
   461         StringBuffer services = new StringBuffer();
       
   462         pos += decodeCharString(pos, services);
       
   463         StringBuffer regexp = new StringBuffer(rdlen);
       
   464         pos += decodeCharString(pos, regexp);
       
   465         DnsName replacement = decodeName(pos);
       
   466 
       
   467         return (order + " " + preference + " " + flags + " " +
       
   468                 services + " " + regexp + " " + replacement);
       
   469     }
       
   470 
       
   471     /*
       
   472      * Returns the rdata of a TXT record that is encoded at msg[pos].
       
   473      * The rdata consists of one or more <character-string>s.
       
   474      */
       
   475     private String decodeTxt(int pos) {
       
   476         StringBuffer buf = new StringBuffer(rdlen);
       
   477         int end = pos + rdlen;
       
   478         while (pos < end) {
       
   479             pos += decodeCharString(pos, buf);
       
   480             if (pos < end) {
       
   481                 buf.append(' ');
       
   482             }
       
   483         }
       
   484         return buf.toString();
       
   485     }
       
   486 
       
   487     /*
       
   488      * Returns the rdata of an HINFO record that is encoded at msg[pos].
       
   489      * The rdata consists of two <character-string>s.
       
   490      */
       
   491     private String decodeHinfo(int pos) {
       
   492         StringBuffer buf = new StringBuffer(rdlen);
       
   493         pos += decodeCharString(pos, buf);
       
   494         buf.append(' ');
       
   495         pos += decodeCharString(pos, buf);
       
   496         return buf.toString();
       
   497     }
       
   498 
       
   499     /*
       
   500      * Decodes the <character-string> at msg[pos] and adds it to buf.
       
   501      * If the string contains one of the meta-characters ' ', '\\', or
       
   502      * '"', then the result is quoted and any embedded '\\' or '"'
       
   503      * chars are escaped with '\\'.  Empty strings are also quoted.
       
   504      * Returns the size of the encoded string, including the initial
       
   505      * length octet.
       
   506      */
       
   507     private int decodeCharString(int pos, StringBuffer buf) {
       
   508         int start = buf.length();       // starting index of this string
       
   509         int len = getUByte(pos++);      // encoded string length
       
   510         boolean quoted = (len == 0);    // quote string if empty
       
   511         for (int i = 0; i < len; i++) {
       
   512             int c = getUByte(pos++);
       
   513             quoted |= (c == ' ');
       
   514             if ((c == '\\') || (c == '"')) {
       
   515                 quoted = true;
       
   516                 buf.append('\\');
       
   517             }
       
   518             buf.append((char) c);
       
   519         }
       
   520         if (quoted) {
       
   521             buf.insert(start, '"');
       
   522             buf.append('"');
       
   523         }
       
   524         return (len + 1);       // size includes initial octet
       
   525     }
       
   526 
       
   527     /*
       
   528      * Returns the rdata of an A record, in dotted-decimal format,
       
   529      * that is encoded at msg[pos].
       
   530      */
       
   531     private String decodeA(int pos) {
       
   532         return ((msg[pos] & 0xff) + "." +
       
   533                 (msg[pos + 1] & 0xff) + "." +
       
   534                 (msg[pos + 2] & 0xff) + "." +
       
   535                 (msg[pos + 3] & 0xff));
       
   536     }
       
   537 
       
   538     /*
       
   539      * Returns the rdata of an AAAA record, in colon-separated format,
       
   540      * that is encoded at msg[pos].  For example:  4321:0:1:2:3:4:567:89ab.
       
   541      * See RFCs 1886 and 2373.
       
   542      */
       
   543     private String decodeAAAA(int pos) {
       
   544         int[] addr6 = new int[8];  // the unsigned 16-bit words of the address
       
   545         for (int i = 0; i < 8; i++) {
       
   546             addr6[i] = getUShort(pos);
       
   547             pos += 2;
       
   548         }
       
   549 
       
   550         // Find longest sequence of two or more zeros, to compress them.
       
   551         int curBase = -1;
       
   552         int curLen = 0;
       
   553         int bestBase = -1;
       
   554         int bestLen = 0;
       
   555         for (int i = 0; i < 8; i++) {
       
   556             if (addr6[i] == 0) {
       
   557                 if (curBase == -1) {    // new sequence
       
   558                     curBase = i;
       
   559                     curLen = 1;
       
   560                 } else {                // extend sequence
       
   561                     ++curLen;
       
   562                     if ((curLen >= 2) && (curLen > bestLen)) {
       
   563                         bestBase = curBase;
       
   564                         bestLen = curLen;
       
   565                     }
       
   566                 }
       
   567             } else {                    // not in sequence
       
   568                 curBase = -1;
       
   569             }
       
   570         }
       
   571 
       
   572         // If addr begins with at least 6 zeros and is not :: or ::1,
       
   573         // or with 5 zeros followed by 0xffff, use the text format for
       
   574         // IPv4-compatible or IPv4-mapped addresses.
       
   575         if (bestBase == 0) {
       
   576             if ((bestLen == 6) ||
       
   577                     ((bestLen == 7) && (addr6[7] > 1))) {
       
   578                 return ("::" + decodeA(pos - 4));
       
   579             } else if ((bestLen == 5) && (addr6[5] == 0xffff)) {
       
   580                 return ("::ffff:" + decodeA(pos - 4));
       
   581             }
       
   582         }
       
   583 
       
   584         // If bestBase != -1, compress zeros in [bestBase, bestBase+bestLen)
       
   585         boolean compress = (bestBase != -1);
       
   586 
       
   587         StringBuilder sb = new StringBuilder(40);
       
   588         if (bestBase == 0) {
       
   589             sb.append(':');
       
   590         }
       
   591         for (int i = 0; i < 8; i++) {
       
   592             if (!compress || (i < bestBase) || (i >= bestBase + bestLen)) {
       
   593                 sb.append(Integer.toHexString(addr6[i]));
       
   594                 if (i < 7) {
       
   595                     sb.append(':');
       
   596                 }
       
   597             } else if (compress && (i == bestBase)) {  // first compressed zero
       
   598                 sb.append(':');
       
   599             }
       
   600         }
       
   601 
       
   602         return sb.toString();
       
   603     }
       
   604 
       
   605     //-------------------------------------------------------------------------
       
   606 
       
   607     private static final boolean debug = false;
       
   608 
       
   609     private static void dprint(String mess) {
       
   610         if (debug) {
       
   611             System.err.println("DNS: " + mess);
       
   612         }
       
   613     }
       
   614 
       
   615 }