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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.net.Network; 22 import android.os.Parcel; 23 import android.os.Parcelable; 24 import android.text.TextUtils; 25 26 import com.android.net.module.util.ByteUtils; 27 28 import java.nio.charset.Charset; 29 import java.time.Instant; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.TreeMap; 36 37 /** 38 * A class representing a discovered mDNS service instance. 39 * 40 * @hide 41 */ 42 public class MdnsServiceInfo implements Parcelable { 43 private static final Charset US_ASCII = Charset.forName("us-ascii"); 44 private static final Charset UTF_8 = Charset.forName("utf-8"); 45 46 /** @hide */ 47 public static final Parcelable.Creator<MdnsServiceInfo> CREATOR = 48 new Parcelable.Creator<MdnsServiceInfo>() { 49 50 @Override 51 public MdnsServiceInfo createFromParcel(Parcel source) { 52 return new MdnsServiceInfo( 53 source.readString(), 54 source.createStringArray(), 55 source.createStringArrayList(), 56 source.createStringArray(), 57 source.readInt(), 58 source.createStringArrayList(), 59 source.createStringArrayList(), 60 source.createStringArrayList(), 61 source.createTypedArrayList(TextEntry.CREATOR), 62 source.readInt(), 63 source.readParcelable(Network.class.getClassLoader()), 64 Instant.ofEpochSecond(source.readLong())); 65 } 66 67 @Override 68 public MdnsServiceInfo[] newArray(int size) { 69 return new MdnsServiceInfo[size]; 70 } 71 }; 72 73 private final String serviceInstanceName; 74 private final String[] serviceType; 75 private final List<String> subtypes; 76 private final String[] hostName; 77 private final int port; 78 @NonNull 79 private final List<String> ipv4Addresses; 80 @NonNull 81 private final List<String> ipv6Addresses; 82 final List<String> textStrings; 83 @Nullable 84 final List<TextEntry> textEntries; 85 private final int interfaceIndex; 86 87 private final Map<String, byte[]> attributes; 88 @Nullable 89 private final Network network; 90 91 @NonNull 92 private final Instant expirationTime; 93 94 /** 95 * Constructs a {@link MdnsServiceInfo} object with default values. 96 * 97 * @hide 98 */ MdnsServiceInfo( String serviceInstanceName, String[] serviceType, @Nullable List<String> subtypes, String[] hostName, int port, @Nullable String ipv4Address, @Nullable String ipv6Address, @Nullable List<String> textStrings, @Nullable List<TextEntry> textEntries, int interfaceIndex)99 public MdnsServiceInfo( 100 String serviceInstanceName, 101 String[] serviceType, 102 @Nullable List<String> subtypes, 103 String[] hostName, 104 int port, 105 @Nullable String ipv4Address, 106 @Nullable String ipv6Address, 107 @Nullable List<String> textStrings, 108 @Nullable List<TextEntry> textEntries, 109 int interfaceIndex) { 110 this( 111 serviceInstanceName, 112 serviceType, 113 subtypes, 114 hostName, 115 port, 116 List.of(ipv4Address), 117 List.of(ipv6Address), 118 textStrings, 119 textEntries, 120 interfaceIndex, 121 /* network= */ null, 122 /* expirationTime= */ Instant.MAX); 123 } 124 125 /** 126 * Constructs a {@link MdnsServiceInfo} object with default values. 127 * 128 * @hide 129 */ MdnsServiceInfo( String serviceInstanceName, String[] serviceType, @Nullable List<String> subtypes, String[] hostName, int port, @NonNull List<String> ipv4Addresses, @NonNull List<String> ipv6Addresses, @Nullable List<String> textStrings, @Nullable List<TextEntry> textEntries, int interfaceIndex, @Nullable Network network, @NonNull Instant expirationTime)130 public MdnsServiceInfo( 131 String serviceInstanceName, 132 String[] serviceType, 133 @Nullable List<String> subtypes, 134 String[] hostName, 135 int port, 136 @NonNull List<String> ipv4Addresses, 137 @NonNull List<String> ipv6Addresses, 138 @Nullable List<String> textStrings, 139 @Nullable List<TextEntry> textEntries, 140 int interfaceIndex, 141 @Nullable Network network, 142 @NonNull Instant expirationTime) { 143 this.serviceInstanceName = serviceInstanceName; 144 this.serviceType = serviceType; 145 this.subtypes = new ArrayList<>(); 146 if (subtypes != null) { 147 this.subtypes.addAll(subtypes); 148 } 149 this.hostName = hostName; 150 this.port = port; 151 this.ipv4Addresses = new ArrayList<>(ipv4Addresses); 152 this.ipv6Addresses = new ArrayList<>(ipv6Addresses); 153 this.textStrings = new ArrayList<>(); 154 if (textStrings != null) { 155 this.textStrings.addAll(textStrings); 156 } 157 this.textEntries = (textEntries == null) ? null : new ArrayList<>(textEntries); 158 159 // The module side sends both {@code textStrings} and {@code textEntries} for backward 160 // compatibility. We should prefer only {@code textEntries} if it's not null. 161 List<TextEntry> entries = 162 (this.textEntries != null) ? this.textEntries : parseTextStrings(this.textStrings); 163 // The map of attributes is case-insensitive. 164 final Map<String, byte[]> attributes = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 165 for (TextEntry entry : entries) { 166 // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4, only the first entry 167 // of the same key should be accepted: 168 // If a client receives a TXT record containing the same key more than once, then the 169 // client MUST silently ignore all but the first occurrence of that attribute. 170 attributes.putIfAbsent(entry.getKey(), entry.getValue()); 171 } 172 this.attributes = Collections.unmodifiableMap(attributes); 173 this.interfaceIndex = interfaceIndex; 174 this.network = network; 175 this.expirationTime = Instant.ofEpochSecond(expirationTime.getEpochSecond()); 176 } 177 parseTextStrings(List<String> textStrings)178 private static List<TextEntry> parseTextStrings(List<String> textStrings) { 179 List<TextEntry> list = new ArrayList(textStrings.size()); 180 for (String textString : textStrings) { 181 TextEntry entry = TextEntry.fromString(textString); 182 if (entry != null) { 183 list.add(entry); 184 } 185 } 186 return Collections.unmodifiableList(list); 187 } 188 189 /** Returns the name of this service instance. */ getServiceInstanceName()190 public String getServiceInstanceName() { 191 return serviceInstanceName; 192 } 193 194 /** Returns the type of this service instance. */ getServiceType()195 public String[] getServiceType() { 196 return serviceType; 197 } 198 199 /** Returns the list of subtypes supported by this service instance. */ getSubtypes()200 public List<String> getSubtypes() { 201 return new ArrayList<>(subtypes); 202 } 203 204 /** Returns {@code true} if this service instance supports any subtypes. */ hasSubtypes()205 public boolean hasSubtypes() { 206 return !subtypes.isEmpty(); 207 } 208 209 /** Returns the host name of this service instance. */ getHostName()210 public String[] getHostName() { 211 return hostName; 212 } 213 214 /** Returns the port number of this service instance. */ getPort()215 public int getPort() { 216 return port; 217 } 218 219 /** Returns the IPV4 addresses of this service instance. */ 220 @NonNull getIpv4Addresses()221 public List<String> getIpv4Addresses() { 222 return Collections.unmodifiableList(ipv4Addresses); 223 } 224 225 /** 226 * Returns the first IPV4 address of this service instance. 227 * 228 * @deprecated Use {@link #getIpv4Addresses()} to get the entire list of IPV4 229 * addresses for 230 * the host. 231 */ 232 @Nullable 233 @Deprecated getIpv4Address()234 public String getIpv4Address() { 235 return ipv4Addresses.isEmpty() ? null : ipv4Addresses.get(0); 236 } 237 238 /** Returns the IPV6 address of this service instance. */ 239 @NonNull getIpv6Addresses()240 public List<String> getIpv6Addresses() { 241 return Collections.unmodifiableList(ipv6Addresses); 242 } 243 244 /** 245 * Returns the first IPV6 address of this service instance. 246 * 247 * @deprecated Use {@link #getIpv6Addresses()} to get the entire list of IPV6 addresses for 248 * the host. 249 */ 250 @Nullable 251 @Deprecated getIpv6Address()252 public String getIpv6Address() { 253 return ipv6Addresses.isEmpty() ? null : ipv6Addresses.get(0); 254 } 255 256 /** 257 * Returns the index of the network interface at which this response was received, or -1 if the 258 * index is not known. 259 */ getInterfaceIndex()260 public int getInterfaceIndex() { 261 return interfaceIndex; 262 } 263 264 /** 265 * Returns the network at which this response was received, or null if the network is unknown. 266 */ 267 @Nullable getNetwork()268 public Network getNetwork() { 269 return network; 270 } 271 272 /** 273 * Returns the timestamp after when this service is expired or {@code null} if the expiration 274 * time is unknown. 275 * 276 * A service is considered expired if any of its DNS record is expired. 277 */ 278 @NonNull getExpirationTime()279 public Instant getExpirationTime() { 280 return expirationTime; 281 } 282 283 /** 284 * Returns attribute value for {@code key} as a UTF-8 string. It's the caller who must make sure 285 * that the value of {@code key} is indeed a UTF-8 string. {@code null} will be returned if no 286 * attribute value exists for {@code key}. 287 */ 288 @Nullable getAttributeByKey(@onNull String key)289 public String getAttributeByKey(@NonNull String key) { 290 byte[] value = getAttributeAsBytes(key); 291 if (value == null) { 292 return null; 293 } 294 return new String(value, UTF_8); 295 } 296 297 /** 298 * Returns the attribute value for {@code key} as a byte array. {@code null} will be returned if 299 * no attribute value exists for {@code key}. 300 */ 301 @Nullable getAttributeAsBytes(@onNull String key)302 public byte[] getAttributeAsBytes(@NonNull String key) { 303 return attributes.get(key); 304 } 305 306 /** Returns an immutable map of all attributes. */ getAttributes()307 public Map<String, String> getAttributes() { 308 Map<String, String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 309 for (Map.Entry<String, byte[]> kv : attributes.entrySet()) { 310 final byte[] value = kv.getValue(); 311 map.put(kv.getKey(), value == null ? null : new String(value, UTF_8)); 312 } 313 return Collections.unmodifiableMap(map); 314 } 315 316 @Override describeContents()317 public int describeContents() { 318 return 0; 319 } 320 321 @Override writeToParcel(Parcel out, int flags)322 public void writeToParcel(Parcel out, int flags) { 323 out.writeString(serviceInstanceName); 324 out.writeStringArray(serviceType); 325 out.writeStringList(subtypes); 326 out.writeStringArray(hostName); 327 out.writeInt(port); 328 out.writeStringList(ipv4Addresses); 329 out.writeStringList(ipv6Addresses); 330 out.writeStringList(textStrings); 331 out.writeTypedList(textEntries); 332 out.writeInt(interfaceIndex); 333 out.writeParcelable(network, 0); 334 out.writeLong(expirationTime.getEpochSecond()); 335 } 336 337 @Override toString()338 public String toString() { 339 return "Name: " + serviceInstanceName 340 + ", type: " + TextUtils.join(".", serviceType) 341 + ", subtypes: " + TextUtils.join(",", subtypes) 342 + ", ip: " + ipv4Addresses 343 + ", ipv6: " + ipv6Addresses 344 + ", port: " + port 345 + ", interfaceIndex: " + interfaceIndex 346 + ", network: " + network 347 + ", textStrings: " + textStrings 348 + ", textEntries: " + textEntries 349 + ", expirationTime: " + expirationTime; 350 } 351 352 353 /** Represents a DNS TXT key-value pair defined by RFC 6763. */ 354 public static final class TextEntry implements Parcelable { 355 /** 356 * The value to use for attributes with no value. 357 * 358 * <p>As per RFC6763 P.16, attributes may have no value, which is different from having an 359 * empty value (which would be an empty byte array). 360 */ 361 public static final byte[] VALUE_NONE = null; 362 public static final Parcelable.Creator<TextEntry> CREATOR = 363 new Parcelable.Creator<TextEntry>() { 364 @Override 365 public TextEntry createFromParcel(Parcel source) { 366 return new TextEntry(source); 367 } 368 369 @Override 370 public TextEntry[] newArray(int size) { 371 return new TextEntry[size]; 372 } 373 }; 374 375 private final String key; 376 private final byte[] value; 377 378 /** Creates a new {@link TextEntry} instance from a '=' separated string. */ 379 @Nullable fromString(String textString)380 public static TextEntry fromString(String textString) { 381 return fromBytes(textString.getBytes(UTF_8)); 382 } 383 384 /** Creates a new {@link TextEntry} instance from a '=' separated byte array. */ 385 @Nullable fromBytes(byte[] textBytes)386 public static TextEntry fromBytes(byte[] textBytes) { 387 int delimitPos = ByteUtils.indexOf(textBytes, (byte) '='); 388 389 // Per https://datatracker.ietf.org/doc/html/rfc6763#section-6.4: 390 // 1. The key MUST be at least one character. DNS-SD TXT record strings 391 // beginning with an '=' character (i.e., the key is missing) MUST be 392 // silently ignored. 393 // 2. If there is no '=' in a DNS-SD TXT record string, then it is a 394 // boolean attribute, simply identified as being present, with no value. 395 if (delimitPos < 0) { 396 return new TextEntry(new String(textBytes, US_ASCII), VALUE_NONE); 397 } else if (delimitPos == 0) { 398 return null; 399 } 400 return new TextEntry( 401 new String(Arrays.copyOf(textBytes, delimitPos), US_ASCII), 402 Arrays.copyOfRange(textBytes, delimitPos + 1, textBytes.length)); 403 } 404 405 /** Creates a new {@link TextEntry} with given key and value of a UTF-8 string. */ TextEntry(String key, String value)406 public TextEntry(String key, String value) { 407 this(key, value == null ? VALUE_NONE : value.getBytes(UTF_8)); 408 } 409 410 /** Creates a new {@link TextEntry} with given key and value of a byte array. */ TextEntry(String key, byte[] value)411 public TextEntry(String key, byte[] value) { 412 this.key = key; 413 this.value = value == VALUE_NONE ? VALUE_NONE : value.clone(); 414 } 415 TextEntry(Parcel in)416 private TextEntry(Parcel in) { 417 key = in.readString(); 418 value = in.createByteArray(); 419 } 420 getKey()421 public String getKey() { 422 return key; 423 } 424 getValue()425 public byte[] getValue() { 426 return value == VALUE_NONE ? VALUE_NONE : value.clone(); 427 } 428 429 /** Converts this {@link TextEntry} instance to '=' separated byte array. */ toBytes()430 public byte[] toBytes() { 431 final byte[] keyBytes = key.getBytes(US_ASCII); 432 if (value == VALUE_NONE) { 433 return keyBytes; 434 } 435 return ByteUtils.concat(keyBytes, new byte[]{'='}, value); 436 } 437 isEmpty()438 public boolean isEmpty() { 439 return TextUtils.isEmpty(key) && (value == VALUE_NONE || value.length == 0); 440 } 441 442 /** Converts this {@link TextEntry} instance to '=' separated string. */ 443 @Override toString()444 public String toString() { 445 if (value == VALUE_NONE) { 446 return key; 447 } 448 return key + "=" + new String(value, UTF_8); 449 } 450 451 @Override equals(@ullable Object other)452 public boolean equals(@Nullable Object other) { 453 if (this == other) { 454 return true; 455 } else if (!(other instanceof TextEntry)) { 456 return false; 457 } 458 TextEntry otherEntry = (TextEntry) other; 459 460 return key.equals(otherEntry.key) && Arrays.equals(value, otherEntry.value); 461 } 462 463 @Override hashCode()464 public int hashCode() { 465 return 31 * key.hashCode() + Arrays.hashCode(value); 466 } 467 468 @Override describeContents()469 public int describeContents() { 470 return 0; 471 } 472 473 @Override writeToParcel(Parcel out, int flags)474 public void writeToParcel(Parcel out, int flags) { 475 out.writeString(key); 476 out.writeByteArray(value); 477 } 478 } 479 } 480