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