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