• 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 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 }