blob: 3448cadf6f0ad638b537de86a34f99056f16be2b [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.net.module.util;
import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_COMPRESSION;
import static com.android.net.module.util.DnsPacket.DnsRecord.NAME_NORMAL;
import android.annotation.NonNull;
import android.text.TextUtils;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.text.DecimalFormat;
import java.text.FieldPosition;
/**
* Utilities for decoding the contents of a DnsPacket.
*
* @hide
*/
public final class DnsPacketUtils {
/**
* Reads the passed ByteBuffer from its current position and decodes a DNS record.
*/
public static class DnsRecordParser {
private static final int MAXLABELSIZE = 63;
private static final int MAXLABELCOUNT = 128;
private static final DecimalFormat sByteFormat = new DecimalFormat();
private static final FieldPosition sPos = new FieldPosition(0);
/**
* Convert label from {@code byte[]} to {@code String}
*
* <p>Follows the same conversion rules of the native code (ns_name.c in libc).
*/
private static String labelToString(@NonNull byte[] label) {
final StringBuffer sb = new StringBuffer();
for (int i = 0; i < label.length; ++i) {
int b = Byte.toUnsignedInt(label[i]);
// Control characters and non-ASCII characters.
if (b <= 0x20 || b >= 0x7f) {
// Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
sb.append('\\');
sByteFormat.format(b, sb, sPos);
} else if (b == '"' || b == '.' || b == ';' || b == '\\' || b == '(' || b == ')'
|| b == '@' || b == '$') {
// Append the byte as an escaped character, e.g., "\:" for 0x3a.
sb.append('\\');
sb.append((char) b);
} else {
// Append the byte as a character, e.g., "a" for 0x61.
sb.append((char) b);
}
}
return sb.toString();
}
/**
* Parses the domain / target name of a DNS record.
*
* As described in RFC 1035 Section 4.1.3, the NAME field of a DNS Resource Record always
* supports Name Compression, whereas domain names contained in the RDATA payload of a DNS
* record may or may not support Name Compression, depending on the record TYPE. Moreover,
* even if Name Compression is supported, its usage is left to the implementation.
*/
public static String parseName(ByteBuffer buf, int depth,
boolean isNameCompressionSupported) throws
BufferUnderflowException, DnsPacket.ParseException {
if (depth > MAXLABELCOUNT) {
throw new DnsPacket.ParseException("Failed to parse name, too many labels");
}
final int len = Byte.toUnsignedInt(buf.get());
final int mask = len & NAME_COMPRESSION;
if (0 == len) {
return "";
} else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION
|| (!isNameCompressionSupported && mask == NAME_COMPRESSION)) {
throw new DnsPacket.ParseException("Parse name fail, bad label type: " + mask);
} else if (mask == NAME_COMPRESSION) {
// Name compression based on RFC 1035 - 4.1.4 Message compression
final int offset = ((len & ~NAME_COMPRESSION) << 8) + Byte.toUnsignedInt(buf.get());
final int oldPos = buf.position();
if (offset >= oldPos - 2) {
throw new DnsPacket.ParseException(
"Parse compression name fail, invalid compression");
}
buf.position(offset);
final String pointed = parseName(buf, depth + 1, isNameCompressionSupported);
buf.position(oldPos);
return pointed;
} else {
final byte[] label = new byte[len];
buf.get(label);
final String head = labelToString(label);
if (head.length() > MAXLABELSIZE) {
throw new DnsPacket.ParseException("Parse name fail, invalid label length");
}
final String tail = parseName(buf, depth + 1, isNameCompressionSupported);
return TextUtils.isEmpty(tail) ? head : head + "." + tail;
}
}
private DnsRecordParser() {}
}
private DnsPacketUtils() {}
}