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  import io.netty.util.internal.PlatformDependent;
25  
26  import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*;
27  
28  final class DnsCodecUtil {
29      private DnsCodecUtil() {
30          // Util class
31      }
32  
33      static void encodeDomainName(String name, ByteBuf buf) {
34          if (ROOT.equals(name)) {
35              // Root domain
36              buf.writeByte(0);
37              return;
38          }
39  
40          int totalLength = 0;
41          final String[] labels = name.split("\\.");
42          for (int i = 0; i < labels.length; i++) {
43              String label = labels[i];
44              final int labelLen = label.length();
45              if (labelLen == 0) {
46                  if (i == labels.length - 1) {
47                      // zero-length label at the end means the end of the name.
48                      break;
49                  } else {
50                      throw new IllegalArgumentException("DNS name contains empty label: " + name);
51                  }
52              }
53              if (labelLen > 63) {
54                  throw new IllegalArgumentException(
55                          "DNS label length " + labelLen + " exceeds maximum of 63: " + name);
56              }
57              int idx = label.indexOf('\0');
58              if (idx != -1) {
59                  throw new IllegalArgumentException(
60                          "DNS label contains null byte at index " + idx);
61              }
62              totalLength += 1 + labelLen;
63              if (totalLength > 255) {
64                  throw new IllegalArgumentException(
65                          "DNS name exceeds maximum length of 255: " + name);
66              }
67              buf.writeByte(labelLen);
68              ByteBufUtil.writeAscii(buf, label);
69          }
70  
71          buf.writeByte(0); // marks end of name field
72      }
73  
74      static String decodeDomainName(ByteBuf in) {
75          int position = -1;
76          int checked = 0;
77          final int end = in.writerIndex();
78          final int readable = in.readableBytes();
79  
80          // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems
81          // some servers do not respect this for empty names. So just workaround this and return an empty name in this
82          // case.
83          //
84          // See:
85          // - https://github.com/netty/netty/issues/5014
86          // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1
87          if (readable == 0) {
88              return ROOT;
89          }
90  
91          final StringBuilder name = new StringBuilder(readable << 1);
92          while (in.isReadable()) {
93              final int len = in.readUnsignedByte();
94              final boolean pointer = (len & 0xc0) == 0xc0;
95              if (pointer) {
96                  if (position == -1) {
97                      position = in.readerIndex() + 1;
98                  }
99  
100                 if (!in.isReadable()) {
101                     throw new CorruptedFrameException("truncated pointer in a name");
102                 }
103 
104                 final int next = (len & 0x3f) << 8 | in.readUnsignedByte();
105                 if (next >= end) {
106                     throw new CorruptedFrameException("name has an out-of-range pointer");
107                 }
108                 in.readerIndex(next);
109 
110                 // check for loops
111                 checked += 2;
112                 if (checked >= end) {
113                     throw new CorruptedFrameException("name contains a loop.");
114                 }
115             } else if (len != 0) {
116                 if (!in.isReadable(len)) {
117                     throw new CorruptedFrameException("truncated label in a name");
118                 }
119                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
120                 if (len > 63) {
121                     throw new TooLongFrameException("label must be <= 63 but was " + len);
122                 }
123                 name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.');
124                 in.skipBytes(len);
125                 // See https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4
126                 if (name.length() > 255) {
127                     throw new TooLongFrameException("domain name must be <= 255 but was " + name.length());
128                 }
129             } else { // len == 0
130                 break;
131             }
132         }
133 
134         if (position != -1) {
135             in.readerIndex(position);
136         }
137 
138         if (name.length() == 0) {
139             return ROOT;
140         }
141 
142         if (name.charAt(name.length() - 1) != '.') {
143             name.append('.');
144         }
145 
146         return name.toString();
147     }
148 
149     /**
150      * Decompress pointer data.
151      * @param compression compressed data
152      * @return decompressed data
153      */
154     static ByteBuf decompressDomainName(ByteBuf compression) {
155         String domainName = decodeDomainName(compression);
156         ByteBuf result = compression.alloc().buffer(domainName.length() << 1);
157         try {
158             encodeDomainName(domainName, result);
159         } catch (Throwable cause) {
160             result.release();
161             PlatformDependent.throwException(cause);
162         }
163         return result;
164     }
165 }