1 /* 2 * Copyright (C) 2021 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.server.connectivity.mdns; 18 19 import static java.util.concurrent.TimeUnit.MILLISECONDS; 20 import static java.util.concurrent.TimeUnit.SECONDS; 21 22 import android.annotation.Nullable; 23 import android.os.SystemClock; 24 import android.text.TextUtils; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.io.IOException; 29 import java.util.Arrays; 30 import java.util.Objects; 31 32 /** 33 * Abstract base class for mDNS records. Stores the header fields and provides methods for reading 34 * the record from and writing it to a packet. 35 */ 36 public abstract class MdnsRecord { 37 public static final int TYPE_A = 0x0001; 38 public static final int TYPE_AAAA = 0x001C; 39 public static final int TYPE_PTR = 0x000C; 40 public static final int TYPE_SRV = 0x0021; 41 public static final int TYPE_TXT = 0x0010; 42 public static final int TYPE_NSEC = 0x002f; 43 public static final int TYPE_ANY = 0x00ff; 44 45 private static final int FLAG_CACHE_FLUSH = 0x8000; 46 47 public static final long RECEIPT_TIME_NOT_SENT = 0L; 48 public static final int CLASS_ANY = 0x00ff; 49 /** Max label length as per RFC 1034/1035 */ 50 public static final int MAX_LABEL_LENGTH = 63; 51 52 /** Status indicating that the record is current. */ 53 public static final int STATUS_OK = 0; 54 /** Status indicating that the record has expired (TTL reached 0). */ 55 public static final int STATUS_EXPIRED = 1; 56 /** Status indicating that the record should be refreshed (Less than half of TTL remains.) */ 57 public static final int STATUS_NEEDS_REFRESH = 2; 58 59 protected final String[] name; 60 private final int type; 61 private final int cls; 62 private final long receiptTimeMillis; 63 private final long ttlMillis; 64 private Object key; 65 66 /** 67 * Constructs a new record with the given name and type. 68 * 69 * @param reader The reader to read the record from. 70 * @param isQuestion Whether the record was included in the questions part of the message. 71 * @throws IOException If an error occurs while reading the packet. 72 */ MdnsRecord(String[] name, int type, MdnsPacketReader reader, boolean isQuestion)73 protected MdnsRecord(String[] name, int type, MdnsPacketReader reader, boolean isQuestion) 74 throws IOException { 75 this.name = name; 76 this.type = type; 77 cls = reader.readUInt16(); 78 receiptTimeMillis = SystemClock.elapsedRealtime(); 79 80 if (isQuestion) { 81 // Questions do not have TTL or data 82 ttlMillis = 0L; 83 } else { 84 ttlMillis = SECONDS.toMillis(reader.readUInt32()); 85 int dataLength = reader.readUInt16(); 86 87 reader.setLimit(dataLength); 88 readData(reader); 89 reader.clearLimit(); 90 } 91 } 92 93 /** 94 * Constructs a new record with the given name and type. 95 * 96 * @param reader The reader to read the record from. 97 * @throws IOException If an error occurs while reading the packet. 98 */ 99 // call to readData(com.android.server.connectivity.mdns.MdnsPacketReader) not allowed on given 100 // receiver. 101 @SuppressWarnings("nullness:method.invocation.invalid") MdnsRecord(String[] name, int type, MdnsPacketReader reader)102 protected MdnsRecord(String[] name, int type, MdnsPacketReader reader) throws IOException { 103 this(name, type, reader, false); 104 } 105 106 /** 107 * Constructs a new record with the given properties. 108 */ MdnsRecord(String[] name, int type, int cls, long receiptTimeMillis, boolean cacheFlush, long ttlMillis)109 protected MdnsRecord(String[] name, int type, int cls, long receiptTimeMillis, 110 boolean cacheFlush, long ttlMillis) { 111 this.name = name; 112 this.type = type; 113 this.cls = cls | (cacheFlush ? FLAG_CACHE_FLUSH : 0); 114 this.receiptTimeMillis = receiptTimeMillis; 115 this.ttlMillis = ttlMillis; 116 } 117 118 /** 119 * Converts an array of labels into their dot-separated string representation. This method 120 * should 121 * be used for logging purposes only. 122 */ labelsToString(String[] labels)123 public static String labelsToString(String[] labels) { 124 if (labels == null) { 125 return null; 126 } 127 return TextUtils.join(".", labels); 128 } 129 130 /** Tests if |list1| is a suffix of |list2|. */ labelsAreSuffix(String[] list1, String[] list2)131 public static boolean labelsAreSuffix(String[] list1, String[] list2) { 132 int offset = list2.length - list1.length; 133 134 if (offset < 1) { 135 return false; 136 } 137 138 for (int i = 0; i < list1.length; ++i) { 139 if (!list1[i].equals(list2[i + offset])) { 140 return false; 141 } 142 } 143 144 return true; 145 } 146 147 /** Returns the record's receipt (creation) time. */ getReceiptTime()148 public final long getReceiptTime() { 149 return receiptTimeMillis; 150 } 151 152 /** Returns the record's name. */ getName()153 public String[] getName() { 154 return name; 155 } 156 157 /** Returns the record's original TTL, in milliseconds. */ getTtl()158 public final long getTtl() { 159 return ttlMillis; 160 } 161 162 /** Returns the record's type. */ getType()163 public final int getType() { 164 return type; 165 } 166 167 /** Return the record's class. */ getRecordClass()168 public final int getRecordClass() { 169 return cls & ~FLAG_CACHE_FLUSH; 170 } 171 172 /** Return whether the cache flush flag is set. */ getCacheFlush()173 public final boolean getCacheFlush() { 174 return (cls & FLAG_CACHE_FLUSH) != 0; 175 } 176 177 /** 178 * Returns the record's remaining TTL. 179 * 180 * If the record was not sent yet (receipt time {@link #RECEIPT_TIME_NOT_SENT}), this is the 181 * original TTL of the record. 182 * @param now The current system time. 183 * @return The remaning TTL, in milliseconds. 184 */ getRemainingTTL(final long now)185 public long getRemainingTTL(final long now) { 186 if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) { 187 return ttlMillis; 188 } 189 190 long age = now - receiptTimeMillis; 191 if (age > ttlMillis) { 192 return 0; 193 } 194 195 return ttlMillis - age; 196 } 197 198 /** 199 * Reads the record's payload from a packet. 200 * 201 * @param reader The reader to use. 202 * @throws IOException If an I/O error occurs. 203 */ readData(MdnsPacketReader reader)204 protected abstract void readData(MdnsPacketReader reader) throws IOException; 205 206 /** 207 * Write the first fields of the record, which are common fields for questions and answers. 208 * 209 * @param writer The writer to use. 210 */ writeHeaderFields(MdnsPacketWriter writer)211 public final void writeHeaderFields(MdnsPacketWriter writer) throws IOException { 212 writer.writeLabels(name); 213 writer.writeUInt16(type); 214 writer.writeUInt16(cls); 215 } 216 217 /** 218 * Writes the record to a packet. 219 * 220 * @param writer The writer to use. 221 * @param now The current system time. This is used when writing the updated TTL. 222 */ 223 @VisibleForTesting write(MdnsPacketWriter writer, long now)224 public final void write(MdnsPacketWriter writer, long now) throws IOException { 225 writeHeaderFields(writer); 226 227 writer.writeUInt32(MILLISECONDS.toSeconds(getRemainingTTL(now))); 228 229 int dataLengthPos = writer.getWritePosition(); 230 writer.writeUInt16(0); // data length 231 int dataPos = writer.getWritePosition(); 232 233 writeData(writer); 234 235 // Calculate amount of data written, and overwrite the data field earlier in the packet. 236 int endPos = writer.getWritePosition(); 237 int dataLength = endPos - dataPos; 238 writer.rewind(dataLengthPos); 239 writer.writeUInt16(dataLength); 240 writer.unrewind(); 241 } 242 243 /** 244 * Writes the record's payload to a packet. 245 * 246 * @param writer The writer to use. 247 * @throws IOException If an I/O error occurs. 248 */ writeData(MdnsPacketWriter writer)249 protected abstract void writeData(MdnsPacketWriter writer) throws IOException; 250 251 /** Gets the status of the record. */ getStatus(final long now)252 public int getStatus(final long now) { 253 if (receiptTimeMillis == RECEIPT_TIME_NOT_SENT) { 254 return STATUS_OK; 255 } 256 final long age = now - receiptTimeMillis; 257 if (age > ttlMillis) { 258 return STATUS_EXPIRED; 259 } 260 if (age > (ttlMillis / 2)) { 261 return STATUS_NEEDS_REFRESH; 262 } 263 return STATUS_OK; 264 } 265 266 @Override equals(@ullable Object other)267 public boolean equals(@Nullable Object other) { 268 if (!(other instanceof MdnsRecord)) { 269 return false; 270 } 271 272 MdnsRecord otherRecord = (MdnsRecord) other; 273 274 return Arrays.equals(name, otherRecord.name) && (type == otherRecord.type); 275 } 276 277 @Override hashCode()278 public int hashCode() { 279 return Objects.hash(Arrays.hashCode(name), type); 280 } 281 282 /** 283 * Returns an opaque object that uniquely identifies this record through a combination of its 284 * type 285 * and name. Suitable for use as a key in caches. 286 */ getKey()287 public final Object getKey() { 288 if (key == null) { 289 key = new Key(type, name); 290 } 291 return key; 292 } 293 294 private static final class Key { 295 private final int recordType; 296 private final String[] recordName; 297 Key(int recordType, String[] recordName)298 public Key(int recordType, String[] recordName) { 299 this.recordType = recordType; 300 this.recordName = recordName; 301 } 302 303 @Override equals(@ullable Object other)304 public boolean equals(@Nullable Object other) { 305 if (this == other) { 306 return true; 307 } 308 if (!(other instanceof Key)) { 309 return false; 310 } 311 312 Key otherKey = (Key) other; 313 314 return (recordType == otherKey.recordType) && Arrays.equals(recordName, 315 otherKey.recordName); 316 } 317 318 @Override hashCode()319 public int hashCode() { 320 return (recordType * 31) + Arrays.hashCode(recordName); 321 } 322 } 323 } 324