• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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