1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.net.module.util;
18 
19 import android.util.ArrayMap;
20 import android.util.Log;
21 
22 import androidx.annotation.NonNull;
23 import androidx.annotation.Nullable;
24 
25 import com.android.net.module.util.DnsPacketUtils.DnsRecordParser;
26 
27 import java.nio.BufferOverflowException;
28 import java.nio.BufferUnderflowException;
29 import java.nio.ByteBuffer;
30 import java.nio.charset.StandardCharsets;
31 import java.util.ArrayList;
32 
33 /**
34  * Utilities for encoding/decoding the domain name or domain search list.
35  *
36  * @hide
37  */
38 public final class DomainUtils {
39     private static final String TAG = "DomainUtils";
40     private static final int MAX_OPTION_LEN = 255;
41 
42     @NonNull
getSubstring(@onNull final String string, @NonNull final String[] labels, int index)43     private static String getSubstring(@NonNull final String string, @NonNull final String[] labels,
44             int index) {
45         int beginIndex = 0;
46         for (int i = 0; i < index; i++) {
47             beginIndex += labels[i].length() + 1; // include the dot
48         }
49         return string.substring(beginIndex);
50     }
51 
52     /**
53      * Encode the given single domain name to byte array, comply with RFC1035 section-3.1.
54      *
55      * @return null if the given domain string is invalid, otherwise, return a byte array
56      *         wrapping the encoded domain, not including any padded octets, caller should
57      *         pad zero octets at the end if needed.
58      */
59     @Nullable
encode(@onNull final String domain)60     public static byte[] encode(@NonNull final String domain) {
61         if (!DnsRecordParser.isHostName(domain)) return null;
62         return encode(new String[]{ domain }, false /* compression */);
63     }
64 
65     /**
66      * Encode the given multiple domain names to byte array, comply with RFC1035 section-3.1
67      * and section 4.1.4 (message compression) if enabled.
68      *
69      * @return Null if encode fails due to BufferOverflowException, otherwise, return a byte
70      *         array wrapping the encoded domains, not including any padded octets, caller
71      *         should pad zero octets at the end if needed. The byte array may be empty if
72      *         the given domain strings are invalid.
73      */
74     @Nullable
encode(@onNull final String[] domains, boolean compression)75     public static byte[] encode(@NonNull final String[] domains, boolean compression) {
76         try {
77             final ByteBuffer buffer = ByteBuffer.allocate(MAX_OPTION_LEN);
78             final ArrayMap<String, Integer> offsetMap = new ArrayMap<>();
79             for (int i = 0; i < domains.length; i++) {
80                 if (!DnsRecordParser.isHostName(domains[i])) {
81                     Log.e(TAG, "Skip invalid domain name " + domains[i]);
82                     continue;
83                 }
84                 final String[] labels = domains[i].split("\\.");
85                 for (int j = 0; j < labels.length; j++) {
86                     if (compression) {
87                         final String suffix = getSubstring(domains[i], labels, j);
88                         if (offsetMap.containsKey(suffix)) {
89                             int offsetOfSuffix = offsetMap.get(suffix);
90                             offsetOfSuffix |= 0xC000;
91                             buffer.putShort((short) offsetOfSuffix);
92                             break; // unnecessary to put the compressed string into map
93                         } else {
94                             offsetMap.put(suffix, buffer.position());
95                         }
96                     }
97                     // encode the domain name string without compression when:
98                     // - compression feature isn't enabled,
99                     // - suffix does not match any string in the map.
100                     final byte[] labelBytes = labels[j].getBytes(StandardCharsets.UTF_8);
101                     buffer.put((byte) labelBytes.length);
102                     buffer.put(labelBytes);
103                     if (j == labels.length - 1) {
104                         // Pad terminate label at the end of last label.
105                         buffer.put((byte) 0);
106                     }
107                 }
108             }
109             buffer.flip();
110             final byte[] out = new byte[buffer.limit()];
111             buffer.get(out);
112             return out;
113         } catch (BufferOverflowException e) {
114             Log.e(TAG, "Fail to encode domain name and stop encoding", e);
115             return null;
116         }
117     }
118 
119     /**
120      * Decode domain name(s) from the given byteBuffer. Decode follows RFC1035 section 3.1 and
121      * section 4.1.4(message compression).
122      *
123      * @return domain name(s) string array with space separated, or empty string if decode fails.
124      */
125     @NonNull
decode(@onNull final ByteBuffer buffer, boolean compression)126     public static ArrayList<String> decode(@NonNull final ByteBuffer buffer, boolean compression) {
127         final ArrayList<String> domainList = new ArrayList<>();
128         while (buffer.remaining() > 0) {
129             try {
130                 // TODO: replace the recursion with loop in parseName and don't need to pass in the
131                 // maxLabelCount parameter to prevent recursion from overflowing stack.
132                 final String domain = DnsRecordParser.parseName(buffer, 0 /* depth */,
133                         15 /* maxLabelCount */, compression);
134                 if (!DnsRecordParser.isHostName(domain)) continue;
135                 domainList.add(domain);
136             } catch (BufferUnderflowException | DnsPacket.ParseException e) {
137                 Log.e(TAG, "Fail to parse domain name and stop parsing", e);
138                 break;
139             }
140         }
141         return domainList;
142     }
143 }
144