nl.sidn.dnslib.message.util.DNSStringUtil.java Source code

Java tutorial

Introduction

Here is the source code for nl.sidn.dnslib.message.util.DNSStringUtil.java

Source

/*
 * ENTRADA, a big data platform for network data analytics
 *
 * Copyright (C) 2016 SIDN [https://www.sidn.nl]
 * 
 * This file is part of ENTRADA.
 * 
 * ENTRADA is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * ENTRADA is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with ENTRADA.  If not, see [<http://www.gnu.org/licenses/].
 *
 */
package nl.sidn.dnslib.message.util;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;

import org.apache.commons.lang3.StringUtils;

import nl.sidn.dnslib.exception.DnsDecodeException;
import nl.sidn.dnslib.exception.DnsEncodeException;

/**
 * DNS Label Types
    
   Registration Procedures
   IESG Approval
   Reference
   [RFC-ietf-dnsext-rfc2671bis-edns0-10]
   Note
   IETF standards action required to allocate new types
   The top 2 bits of the first byte of an DNS label indicates the type of label.
   Registration of further Extended Label Types is closed per [RFC-ietf-dnsext-rfc2671bis-edns0-10].
       
   Value    Type    Status    Reference 
   0 0   Normal label lower 6 bits is the length of the label   Standard   [RFC1035]
   1 1   Compressed label the lower 6 bits and the 8 bits from next octet form a pointer to the compression target.   Standard   [RFC1035]
   0 1   Extended label type the lower 6 bits of this type (section 3) indicate the type of label in use   Proposed   [RFC-ietf-dnsext-rfc2671bis-edns0-10]
   0 1 0 0 0 0 0 1   Binary Label   Experimental not recommended   [RFC3364][RFC3363][RFC2673]
   0 1 1 1 1 1 1 1   Reserved for future expansion.   Proposed   [RFC-ietf-dnsext-rfc2671bis-edns0-10]
   1 0   Unallocated      
    
 *
 */
public class DNSStringUtil {

    //max length of a rfc1035 character-string (excluding length byte)
    private static int MAX_CHARACTER_STRING_LENGTH = 255;

    private static int MAX_POINTER_CHAIN_LENGTH = 10; //TODO: what is the optimal value?
    /*
         
        4.1.4. Message compression
        
       In order to reduce the size of messages, the domain system utilizes a
       compression scheme which eliminates the repetition of domain names in a
       message.  In this scheme, an entire domain name or a list of labels at
       the end of a domain name is replaced with a pointer to a prior occurance
       of the same name.
           
       The pointer takes the form of a two octet sequence:
           
      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
      | 1  1|                OFFSET                   |
      +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
           
       The first two bits are ones.  This allows a pointer to be distinguished
       from a label, since the label must begin with two zero bits because
       labels are restricted to 63 octets or less.  (The 10 and 01 combinations
       are reserved for future use.)  The OFFSET field specifies an offset from
       the start of the message (i.e., the first octet of the ID field in the
       domain header).  A zero offset specifies the first byte of the ID field,
       etc.
           
       The compression scheme allows a domain name in a message to be
       represented as either:
           
     - a sequence of labels ending in a zero octet
           
     - a pointer
           
     - a sequence of labels ending with a pointer
           
       Pointers can only be used for occurances of a domain name where the
       format is not class specific.  If this were not the case, a name server
       or resolver would be required to know the format of all RRs it handled.
       As yet, there are no such cases, but they may occur in future RDATA
       formats.
         
         
     */

    private static byte UNCOMPRESSED_NAME_BIT_MASK = (byte) 0x3f; //0011 1111
    private static byte COMPRESSED_NAME_BIT_MASK = (byte) 0xc0; //1100 0000

    public static boolean isUncompressedName(byte namePrefix) {
        return (namePrefix | UNCOMPRESSED_NAME_BIT_MASK) == UNCOMPRESSED_NAME_BIT_MASK;
    }

    public static boolean isCompressedName(byte namePrefix) {
        return (namePrefix & COMPRESSED_NAME_BIT_MASK) == COMPRESSED_NAME_BIT_MASK;
    }

    public static String readName(NetworkData buffer) {
        short length = buffer.readUnsignedByte();

        if (length == 0) {
            /* zero lentgh label means "." root */
            return ".";
        }

        if (length > 5000) {
            //large value chosen to allow decoding illegal long labels (>63)
            //but have some protection against running out of memmory for 
            //huge erroneous huge label sizes.
            throw new DnsDecodeException("Unsupported label length found, value: " + (int) length);
        }

        if (isUncompressedName((byte) length)) {
            return readUncompressedName(length, buffer) + ".";
        } else if (isCompressedName((byte) length)) {
            return readCompressedName(buffer) + ".";
        }

        throw new DnsDecodeException("Unsupported label found");
    }

    public static String readUncompressedName(short length, NetworkData buffer) {
        StringBuilder qnameBuilder = new StringBuilder();

        //read the length of the first label

        while (length > 0) {
            if (qnameBuilder.length() > 0) {
                //add the "." between labels
                qnameBuilder.append(".");
            }
            //read the label
            byte[] bytes = new byte[length];
            buffer.readBytes(bytes);
            qnameBuilder.append(new String(bytes));

            //read the length of the next label
            length = buffer.readUnsignedByte();

            //check if the last label is a pointer
            if (isCompressedName((byte) length)) {
                qnameBuilder.append(".");
                qnameBuilder.append(readCompressedName(buffer));
                //the pointer is the last label, quit the loop
                break;
            }
        }
        return qnameBuilder.toString();
    }

    public static String readCompressedName(NetworkData buffer) {

        // save location in the stream (after reading the 2 (offset) bytes)
        int currentPosition = buffer.getReaderIndex() + 1;
        short length;

        //protected against infinite loop (attack)
        int maxLoop = 0;
        do {
            maxLoop++;
            // go back one byte to read the 16bit offset as a char
            buffer.setReaderIndex(buffer.getReaderIndex() - 1);

            if (maxLoop == MAX_POINTER_CHAIN_LENGTH) {
                //protection against infinite loops
                throw new DnsDecodeException("Illegal pointer chain size: " + maxLoop);
            }

            //read 16 bits
            char offset = buffer.readUnsignedChar();
            //clear the first 2 bits used to indicate compressen vs uncompressed label
            offset = (char) (offset ^ (1 << 14)); // flip bit 14 to 0
            offset = (char) (offset ^ (1 << 15)); // flip bit 15 to 0

            if ((byte) offset >= (buffer.getReaderIndex() - 2)) {
                throw new DnsDecodeException("Message compression pointer offset higher than current index");
            }

            // goto the pointer location in the buffer
            buffer.setReaderIndex(offset);

            /* read the uncompressed name at the offset this
             * name can also end with a compressed part.
             * this will cause the current method to be called again
             * which must therefore be reentrant.
             */
            length = buffer.readUnsignedByte();
        } while (isCompressedName((byte) length));

        //read the label
        byte[] bytes = new byte[length];
        buffer.readBytes(bytes);
        String qName = new String(bytes);

        //go back to the location after the first pointer
        buffer.setReaderIndex(currentPosition);
        return qName;
    }

    public static void writeName(String name, NetworkData buffer) {

        //write nameserver string
        String[] labels = StringUtils.split(name, ".");
        for (String label : labels) {
            //write label length
            buffer.writeByte(label.length());
            buffer.writeBytes(label.getBytes());
        }

        //write root with zero byte
        buffer.writeByte(0);

    }

    public static byte[] writeName(String name) {

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        try {
            //write nameserver string
            String[] labels = StringUtils.split(name, ".");
            for (String label : labels) {
                //write label length
                dos.writeByte(label.length());
                dos.write(label.getBytes());
            }

            //write root with zero byte
            dos.writeByte(0);
        } catch (IOException e) {
            throw new RuntimeException("Error while wrting name", e);
        }

        return bos.toByteArray();

    }

    public static String readCharacterString(NetworkData buffer) {
        int length = buffer.readUnsignedByte();
        if (length > MAX_CHARACTER_STRING_LENGTH) {
            throw new DnsDecodeException("Illegal character string length (> 255), length = " + length);
        }
        if (length > 0) {
            byte[] characterString = new byte[length];
            buffer.readBytes(characterString);
            return new String(characterString);
        }

        return "";

    }

    public static void writeCharacterString(String value, NetworkData buffer) {
        byte[] data = value.getBytes();
        if (data.length > MAX_CHARACTER_STRING_LENGTH) {
            throw new DnsEncodeException("Illegal character string length (> 255), length = " + data.length);
        }
        if (data.length > 0) {
            buffer.writeByte(data.length);
            buffer.writeBytes(data);
        }
    }

}