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