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