View Javadoc
1   /*
2    * Copyright 2019 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.codec.dns;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufUtil;
21  import io.netty.handler.codec.CorruptedFrameException;
22  import io.netty.handler.codec.TooLongFrameException;
23  import io.netty.util.CharsetUtil;
24  
25  import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*;
26  
27  final class DnsCodecUtil {
28      private DnsCodecUtil() {
29          // Util class
30      }
31  
32      static void encodeDomainName(String name, ByteBuf buf) {
33          if (ROOT.equals(name)) {
34              // Root domain
35              buf.writeByte(0);
36              return;
37          }
38  
39          int totalLength = 0;
40          final String[] labels = name.split("\\.");
41          for (int i = 0; i < labels.length; i++) {
42              String label = labels[i];
43              final int labelLen = label.length();
44              if (labelLen == 0) {
45                  if (i == labels.length - 1) {
46                      // zero-length label at the end means the end of the name.
47                      break;
48                  } else {
49                      throw new IllegalArgumentException("DNS name contains empty label: " + name);
50                  }
51              }
52              if (labelLen > 63) {
53                  throw new IllegalArgumentException(
54                          "DNS label length " + labelLen + " exceeds maximum of 63: " + name);
55              }
56              int idx = label.indexOf('\0');
57              if (idx != -1) {
58                  throw new IllegalArgumentException(
59                          "DNS label contains null byte at index " + idx);
60              }
61              totalLength += 1 + labelLen;
62              if (totalLength > 255) {
63                  throw new IllegalArgumentException(
64                          "DNS name exceeds maximum length of 255: " + name);
65              }
66              buf.writeByte(labelLen);
67              ByteBufUtil.writeAscii(buf, label);
68          }
69  
70          buf.writeByte(0); // marks end of name field
71      }
72  
73      static String decodeDomainName(ByteBuf in) {
74          int position = -1;
75          int checked = 0;
76          final int end = in.writerIndex();
77          final int readable = in.readableBytes();
78  
79          // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems
80          // some servers do not respect this for empty names. So just workaround this and return an empty name in this
81          // case.
82          //
83          // See:
84          // - https://github.com/netty/netty/issues/5014
85          // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1
86          if (readable == 0) {
87              return ROOT;
88          }
89  
90          final StringBuilder name = new StringBuilder(readable << 1);
91          while (in.isReadable()) {
92              final int len = in.readUnsignedByte();
93              final boolean pointer = (len & 0xc0) == 0xc0;
94              if (pointer) {
95                  if (position == -1) {
96                      position = in.readerIndex() + 1;
97                  }
98  
99                  if (!in.isReadable()) {
100                     throw new CorruptedFrameException("truncated pointer in a name");
101                 }
102 
103                 final int next = (len & 0x3f) << 8 | in.readUnsignedByte();
104                 if (next >= end) {
105                     throw new CorruptedFrameException("name has an out-of-range pointer");
106                 }
107                 in.readerIndex(next);
108 
109                 // check for loops
110                 checked += 2;
111                 if (checked >= end) {
112                     throw new CorruptedFrameException("name contains a loop.");
113                 }
114             } else if (len != 0) {
115                 if (!in.isReadable(len)) {
116                     throw new CorruptedFrameException("truncated label in a name");
117                 }
118                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
119                 if (len > 63) {
120                     throw new TooLongFrameException("label must be <= 63 but was " + len);
121                 }
122                 name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
123                 in.skipBytes(len);
124                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
125                 if (name.length() > 255) {
126                     throw new TooLongFrameException("domain name must be <= 255 but was " + name.length());
127                 }
128             } else { // len == 0
129                 break;
130             }
131         }
132 
133         if (position != -1) {
134             in.readerIndex(position);
135         }
136 
137         if (name.length() == 0) {
138             return ROOT;
139         }
140 
141         if (name.charAt(name.length() - 1) != '.') {
142             name.append('.');
143         }
144 
145         return name.toString();
146     }
147 
148     /**
149      * Decompress pointer data.
150      * @param compression compressed data
151      * @return decompressed data
152      */
153     static ByteBuf decompressDomainName(ByteBuf compression) {
154         String domainName = decodeDomainName(compression);
155         ByteBuf result = compression.alloc().buffer(domainName.length() << 1);
156         encodeDomainName(domainName, result);
157         return result;
158     }
159 }