blob: b8653198995353476046d3f4b366de05ba5a9a07 [file] [log] [blame]
/*
* Copyright (C) 2021 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.server.connectivity.mdns;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.annotation.Nullable;
import android.os.SystemClock;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import com.android.server.connectivity.mdns.util.MdnsUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;
/**
* Abstract base class for mDNS records. Stores the header fields and provides methods for reading
* the record from and writing it to a packet.
*/
public abstract class MdnsRecord {
public static final int TYPE_A = 0x0001;
public static final int TYPE_AAAA = 0x001C;
public static final int TYPE_PTR = 0x000C;
public static final int TYPE_SRV = 0x0021;
public static final int TYPE_TXT = 0x0010;
public static final int TYPE_KEY = 0x0019;
public static final int TYPE_NSEC = 0x002f;
public static final int TYPE_ANY = 0x00ff;
private static final int FLAG_CACHE_FLUSH = 0x8000;
public static final long RECEIPT_TIME_NOT_SENT = 0L;
public static final int CLASS_ANY = 0x00ff;
/** Max label length as per RFC 1034/1035 */
public static final int MAX_LABEL_LENGTH = 63;
/** Status indicating that the record is current. */
public static final int STATUS_OK = 0;
/** Status indicating that the record has expired (TTL reached 0). */
public static final int STATUS_EXPIRED = 1;
/** Status indicating that the record should be refreshed (Less than half of TTL remains.) */
public static final int STATUS_NEEDS_REFRESH = 2;
protected final String[] name;
private final int type;
private final int cls;
private final long receiptTimeMillis;
private final long ttlMillis;
private Object key;
/**
* Constructs a new record with the given name and type.
*
* @param reader The reader to read the record from.
* @param isQuestion Whether the record was included in the questions part of the message.
* @throws IOException If an error occurs while reading the packet.
*/
protected MdnsRecord(String[] name, int type, MdnsPacketReader reader, boolean isQuestion)
throws IOException {
this.name = name;
this.type = type;
cls = reader.readUInt16();
receiptTimeMillis = SystemClock.elapsedRealtime();
if (isQuestion) {
// Questions do not have TTL or data
ttlMillis = 0L;
} else {
ttlMillis = SECONDS.toMillis(reader.readUInt32());
int dataLength = reader.readUInt16();
reader.setLimit(dataLength);
readData(reader);
reader.clearLimit();
}
}
/**
* Constructs a new record with the given name and type.
*
* @param reader The reader to read the record from.
* @throws IOException If an error occurs while reading the packet.
*/
// call to readData(com.android.server.connectivity.mdns.MdnsPacketReader) not allowed on given
// receiver.
@SuppressWarnings("nullness:method.invocation.invalid")
protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException {
this(name, type, reader, false);
}
/**
* Constructs a new record with the given properties.
*/
protected MdnsRecord(String[] name, int type, int cls, long receiptTimeMillis,
boolean cacheFlush, long ttlMillis) {
this.name = name;
this.type = type;
this.cls = cls | (cacheFlush ? FLAG_CACHE_FLUSH : 0);
this.receiptTimeMillis = receiptTimeMillis;
this.ttlMillis = ttlMillis;
}
/**
* Converts an array of labels into their dot-separated string representation. This method
* should
* be used for logging purposes only.
*/
public static String labelsToString(String[] labels) {
if (labels == null) {
return null;
}
return TextUtils.join(".", labels);
}
/** Tests if |list1| is a suffix of |list2|. */
public static boolean labelsAreSuffix(String[] list1, String[] list2) {
int offset = list2.length - list1.length;
if (offset < 1) {
return false;
}
for (int i = 0; i < list1.length; ++i) {
if (!MdnsUtils.equalsIgnoreDnsCase(list1[i], list2[i + offset])) {
return false;
}
}
return true;
}
/** Returns the record's receipt (creation) time. */
public final long getReceiptTime() {
return receiptTimeMillis;
}
/** Returns the record's name. */
public String[] getName() {
return name;
}
/** Returns the record's original TTL, in milliseconds. */
public final long getTtl() {
return ttlMillis;
}
/** Returns the record's type. */
public final int getType() {
return type;
}
/** Return the record's class. */
public final int getRecordClass() {
return cls & ~FLAG_CACHE_FLUSH;
}
/** Return whether the cache flush flag is set. */
public final boolean getCacheFlush() {
return (cls & FLAG_CACHE_FLUSH) != 0;
}
/**
* For questions, returns whether a unicast reply was requested.
*
* In practice this is identical to {@link #getCacheFlush()}, as the "cache flush" flag in
* replies is the same as "unicast reply requested" in questions.
*/
public final boolean isUnicastReplyRequested() {
return (cls & MdnsConstants.QCLASS_UNICAST) != 0;
}
/**
* Returns the record's remaining TTL.
*
* If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the
* original TTL of the record.
* @param now The current system time.
* @return The remaning TTL, in milliseconds.
*/
public long getRemainingTTL(final long now) {
if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) {
return ttlMillis;
}
long age = now - receiptTimeMillis;
if (age > ttlMillis) {
return 0;
}
return ttlMillis - age;
}
/**
* Reads the record's payload from a packet.
*
* @param reader The reader to use.
* @throws IOException If an I/O error occurs.
*/
protected abstract void readData(MdnsPacketReader reader) throws IOException;
/**
* Write the first fields of the record, which are common fields for questions and answers.
*
* @param writer The writer to use.
*/
public final void writeHeaderFields(MdnsPacketWriter writer) throws IOException {
writer.writeLabels(name);
writer.writeUInt16(type);
writer.writeUInt16(cls);
}
/**
* Writes the record to a packet.
*
* @param writer The writer to use.
* @param now The current system time. This is used when writing the updated TTL.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public final void write(MdnsPacketWriter writer, long now) throws IOException {
writeHeaderFields(writer);
writer.writeUInt32(MILLISECONDS.toSeconds(getRemainingTTL(now)));
int dataLengthPos = writer.getWritePosition();
writer.writeUInt16(0); // data length
int dataPos = writer.getWritePosition();
writeData(writer);
// Calculate amount of data written, and overwrite the data field earlier in the packet.
int endPos = writer.getWritePosition();
int dataLength = endPos - dataPos;
writer.rewind(dataLengthPos);
writer.writeUInt16(dataLength);
writer.unrewind();
}
/**
* Writes the record's payload to a packet.
*
* @param writer The writer to use.
* @throws IOException If an I/O error occurs.
*/
protected abstract void writeData(MdnsPacketWriter writer) throws IOException;
/** Gets the status of the record. */
public int getStatus(final long now) {
if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) {
return STATUS_OK;
}
final long age = now - receiptTimeMillis;
if (age > ttlMillis) {
return STATUS_EXPIRED;
}
if (age > (ttlMillis / 2)) {
return STATUS_NEEDS_REFRESH;
}
return STATUS_OK;
}
@Override
public boolean equals(@Nullable Object other) {
if (!(other instanceof MdnsRecord)) {
return false;
}
MdnsRecord otherRecord = (MdnsRecord) other;
return MdnsUtils.equalsDnsLabelIgnoreDnsCase(name, otherRecord.name) && (type
== otherRecord.type);
}
@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(MdnsUtils.toDnsLabelsLowerCase(name)), type);
}
/**
* Returns an opaque object that uniquely identifies this record through a combination of its
* type
* and name. Suitable for use as a key in caches.
*/
public final Object getKey() {
if (key == null) {
key = new Key(type, name);
}
return key;
}
private static final class Key {
private final int recordType;
private final String[] recordName;
public Key(int recordType, String[] recordName) {
this.recordType = recordType;
this.recordName = MdnsUtils.toDnsLabelsLowerCase(recordName);
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Key)) {
return false;
}
Key otherKey = (Key) other;
return (recordType == otherKey.recordType) && Arrays.equals(recordName,
otherKey.recordName);
}
@Override
public int hashCode() {
return (recordType * 31) + Arrays.hashCode(recordName);
}
}
}